From 71f6cdc5efad9dbee2a27217ba8db14be9ff4ff8 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Fri, 20 Feb 2026 22:14:59 +0800 Subject: [PATCH 01/33] Add LDL Windows ToolBox and docs Introduce LDLWinToolBox.bat (menu-driven Windows maintenance tool) plus supporting documentation (ANALYSIS.md and PROMPT_GUIDE.md). The batch script adds an admin elevation check, an Advanced System Cleanup (stops wuauserv/bits, purges Temp/Prefetch/SoftwareDistribution, rebuilds directories, flushes DNS), an Event Viewer log clearer using wevtutil, and a Manual SSD TRIM workflow (lists volumes with PowerShell, sanitizes drive input, runs defrag /L). Documentation provides a technical analysis of functionality and a user prompt guide for the tool. --- ANALYSIS.md | 23 ++++++++++ LDLWinToolBox.bat | 111 ++++++++++++++++++++++++++++++++++++++++++++++ PROMPT_GUIDE.md | 28 ++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 ANALYSIS.md create mode 100644 LDLWinToolBox.bat create mode 100644 PROMPT_GUIDE.md diff --git a/ANALYSIS.md b/ANALYSIS.md new file mode 100644 index 0000000..c9e36ee --- /dev/null +++ b/ANALYSIS.md @@ -0,0 +1,23 @@ +# Technical Analysis: LDL Windows ToolBox + +## 1. Privilege Elevation +The script utilizes a dual-layer check for administrative rights. [cite_start]It first attempts to access a protected system directory using `cacls.exe`[cite: 14]. [cite_start]If access is denied, it leverages a PowerShell one-liner to re-launch the batch file with the `RunAs` verb, ensuring the user is prompted for the necessary permissions to execute system-level commands like `net stop` and `defrag`[cite: 14]. + +## 2. Cleanup Methodology +The "Advanced System Cleanup" module is more thorough than standard disk cleanup tools: +- [cite_start]**Service Management:** By stopping `wuauserv` and `bits`, the script can target the `%WinDir%\SoftwareDistribution\Download` folder, which often contains large amounts of stale update data[cite: 16]. +- [cite_start]**Directory Reconstruction:** Instead of merely deleting files, the script uses a loop to remove and then recreate vital temporary directories (`rd` followed by `md`)[cite: 17]. [cite_start]This ensures that any corrupted directory structures are refreshed[cite: 17]. +- [cite_start]**DNS Optimization:** Includes an `ipconfig /flushdns` command to clear the resolver cache, resolving potential network connectivity or redirection issues[cite: 18]. + +## 3. Log Management +[cite_start]The script utilizes `wevtutil.exe` to enumerate and clear every individual log provider registered in Windows[cite: 19]. [cite_start]This is highly effective for system privacy and for troubleshooting by starting with a clean slate[cite: 19]. + +## 4. Storage Optimization (SSD TRIM) +The TRIM module is optimized for NVMe architecture: +- [cite_start]**Discovery:** Uses the modern PowerShell `Get-Volume` cmdlet to provide accurate drive letters and sizes, as `wmic` is deprecated in newer Windows builds[cite: 20]. +- [cite_start]**Optimization Strategy:** The script executes `defrag /L`, which sends a re-trim hint to the SSD controller[cite: 22]. +- [cite_start]**Hardware Benefits:** For a Kingston KC3000, this triggers the Phison controller to perform internal garbage collection on free blocks, maintaining 7,000MB/s+ write speeds without the wear and tear of a physical defragmentation[cite: 22]. + +## 5. Safety Assessment +- [cite_start]**Non-Destructive:** The script targets only temporary locations (`%Temp%`, `Prefetch`, logs) and does not interact with user libraries or system binaries[cite: 16]. +- [cite_start]**Input Sanitization:** The SSD module includes logic to clean user input (removing colons/spaces), preventing command execution errors[cite: 21]. \ No newline at end of file diff --git a/LDLWinToolBox.bat b/LDLWinToolBox.bat new file mode 100644 index 0000000..e470b6d --- /dev/null +++ b/LDLWinToolBox.bat @@ -0,0 +1,111 @@ +@echo off +setlocal EnableDelayedExpansion + +:: --- AUTO ADMIN REQUEST --- +:: Check for Administrator privileges +>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32%\config\system" +if '%errorlevel%' NEQ '0' ( + echo Requesting administrative privileges... + powershell -Command "Start-Process -FilePath '%0' -Verb RunAs" + exit /B +) +pushd "%CD%" +CD /D "%~dp0" +:: --- END AUTO ADMIN --- + +:main_menu +cls +echo =============================================== +echo LDL Windows ToolBox +echo =============================================== +echo [1] Advanced System Cleanup +echo [2] Clear Event Viewer Logs +echo [3] Manual SSD TRIM (Optimized for KC3000) +echo [4] Exit +echo =============================================== +set /p toolbox_choice="Select an option: " + +if "%toolbox_choice%"=="1" goto cleanup +if "%toolbox_choice%"=="2" goto event_logs +if "%toolbox_choice%"=="3" goto ssd_trim +if "%toolbox_choice%"=="4" exit +goto main_menu + +:cleanup +cls +echo =============================================== +echo ADVANCED SYSTEM CLEANUP TOOL +echo =============================================== +echo. +echo [1/4] Stopping background services... +net stop wuauserv >nul 2>&1 +net stop bits >nul 2>&1 + +echo [2/4] Deleting temporary and junk files... +del /s /f /q "%WinDir%\Temp\*.*" >nul 2>&1 +del /s /f /q "%WinDir%\Prefetch\*.*" >nul 2>&1 +del /s /f /q "%Temp%\*.*" >nul 2>&1 +del /s /f /q "%AppData%\Temp\*.*" >nul 2>&1 +del /s /f /q "%LocalAppdata%\Temp\*.*" >nul 2>&1 +del /s /f /q "%WinDir%\SoftwareDistribution\Download\*.*" >nul 2>&1 +del /s /f /q "%WinDir%\System32\winevt\Logs\*.*" >nul 2>&1 +rd /s /q "%SYSTEMDRIVE%\AMD" >nul 2>&1 +rd /s /q "%SYSTEMDRIVE%\NVIDIA" >nul 2>&1 +rd /s /q "%SYSTEMDRIVE%\INTEL" >nul 2>&1 + +echo [3/4] Rebuilding directory structure... +for %%d in ("%WinDir%\Temp" "%WinDir%\Prefetch" "%Temp%" "%AppData%\Temp" "%LocalAppdata%\Temp") do ( + rd /s /q "%%~d" >nul 2>&1 + md "%%~d" >nul 2>&1 +) + +echo [4/4] Finalizing optimizations... +ipconfig /flushdns >nul 2>&1 +net start wuauserv >nul 2>&1 +net start bits >nul 2>&1 +echo. +echo SYSTEM CLEAN UP COMPLETE! +pause +goto main_menu + +:event_logs +cls +echo =============================================== +echo CLEAR EVENT VIEWER LOGS +echo =============================================== +for /F "tokens=*" %%G in ('wevtutil.exe el') DO ( + echo clearing "%%G" + wevtutil.exe cl "%%G" +) +echo. +echo All Event Logs have been cleared! +pause +goto main_menu + +:ssd_trim +cls +echo =============================================== +echo MANUAL SSD TRIM TOOL (KC3000) +echo =============================================== +echo. +echo Current Drives Connected: +powershell -Command "Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} | ft -AutoSize" +echo. +set /p trim_drive="Enter Drive Letter to TRIM (e.g. C): " +set trim_drive=%trim_drive::=% +set trim_drive=%trim_drive: =% +if "%trim_drive%"=="" goto main_menu + +echo. +echo ----------------------------------------------- +echo Optimizing Drive %trim_drive%: ... +echo ----------------------------------------------- +defrag %trim_drive%: /L /V +echo. +echo ----------------------------------------------- +echo Done. +echo [1] Return to Menu +echo [2] Exit +set /p final="Choose an option: " +if "%final%"=="1" goto main_menu +exit \ No newline at end of file diff --git a/PROMPT_GUIDE.md b/PROMPT_GUIDE.md new file mode 100644 index 0000000..6cb94d8 --- /dev/null +++ b/PROMPT_GUIDE.md @@ -0,0 +1,28 @@ +# LDL Windows ToolBox - User Prompt Guide + +This guide explains how to navigate and use the different modules within the `LDLWinToolBox.bat` script. + +## 1. Launching the Tool +[cite_start]The script automatically checks for Administrator privileges[cite: 14]. +- **If prompted by UAC:** Click "Yes" to allow the tool to perform system-level optimizations. +- [cite_start]**Main Menu:** Use the numeric keys `1-4` to select your desired operation[cite: 14]. + +## 2. Advanced System Cleanup (Option 1) +[cite_start]This module performs a deep clean of temporary system data[cite: 15]. +- [cite_start]**What happens:** The tool stops the Windows Update and BITS services to unlock protected folders[cite: 16]. +- **User Action:** Once selected, the process is automated. [cite_start]Wait for the "SYSTEM CLEAN UP COMPLETE!" message before pressing any key to return to the menu[cite: 18]. + +## 3. Clear Event Viewer Logs (Option 2) +[cite_start]Clears all system, security, and application logs[cite: 19]. +- **User Action:** The script will list each log as it is cleared. [cite_start]When finished, press any key to return to the main menu[cite: 19]. + +## 4. Manual SSD TRIM (Option 3) +[cite_start]Designed specifically for high-performance NVMe drives like the Kingston KC3000[cite: 20]. +- [cite_start]**Step 1:** Review the "Current Drives Connected" list generated by the script[cite: 20]. +- [cite_start]**Step 2:** When prompted, type only the drive letter (e.g., `C`) and press Enter[cite: 21]. +- [cite_start]**Step 3:** The script will automatically strip colons or spaces if you accidentally include them[cite: 21]. +- [cite_start]**Step 4:** Review the Verbose (/V) output to see the optimization results[cite: 22]. +- [cite_start]**Step 5:** Choose `1` to return to the menu or `2` to exit the tool[cite: 23]. + +## 5. Exiting the Tool +[cite_start]Select Option `4` from the main menu or Option `2` from the TRIM sub-menu to safely close the application[cite: 14, 23]. \ No newline at end of file From 6b5642f21a5e8be4bc39c3d52e0e9885bfb60420 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Fri, 20 Feb 2026 22:18:13 +0800 Subject: [PATCH 02/33] Docs: remove citation tags and expand guides Clean up and expand documentation in ANALYSIS.md and PROMPT_GUIDE.md. Removed inline citation markers and reorganized several sections into clearer bullet points for Privilege Elevation, Cleanup Methodology, Log Management, SSD TRIM, and Safety Assessment. Added a new Project & Prompt Analysis and a Rules/Guidelines section to ANALYSIS.md, and appended a Rules for Better Prompts section to PROMPT_GUIDE.md. Minor wording edits improve readability and preserve existing guidance about admin elevation, input sanitization, and preserving history. --- ANALYSIS.md | 47 +++++++++++++++++++++++++++++++++++++---------- PROMPT_GUIDE.md | 46 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/ANALYSIS.md b/ANALYSIS.md index c9e36ee..fa2a65b 100644 --- a/ANALYSIS.md +++ b/ANALYSIS.md @@ -1,23 +1,50 @@ # Technical Analysis: LDL Windows ToolBox ## 1. Privilege Elevation -The script utilizes a dual-layer check for administrative rights. [cite_start]It first attempts to access a protected system directory using `cacls.exe`[cite: 14]. [cite_start]If access is denied, it leverages a PowerShell one-liner to re-launch the batch file with the `RunAs` verb, ensuring the user is prompted for the necessary permissions to execute system-level commands like `net stop` and `defrag`[cite: 14]. + +The script utilizes a dual-layer check for administrative rights. It first attempts to access a protected system directory using `cacls.exe`. If access is denied, it leverages a PowerShell one-liner to re-launch the batch file with the `RunAs` verb, ensuring the user is prompted for the necessary permissions to execute system-level commands like `net stop` and `defrag`. ## 2. Cleanup Methodology + The "Advanced System Cleanup" module is more thorough than standard disk cleanup tools: -- [cite_start]**Service Management:** By stopping `wuauserv` and `bits`, the script can target the `%WinDir%\SoftwareDistribution\Download` folder, which often contains large amounts of stale update data[cite: 16]. -- [cite_start]**Directory Reconstruction:** Instead of merely deleting files, the script uses a loop to remove and then recreate vital temporary directories (`rd` followed by `md`)[cite: 17]. [cite_start]This ensures that any corrupted directory structures are refreshed[cite: 17]. -- [cite_start]**DNS Optimization:** Includes an `ipconfig /flushdns` command to clear the resolver cache, resolving potential network connectivity or redirection issues[cite: 18]. + +- **Service Management:** By stopping `wuauserv` and `bits`, the script can target the `%WinDir%\SoftwareDistribution\Download` folder, which often contains large amounts of stale update data. +- **Directory Reconstruction:** Instead of merely deleting files, the script uses a loop to remove and then recreate vital temporary directories (`rd` followed by `md`). This ensures that any corrupted directory structures are refreshed. +- **DNS Optimization:** Includes an `ipconfig /flushdns` command to clear the resolver cache, resolving potential network connectivity or redirection issues. ## 3. Log Management -[cite_start]The script utilizes `wevtutil.exe` to enumerate and clear every individual log provider registered in Windows[cite: 19]. [cite_start]This is highly effective for system privacy and for troubleshooting by starting with a clean slate[cite: 19]. + +The script utilizes `wevtutil.exe` to enumerate and clear every individual log provider registered in Windows. This is highly effective for system privacy and for troubleshooting by starting with a clean slate. ## 4. Storage Optimization (SSD TRIM) + The TRIM module is optimized for NVMe architecture: -- [cite_start]**Discovery:** Uses the modern PowerShell `Get-Volume` cmdlet to provide accurate drive letters and sizes, as `wmic` is deprecated in newer Windows builds[cite: 20]. -- [cite_start]**Optimization Strategy:** The script executes `defrag /L`, which sends a re-trim hint to the SSD controller[cite: 22]. -- [cite_start]**Hardware Benefits:** For a Kingston KC3000, this triggers the Phison controller to perform internal garbage collection on free blocks, maintaining 7,000MB/s+ write speeds without the wear and tear of a physical defragmentation[cite: 22]. + +- **Discovery:** Uses the modern PowerShell `Get-Volume` cmdlet to provide accurate drive letters and sizes, as `wmic` is deprecated in newer Windows builds. +- **Optimization Strategy:** The script executes `defrag /L`, which sends a re-trim hint to the SSD controller. +- **Hardware Benefits:** For a Kingston KC3000, this triggers the Phison controller to perform internal garbage collection on free blocks, maintaining 7,000MB/s+ write speeds without the wear and tear of a physical defragmentation. ## 5. Safety Assessment -- [cite_start]**Non-Destructive:** The script targets only temporary locations (`%Temp%`, `Prefetch`, logs) and does not interact with user libraries or system binaries[cite: 16]. -- [cite_start]**Input Sanitization:** The SSD module includes logic to clean user input (removing colons/spaces), preventing command execution errors[cite: 21]. \ No newline at end of file + +- **Non-Destructive:** The script targets only temporary locations (`%Temp%`, `Prefetch`, logs) and does not interact with user libraries or system binaries. +- **Input Sanitization:** The SSD module includes logic to clean user input (removing colons/spaces), preventing command execution errors. + +## 6. Project & Prompt Analysis + +### Project Analysis + +The LDLWinToolBox project is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, event log purging, and SSD TRIM optimization into a single, cohesive menu-driven interface. It relies seamlessly on standard Windows and PowerShell commands, ensuring no external dependencies are needed. + +### Prompt Files Analysis + +The existing `PROMPT_GUIDE.md` provides user-centric instructions on operating the batch script, while this `ANALYSIS.md` details the technical implementations and architectural choices. Both accurately reflect the current script's capabilities and align with the core requirements (batch standard, auto admin check). + +## 7. Rules to Apply for Next Time (Future Prompts) + +To ensure continuous improvement and maintain the integrity of the project during future prompting, apply the following rules: + +1. **Maintain Batch Standard:** Any new features or module additions must strictly use standard Windows Batch (`.bat`) commands. Utilize PowerShell one-liners only when native DOS commands lack the necessary functionality (e.g., UI prompts or advanced volume queries). +2. **Preserve Auto-Admin:** Do not modify the existing dual-layer UAC elevation logic at the beginning of the script. All new operations must assume execution under elevated privileges. +3. **Keep History Intact:** When requesting updates or new features, strictly mandate that all existing historical analysis and documentation in `ANALYSIS.md` and `PROMPT_GUIDE.md` be preserved. +4. **Safety First Methodology:** Any destructive or system-altering commands must be scoped precisely and include silent execution flags (`>nul 2>&1` where appropriate) to avoid cluttering the CLI output, maintaining the clean menu experience. +5. **Input Validation:** Ensure input sanitization (e.g., removing spaces and colons as done in the SSD Trim module) is explicitly required for any new feature taking user input. diff --git a/PROMPT_GUIDE.md b/PROMPT_GUIDE.md index 6cb94d8..bb03f10 100644 --- a/PROMPT_GUIDE.md +++ b/PROMPT_GUIDE.md @@ -3,26 +3,44 @@ This guide explains how to navigate and use the different modules within the `LDLWinToolBox.bat` script. ## 1. Launching the Tool -[cite_start]The script automatically checks for Administrator privileges[cite: 14]. + +The script automatically checks for Administrator privileges. + - **If prompted by UAC:** Click "Yes" to allow the tool to perform system-level optimizations. -- [cite_start]**Main Menu:** Use the numeric keys `1-4` to select your desired operation[cite: 14]. +- **Main Menu:** Use the numeric keys `1-4` to select your desired operation. ## 2. Advanced System Cleanup (Option 1) -[cite_start]This module performs a deep clean of temporary system data[cite: 15]. -- [cite_start]**What happens:** The tool stops the Windows Update and BITS services to unlock protected folders[cite: 16]. -- **User Action:** Once selected, the process is automated. [cite_start]Wait for the "SYSTEM CLEAN UP COMPLETE!" message before pressing any key to return to the menu[cite: 18]. + +This module performs a deep clean of temporary system data. + +- **What happens:** The tool stops the Windows Update and BITS services to unlock protected folders. +- **User Action:** Once selected, the process is automated. Wait for the "SYSTEM CLEAN UP COMPLETE!" message before pressing any key to return to the menu. ## 3. Clear Event Viewer Logs (Option 2) -[cite_start]Clears all system, security, and application logs[cite: 19]. -- **User Action:** The script will list each log as it is cleared. [cite_start]When finished, press any key to return to the main menu[cite: 19]. + +Clears all system, security, and application logs. + +- **User Action:** The script will list each log as it is cleared. When finished, press any key to return to the main menu. ## 4. Manual SSD TRIM (Option 3) -[cite_start]Designed specifically for high-performance NVMe drives like the Kingston KC3000[cite: 20]. -- [cite_start]**Step 1:** Review the "Current Drives Connected" list generated by the script[cite: 20]. -- [cite_start]**Step 2:** When prompted, type only the drive letter (e.g., `C`) and press Enter[cite: 21]. -- [cite_start]**Step 3:** The script will automatically strip colons or spaces if you accidentally include them[cite: 21]. -- [cite_start]**Step 4:** Review the Verbose (/V) output to see the optimization results[cite: 22]. -- [cite_start]**Step 5:** Choose `1` to return to the menu or `2` to exit the tool[cite: 23]. + +Designed specifically for high-performance NVMe drives like the Kingston KC3000. + +- **Step 1:** Review the "Current Drives Connected" list generated by the script. +- **Step 2:** When prompted, type only the drive letter (e.g., `C`) and press Enter. +- **Step 3:** The script will automatically strip colons or spaces if you accidentally include them. +- **Step 4:** Review the Verbose (/V) output to see the optimization results. +- **Step 5:** Choose `1` to return to the menu or `2` to exit the tool. ## 5. Exiting the Tool -[cite_start]Select Option `4` from the main menu or Option `2` from the TRIM sub-menu to safely close the application[cite: 14, 23]. \ No newline at end of file + +Select Option `4` from the main menu or Option `2` from the TRIM sub-menu to safely close the application. + +## 6. Rules for Better Prompts (Future Development) + +When interacting with AI to update or expand this toolbox, utilize the following rules to ensure quality and consistency: + +1. **Specify Standard Windows Commands:** Always request that updates are written using native `.bat` syntax. +2. **Reiterate Privilege Requirements:** Remind the AI that the tool operates with auto-requested Administrator privileges so it can formulate commands confidently. +3. **Preserve Documentation:** Explicitly state: "Keep all previous analysis and history intact. Only append your updates to `ANALYSIS.md` and `PROMPT_GUIDE.md`." +4. **Demand Input Sanitization:** Instruct the AI to incorporate proper variable trimming and sanitization for any newly added menus requiring user input. From 4a7e8d5efc82a9dc33a4b972b8bdb74dfefb68b0 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Fri, 20 Feb 2026 22:23:42 +0800 Subject: [PATCH 03/33] Add timestamped logging and concise console output Initialize a timestamped run log and route verbose command output into it while keeping console messages high-level and user-friendly. LDLWinToolBox.bat: add LOGFILE setup, switch menu/input handling to delayed expansion, emit brief console echoes and append detailed outputs (del, rd, net, wevtutil, defrag, ipconfig) to the log, refactor cleanup loops to log actions, and capture defrag output to a temp file for display+logging. ANALYSIS.md and PROMPT_GUIDE.md: document the new logging/verbosity behavior, add guidance for UX and future prompt rules regarding logging and safe verbosity. Overall goal: preserve existing functionality while improving auditing and avoiding overwhelming console output. --- ANALYSIS.md | 14 +++-- LDLWinToolBox.bat | 133 +++++++++++++++++++++++++++++++++++----------- PROMPT_GUIDE.md | 10 ++-- 3 files changed, 120 insertions(+), 37 deletions(-) diff --git a/ANALYSIS.md b/ANALYSIS.md index fa2a65b..c848452 100644 --- a/ANALYSIS.md +++ b/ANALYSIS.md @@ -29,7 +29,14 @@ The TRIM module is optimized for NVMe architecture: - **Non-Destructive:** The script targets only temporary locations (`%Temp%`, `Prefetch`, logs) and does not interact with user libraries or system binaries. - **Input Sanitization:** The SSD module includes logic to clean user input (removing colons/spaces), preventing command execution errors. -## 6. Project & Prompt Analysis +## 6. Logging and Verbosity Control + +The script implements a dynamic verbose logging mechanism. Upon execution, it generates a timestamped log file (`LDLWinToolBox_yyMMddHHmmss.log`). + +- **User Experience (UX):** It displays clear, high-level, human-readable operations on the console (e.g., "Cleaning \Windows\Temp"), providing transparency without overwhelming the user with "scary" massive walls of file paths. +- **Detailed Auditing:** The actual verbose output of all underlying commands (`del`, `rd`, `wevtutil`, `defrag`) is redirected and appended to the log file via `>> "!LOGFILE!" 2>&1`, ensuring complete historical records for troubleshooting. + +## 7. Project & Prompt Analysis ### Project Analysis @@ -39,12 +46,13 @@ The LDLWinToolBox project is a standalone Windows Batch script (`LDLWinToolBox.b The existing `PROMPT_GUIDE.md` provides user-centric instructions on operating the batch script, while this `ANALYSIS.md` details the technical implementations and architectural choices. Both accurately reflect the current script's capabilities and align with the core requirements (batch standard, auto admin check). -## 7. Rules to Apply for Next Time (Future Prompts) +## 8. Rules to Apply for Next Time (Future Prompts) To ensure continuous improvement and maintain the integrity of the project during future prompting, apply the following rules: 1. **Maintain Batch Standard:** Any new features or module additions must strictly use standard Windows Batch (`.bat`) commands. Utilize PowerShell one-liners only when native DOS commands lack the necessary functionality (e.g., UI prompts or advanced volume queries). 2. **Preserve Auto-Admin:** Do not modify the existing dual-layer UAC elevation logic at the beginning of the script. All new operations must assume execution under elevated privileges. 3. **Keep History Intact:** When requesting updates or new features, strictly mandate that all existing historical analysis and documentation in `ANALYSIS.md` and `PROMPT_GUIDE.md` be preserved. -4. **Safety First Methodology:** Any destructive or system-altering commands must be scoped precisely and include silent execution flags (`>nul 2>&1` where appropriate) to avoid cluttering the CLI output, maintaining the clean menu experience. +4. **Safety First Methodology:** Any destructive or system-altering commands must be scoped precisely to avoid cluttering the CLI output, maintaining the clean menu experience. 5. **Input Validation:** Ensure input sanitization (e.g., removing spaces and colons as done in the SSD Trim module) is explicitly required for any new feature taking user input. +6. **Logging Principle:** When executing batch commands, echo a clean, understandable summary to the console and redirect the verbose raw output (`>> "!LOGFILE!" 2>&1`) to the dynamic timestamped log file. Do NOT flood the console with raw data that might intimidate users. diff --git a/LDLWinToolBox.bat b/LDLWinToolBox.bat index e470b6d..52784d3 100644 --- a/LDLWinToolBox.bat +++ b/LDLWinToolBox.bat @@ -2,7 +2,6 @@ setlocal EnableDelayedExpansion :: --- AUTO ADMIN REQUEST --- -:: Check for Administrator privileges >nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32%\config\system" if '%errorlevel%' NEQ '0' ( echo Requesting administrative privileges... @@ -13,6 +12,16 @@ pushd "%CD%" CD /D "%~dp0" :: --- END AUTO ADMIN --- +:: --- INITIALIZE LOGGING --- +for /f "delims=" %%a in ('powershell -Command "Get-Date -Format yyMMddHHmmss"') do set "LOG_TIME=%%a" +set "LOGFILE=LDLWinToolBox_!LOG_TIME!.log" + +echo =============================================== > "!LOGFILE!" +echo LDL Windows ToolBox Run Log >> "!LOGFILE!" +echo Date: !LOG_TIME! >> "!LOGFILE!" +echo =============================================== >> "!LOGFILE!" +echo. >> "!LOGFILE!" + :main_menu cls echo =============================================== @@ -25,10 +34,10 @@ echo [4] Exit echo =============================================== set /p toolbox_choice="Select an option: " -if "%toolbox_choice%"=="1" goto cleanup -if "%toolbox_choice%"=="2" goto event_logs -if "%toolbox_choice%"=="3" goto ssd_trim -if "%toolbox_choice%"=="4" exit +if "!toolbox_choice!"=="1" goto cleanup +if "!toolbox_choice!"=="2" goto event_logs +if "!toolbox_choice!"=="3" goto ssd_trim +if "!toolbox_choice!"=="4" exit goto main_menu :cleanup @@ -36,35 +45,80 @@ cls echo =============================================== echo ADVANCED SYSTEM CLEANUP TOOL echo =============================================== +echo All operations are being logged to: +echo !LOGFILE! +echo =============================================== echo. echo [1/4] Stopping background services... -net stop wuauserv >nul 2>&1 -net stop bits >nul 2>&1 +echo [1/4] Stopping background services... >> "!LOGFILE!" +echo - Stopping Windows Update (wuauserv)... +echo - Stopping Windows Update (wuauserv)... >> "!LOGFILE!" +net stop wuauserv >> "!LOGFILE!" 2>&1 + +echo - Stopping Background Intelligent Transfer Service (bits)... +echo - Stopping Background Intelligent Transfer Service (bits)... >> "!LOGFILE!" +net stop bits >> "!LOGFILE!" 2>&1 + +echo. echo [2/4] Deleting temporary and junk files... -del /s /f /q "%WinDir%\Temp\*.*" >nul 2>&1 -del /s /f /q "%WinDir%\Prefetch\*.*" >nul 2>&1 -del /s /f /q "%Temp%\*.*" >nul 2>&1 -del /s /f /q "%AppData%\Temp\*.*" >nul 2>&1 -del /s /f /q "%LocalAppdata%\Temp\*.*" >nul 2>&1 -del /s /f /q "%WinDir%\SoftwareDistribution\Download\*.*" >nul 2>&1 -del /s /f /q "%WinDir%\System32\winevt\Logs\*.*" >nul 2>&1 -rd /s /q "%SYSTEMDRIVE%\AMD" >nul 2>&1 -rd /s /q "%SYSTEMDRIVE%\NVIDIA" >nul 2>&1 -rd /s /q "%SYSTEMDRIVE%\INTEL" >nul 2>&1 +echo [2/4] Deleting temporary and junk files... >> "!LOGFILE!" +for %%f in ( + "%WinDir%\Temp\*.*" + "%WinDir%\Prefetch\*.*" + "%Temp%\*.*" + "%AppData%\Temp\*.*" + "%LocalAppdata%\Temp\*.*" + "%WinDir%\SoftwareDistribution\Download\*.*" + "%WinDir%\System32\winevt\Logs\*.*" +) do ( + echo - Cleaning %%~f + echo - Cleaning %%~f >> "!LOGFILE!" + del /s /f /q "%%~f" >> "!LOGFILE!" 2>&1 +) + +for %%d in ( + "%SYSTEMDRIVE%\AMD" + "%SYSTEMDRIVE%\NVIDIA" + "%SYSTEMDRIVE%\INTEL" +) do ( + if exist "%%~d" ( + echo - Removing Directory %%~d + echo - Removing Directory %%~d >> "!LOGFILE!" + rd /s /q "%%~d" >> "!LOGFILE!" 2>&1 + ) +) + +echo. echo [3/4] Rebuilding directory structure... +echo [3/4] Rebuilding directory structure... >> "!LOGFILE!" for %%d in ("%WinDir%\Temp" "%WinDir%\Prefetch" "%Temp%" "%AppData%\Temp" "%LocalAppdata%\Temp") do ( - rd /s /q "%%~d" >nul 2>&1 - md "%%~d" >nul 2>&1 + echo - Rebuilding %%~d + echo - Rebuilding %%~d >> "!LOGFILE!" + rd /s /q "%%~d" >> "!LOGFILE!" 2>&1 + md "%%~d" >> "!LOGFILE!" 2>&1 ) +echo. echo [4/4] Finalizing optimizations... -ipconfig /flushdns >nul 2>&1 -net start wuauserv >nul 2>&1 -net start bits >nul 2>&1 +echo [4/4] Finalizing optimizations... >> "!LOGFILE!" + +echo - Flushing DNS Resolver Cache... +echo - Flushing DNS Resolver Cache... >> "!LOGFILE!" +ipconfig /flushdns >> "!LOGFILE!" 2>&1 + +echo - Starting Windows Update (wuauserv)... +echo - Starting Windows Update (wuauserv)... >> "!LOGFILE!" +net start wuauserv >> "!LOGFILE!" 2>&1 + +echo - Starting Background Intelligent Transfer Service (bits)... +echo - Starting Background Intelligent Transfer Service (bits)... >> "!LOGFILE!" +net start bits >> "!LOGFILE!" 2>&1 + echo. echo SYSTEM CLEAN UP COMPLETE! +echo SYSTEM CLEAN UP COMPLETE! >> "!LOGFILE!" pause goto main_menu @@ -73,12 +127,20 @@ cls echo =============================================== echo CLEAR EVENT VIEWER LOGS echo =============================================== +echo All operations are being logged to: +echo !LOGFILE! +echo =============================================== +echo. +echo Clearing Event Logs... >> "!LOGFILE!" + for /F "tokens=*" %%G in ('wevtutil.exe el') DO ( - echo clearing "%%G" - wevtutil.exe cl "%%G" + echo - Clearing log: "%%G" + echo - Clearing log: "%%G" >> "!LOGFILE!" + wevtutil.exe cl "%%G" >> "!LOGFILE!" 2>&1 ) echo. echo All Event Logs have been cleared! +echo All Event Logs have been cleared! >> "!LOGFILE!" pause goto main_menu @@ -87,25 +149,36 @@ cls echo =============================================== echo MANUAL SSD TRIM TOOL (KC3000) echo =============================================== +echo All operations are being logged to: +echo !LOGFILE! +echo =============================================== echo. echo Current Drives Connected: powershell -Command "Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} | ft -AutoSize" echo. +set trim_drive= set /p trim_drive="Enter Drive Letter to TRIM (e.g. C): " -set trim_drive=%trim_drive::=% -set trim_drive=%trim_drive: =% -if "%trim_drive%"=="" goto main_menu +if "!trim_drive!"=="" goto main_menu +set trim_drive=!trim_drive::=! +set trim_drive=!trim_drive: =! +if "!trim_drive!"=="" goto main_menu echo. echo ----------------------------------------------- -echo Optimizing Drive %trim_drive%: ... +echo Optimizing Drive !trim_drive!: ... +echo Optimizing Drive !trim_drive!: ... >> "!LOGFILE!" echo ----------------------------------------------- -defrag %trim_drive%: /L /V +defrag !trim_drive!: /L /V > "%TEMP%\defrag_out.txt" 2>&1 +type "%TEMP%\defrag_out.txt" +type "%TEMP%\defrag_out.txt" >> "!LOGFILE!" +del /q "%TEMP%\defrag_out.txt" >nul 2>&1 + echo. echo ----------------------------------------------- echo Done. +echo Done. >> "!LOGFILE!" echo [1] Return to Menu echo [2] Exit set /p final="Choose an option: " -if "%final%"=="1" goto main_menu +if "!final!"=="1" goto main_menu exit \ No newline at end of file diff --git a/PROMPT_GUIDE.md b/PROMPT_GUIDE.md index bb03f10..a0c2bbc 100644 --- a/PROMPT_GUIDE.md +++ b/PROMPT_GUIDE.md @@ -7,20 +7,21 @@ This guide explains how to navigate and use the different modules within the `LD The script automatically checks for Administrator privileges. - **If prompted by UAC:** Click "Yes" to allow the tool to perform system-level optimizations. +- **Log Generation:** Every run creates a timestamped log file (`LDLWinToolBox_yyMMddHHmmss.log`) in the same folder as the script to record all detailed operations. - **Main Menu:** Use the numeric keys `1-4` to select your desired operation. ## 2. Advanced System Cleanup (Option 1) This module performs a deep clean of temporary system data. -- **What happens:** The tool stops the Windows Update and BITS services to unlock protected folders. -- **User Action:** Once selected, the process is automated. Wait for the "SYSTEM CLEAN UP COMPLETE!" message before pressing any key to return to the menu. +- **What happens:** The tool stops the Windows Update and BITS services to unlock protected folders. It will visibly display a friendly message indicating which folder it is currently cleaning. +- **User Action:** Once selected, the process is automated. Wait for the "SYSTEM CLEAN UP COMPLETE!" message before pressing any key to return to the menu. You can review the log file later for detailed deletion records. ## 3. Clear Event Viewer Logs (Option 2) Clears all system, security, and application logs. -- **User Action:** The script will list each log as it is cleared. When finished, press any key to return to the main menu. +- **User Action:** The script will list each log as it is cleared cleanly. When finished, press any key to return to the main menu. Raw verbose logs are pushed to the text file. ## 4. Manual SSD TRIM (Option 3) @@ -29,7 +30,7 @@ Designed specifically for high-performance NVMe drives like the Kingston KC3000. - **Step 1:** Review the "Current Drives Connected" list generated by the script. - **Step 2:** When prompted, type only the drive letter (e.g., `C`) and press Enter. - **Step 3:** The script will automatically strip colons or spaces if you accidentally include them. -- **Step 4:** Review the Verbose (/V) output to see the optimization results. +- **Step 4:** Review the optimization results printed dynamically onto the screen (and into the log). - **Step 5:** Choose `1` to return to the menu or `2` to exit the tool. ## 5. Exiting the Tool @@ -44,3 +45,4 @@ When interacting with AI to update or expand this toolbox, utilize the following 2. **Reiterate Privilege Requirements:** Remind the AI that the tool operates with auto-requested Administrator privileges so it can formulate commands confidently. 3. **Preserve Documentation:** Explicitly state: "Keep all previous analysis and history intact. Only append your updates to `ANALYSIS.md` and `PROMPT_GUIDE.md`." 4. **Demand Input Sanitization:** Instruct the AI to incorporate proper variable trimming and sanitization for any newly added menus requiring user input. +5. **Enforce Clean Verbosity:** Demand that scripts echo clear, friendly summaries to the console, while routing "scary" raw verbose output (like massive lists of deleted files) smoothly into the `!LOGFILE!`. From df3e8bf88bcd29505fdc3323893fa622aff7ede4 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Fri, 20 Feb 2026 22:39:32 +0800 Subject: [PATCH 04/33] Add SFC/DISM, winget, net-reset & space calc Expand LDLWinToolBox functionality: menu increased from 4 to 8 options and added System Integrity Repair (SFC + DISM), Windows Component Store cleanup (DISM StartComponentCleanup), Winget-based app updates, and a Complete Network Reset. Implement a PowerShell-based free-space measurement (Win32_LogicalDisk) before/after cleanup to calculate and display total MB freed. Update event log and SSD TRIM messages, add confirmations/warnings for long-running tasks, and ensure verbose output continues to be redirected to the timestamped log. Update ANALYSIS.md and PROMPT_GUIDE.md to reflect the new features, updated numbering, and revised rules for verbosity and process warnings. --- ANALYSIS.md | 50 +++++++--------- LDLWinToolBox.bat | 146 +++++++++++++++++++++++++++++++++++++++++----- PROMPT_GUIDE.md | 41 +++++-------- 3 files changed, 168 insertions(+), 69 deletions(-) diff --git a/ANALYSIS.md b/ANALYSIS.md index c848452..20596b4 100644 --- a/ANALYSIS.md +++ b/ANALYSIS.md @@ -10,49 +10,43 @@ The "Advanced System Cleanup" module is more thorough than standard disk cleanup - **Service Management:** By stopping `wuauserv` and `bits`, the script can target the `%WinDir%\SoftwareDistribution\Download` folder, which often contains large amounts of stale update data. - **Directory Reconstruction:** Instead of merely deleting files, the script uses a loop to remove and then recreate vital temporary directories (`rd` followed by `md`). This ensures that any corrupted directory structures are refreshed. -- **DNS Optimization:** Includes an `ipconfig /flushdns` command to clear the resolver cache, resolving potential network connectivity or redirection issues. +- **Space Saved Calculation:** Uses PowerShell WMI/CIM calls to parse `Win32_LogicalDisk` free space in MB before and after cleanup to determine exact megabytes cleaned dynamically, providing valuable user feedback. -## 3. Log Management +## 3. Extended Administrative Tools -The script utilizes `wevtutil.exe` to enumerate and clear every individual log provider registered in Windows. This is highly effective for system privacy and for troubleshooting by starting with a clean slate. +- **System Integrity Repair:** Uses `sfc /scannow` and `DISM /RestoreHealth` combined for deep system repair. It properly warns users about the extended duration of these tasks and guarantees an abort mechanism before proceeding. +- **Component Store Cleanup:** Utilizes `DISM /StartComponentCleanup` to clear old Windows Update caches safely. Users are explicitly warned NOT to interrupt this potentially dangerous process to prevent OS corruption. +- **Application Updater:** Employs the native Windows Package Manager (`winget`) with headless flags (`--accept-package-agreements`, `--accept-source-agreements`) to silently upgrade software, logging standard output cleanly. +- **Network Reset:** Leverages `netsh winsock reset` and `netsh int ip reset` along with DNS flushing to reset the full network stack, returning network interfaces to default states. -## 4. Storage Optimization (SSD TRIM) - -The TRIM module is optimized for NVMe architecture: - -- **Discovery:** Uses the modern PowerShell `Get-Volume` cmdlet to provide accurate drive letters and sizes, as `wmic` is deprecated in newer Windows builds. -- **Optimization Strategy:** The script executes `defrag /L`, which sends a re-trim hint to the SSD controller. -- **Hardware Benefits:** For a Kingston KC3000, this triggers the Phison controller to perform internal garbage collection on free blocks, maintaining 7,000MB/s+ write speeds without the wear and tear of a physical defragmentation. - -## 5. Safety Assessment - -- **Non-Destructive:** The script targets only temporary locations (`%Temp%`, `Prefetch`, logs) and does not interact with user libraries or system binaries. -- **Input Sanitization:** The SSD module includes logic to clean user input (removing colons/spaces), preventing command execution errors. - -## 6. Logging and Verbosity Control +## 4. Log Management & Verbosity Control The script implements a dynamic verbose logging mechanism. Upon execution, it generates a timestamped log file (`LDLWinToolBox_yyMMddHHmmss.log`). - **User Experience (UX):** It displays clear, high-level, human-readable operations on the console (e.g., "Cleaning \Windows\Temp"), providing transparency without overwhelming the user with "scary" massive walls of file paths. - **Detailed Auditing:** The actual verbose output of all underlying commands (`del`, `rd`, `wevtutil`, `defrag`) is redirected and appended to the log file via `>> "!LOGFILE!" 2>&1`, ensuring complete historical records for troubleshooting. +- **Event Viewer Logs:** Utilizes `wevtutil.exe` to enumerate and clear every individual log provider registered in Windows. -## 7. Project & Prompt Analysis +## 5. Storage Optimization (SSD TRIM) -### Project Analysis +The TRIM module is optimized for NVMe architecture: -The LDLWinToolBox project is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, event log purging, and SSD TRIM optimization into a single, cohesive menu-driven interface. It relies seamlessly on standard Windows and PowerShell commands, ensuring no external dependencies are needed. +- **Discovery:** Uses the modern PowerShell `Get-Volume` cmdlet to provide accurate drive letters and sizes. +- **Optimization Strategy:** Executes `defrag /L`, which sends a re-trim hint to the SSD controller. +- **Hardware Benefits:** For devices like the Kingston KC3000, this triggers the Phison controller to perform internal garbage collection. +- **Input Sanitization:** The module includes logic to clean user input (removing colons/spaces), preventing command execution errors. -### Prompt Files Analysis +## 6. Project & Prompt Analysis -The existing `PROMPT_GUIDE.md` provides user-centric instructions on operating the batch script, while this `ANALYSIS.md` details the technical implementations and architectural choices. Both accurately reflect the current script's capabilities and align with the core requirements (batch standard, auto admin check). +The LDLWinToolBox project is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, script verifications, and SSD TRIM optimization into a single, cohesive menu-driven interface. -## 8. Rules to Apply for Next Time (Future Prompts) +## 7. Rules to Apply for Next Time (Future Prompts) To ensure continuous improvement and maintain the integrity of the project during future prompting, apply the following rules: -1. **Maintain Batch Standard:** Any new features or module additions must strictly use standard Windows Batch (`.bat`) commands. Utilize PowerShell one-liners only when native DOS commands lack the necessary functionality (e.g., UI prompts or advanced volume queries). -2. **Preserve Auto-Admin:** Do not modify the existing dual-layer UAC elevation logic at the beginning of the script. All new operations must assume execution under elevated privileges. +1. **Maintain Batch Standard:** Any new features or module additions must strictly use standard Windows Batch (`.bat`) commands. Utilize PowerShell one-liners only when native DOS commands lack the necessary functionality. +2. **Preserve Auto-Admin:** Do not modify the existing dual-layer UAC elevation logic at the beginning of the script. 3. **Keep History Intact:** When requesting updates or new features, strictly mandate that all existing historical analysis and documentation in `ANALYSIS.md` and `PROMPT_GUIDE.md` be preserved. -4. **Safety First Methodology:** Any destructive or system-altering commands must be scoped precisely to avoid cluttering the CLI output, maintaining the clean menu experience. -5. **Input Validation:** Ensure input sanitization (e.g., removing spaces and colons as done in the SSD Trim module) is explicitly required for any new feature taking user input. -6. **Logging Principle:** When executing batch commands, echo a clean, understandable summary to the console and redirect the verbose raw output (`>> "!LOGFILE!" 2>&1`) to the dynamic timestamped log file. Do NOT flood the console with raw data that might intimidate users. +4. **Safety First Methodology:** Any destructive or system-altering commands must be scoped precisely to avoid cluttering the CLI output. +5. **Enforce Clean Verbosity:** Echo clear, friendly summaries to the console, while routing raw verbose output into the `!LOGFILE!`. +6. **Long-Running Process Handling:** Any command that blocks the main thread for over a minute must explicitly warn the user beforehand, explain whether it is safe to manually interrupt by closing the window, and provide a (Y/N) confirmation exit hatch. diff --git a/LDLWinToolBox.bat b/LDLWinToolBox.bat index 52784d3..644263f 100644 --- a/LDLWinToolBox.bat +++ b/LDLWinToolBox.bat @@ -27,17 +27,25 @@ cls echo =============================================== echo LDL Windows ToolBox echo =============================================== -echo [1] Advanced System Cleanup -echo [2] Clear Event Viewer Logs -echo [3] Manual SSD TRIM (Optimized for KC3000) -echo [4] Exit +echo [1] Advanced System Cleanup (with Space Calculator) +echo [2] System Integrity Repair (SFC + DISM) +echo [3] Windows Component Store Cleanup (WinSxS) +echo [4] Update All Installed Apps (Winget) +echo [5] Complete Network Reset +echo [6] Clear Event Viewer Logs +echo [7] Manual SSD TRIM (Optimized for KC3000) +echo [8] Exit echo =============================================== set /p toolbox_choice="Select an option: " if "!toolbox_choice!"=="1" goto cleanup -if "!toolbox_choice!"=="2" goto event_logs -if "!toolbox_choice!"=="3" goto ssd_trim -if "!toolbox_choice!"=="4" exit +if "!toolbox_choice!"=="2" goto sys_repair +if "!toolbox_choice!"=="3" goto win_sxs +if "!toolbox_choice!"=="4" goto app_update +if "!toolbox_choice!"=="5" goto net_reset +if "!toolbox_choice!"=="6" goto event_logs +if "!toolbox_choice!"=="7" goto ssd_trim +if "!toolbox_choice!"=="8" exit goto main_menu :cleanup @@ -49,6 +57,9 @@ echo All operations are being logged to: echo !LOGFILE! echo =============================================== echo. +echo Calculating current free space... +for /f "usebackq" %%a in (`powershell -Command "[math]::Round((Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='%SYSTEMDRIVE%'\").FreeSpace / 1MB)"`) do set "free_before_mb=%%a" + echo [1/4] Stopping background services... echo [1/4] Stopping background services... >> "!LOGFILE!" @@ -104,10 +115,6 @@ echo. echo [4/4] Finalizing optimizations... echo [4/4] Finalizing optimizations... >> "!LOGFILE!" -echo - Flushing DNS Resolver Cache... -echo - Flushing DNS Resolver Cache... >> "!LOGFILE!" -ipconfig /flushdns >> "!LOGFILE!" 2>&1 - echo - Starting Windows Update (wuauserv)... echo - Starting Windows Update (wuauserv)... >> "!LOGFILE!" net start wuauserv >> "!LOGFILE!" 2>&1 @@ -116,9 +123,118 @@ echo - Starting Background Intelligent Transfer Service (bits)... echo - Starting Background Intelligent Transfer Service (bits)... >> "!LOGFILE!" net start bits >> "!LOGFILE!" 2>&1 +for /f "usebackq" %%a in (`powershell -Command "[math]::Round((Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='%SYSTEMDRIVE%'\").FreeSpace / 1MB)"`) do set "free_after_mb=%%a" +set /a "space_saved_mb=free_after_mb - free_before_mb" +if !space_saved_mb! LSS 0 set "space_saved_mb=0" + echo. echo SYSTEM CLEAN UP COMPLETE! echo SYSTEM CLEAN UP COMPLETE! >> "!LOGFILE!" +echo -^> Total Space Freed: !space_saved_mb! MB +echo -^> Total Space Freed: !space_saved_mb! MB >> "!LOGFILE!" +pause +goto main_menu + +:sys_repair +cls +echo =============================================== +echo SYSTEM INTEGRITY REPAIR (SFC + DISM) +echo =============================================== +echo WARNING: This process can take 15-45 minutes. +echo -^> It CAN be safely interrupted by closing the window. +echo -^> However, it is recommended to let it finish. +echo =============================================== +set /p confirm="Do you want to proceed? (Y/N): " +if /i "!confirm!" NEQ "Y" goto main_menu + +echo. +echo [1/2] Running System File Checker (SFC)... +echo Running SFC >> "!LOGFILE!" +sfc /scannow >> "!LOGFILE!" 2>&1 + +echo [2/2] Running DISM RestoreHealth... +echo Running DISM RestoreHealth >> "!LOGFILE!" +DISM /Online /Cleanup-Image /RestoreHealth >> "!LOGFILE!" 2>&1 + +echo. +echo SYSTEM INTEGRITY REPAIR COMPLETE! +echo SYSTEM INTEGRITY REPAIR COMPLETE! >> "!LOGFILE!" +pause +goto main_menu + +:win_sxs +cls +echo =============================================== +echo WINDOWS COMPONENT STORE CLEANUP (WinSxS) +echo =============================================== +echo WARNING: This deeply cleans old Windows Update files. +echo -^> It can take 10-30 minutes and may appear stuck. +echo -^> DO NOT interrupt this process (can corrupt updates). +echo =============================================== +set /p confirm="Do you want to proceed? (Y/N): " +if /i "!confirm!" NEQ "Y" goto main_menu + +echo. +echo Cleaning Windows Component Store... +echo Running WinSxS Cleanup >> "!LOGFILE!" +DISM.exe /Online /Cleanup-Image /StartComponentCleanup >> "!LOGFILE!" 2>&1 + +echo. +echo WINSXS CLEANUP COMPLETE! +echo WINSXS CLEANUP COMPLETE! >> "!LOGFILE!" +pause +goto main_menu + +:app_update +cls +echo =============================================== +echo UPDATE INSTALLED APPS (WINGET) +echo =============================================== +echo WARNING: Silently updates all apps installed via Winget. +echo -^> May take several minutes. +echo -^> It CAN be safely interrupted. +echo =============================================== +set /p confirm="Do you want to proceed? (Y/N): " +if /i "!confirm!" NEQ "Y" goto main_menu + +echo. +echo Upgrading all installed applications (this may take a while)... +echo Running Winget Upgrade All >> "!LOGFILE!" +winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements >> "!LOGFILE!" 2>&1 + +echo. +echo APP UPDATE COMPLETE! +echo APP UPDATE COMPLETE! >> "!LOGFILE!" +pause +goto main_menu + +:net_reset +cls +echo =============================================== +echo COMPLETE NETWORK RESET +echo =============================================== +echo This will reset your network adapters to factory defaults. +echo -^> A system restart will be required afterward. +echo =============================================== +set /p confirm="Do you want to proceed? (Y/N): " +if /i "!confirm!" NEQ "Y" goto main_menu + +echo. +echo Resetting Winsock... +echo Resetting Winsock >> "!LOGFILE!" +netsh winsock reset >> "!LOGFILE!" 2>&1 + +echo Resetting TCP/IP... +echo Resetting TCP/IP >> "!LOGFILE!" +netsh int ip reset >> "!LOGFILE!" 2>&1 + +echo Flushing DNS... +echo Flushing DNS >> "!LOGFILE!" +ipconfig /flushdns >> "!LOGFILE!" 2>&1 + +echo. +echo NETWORK RESET COMPLETE! Please RESTART your computer. +echo NETWORK RESET COMPLETE! >> "!LOGFILE!" pause goto main_menu @@ -139,8 +255,8 @@ for /F "tokens=*" %%G in ('wevtutil.exe el') DO ( wevtutil.exe cl "%%G" >> "!LOGFILE!" 2>&1 ) echo. -echo All Event Logs have been cleared! -echo All Event Logs have been cleared! >> "!LOGFILE!" +echo EVENT LOGS CLEARED! +echo EVENT LOGS CLEARED! >> "!LOGFILE!" pause goto main_menu @@ -175,8 +291,8 @@ del /q "%TEMP%\defrag_out.txt" >nul 2>&1 echo. echo ----------------------------------------------- -echo Done. -echo Done. >> "!LOGFILE!" +echo SSD TRIM COMPLETE! +echo SSD TRIM COMPLETE! >> "!LOGFILE!" echo [1] Return to Menu echo [2] Exit set /p final="Choose an option: " diff --git a/PROMPT_GUIDE.md b/PROMPT_GUIDE.md index a0c2bbc..3c077ff 100644 --- a/PROMPT_GUIDE.md +++ b/PROMPT_GUIDE.md @@ -8,36 +8,24 @@ The script automatically checks for Administrator privileges. - **If prompted by UAC:** Click "Yes" to allow the tool to perform system-level optimizations. - **Log Generation:** Every run creates a timestamped log file (`LDLWinToolBox_yyMMddHHmmss.log`) in the same folder as the script to record all detailed operations. -- **Main Menu:** Use the numeric keys `1-4` to select your desired operation. +- **Main Menu:** Use the numeric keys `1-8` to select your desired operation. -## 2. Advanced System Cleanup (Option 1) +## 2. Main Features -This module performs a deep clean of temporary system data. +- **[1] Advanced System Cleanup:** Deep cleans temporary system data. Visualizes current folder processing and calculates total Space Freed (MB) at completion. +- **[2] System Integrity Repair (SFC + DISM):** Scans the OS for corrupted files and repairs them from the Windows cache. Will warn users before running (takes 15-45mins, can be safely interrupted). +- **[3] Windows Component Store Cleanup (WinSxS):** Removes old Windows Update install files. Will heavily warn users NOT to interrupt this process as it may corrupt future updates. +- **[4] Update All Installed Apps (Winget):** Uses Windows Package Manager to blindly update installed software automatically. +- **[5] Complete Network Reset:** Resets Winsock, TCP/IP, and DNS cache. Requires a system restart when finished. +- **[6] Clear Event Viewer Logs:** Clears all system, security, and application logs into a clean state. +- **[7] Manual SSD TRIM:** Queries your NVMe volumes via PowerShell and runs manual garbage collection on them. Type only the drive letter (e.g., `C`) when prompted. -- **What happens:** The tool stops the Windows Update and BITS services to unlock protected folders. It will visibly display a friendly message indicating which folder it is currently cleaning. -- **User Action:** Once selected, the process is automated. Wait for the "SYSTEM CLEAN UP COMPLETE!" message before pressing any key to return to the menu. You can review the log file later for detailed deletion records. +## 3. General User Actions -## 3. Clear Event Viewer Logs (Option 2) +- **Confirmations:** Whenever a tool mentions it will take a long time, type `Y` to continue or any other key to abort and return to the menu. +- **Exiting the Tool:** Select Option `8` from the main menu or Option `2` from the TRIM sub-menu to safely close the application. -Clears all system, security, and application logs. - -- **User Action:** The script will list each log as it is cleared cleanly. When finished, press any key to return to the main menu. Raw verbose logs are pushed to the text file. - -## 4. Manual SSD TRIM (Option 3) - -Designed specifically for high-performance NVMe drives like the Kingston KC3000. - -- **Step 1:** Review the "Current Drives Connected" list generated by the script. -- **Step 2:** When prompted, type only the drive letter (e.g., `C`) and press Enter. -- **Step 3:** The script will automatically strip colons or spaces if you accidentally include them. -- **Step 4:** Review the optimization results printed dynamically onto the screen (and into the log). -- **Step 5:** Choose `1` to return to the menu or `2` to exit the tool. - -## 5. Exiting the Tool - -Select Option `4` from the main menu or Option `2` from the TRIM sub-menu to safely close the application. - -## 6. Rules for Better Prompts (Future Development) +## 4. Rules for Better Prompts (Future Development) When interacting with AI to update or expand this toolbox, utilize the following rules to ensure quality and consistency: @@ -45,4 +33,5 @@ When interacting with AI to update or expand this toolbox, utilize the following 2. **Reiterate Privilege Requirements:** Remind the AI that the tool operates with auto-requested Administrator privileges so it can formulate commands confidently. 3. **Preserve Documentation:** Explicitly state: "Keep all previous analysis and history intact. Only append your updates to `ANALYSIS.md` and `PROMPT_GUIDE.md`." 4. **Demand Input Sanitization:** Instruct the AI to incorporate proper variable trimming and sanitization for any newly added menus requiring user input. -5. **Enforce Clean Verbosity:** Demand that scripts echo clear, friendly summaries to the console, while routing "scary" raw verbose output (like massive lists of deleted files) smoothly into the `!LOGFILE!`. +5. **Enforce Clean Verbosity:** Demand that scripts echo clear, friendly summaries to the console, while routing "scary" raw verbose output smoothly into the `!LOGFILE!`. +6. **Require Process Warnings:** Insist that the AI implements safety checks `(Y/N)` explicitly stating if long-running processes are safe to abandon/interrupt via window closing. From 23ca5e40360d1a7879f5ca116e81d2c6f2b2bcf6 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Fri, 20 Feb 2026 22:51:48 +0800 Subject: [PATCH 05/33] Create logo.png --- images/logo.png | Bin 0 -> 410598 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/logo.png diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b3c37ab8e3477e871b6ea928bd9a032d206e2ad1 GIT binary patch literal 410598 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 literal 0 HcmV?d00001 From 4d87b9ff58bf3b381a6b5d8caf079e0f6083c2fa Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Fri, 20 Feb 2026 22:51:50 +0800 Subject: [PATCH 06/33] Create README.md --- README.md | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6d5964 --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ + + + + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![Apache License 2.0][license-shield]][license-url] + + +
+
+ + Logo + + +

LDL Windows ToolBox

+ +

+ A cohesive, menu-driven Windows Batch utility that safely automates advanced system cleanup, integrity repair, components update, and NVMe SSD optimizations. +
+ Explore the docs » +
+
+ View Demo + · + Report Bug + · + Request Feature +

+
+ + +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. Usage
  6. +
  7. Contributing
  8. +
  9. License
  10. +
  11. Contact
  12. +
  13. Acknowledgments
  14. +
+
+ + + +## About The Project + +The LDL Windows ToolBox is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, script verifications, and SSD TRIM optimization into a single, cohesive menu-driven interface. + +It safely automates otherwise tedious system administration tasks while maintaining comprehensive, timestamped logs (`LDLWinToolBox_yyMMddHHmmss.log`) of all actions to ensure complete historical records and safety. + +

(back to top)

+ +### Built With + +- [![Windows Batch][Batch-shield]][Batch-url] +- [![PowerShell][PowerShell-shield]][PowerShell-url] + +

(back to top)

+ + + +## Getting Started + +To get a local copy up and running follow these simple steps. + +### Prerequisites + +- Windows 10 or Windows 11 +- Administrator rights (the script will automatically securely request this using `RunAs` if launched without it) + +### Installation + +1. Clone the repo + ```sh + git clone https://github.com/LoveDoLove/LDLWinToolBox.git + ``` +2. Double-click on `LDLWinToolBox.bat` to launch the interactive menu. + +

(back to top)

+ + + +## Usage + +Upon launching, the interactive menu provides numerical options (1-8) to execute tools: + +- **[1] Advanced System Cleanup**: Deeply cleans temporary system data, calculates Space Freed (MB). +- **[2] System Integrity Repair**: Executes `SFC` and `DISM` to scan and repair corrupt OS files. +- **[3] Windows Component Store Cleanup**: Removes superseded Windows Update install files (WinSxS). +- **[4] Update All Installed Apps**: Silently updates all `winget`-installed apps. +- **[5] Complete Network Reset**: Resets Winsock, TCP/IP, and DNS cache entirely. +- **[6] Clear Event Viewer Logs**: Flushes system, security, and application logs. +- **[7] Manual SSD TRIM**: Optimized for NVMe drives, triggers manual SSD re-trim using Windows defrag. + +_For more detailed background checks on each process, please refer to [ANALYSIS.md](ANALYSIS.md) and [PROMPT_GUIDE.md](PROMPT_GUIDE.md)_ + +

(back to top)

+ +See the [open issues](https://github.com/LoveDoLove/LDLWinToolBox/issues) for a full list of proposed features (and known issues). + +

(back to top)

+ + + +## Contributing + +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". +Don't forget to give the project a star! Thanks again! + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +

(back to top)

+ +### Top contributors: + + + contrib.rocks image + + + + +## License + +Distributed under the Apache License 2.0. See `LICENSE` for more information. + +

(back to top)

+ + + +## Contact + +LoveDoLove - [Telegram Channel](https://t.me/lovedoloveofficialchannel) - [Discord](https://discord.com/invite/FyYEmtRCRE) + +Project Link: [https://github.com/LoveDoLove/LDLWinToolBox](https://github.com/LoveDoLove/LDLWinToolBox) + +

(back to top)

+ + + +## Acknowledgments + +- [Best-README-Template](https://github.com/othneildrew/Best-README-Template) +- [RunAs PowerShell Module](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/start-process?view=powershell-7.2) +- [Winget Tool](https://docs.microsoft.com/en-us/windows/package-manager/winget/) + +

(back to top)

+ + + + +[contributors-shield]: https://img.shields.io/github/contributors/LoveDoLove/LDLWinToolBox.svg?style=for-the-badge +[contributors-url]: https://github.com/LoveDoLove/LDLWinToolBox/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/LoveDoLove/LDLWinToolBox.svg?style=for-the-badge +[forks-url]: https://github.com/LoveDoLove/LDLWinToolBox/network/members +[stars-shield]: https://img.shields.io/github/stars/LoveDoLove/LDLWinToolBox.svg?style=for-the-badge +[stars-url]: https://github.com/LoveDoLove/LDLWinToolBox/stargazers +[issues-shield]: https://img.shields.io/github/issues/LoveDoLove/LDLWinToolBox.svg?style=for-the-badge +[issues-url]: https://github.com/LoveDoLove/LDLWinToolBox/issues +[license-shield]: https://img.shields.io/github/license/LoveDoLove/LDLWinToolBox.svg?style=for-the-badge +[license-url]: https://github.com/LoveDoLove/LDLWinToolBox/blob/master/LICENSE +[Batch-shield]: https://img.shields.io/badge/Windows_Batch-0078D6?style=for-the-badge&logo=windows&logoColor=white +[Batch-url]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands +[PowerShell-shield]: https://img.shields.io/badge/PowerShell-5391FE?style=for-the-badge&logo=powershell&logoColor=white +[PowerShell-url]: https://docs.microsoft.com/en-us/powershell/ From 61100eed2cb88e2d7b1842a47118eda7c0c50cf8 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 7 Jun 2026 16:55:26 +0800 Subject: [PATCH 07/33] Init AGENT and MEMORY --- AGENTS.md | 74 ++++++++++++++++++++++++++++++++++++++++++ MEMORY.md | 77 ++++++++++++++++++++++++++++++++++++++++++++ memory/2026-06-07.md | 16 +++++++++ memory/tasks.md | 16 +++++++++ 4 files changed, 183 insertions(+) create mode 100644 AGENTS.md create mode 100644 MEMORY.md create mode 100644 memory/2026-06-07.md create mode 100644 memory/tasks.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fb22d8f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,74 @@ +@C:\Users\LoveDoLove\.codex\RTK.md + +# AGENTS.md - LDLWinToolBox + +## Agent Role + +You are the AI maintainer for `LDLWinToolBox`, a standalone Windows Batch utility for administrative cleanup, repair, update, network reset, log clearing, and SSD TRIM workflows. + +Work from repository facts first. Preserve existing history and project decisions unless the user explicitly asks to replace them. + +## Startup Order + +On every new session: + +1. Read `AGENTS.md` and `MEMORY.md`. +2. Analyze the project source and repository structure first, especially `LDLWinToolBox.bat`, `README.md`, `.github/`, and tracked metadata. +3. Only after project analysis, analyze prompt/history files: `ANALYSIS.md`, `PROMPT_GUIDE.md`, `memory/tasks.md`, and `memory/YYYY-MM-DD.md` if present. +4. Continue from the stored rules and tasks while keeping prior history intact. +5. When updating history, append dated notes or update status clearly; do not delete older decisions unless the user explicitly requests cleanup. + +## Command Rules + +- Follow `C:\Users\LoveDoLove\.codex\RTK.md`: prefix shell commands with `rtk`. +- Prefer Windows BAT/Command standard commands through `rtk cmd /c ...`. +- Project implementation must remain centered on `.bat` and standard Windows commands. +- Use PowerShell only as a narrow one-line bridge where native Batch lacks the required Windows capability, matching current patterns such as UAC `RunAs`, timestamp generation, disk free-space queries, or volume enumeration. +- Avoid destructive commands during development unless they are scoped, reviewed, and explicitly requested. + +## Project Rules + +- Main executable: `LDLWinToolBox.bat`. +- Keep the app menu-driven and suitable for Windows 10/11. +- The script must auto-check Administrator permission and auto-request elevation with UAC before system-level operations. +- Preserve timestamped logging with `LDLWinToolBox_yyMMddHHmmss.log`. +- Console output should stay concise and user-readable; raw command output should go to `!LOGFILE!`. +- Long-running or risky operations must warn the user, explain interrupt safety, and ask for `(Y/N)` confirmation. +- Sanitize user input for every new menu feature that accepts values. +- Keep existing documentation and analysis history in `ANALYSIS.md` and `PROMPT_GUIDE.md`; append updates instead of replacing historical context. + +## Current Implemented Features + +Current `LDLWinToolBox.bat` menu implementation: + +1. Advanced System Cleanup with space calculator. +2. System Integrity Repair using `sfc /scannow` and `DISM /RestoreHealth`. +3. Windows Component Store Cleanup using `DISM /StartComponentCleanup`. +4. Update all installed apps using `winget upgrade --all`. +5. Complete Network Reset using Winsock, TCP/IP reset, and DNS flush. +6. Clear Event Viewer Logs using `wevtutil`. +7. Manual SSD TRIM using `defrag /L /V`. +8. Exit. + +Declared future or user-requested feature targets: + +- Disable BitLocker `[Plan]`. +- Kill Browser AI using the user-specified command: + `iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex` + +Remote script execution is high risk. Do not run this command during development. If it is implemented as a menu feature, add an explicit warning and confirmation before execution. + +## Skill Rules + +- Before coding, reviewing, or refactoring, check for applicable local skills under `.agents/skills/`. +- Repository-local skill packages must be cloned from public GitHub open-source skills. Do not hand-write custom skill packages in this repo. +- For every installed repo-local skill, preserve upstream provenance: source URL, commit or tag, and license. +- Current scan on 2026-06-07 found no tracked `.agents/skills/` directory in this repository. +- The session-level `karpathy-guidelines` skill exists outside this repo and may be used for disciplined coding behavior, but it is not currently a repo-local cloned skill asset. + +## Memory Files + +- `MEMORY.md`: long-term user preferences, project goals, stable constraints, and project snapshot. +- `memory/tasks.md`: cross-session todo and progress. +- `memory/YYYY-MM-DD.md`: daily AI work log, decisions, and notes. + diff --git a/MEMORY.md b/MEMORY.md new file mode 100644 index 0000000..744e027 --- /dev/null +++ b/MEMORY.md @@ -0,0 +1,77 @@ +# MEMORY.md + +Last updated: 2026-06-07 + +## User Preferences + +- User prefers Chinese-language collaboration when discussing work, while repository documentation may remain English if that matches the existing files. +- Always analyze the current project first, then analyze prompt/history files, then apply future rules while preserving history. +- Use Windows BAT/Command standard commands for this project. +- Follow RTK command discipline from `C:\Users\LoveDoLove\.codex\RTK.md`; in this PowerShell environment, use `rtk cmd /c ...` for standard Windows commands. +- Keep changes surgical and verifiable. Do not refactor unrelated code. +- Skill packages under `.agents/skills/` must be cloned from public GitHub open-source skills, not authored manually in this repository. + +## Project Snapshot + +- App name: `LDLWinToolBox` +- Repository path: `D:\Projects\WinProjects\LDLWinToolBox` +- Git remote: `https://github.com/LoveDoLove/LDLWinToolBox.git` +- Current branch at scan time: `lovedolove` +- License: Apache License 2.0 +- Primary executable: `LDLWinToolBox.bat` +- Primary docs: `README.md`, `ANALYSIS.md`, `PROMPT_GUIDE.md` +- GitHub metadata: `.github/FUNDING.yml`, `.github/ISSUE_TEMPLATE/bug-report---.md`, `.github/ISSUE_TEMPLATE/feature-request---.md` +- Asset: `images/logo.png` +- Ignored local template observed: `BLANK_README.md` + +## Current Repository Logic + +`LDLWinToolBox.bat` is a standalone menu-driven Windows Batch script. It initializes delayed expansion, checks for Administrator access, relaunches with UAC through PowerShell `Start-Process -Verb RunAs` when needed, switches to the script directory, and creates a timestamped log file named `LDLWinToolBox_yyMMddHHmmss.log`. + +Implemented menu behavior: + +1. Advanced System Cleanup: calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, Event Viewer log files, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. +2. System Integrity Repair: asks confirmation, runs `sfc /scannow`, then `DISM /Online /Cleanup-Image /RestoreHealth`. +3. Windows Component Store Cleanup: asks confirmation, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup`. +4. Update All Installed Apps: asks confirmation, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements`. +5. Complete Network Reset: asks confirmation, runs `netsh winsock reset`, `netsh int ip reset`, and `ipconfig /flushdns`; tells user to restart. +6. Clear Event Viewer Logs: enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. +7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes the entered drive letter, runs `defrag : /L /V`, displays output, and appends it to the log. +8. Exit: closes the tool. + +## Declared Feature Targets + +The user listed these as available or planned feature targets, but the current scanned `LDLWinToolBox.bat` implementation does not yet expose them as tools: + +- Disable BitLocker `[Plan]`. +- Kill Browser AI using: + `iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex` + +Treat the remote `iwr | iex` command as high risk. Do not execute it during analysis. If implemented later, add a clear warning, confirmation prompt, and logging. + +## Documentation And Prompt Files + +- `README.md` describes the app, prerequisites, installation, usage, license, and contact info. +- `ANALYSIS.md` records technical analysis and future prompt rules. +- `PROMPT_GUIDE.md` explains how users should launch, select menu options, and prompt AI for future changes. +- Existing prompt rules emphasize Batch standard, auto-admin preservation, history preservation, input sanitization, clean verbosity, and long-running process warnings. + +## Known Gaps And Risks + +- Current README says options `1-8` execute tools, but option `8` is currently `Exit`. +- User-declared feature targets `Disable BitLocker` and `Kill Browser AI` are not implemented in the scanned script. +- The current admin-check path in `LDLWinToolBox.bat` should be reviewed before future releases because the line includes `system32%` in the protected path. +- Cleanup deletes Event Viewer log files directly and option 6 also clears logs through `wevtutil`; future changes should keep this behavior intentional and clearly documented. +- `BLANK_README.md` is present locally but ignored by git and appears to be an unused Best-README-Template source file. +- No tracked `.agents/skills/` directory exists at the 2026-06-07 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. + +## Persistent Working Rules + +- Preserve the app as a single-file Windows Batch tool unless the user explicitly asks for a different architecture. +- Prefer native Windows commands and Batch syntax for implementation. +- Keep PowerShell calls minimal, one-line, and justified by Windows capability gaps. +- Preserve auto-admin behavior and timestamped logs. +- Keep console messages readable and route verbose command output into the log. +- Add `(Y/N)` confirmation for long-running, destructive, privacy-affecting, or remote-execution operations. +- Keep prompt/history updates append-friendly and date-stamped. + diff --git a/memory/2026-06-07.md b/memory/2026-06-07.md new file mode 100644 index 0000000..002fded --- /dev/null +++ b/memory/2026-06-07.md @@ -0,0 +1,16 @@ +# 2026-06-07 + +## Work Log + +- Read external RTK rule file from `C:\Users\LoveDoLove\.codex\RTK.md`; it requires shell commands to be prefixed with `rtk`. +- Verified `rtk` exists, then used `rtk cmd /c ...` for standard Windows command access. +- Scanned tracked project files, local ignored template file, GitHub issue templates, funding metadata, branch, remote, and recent commits. +- Found that `AGENTS.md`, `MEMORY.md`, `memory/`, and `.agents/skills/` were not present in the repository before this update. +- Created the memory mechanism files and recorded current implementation details, user rules, known gaps, and future tasks. + +## Decisions + +- Did not create `.agents/skills/` because the user requires repo-local skills to be cloned from public GitHub open-source skills, not hand-written. +- Did not modify `LDLWinToolBox.bat`; the request was to update AI memory and agent instructions after scanning. +- Recorded `Disable BitLocker [Plan]` and `Kill Browser AI` as declared feature targets because they are not implemented in the scanned menu. + diff --git a/memory/tasks.md b/memory/tasks.md new file mode 100644 index 0000000..d343a10 --- /dev/null +++ b/memory/tasks.md @@ -0,0 +1,16 @@ +# memory/tasks.md + +Last updated: 2026-06-07 + +## Pending + +- [ ] Verify and, if needed, fix the admin privilege check path in `LDLWinToolBox.bat` because the scanned line contains `system32%`. +- [ ] Decide exact behavior and safety prompts for `Disable BitLocker [Plan]`. +- [ ] Decide whether to implement `Kill Browser AI`; if yes, review the remote script source and add explicit confirmation before any `iwr | iex` execution. +- [ ] If repo-local skills are needed, clone public GitHub open-source skills into `.agents/skills//` and record URL, commit or tag, and license. + +## Completed + +- [x] 2026-06-07: Scanned current repository logic, docs, prompt files, Git metadata, and issue templates. +- [x] 2026-06-07: Created `AGENTS.md`, `MEMORY.md`, `memory/tasks.md`, and daily work log to restore AI identity and project state in future sessions. + From d1917471d8cfa82ba43528a88dc2462b53ccde13 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 7 Jun 2026 16:55:38 +0800 Subject: [PATCH 08/33] REMOVE OLD --- ANALYSIS.md | 52 ------------------------------------------------- PROMPT_GUIDE.md | 37 ----------------------------------- 2 files changed, 89 deletions(-) delete mode 100644 ANALYSIS.md delete mode 100644 PROMPT_GUIDE.md diff --git a/ANALYSIS.md b/ANALYSIS.md deleted file mode 100644 index 20596b4..0000000 --- a/ANALYSIS.md +++ /dev/null @@ -1,52 +0,0 @@ -# Technical Analysis: LDL Windows ToolBox - -## 1. Privilege Elevation - -The script utilizes a dual-layer check for administrative rights. It first attempts to access a protected system directory using `cacls.exe`. If access is denied, it leverages a PowerShell one-liner to re-launch the batch file with the `RunAs` verb, ensuring the user is prompted for the necessary permissions to execute system-level commands like `net stop` and `defrag`. - -## 2. Cleanup Methodology - -The "Advanced System Cleanup" module is more thorough than standard disk cleanup tools: - -- **Service Management:** By stopping `wuauserv` and `bits`, the script can target the `%WinDir%\SoftwareDistribution\Download` folder, which often contains large amounts of stale update data. -- **Directory Reconstruction:** Instead of merely deleting files, the script uses a loop to remove and then recreate vital temporary directories (`rd` followed by `md`). This ensures that any corrupted directory structures are refreshed. -- **Space Saved Calculation:** Uses PowerShell WMI/CIM calls to parse `Win32_LogicalDisk` free space in MB before and after cleanup to determine exact megabytes cleaned dynamically, providing valuable user feedback. - -## 3. Extended Administrative Tools - -- **System Integrity Repair:** Uses `sfc /scannow` and `DISM /RestoreHealth` combined for deep system repair. It properly warns users about the extended duration of these tasks and guarantees an abort mechanism before proceeding. -- **Component Store Cleanup:** Utilizes `DISM /StartComponentCleanup` to clear old Windows Update caches safely. Users are explicitly warned NOT to interrupt this potentially dangerous process to prevent OS corruption. -- **Application Updater:** Employs the native Windows Package Manager (`winget`) with headless flags (`--accept-package-agreements`, `--accept-source-agreements`) to silently upgrade software, logging standard output cleanly. -- **Network Reset:** Leverages `netsh winsock reset` and `netsh int ip reset` along with DNS flushing to reset the full network stack, returning network interfaces to default states. - -## 4. Log Management & Verbosity Control - -The script implements a dynamic verbose logging mechanism. Upon execution, it generates a timestamped log file (`LDLWinToolBox_yyMMddHHmmss.log`). - -- **User Experience (UX):** It displays clear, high-level, human-readable operations on the console (e.g., "Cleaning \Windows\Temp"), providing transparency without overwhelming the user with "scary" massive walls of file paths. -- **Detailed Auditing:** The actual verbose output of all underlying commands (`del`, `rd`, `wevtutil`, `defrag`) is redirected and appended to the log file via `>> "!LOGFILE!" 2>&1`, ensuring complete historical records for troubleshooting. -- **Event Viewer Logs:** Utilizes `wevtutil.exe` to enumerate and clear every individual log provider registered in Windows. - -## 5. Storage Optimization (SSD TRIM) - -The TRIM module is optimized for NVMe architecture: - -- **Discovery:** Uses the modern PowerShell `Get-Volume` cmdlet to provide accurate drive letters and sizes. -- **Optimization Strategy:** Executes `defrag /L`, which sends a re-trim hint to the SSD controller. -- **Hardware Benefits:** For devices like the Kingston KC3000, this triggers the Phison controller to perform internal garbage collection. -- **Input Sanitization:** The module includes logic to clean user input (removing colons/spaces), preventing command execution errors. - -## 6. Project & Prompt Analysis - -The LDLWinToolBox project is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, script verifications, and SSD TRIM optimization into a single, cohesive menu-driven interface. - -## 7. Rules to Apply for Next Time (Future Prompts) - -To ensure continuous improvement and maintain the integrity of the project during future prompting, apply the following rules: - -1. **Maintain Batch Standard:** Any new features or module additions must strictly use standard Windows Batch (`.bat`) commands. Utilize PowerShell one-liners only when native DOS commands lack the necessary functionality. -2. **Preserve Auto-Admin:** Do not modify the existing dual-layer UAC elevation logic at the beginning of the script. -3. **Keep History Intact:** When requesting updates or new features, strictly mandate that all existing historical analysis and documentation in `ANALYSIS.md` and `PROMPT_GUIDE.md` be preserved. -4. **Safety First Methodology:** Any destructive or system-altering commands must be scoped precisely to avoid cluttering the CLI output. -5. **Enforce Clean Verbosity:** Echo clear, friendly summaries to the console, while routing raw verbose output into the `!LOGFILE!`. -6. **Long-Running Process Handling:** Any command that blocks the main thread for over a minute must explicitly warn the user beforehand, explain whether it is safe to manually interrupt by closing the window, and provide a (Y/N) confirmation exit hatch. diff --git a/PROMPT_GUIDE.md b/PROMPT_GUIDE.md deleted file mode 100644 index 3c077ff..0000000 --- a/PROMPT_GUIDE.md +++ /dev/null @@ -1,37 +0,0 @@ -# LDL Windows ToolBox - User Prompt Guide - -This guide explains how to navigate and use the different modules within the `LDLWinToolBox.bat` script. - -## 1. Launching the Tool - -The script automatically checks for Administrator privileges. - -- **If prompted by UAC:** Click "Yes" to allow the tool to perform system-level optimizations. -- **Log Generation:** Every run creates a timestamped log file (`LDLWinToolBox_yyMMddHHmmss.log`) in the same folder as the script to record all detailed operations. -- **Main Menu:** Use the numeric keys `1-8` to select your desired operation. - -## 2. Main Features - -- **[1] Advanced System Cleanup:** Deep cleans temporary system data. Visualizes current folder processing and calculates total Space Freed (MB) at completion. -- **[2] System Integrity Repair (SFC + DISM):** Scans the OS for corrupted files and repairs them from the Windows cache. Will warn users before running (takes 15-45mins, can be safely interrupted). -- **[3] Windows Component Store Cleanup (WinSxS):** Removes old Windows Update install files. Will heavily warn users NOT to interrupt this process as it may corrupt future updates. -- **[4] Update All Installed Apps (Winget):** Uses Windows Package Manager to blindly update installed software automatically. -- **[5] Complete Network Reset:** Resets Winsock, TCP/IP, and DNS cache. Requires a system restart when finished. -- **[6] Clear Event Viewer Logs:** Clears all system, security, and application logs into a clean state. -- **[7] Manual SSD TRIM:** Queries your NVMe volumes via PowerShell and runs manual garbage collection on them. Type only the drive letter (e.g., `C`) when prompted. - -## 3. General User Actions - -- **Confirmations:** Whenever a tool mentions it will take a long time, type `Y` to continue or any other key to abort and return to the menu. -- **Exiting the Tool:** Select Option `8` from the main menu or Option `2` from the TRIM sub-menu to safely close the application. - -## 4. Rules for Better Prompts (Future Development) - -When interacting with AI to update or expand this toolbox, utilize the following rules to ensure quality and consistency: - -1. **Specify Standard Windows Commands:** Always request that updates are written using native `.bat` syntax. -2. **Reiterate Privilege Requirements:** Remind the AI that the tool operates with auto-requested Administrator privileges so it can formulate commands confidently. -3. **Preserve Documentation:** Explicitly state: "Keep all previous analysis and history intact. Only append your updates to `ANALYSIS.md` and `PROMPT_GUIDE.md`." -4. **Demand Input Sanitization:** Instruct the AI to incorporate proper variable trimming and sanitization for any newly added menus requiring user input. -5. **Enforce Clean Verbosity:** Demand that scripts echo clear, friendly summaries to the console, while routing "scary" raw verbose output smoothly into the `!LOGFILE!`. -6. **Require Process Warnings:** Insist that the AI implements safety checks `(Y/N)` explicitly stating if long-running processes are safe to abandon/interrupt via window closing. From c8fe4757073d420c21cdc3a1e7ccbb94184ac542 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 7 Jun 2026 17:36:25 +0800 Subject: [PATCH 09/33] Complete toolbox safety features --- AGENTS.md | 14 ++-- LDLWinToolBox.bat | 167 +++++++++++++++++++++++++++++++++++++++---- MEMORY.md | 31 ++++---- README.md | 11 +-- memory/2026-06-07.md | 9 +++ memory/tasks.md | 10 +-- 6 files changed, 198 insertions(+), 44 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fb22d8f..767c227 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ On every new session: 1. Read `AGENTS.md` and `MEMORY.md`. 2. Analyze the project source and repository structure first, especially `LDLWinToolBox.bat`, `README.md`, `.github/`, and tracked metadata. -3. Only after project analysis, analyze prompt/history files: `ANALYSIS.md`, `PROMPT_GUIDE.md`, `memory/tasks.md`, and `memory/YYYY-MM-DD.md` if present. +3. Only after project analysis, analyze prompt/history files if present: `ANALYSIS.md`, `PROMPT_GUIDE.md`, `memory/tasks.md`, and `memory/YYYY-MM-DD.md`. 4. Continue from the stored rules and tasks while keeping prior history intact. 5. When updating history, append dated notes or update status clearly; do not delete older decisions unless the user explicitly requests cleanup. @@ -35,7 +35,7 @@ On every new session: - Console output should stay concise and user-readable; raw command output should go to `!LOGFILE!`. - Long-running or risky operations must warn the user, explain interrupt safety, and ask for `(Y/N)` confirmation. - Sanitize user input for every new menu feature that accepts values. -- Keep existing documentation and analysis history in `ANALYSIS.md` and `PROMPT_GUIDE.md`; append updates instead of replacing historical context. +- Keep existing documentation and analysis history intact. If `ANALYSIS.md` or `PROMPT_GUIDE.md` exists, append updates instead of replacing historical context. ## Current Implemented Features @@ -48,13 +48,10 @@ Current `LDLWinToolBox.bat` menu implementation: 5. Complete Network Reset using Winsock, TCP/IP reset, and DNS flush. 6. Clear Event Viewer Logs using `wevtutil`. 7. Manual SSD TRIM using `defrag /L /V`. -8. Exit. - -Declared future or user-requested feature targets: - -- Disable BitLocker `[Plan]`. -- Kill Browser AI using the user-specified command: +8. Disable BitLocker `(Plan)` using `manage-bde -status` and guarded `manage-bde -off :`. +9. Kill Browser AI using the user-specified command: `iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex` +10. Exit. Remote script execution is high risk. Do not run this command during development. If it is implemented as a menu feature, add an explicit warning and confirmation before execution. @@ -71,4 +68,3 @@ Remote script execution is high risk. Do not run this command during development - `MEMORY.md`: long-term user preferences, project goals, stable constraints, and project snapshot. - `memory/tasks.md`: cross-session todo and progress. - `memory/YYYY-MM-DD.md`: daily AI work log, decisions, and notes. - diff --git a/LDLWinToolBox.bat b/LDLWinToolBox.bat index 644263f..52d3bf8 100644 --- a/LDLWinToolBox.bat +++ b/LDLWinToolBox.bat @@ -2,10 +2,10 @@ setlocal EnableDelayedExpansion :: --- AUTO ADMIN REQUEST --- ->nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32%\config\system" -if '%errorlevel%' NEQ '0' ( +fltmc >nul 2>&1 +if errorlevel 1 ( echo Requesting administrative privileges... - powershell -Command "Start-Process -FilePath '%0' -Verb RunAs" + powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" exit /B ) pushd "%CD%" @@ -34,7 +34,9 @@ echo [4] Update All Installed Apps (Winget) echo [5] Complete Network Reset echo [6] Clear Event Viewer Logs echo [7] Manual SSD TRIM (Optimized for KC3000) -echo [8] Exit +echo [8] Disable BitLocker (Plan) +echo [9] Kill Browser AI +echo [10] Exit echo =============================================== set /p toolbox_choice="Select an option: " @@ -45,7 +47,9 @@ if "!toolbox_choice!"=="4" goto app_update if "!toolbox_choice!"=="5" goto net_reset if "!toolbox_choice!"=="6" goto event_logs if "!toolbox_choice!"=="7" goto ssd_trim -if "!toolbox_choice!"=="8" exit +if "!toolbox_choice!"=="8" goto bitlocker_disable +if "!toolbox_choice!"=="9" goto kill_browser_ai +if "!toolbox_choice!"=="10" exit goto main_menu :cleanup @@ -82,13 +86,15 @@ for %%f in ( "%AppData%\Temp\*.*" "%LocalAppdata%\Temp\*.*" "%WinDir%\SoftwareDistribution\Download\*.*" - "%WinDir%\System32\winevt\Logs\*.*" ) do ( echo - Cleaning %%~f echo - Cleaning %%~f >> "!LOGFILE!" del /s /f /q "%%~f" >> "!LOGFILE!" 2>&1 ) +echo - Event Viewer logs are handled by menu option 6 using wevtutil. +echo - Event Viewer logs are handled by menu option 6 using wevtutil. >> "!LOGFILE!" + for %%d in ( "%SYSTEMDRIVE%\AMD" "%SYSTEMDRIVE%\NVIDIA" @@ -272,12 +278,17 @@ echo. echo Current Drives Connected: powershell -Command "Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} | ft -AutoSize" echo. -set trim_drive= -set /p trim_drive="Enter Drive Letter to TRIM (e.g. C): " -if "!trim_drive!"=="" goto main_menu -set trim_drive=!trim_drive::=! -set trim_drive=!trim_drive: =! -if "!trim_drive!"=="" goto main_menu +choice /c 0ABCDEFGHIJKLMNOPQRSTUVWXYZ /n /m "Press 0 to return, or drive letter to TRIM (A-Z): " +set "drive_choice=!errorlevel!" +if "!drive_choice!"=="1" goto main_menu +call :set_drive_from_choice !drive_choice! +set "trim_drive=!selected_drive!" +if not exist "!trim_drive!:\" ( + echo Drive !trim_drive!: was not found. + echo TRIM drive not found: !trim_drive!: >> "!LOGFILE!" + pause + goto main_menu +) echo. echo ----------------------------------------------- @@ -297,4 +308,134 @@ echo [1] Return to Menu echo [2] Exit set /p final="Choose an option: " if "!final!"=="1" goto main_menu -exit \ No newline at end of file +exit + +:bitlocker_disable +cls +echo =============================================== +echo DISABLE BITLOCKER (PLAN) +echo =============================================== +echo WARNING: This starts BitLocker decryption for the +echo selected drive and turns BitLocker off. +echo -^> Decryption can take a long time. +echo -^> Keep the PC powered on until Windows finishes. +echo -^> Do this only when protection is no longer needed. +echo =============================================== +echo. +where manage-bde.exe >nul 2>&1 +if errorlevel 1 ( + echo manage-bde.exe was not found on this system. + echo manage-bde.exe was not found. >> "!LOGFILE!" + pause + goto main_menu +) + +echo Current BitLocker status: +echo Current BitLocker status: >> "!LOGFILE!" +manage-bde -status +manage-bde -status >> "!LOGFILE!" 2>&1 +echo. +choice /c 0ABCDEFGHIJKLMNOPQRSTUVWXYZ /n /m "Press 0 to return, or drive letter to disable BitLocker (A-Z): " +set "drive_choice=!errorlevel!" +if "!drive_choice!"=="1" goto main_menu +call :set_drive_from_choice !drive_choice! +set "bitlocker_drive=!selected_drive!" +if not exist "!bitlocker_drive!:\" ( + echo Drive !bitlocker_drive!: was not found. + echo BitLocker drive not found: !bitlocker_drive!: >> "!LOGFILE!" + pause + goto main_menu +) + +echo. +echo Selected drive status: +echo Selected BitLocker drive status for !bitlocker_drive!: >> "!LOGFILE!" +manage-bde -status !bitlocker_drive!: +manage-bde -status !bitlocker_drive!: >> "!LOGFILE!" 2>&1 +echo. +set confirm= +set /p confirm="Type DISABLE to start decryption for !bitlocker_drive!: " +if /i "!confirm!" NEQ "DISABLE" goto main_menu + +echo. +echo Starting BitLocker decryption on !bitlocker_drive!: ... +echo Starting BitLocker decryption on !bitlocker_drive!: >> "!LOGFILE!" +manage-bde -off !bitlocker_drive!: >> "!LOGFILE!" 2>&1 +if errorlevel 1 ( + echo BITLOCKER DISABLE FAILED. Check !LOGFILE!. + echo BITLOCKER DISABLE FAILED. >> "!LOGFILE!" +) else ( + echo BITLOCKER DECRYPTION STARTED. Check Windows BitLocker status for progress. + echo BITLOCKER DECRYPTION STARTED. >> "!LOGFILE!" +) + +echo. +echo Updated status: +echo Updated BitLocker status for !bitlocker_drive!: >> "!LOGFILE!" +manage-bde -status !bitlocker_drive!: +manage-bde -status !bitlocker_drive!: >> "!LOGFILE!" 2>&1 +pause +goto main_menu + +:set_drive_from_choice +set "selected_drive=" +if "%~1"=="2" set "selected_drive=A" +if "%~1"=="3" set "selected_drive=B" +if "%~1"=="4" set "selected_drive=C" +if "%~1"=="5" set "selected_drive=D" +if "%~1"=="6" set "selected_drive=E" +if "%~1"=="7" set "selected_drive=F" +if "%~1"=="8" set "selected_drive=G" +if "%~1"=="9" set "selected_drive=H" +if "%~1"=="10" set "selected_drive=I" +if "%~1"=="11" set "selected_drive=J" +if "%~1"=="12" set "selected_drive=K" +if "%~1"=="13" set "selected_drive=L" +if "%~1"=="14" set "selected_drive=M" +if "%~1"=="15" set "selected_drive=N" +if "%~1"=="16" set "selected_drive=O" +if "%~1"=="17" set "selected_drive=P" +if "%~1"=="18" set "selected_drive=Q" +if "%~1"=="19" set "selected_drive=R" +if "%~1"=="20" set "selected_drive=S" +if "%~1"=="21" set "selected_drive=T" +if "%~1"=="22" set "selected_drive=U" +if "%~1"=="23" set "selected_drive=V" +if "%~1"=="24" set "selected_drive=W" +if "%~1"=="25" set "selected_drive=X" +if "%~1"=="26" set "selected_drive=Y" +if "%~1"=="27" set "selected_drive=Z" +exit /b + +:kill_browser_ai +cls +echo =============================================== +echo KILL BROWSER AI +echo =============================================== +echo WARNING: This downloads and executes a remote +echo PowerShell script from the configured gist URL. +echo -^> It may close browser or AI-related processes. +echo -^> Network access is required. +echo -^> Do not run if you do not trust the source. +echo =============================================== +echo. +echo Source: +echo https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 +echo. +set confirm= +set /p confirm="Type KILL to run Kill Browser AI: " +if /i "!confirm!" NEQ "KILL" goto main_menu + +echo. +echo Running Kill Browser AI... +echo Running Kill Browser AI remote script. >> "!LOGFILE!" +powershell -NoProfile -ExecutionPolicy Bypass -Command "try { iwr -useb 'https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1' | iex; exit 0 } catch { Write-Error $_; exit 1 }" >> "!LOGFILE!" 2>&1 +if errorlevel 1 ( + echo KILL BROWSER AI FAILED. Check !LOGFILE!. + echo KILL BROWSER AI FAILED. >> "!LOGFILE!" +) else ( + echo KILL BROWSER AI COMPLETE. + echo KILL BROWSER AI COMPLETE. >> "!LOGFILE!" +) +pause +goto main_menu diff --git a/MEMORY.md b/MEMORY.md index 744e027..ea915e3 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -19,7 +19,8 @@ Last updated: 2026-06-07 - Current branch at scan time: `lovedolove` - License: Apache License 2.0 - Primary executable: `LDLWinToolBox.bat` -- Primary docs: `README.md`, `ANALYSIS.md`, `PROMPT_GUIDE.md` +- Primary docs: `README.md`, `AGENTS.md`, `MEMORY.md`, `memory/tasks.md` +- Prompt/history docs observed as absent at the latest scan: `ANALYSIS.md`, `PROMPT_GUIDE.md` - GitHub metadata: `.github/FUNDING.yml`, `.github/ISSUE_TEMPLATE/bug-report---.md`, `.github/ISSUE_TEMPLATE/feature-request---.md` - Asset: `images/logo.png` - Ignored local template observed: `BLANK_README.md` @@ -36,32 +37,35 @@ Implemented menu behavior: 4. Update All Installed Apps: asks confirmation, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements`. 5. Complete Network Reset: asks confirmation, runs `netsh winsock reset`, `netsh int ip reset`, and `ipconfig /flushdns`; tells user to restart. 6. Clear Event Viewer Logs: enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. -7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes the entered drive letter, runs `defrag : /L /V`, displays output, and appends it to the log. -8. Exit: closes the tool. +7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes and validates a single drive letter, confirms the drive exists, runs `defrag : /L /V`, displays output, and appends it to the log. +8. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. +9. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then runs the configured gist command and logs the result. +10. Exit: closes the tool. -## Declared Feature Targets +## Implemented Feature Targets -The user listed these as available or planned feature targets, but the current scanned `LDLWinToolBox.bat` implementation does not yet expose them as tools: +The user-listed feature targets below were implemented in `LDLWinToolBox.bat` on 2026-06-07: -- Disable BitLocker `[Plan]`. +- Disable BitLocker `[Plan]` with status display, drive validation, and `DISABLE` confirmation. - Kill Browser AI using: `iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex` -Treat the remote `iwr | iex` command as high risk. Do not execute it during analysis. If implemented later, add a clear warning, confirmation prompt, and logging. +Treat the remote `iwr | iex` command as high risk. Do not execute it during analysis. The menu feature requires a clear warning, `KILL` confirmation, and logging. ## Documentation And Prompt Files - `README.md` describes the app, prerequisites, installation, usage, license, and contact info. -- `ANALYSIS.md` records technical analysis and future prompt rules. -- `PROMPT_GUIDE.md` explains how users should launch, select menu options, and prompt AI for future changes. +- `AGENTS.md` records AI working rules, startup order, project rules, and current feature inventory. +- `MEMORY.md` records long-term user preferences, repository facts, current logic, risks, and persistent rules. +- `memory/tasks.md` tracks cross-session work. +- `ANALYSIS.md` and `PROMPT_GUIDE.md` were not present in the latest working tree scan; if restored later, preserve their history and append updates. - Existing prompt rules emphasize Batch standard, auto-admin preservation, history preservation, input sanitization, clean verbosity, and long-running process warnings. ## Known Gaps And Risks -- Current README says options `1-8` execute tools, but option `8` is currently `Exit`. -- User-declared feature targets `Disable BitLocker` and `Kill Browser AI` are not implemented in the scanned script. -- The current admin-check path in `LDLWinToolBox.bat` should be reviewed before future releases because the line includes `system32%` in the protected path. -- Cleanup deletes Event Viewer log files directly and option 6 also clears logs through `wevtutil`; future changes should keep this behavior intentional and clearly documented. +- The admin-check path issue was fixed by replacing the typo-prone `cacls` path check with `fltmc`. +- Cleanup no longer deletes Event Viewer log files directly; option 6 remains the safe `wevtutil` path for clearing logs. +- The Kill Browser AI gist content could not be verified from the local environment during implementation; keep the `KILL` confirmation and source warning. - `BLANK_README.md` is present locally but ignored by git and appears to be an unused Best-README-Template source file. - No tracked `.agents/skills/` directory exists at the 2026-06-07 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. @@ -74,4 +78,3 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - Keep console messages readable and route verbose command output into the log. - Add `(Y/N)` confirmation for long-running, destructive, privacy-affecting, or remote-execution operations. - Keep prompt/history updates append-friendly and date-stamped. - diff --git a/README.md b/README.md index b6d5964..4758e22 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

LDL Windows ToolBox

- A cohesive, menu-driven Windows Batch utility that safely automates advanced system cleanup, integrity repair, components update, and NVMe SSD optimizations. + A cohesive, menu-driven Windows Batch utility that safely automates advanced system cleanup, integrity repair, component updates, network repair, BitLocker decryption planning, browser AI cleanup, and NVMe SSD optimizations.
Explore the docs »
@@ -60,7 +60,7 @@ ## About The Project -The LDL Windows ToolBox is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, script verifications, and SSD TRIM optimization into a single, cohesive menu-driven interface. +The LDL Windows ToolBox is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, script verifications, network reset, BitLocker decryption planning, browser AI cleanup, and SSD TRIM optimization into a single, cohesive menu-driven interface. It safely automates otherwise tedious system administration tasks while maintaining comprehensive, timestamped logs (`LDLWinToolBox_yyMMddHHmmss.log`) of all actions to ensure complete historical records and safety. @@ -98,7 +98,7 @@ To get a local copy up and running follow these simple steps. ## Usage -Upon launching, the interactive menu provides numerical options (1-8) to execute tools: +Upon launching, the interactive menu provides numerical options (1-10) to execute tools: - **[1] Advanced System Cleanup**: Deeply cleans temporary system data, calculates Space Freed (MB). - **[2] System Integrity Repair**: Executes `SFC` and `DISM` to scan and repair corrupt OS files. @@ -107,8 +107,11 @@ Upon launching, the interactive menu provides numerical options (1-8) to execute - **[5] Complete Network Reset**: Resets Winsock, TCP/IP, and DNS cache entirely. - **[6] Clear Event Viewer Logs**: Flushes system, security, and application logs. - **[7] Manual SSD TRIM**: Optimized for NVMe drives, triggers manual SSD re-trim using Windows defrag. +- **[8] Disable BitLocker (Plan)**: Shows BitLocker status, validates a selected drive letter, then starts `manage-bde -off` only after typing `DISABLE`. +- **[9] Kill Browser AI**: Runs the configured remote PowerShell cleanup command only after typing `KILL`. +- **[10] Exit**: Closes the toolbox. -_For more detailed background checks on each process, please refer to [ANALYSIS.md](ANALYSIS.md) and [PROMPT_GUIDE.md](PROMPT_GUIDE.md)_ +_For AI maintenance context and persistent project rules, please refer to [AGENTS.md](AGENTS.md), [MEMORY.md](MEMORY.md), and [memory/tasks.md](memory/tasks.md)._

(back to top)

diff --git a/memory/2026-06-07.md b/memory/2026-06-07.md index 002fded..4e4c487 100644 --- a/memory/2026-06-07.md +++ b/memory/2026-06-07.md @@ -14,3 +14,12 @@ - Did not modify `LDLWinToolBox.bat`; the request was to update AI memory and agent instructions after scanning. - Recorded `Disable BitLocker [Plan]` and `Kill Browser AI` as declared feature targets because they are not implemented in the scanned menu. +## Later Work + +- Implemented `Disable BitLocker (Plan)` as menu option 8 using `manage-bde -status`, single-drive validation, typed `DISABLE` confirmation, and `manage-bde -off`. +- Implemented `Kill Browser AI` as menu option 9 with a remote script warning, typed `KILL` confirmation, and logged PowerShell execution. +- Moved `Exit` to menu option 10. +- Replaced the malformed admin `cacls` path check with `fltmc` for a cleaner elevation test. +- Hardened SSD TRIM drive-letter validation before calling `defrag`. +- Removed direct Event Viewer log-file deletion from Advanced System Cleanup; Event Viewer cleanup remains menu option 6 through `wevtutil`. +- Updated README, AGENTS, MEMORY, and task tracking to match the implemented menu. diff --git a/memory/tasks.md b/memory/tasks.md index d343a10..e3e6312 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -4,13 +4,15 @@ Last updated: 2026-06-07 ## Pending -- [ ] Verify and, if needed, fix the admin privilege check path in `LDLWinToolBox.bat` because the scanned line contains `system32%`. -- [ ] Decide exact behavior and safety prompts for `Disable BitLocker [Plan]`. -- [ ] Decide whether to implement `Kill Browser AI`; if yes, review the remote script source and add explicit confirmation before any `iwr | iex` execution. +- [ ] Review the remote `kill_ai.ps1` gist source when it is reachable, and keep the `KILL` confirmation unless a trusted local implementation replaces it. +- [ ] Decide whether `ANALYSIS.md` and `PROMPT_GUIDE.md` should be restored or remain replaced by `AGENTS.md`, `MEMORY.md`, and `memory/`. - [ ] If repo-local skills are needed, clone public GitHub open-source skills into `.agents/skills//` and record URL, commit or tag, and license. ## Completed - [x] 2026-06-07: Scanned current repository logic, docs, prompt files, Git metadata, and issue templates. - [x] 2026-06-07: Created `AGENTS.md`, `MEMORY.md`, `memory/tasks.md`, and daily work log to restore AI identity and project state in future sessions. - +- [x] 2026-06-07: Fixed admin privilege check by replacing the malformed `cacls` protected-path check with `fltmc`. +- [x] 2026-06-07: Implemented `Disable BitLocker (Plan)` with status display, drive validation, `DISABLE` confirmation, and `manage-bde -off`. +- [x] 2026-06-07: Implemented `Kill Browser AI` with source warning, `KILL` confirmation, and logged PowerShell execution. +- [x] 2026-06-07: Hardened SSD TRIM drive input validation and stopped direct Event Viewer log deletion from cleanup. From 62209cbf32c802c18040cac1018ccc26b97fa643 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 7 Jun 2026 20:42:52 +0800 Subject: [PATCH 10/33] Add View Log History feature --- AGENTS.md | 6 +- LDLWinToolBox.bat | 392 ++++++++++++++++++++++++++++++++----------- MEMORY.md | 16 +- README.md | 9 +- memory/2026-06-07.md | 13 ++ memory/tasks.md | 3 +- 6 files changed, 332 insertions(+), 107 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 767c227..7aca4da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,8 +31,9 @@ On every new session: - Main executable: `LDLWinToolBox.bat`. - Keep the app menu-driven and suitable for Windows 10/11. - The script must auto-check Administrator permission and auto-request elevation with UAC before system-level operations. -- Preserve timestamped logging with `LDLWinToolBox_yyMMddHHmmss.log`. +- Preserve timestamped structured logging under `logs\LDLWinToolBox_yyMMddHHmmss.log`. - Console output should stay concise and user-readable; raw command output should go to `!LOGFILE!`. +- Logs should include a session header, feature sections, user cancellation notes, command start/end markers, and exit codes for key system commands. - Long-running or risky operations must warn the user, explain interrupt safety, and ask for `(Y/N)` confirmation. - Sanitize user input for every new menu feature that accepts values. - Keep existing documentation and analysis history intact. If `ANALYSIS.md` or `PROMPT_GUIDE.md` exists, append updates instead of replacing historical context. @@ -51,7 +52,8 @@ Current `LDLWinToolBox.bat` menu implementation: 8. Disable BitLocker `(Plan)` using `manage-bde -status` and guarded `manage-bde -off :`. 9. Kill Browser AI using the user-specified command: `iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex` -10. Exit. +10. View Log History using a read-only paged console viewer for recent `logs\LDLWinToolBox_*.log` files. +11. Exit. Remote script execution is high risk. Do not run this command during development. If it is implemented as a menu feature, add an explicit warning and confirmation before execution. diff --git a/LDLWinToolBox.bat b/LDLWinToolBox.bat index 52d3bf8..d077547 100644 --- a/LDLWinToolBox.bat +++ b/LDLWinToolBox.bat @@ -10,17 +10,20 @@ if errorlevel 1 ( ) pushd "%CD%" CD /D "%~dp0" +set "SCRIPT_FILE=%~f0" +set "SCRIPT_DIR=%~dp0" :: --- END AUTO ADMIN --- :: --- INITIALIZE LOGGING --- for /f "delims=" %%a in ('powershell -Command "Get-Date -Format yyMMddHHmmss"') do set "LOG_TIME=%%a" -set "LOGFILE=LDLWinToolBox_!LOG_TIME!.log" - -echo =============================================== > "!LOGFILE!" -echo LDL Windows ToolBox Run Log >> "!LOGFILE!" -echo Date: !LOG_TIME! >> "!LOGFILE!" -echo =============================================== >> "!LOGFILE!" -echo. >> "!LOGFILE!" +set "LOG_DIR=%~dp0logs" +if not exist "!LOG_DIR!" md "!LOG_DIR!" >nul 2>&1 +if not exist "!LOG_DIR!" ( + echo Failed to create logs directory. Using script directory for logs. + set "LOG_DIR=%~dp0" +) +set "LOGFILE=!LOG_DIR!\LDLWinToolBox_!LOG_TIME!.log" +call :init_log :main_menu cls @@ -36,9 +39,13 @@ echo [6] Clear Event Viewer Logs echo [7] Manual SSD TRIM (Optimized for KC3000) echo [8] Disable BitLocker (Plan) echo [9] Kill Browser AI -echo [10] Exit +echo [10] View Log History +echo [11] Exit +echo =============================================== +echo Log: !LOGFILE! echo =============================================== set /p toolbox_choice="Select an option: " +call :log_only INFO "Menu selection: !toolbox_choice!" if "!toolbox_choice!"=="1" goto cleanup if "!toolbox_choice!"=="2" goto sys_repair @@ -49,7 +56,12 @@ if "!toolbox_choice!"=="6" goto event_logs if "!toolbox_choice!"=="7" goto ssd_trim if "!toolbox_choice!"=="8" goto bitlocker_disable if "!toolbox_choice!"=="9" goto kill_browser_ai -if "!toolbox_choice!"=="10" exit +if "!toolbox_choice!"=="10" goto log_history +if "!toolbox_choice!"=="11" ( + call :log INFO "Exiting LDL Windows ToolBox." + exit +) +call :log WARN "Invalid menu selection: !toolbox_choice!" goto main_menu :cleanup @@ -61,23 +73,25 @@ echo All operations are being logged to: echo !LOGFILE! echo =============================================== echo. -echo Calculating current free space... +call :log_section "Advanced System Cleanup" +call :log INFO "Calculating current free space..." for /f "usebackq" %%a in (`powershell -Command "[math]::Round((Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='%SYSTEMDRIVE%'\").FreeSpace / 1MB)"`) do set "free_before_mb=%%a" +call :log_only INFO "Free space before cleanup: !free_before_mb! MB" -echo [1/4] Stopping background services... -echo [1/4] Stopping background services... >> "!LOGFILE!" +call :log INFO "[1/4] Stopping background services..." -echo - Stopping Windows Update (wuauserv)... -echo - Stopping Windows Update (wuauserv)... >> "!LOGFILE!" +call :log INFO "- Stopping Windows Update (wuauserv)..." +call :log_command_start "net stop wuauserv" net stop wuauserv >> "!LOGFILE!" 2>&1 +call :log_result "net stop wuauserv" !errorlevel! -echo - Stopping Background Intelligent Transfer Service (bits)... -echo - Stopping Background Intelligent Transfer Service (bits)... >> "!LOGFILE!" +call :log INFO "- Stopping Background Intelligent Transfer Service (bits)..." +call :log_command_start "net stop bits" net stop bits >> "!LOGFILE!" 2>&1 +call :log_result "net stop bits" !errorlevel! echo. -echo [2/4] Deleting temporary and junk files... -echo [2/4] Deleting temporary and junk files... >> "!LOGFILE!" +call :log INFO "[2/4] Deleting temporary and junk files..." for %%f in ( "%WinDir%\Temp\*.*" @@ -87,13 +101,13 @@ for %%f in ( "%LocalAppdata%\Temp\*.*" "%WinDir%\SoftwareDistribution\Download\*.*" ) do ( - echo - Cleaning %%~f - echo - Cleaning %%~f >> "!LOGFILE!" + call :log INFO "- Cleaning %%~f" + call :log_command_start "del /s /f /q %%~f" del /s /f /q "%%~f" >> "!LOGFILE!" 2>&1 + call :log_result "del /s /f /q %%~f" !errorlevel! ) -echo - Event Viewer logs are handled by menu option 6 using wevtutil. -echo - Event Viewer logs are handled by menu option 6 using wevtutil. >> "!LOGFILE!" +call :log INFO "- Event Viewer logs are handled by menu option 6 using wevtutil." for %%d in ( "%SYSTEMDRIVE%\AMD" @@ -101,43 +115,46 @@ for %%d in ( "%SYSTEMDRIVE%\INTEL" ) do ( if exist "%%~d" ( - echo - Removing Directory %%~d - echo - Removing Directory %%~d >> "!LOGFILE!" + call :log INFO "- Removing Directory %%~d" + call :log_command_start "rd /s /q %%~d" rd /s /q "%%~d" >> "!LOGFILE!" 2>&1 + call :log_result "rd /s /q %%~d" !errorlevel! ) ) echo. -echo [3/4] Rebuilding directory structure... -echo [3/4] Rebuilding directory structure... >> "!LOGFILE!" +call :log INFO "[3/4] Rebuilding directory structure..." for %%d in ("%WinDir%\Temp" "%WinDir%\Prefetch" "%Temp%" "%AppData%\Temp" "%LocalAppdata%\Temp") do ( - echo - Rebuilding %%~d - echo - Rebuilding %%~d >> "!LOGFILE!" + call :log INFO "- Rebuilding %%~d" + call :log_command_start "rd /s /q %%~d" rd /s /q "%%~d" >> "!LOGFILE!" 2>&1 + call :log_result "rd /s /q %%~d" !errorlevel! + call :log_command_start "md %%~d" md "%%~d" >> "!LOGFILE!" 2>&1 + call :log_result "md %%~d" !errorlevel! ) echo. -echo [4/4] Finalizing optimizations... -echo [4/4] Finalizing optimizations... >> "!LOGFILE!" +call :log INFO "[4/4] Finalizing optimizations..." -echo - Starting Windows Update (wuauserv)... -echo - Starting Windows Update (wuauserv)... >> "!LOGFILE!" +call :log INFO "- Starting Windows Update (wuauserv)..." +call :log_command_start "net start wuauserv" net start wuauserv >> "!LOGFILE!" 2>&1 +call :log_result "net start wuauserv" !errorlevel! -echo - Starting Background Intelligent Transfer Service (bits)... -echo - Starting Background Intelligent Transfer Service (bits)... >> "!LOGFILE!" +call :log INFO "- Starting Background Intelligent Transfer Service (bits)..." +call :log_command_start "net start bits" net start bits >> "!LOGFILE!" 2>&1 +call :log_result "net start bits" !errorlevel! for /f "usebackq" %%a in (`powershell -Command "[math]::Round((Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='%SYSTEMDRIVE%'\").FreeSpace / 1MB)"`) do set "free_after_mb=%%a" set /a "space_saved_mb=free_after_mb - free_before_mb" if !space_saved_mb! LSS 0 set "space_saved_mb=0" +call :log_only INFO "Free space after cleanup: !free_after_mb! MB" echo. -echo SYSTEM CLEAN UP COMPLETE! -echo SYSTEM CLEAN UP COMPLETE! >> "!LOGFILE!" -echo -^> Total Space Freed: !space_saved_mb! MB -echo -^> Total Space Freed: !space_saved_mb! MB >> "!LOGFILE!" +call :log INFO "SYSTEM CLEAN UP COMPLETE" +call :log INFO "Total Space Freed: !space_saved_mb! MB" pause goto main_menu @@ -150,21 +167,26 @@ echo WARNING: This process can take 15-45 minutes. echo -^> It CAN be safely interrupted by closing the window. echo -^> However, it is recommended to let it finish. echo =============================================== +call :log_section "System Integrity Repair" set /p confirm="Do you want to proceed? (Y/N): " -if /i "!confirm!" NEQ "Y" goto main_menu +if /i "!confirm!" NEQ "Y" ( + call :log INFO "System Integrity Repair cancelled by user." + goto main_menu +) echo. -echo [1/2] Running System File Checker (SFC)... -echo Running SFC >> "!LOGFILE!" +call :log INFO "[1/2] Running System File Checker (SFC)..." +call :log_command_start "sfc /scannow" sfc /scannow >> "!LOGFILE!" 2>&1 +call :log_result "sfc /scannow" !errorlevel! -echo [2/2] Running DISM RestoreHealth... -echo Running DISM RestoreHealth >> "!LOGFILE!" +call :log INFO "[2/2] Running DISM RestoreHealth..." +call :log_command_start "DISM /Online /Cleanup-Image /RestoreHealth" DISM /Online /Cleanup-Image /RestoreHealth >> "!LOGFILE!" 2>&1 +call :log_result "DISM /Online /Cleanup-Image /RestoreHealth" !errorlevel! echo. -echo SYSTEM INTEGRITY REPAIR COMPLETE! -echo SYSTEM INTEGRITY REPAIR COMPLETE! >> "!LOGFILE!" +call :log INFO "SYSTEM INTEGRITY REPAIR COMPLETE" pause goto main_menu @@ -177,17 +199,21 @@ echo WARNING: This deeply cleans old Windows Update files. echo -^> It can take 10-30 minutes and may appear stuck. echo -^> DO NOT interrupt this process (can corrupt updates). echo =============================================== +call :log_section "Windows Component Store Cleanup" set /p confirm="Do you want to proceed? (Y/N): " -if /i "!confirm!" NEQ "Y" goto main_menu +if /i "!confirm!" NEQ "Y" ( + call :log INFO "Windows Component Store Cleanup cancelled by user." + goto main_menu +) echo. -echo Cleaning Windows Component Store... -echo Running WinSxS Cleanup >> "!LOGFILE!" +call :log INFO "Cleaning Windows Component Store..." +call :log_command_start "DISM.exe /Online /Cleanup-Image /StartComponentCleanup" DISM.exe /Online /Cleanup-Image /StartComponentCleanup >> "!LOGFILE!" 2>&1 +call :log_result "DISM.exe /Online /Cleanup-Image /StartComponentCleanup" !errorlevel! echo. -echo WINSXS CLEANUP COMPLETE! -echo WINSXS CLEANUP COMPLETE! >> "!LOGFILE!" +call :log INFO "WINSXS CLEANUP COMPLETE" pause goto main_menu @@ -200,17 +226,21 @@ echo WARNING: Silently updates all apps installed via Winget. echo -^> May take several minutes. echo -^> It CAN be safely interrupted. echo =============================================== +call :log_section "Update Installed Apps" set /p confirm="Do you want to proceed? (Y/N): " -if /i "!confirm!" NEQ "Y" goto main_menu +if /i "!confirm!" NEQ "Y" ( + call :log INFO "Update Installed Apps cancelled by user." + goto main_menu +) echo. -echo Upgrading all installed applications (this may take a while)... -echo Running Winget Upgrade All >> "!LOGFILE!" +call :log INFO "Upgrading all installed applications (this may take a while)..." +call :log_command_start "winget upgrade --all" winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements >> "!LOGFILE!" 2>&1 +call :log_result "winget upgrade --all" !errorlevel! echo. -echo APP UPDATE COMPLETE! -echo APP UPDATE COMPLETE! >> "!LOGFILE!" +call :log INFO "APP UPDATE COMPLETE" pause goto main_menu @@ -222,25 +252,31 @@ echo =============================================== echo This will reset your network adapters to factory defaults. echo -^> A system restart will be required afterward. echo =============================================== +call :log_section "Complete Network Reset" set /p confirm="Do you want to proceed? (Y/N): " -if /i "!confirm!" NEQ "Y" goto main_menu +if /i "!confirm!" NEQ "Y" ( + call :log INFO "Complete Network Reset cancelled by user." + goto main_menu +) echo. -echo Resetting Winsock... -echo Resetting Winsock >> "!LOGFILE!" +call :log INFO "Resetting Winsock..." +call :log_command_start "netsh winsock reset" netsh winsock reset >> "!LOGFILE!" 2>&1 +call :log_result "netsh winsock reset" !errorlevel! -echo Resetting TCP/IP... -echo Resetting TCP/IP >> "!LOGFILE!" +call :log INFO "Resetting TCP/IP..." +call :log_command_start "netsh int ip reset" netsh int ip reset >> "!LOGFILE!" 2>&1 +call :log_result "netsh int ip reset" !errorlevel! -echo Flushing DNS... -echo Flushing DNS >> "!LOGFILE!" +call :log INFO "Flushing DNS..." +call :log_command_start "ipconfig /flushdns" ipconfig /flushdns >> "!LOGFILE!" 2>&1 +call :log_result "ipconfig /flushdns" !errorlevel! echo. -echo NETWORK RESET COMPLETE! Please RESTART your computer. -echo NETWORK RESET COMPLETE! >> "!LOGFILE!" +call :log INFO "NETWORK RESET COMPLETE. Please RESTART your computer." pause goto main_menu @@ -253,16 +289,16 @@ echo All operations are being logged to: echo !LOGFILE! echo =============================================== echo. -echo Clearing Event Logs... >> "!LOGFILE!" +call :log_section "Clear Event Viewer Logs" for /F "tokens=*" %%G in ('wevtutil.exe el') DO ( - echo - Clearing log: "%%G" - echo - Clearing log: "%%G" >> "!LOGFILE!" + call :log INFO "- Clearing log: %%G" + call :log_command_start "wevtutil.exe cl %%G" wevtutil.exe cl "%%G" >> "!LOGFILE!" 2>&1 + call :log_result "wevtutil.exe cl %%G" !errorlevel! ) echo. -echo EVENT LOGS CLEARED! -echo EVENT LOGS CLEARED! >> "!LOGFILE!" +call :log INFO "EVENT LOGS CLEARED" pause goto main_menu @@ -275,17 +311,23 @@ echo All operations are being logged to: echo !LOGFILE! echo =============================================== echo. +call :log_section "Manual SSD TRIM" echo Current Drives Connected: +call :log_only INFO "Current drives connected:" powershell -Command "Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} | ft -AutoSize" +powershell -Command "Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} | ft -AutoSize" >> "!LOGFILE!" 2>&1 echo. choice /c 0ABCDEFGHIJKLMNOPQRSTUVWXYZ /n /m "Press 0 to return, or drive letter to TRIM (A-Z): " set "drive_choice=!errorlevel!" -if "!drive_choice!"=="1" goto main_menu +if "!drive_choice!"=="1" ( + call :log INFO "Manual SSD TRIM cancelled by user." + goto main_menu +) call :set_drive_from_choice !drive_choice! set "trim_drive=!selected_drive!" +call :log_only INFO "Selected TRIM drive: !trim_drive!:" if not exist "!trim_drive!:\" ( - echo Drive !trim_drive!: was not found. - echo TRIM drive not found: !trim_drive!: >> "!LOGFILE!" + call :log ERROR "Drive !trim_drive!: was not found." pause goto main_menu ) @@ -295,19 +337,23 @@ echo ----------------------------------------------- echo Optimizing Drive !trim_drive!: ... echo Optimizing Drive !trim_drive!: ... >> "!LOGFILE!" echo ----------------------------------------------- +call :log_command_start "defrag !trim_drive!: /L /V" defrag !trim_drive!: /L /V > "%TEMP%\defrag_out.txt" 2>&1 +set "defrag_rc=!errorlevel!" type "%TEMP%\defrag_out.txt" type "%TEMP%\defrag_out.txt" >> "!LOGFILE!" del /q "%TEMP%\defrag_out.txt" >nul 2>&1 +call :log_result "defrag !trim_drive!: /L /V" !defrag_rc! echo. echo ----------------------------------------------- -echo SSD TRIM COMPLETE! -echo SSD TRIM COMPLETE! >> "!LOGFILE!" +call :log INFO "SSD TRIM COMPLETE" echo [1] Return to Menu echo [2] Exit set /p final="Choose an option: " +call :log_only INFO "SSD TRIM final selection: !final!" if "!final!"=="1" goto main_menu +call :log INFO "Exiting LDL Windows ToolBox." exit :bitlocker_disable @@ -322,58 +368,70 @@ echo -^> Keep the PC powered on until Windows finishes. echo -^> Do this only when protection is no longer needed. echo =============================================== echo. +call :log_section "Disable BitLocker" where manage-bde.exe >nul 2>&1 if errorlevel 1 ( - echo manage-bde.exe was not found on this system. - echo manage-bde.exe was not found. >> "!LOGFILE!" + call :log ERROR "manage-bde.exe was not found on this system." pause goto main_menu ) echo Current BitLocker status: -echo Current BitLocker status: >> "!LOGFILE!" +call :log_only INFO "Current BitLocker status:" manage-bde -status +call :log_command_start "manage-bde -status" manage-bde -status >> "!LOGFILE!" 2>&1 +call :log_result "manage-bde -status" !errorlevel! echo. choice /c 0ABCDEFGHIJKLMNOPQRSTUVWXYZ /n /m "Press 0 to return, or drive letter to disable BitLocker (A-Z): " set "drive_choice=!errorlevel!" -if "!drive_choice!"=="1" goto main_menu +if "!drive_choice!"=="1" ( + call :log INFO "Disable BitLocker cancelled by user." + goto main_menu +) call :set_drive_from_choice !drive_choice! set "bitlocker_drive=!selected_drive!" +call :log_only INFO "Selected BitLocker drive: !bitlocker_drive!:" if not exist "!bitlocker_drive!:\" ( - echo Drive !bitlocker_drive!: was not found. - echo BitLocker drive not found: !bitlocker_drive!: >> "!LOGFILE!" + call :log ERROR "Drive !bitlocker_drive!: was not found." pause goto main_menu ) echo. echo Selected drive status: -echo Selected BitLocker drive status for !bitlocker_drive!: >> "!LOGFILE!" +call :log_only INFO "Selected BitLocker drive status for !bitlocker_drive!:" manage-bde -status !bitlocker_drive!: +call :log_command_start "manage-bde -status !bitlocker_drive!:" manage-bde -status !bitlocker_drive!: >> "!LOGFILE!" 2>&1 +call :log_result "manage-bde -status !bitlocker_drive!:" !errorlevel! echo. set confirm= set /p confirm="Type DISABLE to start decryption for !bitlocker_drive!: " -if /i "!confirm!" NEQ "DISABLE" goto main_menu +if /i "!confirm!" NEQ "DISABLE" ( + call :log INFO "Disable BitLocker confirmation not provided. Returning to menu." + goto main_menu +) echo. -echo Starting BitLocker decryption on !bitlocker_drive!: ... -echo Starting BitLocker decryption on !bitlocker_drive!: >> "!LOGFILE!" +call :log INFO "Starting BitLocker decryption on !bitlocker_drive!: ..." +call :log_command_start "manage-bde -off !bitlocker_drive!:" manage-bde -off !bitlocker_drive!: >> "!LOGFILE!" 2>&1 -if errorlevel 1 ( - echo BITLOCKER DISABLE FAILED. Check !LOGFILE!. - echo BITLOCKER DISABLE FAILED. >> "!LOGFILE!" +set "bitlocker_rc=!errorlevel!" +call :log_result "manage-bde -off !bitlocker_drive!:" !bitlocker_rc! +if not "!bitlocker_rc!"=="0" ( + call :log ERROR "BITLOCKER DISABLE FAILED. Check !LOGFILE!." ) else ( - echo BITLOCKER DECRYPTION STARTED. Check Windows BitLocker status for progress. - echo BITLOCKER DECRYPTION STARTED. >> "!LOGFILE!" + call :log INFO "BITLOCKER DECRYPTION STARTED. Check Windows BitLocker status for progress." ) echo. echo Updated status: -echo Updated BitLocker status for !bitlocker_drive!: >> "!LOGFILE!" +call :log_only INFO "Updated BitLocker status for !bitlocker_drive!:" manage-bde -status !bitlocker_drive!: +call :log_command_start "manage-bde -status !bitlocker_drive!:" manage-bde -status !bitlocker_drive!: >> "!LOGFILE!" 2>&1 +call :log_result "manage-bde -status !bitlocker_drive!:" !errorlevel! pause goto main_menu @@ -407,6 +465,139 @@ if "%~1"=="26" set "selected_drive=Y" if "%~1"=="27" set "selected_drive=Z" exit /b +:init_log +> "!LOGFILE!" ( + echo =============================================================================== + echo LDL Windows ToolBox Run Log + echo =============================================================================== + echo Session ID : !LOG_TIME! + echo Started : %DATE% %TIME% + echo Script : !SCRIPT_FILE! + echo Script Dir : !SCRIPT_DIR! + echo Work Dir : %CD% + echo User : %USERDOMAIN%\%USERNAME% + echo Computer : %COMPUTERNAME% + echo OS : %OS% + echo SystemRoot : %SystemRoot% + echo Temp : %TEMP% + echo Log File : !LOGFILE! + echo =============================================================================== + echo. +) +call :log_only INFO "Logging initialized." +exit /b + +:log +set "LOG_LEVEL=%~1" +set "LOG_MESSAGE=%~2" +set "LOG_STAMP=%DATE% %TIME%" +>> "!LOGFILE!" echo [!LOG_STAMP!] [!LOG_LEVEL!] !LOG_MESSAGE! +echo !LOG_MESSAGE! +exit /b + +:log_only +set "LOG_LEVEL=%~1" +set "LOG_MESSAGE=%~2" +set "LOG_STAMP=%DATE% %TIME%" +>> "!LOGFILE!" echo [!LOG_STAMP!] [!LOG_LEVEL!] !LOG_MESSAGE! +exit /b + +:log_section +call :log_only INFO "-------------------------------------------------------------------------------" +call :log INFO "== %~1 ==" +exit /b + +:log_command_start +call :log_only CMD "START %~1" +exit /b + +:log_result +set "LOG_COMMAND=%~1" +set "LOG_CODE=%~2" +if "!LOG_CODE!"=="0" ( + call :log_only OK "END !LOG_COMMAND! exit=!LOG_CODE!" +) else ( + call :log WARN "END !LOG_COMMAND! exit=!LOG_CODE! - check log details." +) +exit /b + +:log_history +cls +echo =============================================== +echo VIEW LOG HISTORY +echo =============================================== +echo Log directory: +echo !LOG_DIR! +echo =============================================== +echo. +call :log_section "View Log History" + +set "LOG_LIST=%TEMP%\LDLWinToolBox_logs_%RANDOM%.tmp" +dir /b /o-d "!LOG_DIR!\LDLWinToolBox_*.log" > "!LOG_LIST!" 2>nul +if errorlevel 1 ( + call :log INFO "No log history found." + if exist "!LOG_LIST!" del /q "!LOG_LIST!" >nul 2>&1 + pause + goto main_menu +) + +set "log_count=0" +for %%N in (1 2 3 4 5 6 7 8 9) do set "log_%%N=" +for /f "usebackq delims=" %%L in ("!LOG_LIST!") do ( + if !log_count! LSS 9 ( + set /a "log_count+=1" + set "log_!log_count!=%%L" + for %%A in ("!LOG_DIR!\%%L") do echo [!log_count!] %%L - %%~zA bytes - %%~tA + ) +) +del /q "!LOG_LIST!" >nul 2>&1 + +if "!log_count!"=="0" ( + call :log INFO "No log history found." + pause + goto main_menu +) + +echo. +echo [0] Return to Menu +choice /c 0123456789 /n /m "Press 0 to return, or 1-9 to view a log: " +set "log_choice=!errorlevel!" +if "!log_choice!"=="1" ( + call :log INFO "View Log History returned to menu." + goto main_menu +) +set /a "log_index=!log_choice!-1" +set "selected_log=" +if "!log_index!"=="1" set "selected_log=!log_1!" +if "!log_index!"=="2" set "selected_log=!log_2!" +if "!log_index!"=="3" set "selected_log=!log_3!" +if "!log_index!"=="4" set "selected_log=!log_4!" +if "!log_index!"=="5" set "selected_log=!log_5!" +if "!log_index!"=="6" set "selected_log=!log_6!" +if "!log_index!"=="7" set "selected_log=!log_7!" +if "!log_index!"=="8" set "selected_log=!log_8!" +if "!log_index!"=="9" set "selected_log=!log_9!" + +if "!selected_log!"=="" ( + call :log WARN "Invalid log history selection: !log_index!" + pause + goto log_history +) + +cls +echo =============================================== +echo Viewing Log: +echo !selected_log! +echo =============================================== +echo Path: !LOG_DIR!\!selected_log! +echo =============================================== +echo. +call :log_only INFO "Viewing log history file: !selected_log!" +more "!LOG_DIR!\!selected_log!" +echo. +pause +goto log_history + :kill_browser_ai cls echo =============================================== @@ -422,20 +613,25 @@ echo. echo Source: echo https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 echo. +call :log_section "Kill Browser AI" +call :log_only WARN "Remote script source: https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1" set confirm= set /p confirm="Type KILL to run Kill Browser AI: " -if /i "!confirm!" NEQ "KILL" goto main_menu +if /i "!confirm!" NEQ "KILL" ( + call :log INFO "Kill Browser AI cancelled by user." + goto main_menu +) echo. -echo Running Kill Browser AI... -echo Running Kill Browser AI remote script. >> "!LOGFILE!" +call :log INFO "Running Kill Browser AI..." +call :log_command_start "PowerShell remote kill_ai.ps1" powershell -NoProfile -ExecutionPolicy Bypass -Command "try { iwr -useb 'https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1' | iex; exit 0 } catch { Write-Error $_; exit 1 }" >> "!LOGFILE!" 2>&1 -if errorlevel 1 ( - echo KILL BROWSER AI FAILED. Check !LOGFILE!. - echo KILL BROWSER AI FAILED. >> "!LOGFILE!" +set "kill_ai_rc=!errorlevel!" +call :log_result "PowerShell remote kill_ai.ps1" !kill_ai_rc! +if not "!kill_ai_rc!"=="0" ( + call :log ERROR "KILL BROWSER AI FAILED. Check !LOGFILE!." ) else ( - echo KILL BROWSER AI COMPLETE. - echo KILL BROWSER AI COMPLETE. >> "!LOGFILE!" + call :log INFO "KILL BROWSER AI COMPLETE." ) pause goto main_menu diff --git a/MEMORY.md b/MEMORY.md index ea915e3..e629db4 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -27,7 +27,16 @@ Last updated: 2026-06-07 ## Current Repository Logic -`LDLWinToolBox.bat` is a standalone menu-driven Windows Batch script. It initializes delayed expansion, checks for Administrator access, relaunches with UAC through PowerShell `Start-Process -Verb RunAs` when needed, switches to the script directory, and creates a timestamped log file named `LDLWinToolBox_yyMMddHHmmss.log`. +`LDLWinToolBox.bat` is a standalone menu-driven Windows Batch script. It initializes delayed expansion, checks for Administrator access, relaunches with UAC through PowerShell `Start-Process -Verb RunAs` when needed, switches to the script directory, and creates a timestamped structured log file named `logs\LDLWinToolBox_yyMMddHHmmss.log`. + +Logging behavior: + +- Creates `logs\` automatically and falls back to the script directory if the log directory cannot be created. +- Writes a session header with script path, working directory, user, computer, OS, system root, temp path, and log path. +- Uses helper labels for `INFO`, `WARN`, `ERROR`, `CMD`, and `OK` log entries. +- Records feature section boundaries, menu selections, user cancellations, key command starts, command exit codes, and major completion messages. +- Keeps raw command output in the same log file while keeping console output concise. +- Provides a read-only Log History viewer that lists recent logs newest-first and opens a selected file with `more`. Implemented menu behavior: @@ -40,7 +49,8 @@ Implemented menu behavior: 7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes and validates a single drive letter, confirms the drive exists, runs `defrag : /L /V`, displays output, and appends it to the log. 8. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. 9. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then runs the configured gist command and logs the result. -10. Exit: closes the tool. +10. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. +11. Exit: closes the tool. ## Implemented Feature Targets @@ -74,7 +84,7 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - Preserve the app as a single-file Windows Batch tool unless the user explicitly asks for a different architecture. - Prefer native Windows commands and Batch syntax for implementation. - Keep PowerShell calls minimal, one-line, and justified by Windows capability gaps. -- Preserve auto-admin behavior and timestamped logs. +- Preserve auto-admin behavior and structured timestamped logs under `logs\`. - Keep console messages readable and route verbose command output into the log. - Add `(Y/N)` confirmation for long-running, destructive, privacy-affecting, or remote-execution operations. - Keep prompt/history updates append-friendly and date-stamped. diff --git a/README.md b/README.md index 4758e22..ae7821c 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The LDL Windows ToolBox is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, script verifications, network reset, BitLocker decryption planning, browser AI cleanup, and SSD TRIM optimization into a single, cohesive menu-driven interface. -It safely automates otherwise tedious system administration tasks while maintaining comprehensive, timestamped logs (`LDLWinToolBox_yyMMddHHmmss.log`) of all actions to ensure complete historical records and safety. +It safely automates otherwise tedious system administration tasks while maintaining comprehensive, timestamped logs (`logs\LDLWinToolBox_yyMMddHHmmss.log`) of all actions to ensure complete historical records and safety.

(back to top)

@@ -98,7 +98,7 @@ To get a local copy up and running follow these simple steps. ## Usage -Upon launching, the interactive menu provides numerical options (1-10) to execute tools: +Upon launching, the interactive menu provides numerical options (1-11) to execute tools: - **[1] Advanced System Cleanup**: Deeply cleans temporary system data, calculates Space Freed (MB). - **[2] System Integrity Repair**: Executes `SFC` and `DISM` to scan and repair corrupt OS files. @@ -109,7 +109,10 @@ Upon launching, the interactive menu provides numerical options (1-10) to execut - **[7] Manual SSD TRIM**: Optimized for NVMe drives, triggers manual SSD re-trim using Windows defrag. - **[8] Disable BitLocker (Plan)**: Shows BitLocker status, validates a selected drive letter, then starts `manage-bde -off` only after typing `DISABLE`. - **[9] Kill Browser AI**: Runs the configured remote PowerShell cleanup command only after typing `KILL`. -- **[10] Exit**: Closes the toolbox. +- **[10] View Log History**: Lists recent toolbox logs and opens the selected log with paged console viewing. +- **[11] Exit**: Closes the toolbox. + +Each run writes a structured log under `logs\` with a session header, environment summary, section markers, user cancellations, command start/end markers, and exit codes for key system commands. The Log History viewer shows the newest logs first and does not delete or modify old log files. _For AI maintenance context and persistent project rules, please refer to [AGENTS.md](AGENTS.md), [MEMORY.md](MEMORY.md), and [memory/tasks.md](memory/tasks.md)._ diff --git a/memory/2026-06-07.md b/memory/2026-06-07.md index 4e4c487..c63a6b0 100644 --- a/memory/2026-06-07.md +++ b/memory/2026-06-07.md @@ -23,3 +23,16 @@ - Hardened SSD TRIM drive-letter validation before calling `defrag`. - Removed direct Event Viewer log-file deletion from Advanced System Cleanup; Event Viewer cleanup remains menu option 6 through `wevtutil`. - Updated README, AGENTS, MEMORY, and task tracking to match the implemented menu. + +## Logging Improvement + +- Moved run logs into `logs\LDLWinToolBox_yyMMddHHmmss.log` with fallback to the script directory if `logs\` cannot be created. +- Added structured log helpers for visible messages, log-only messages, feature sections, command start markers, and command exit-code results. +- Added a session header with environment details. +- Logged menu selections, cancellations, command start/end events, exit codes, selected drives, and completion summaries. + +## Log History Viewer + +- Added menu option 10, `View Log History`, and moved `Exit` to option 11. +- The viewer lists the newest 9 `logs\LDLWinToolBox_*.log` files and opens the selected log with `more`. +- The feature is read-only and logs the user's history-view selection. diff --git a/memory/tasks.md b/memory/tasks.md index e3e6312..36344d1 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -5,7 +5,6 @@ Last updated: 2026-06-07 ## Pending - [ ] Review the remote `kill_ai.ps1` gist source when it is reachable, and keep the `KILL` confirmation unless a trusted local implementation replaces it. -- [ ] Decide whether `ANALYSIS.md` and `PROMPT_GUIDE.md` should be restored or remain replaced by `AGENTS.md`, `MEMORY.md`, and `memory/`. - [ ] If repo-local skills are needed, clone public GitHub open-source skills into `.agents/skills//` and record URL, commit or tag, and license. ## Completed @@ -16,3 +15,5 @@ Last updated: 2026-06-07 - [x] 2026-06-07: Implemented `Disable BitLocker (Plan)` with status display, drive validation, `DISABLE` confirmation, and `manage-bde -off`. - [x] 2026-06-07: Implemented `Kill Browser AI` with source warning, `KILL` confirmation, and logged PowerShell execution. - [x] 2026-06-07: Hardened SSD TRIM drive input validation and stopped direct Event Viewer log deletion from cleanup. +- [x] 2026-06-07: Improved logging with `logs\`, session headers, log helper labels, feature sections, user cancellation records, command start/end markers, and exit codes. +- [x] 2026-06-07: Added read-only `View Log History` menu option for recent `logs\LDLWinToolBox_*.log` files. From efd1687303160ce460b07b0cb41cd1c60f45c73b Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sat, 13 Jun 2026 19:46:14 +0800 Subject: [PATCH 11/33] Update memory and task status --- AGENTS.md | 3 ++- MEMORY.md | 9 ++++++--- memory/2026-06-09.md | 15 +++++++++++++++ memory/2026-06-13.md | 14 ++++++++++++++ memory/tasks.md | 7 ++++--- 5 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 memory/2026-06-09.md create mode 100644 memory/2026-06-13.md diff --git a/AGENTS.md b/AGENTS.md index 7aca4da..256cca4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,7 @@ On every new session: - Preserve timestamped structured logging under `logs\LDLWinToolBox_yyMMddHHmmss.log`. - Console output should stay concise and user-readable; raw command output should go to `!LOGFILE!`. - Logs should include a session header, feature sections, user cancellation notes, command start/end markers, and exit codes for key system commands. +- Runtime logs are ignored by git through the existing `*.log` ignore rule. - Long-running or risky operations must warn the user, explain interrupt safety, and ask for `(Y/N)` confirmation. - Sanitize user input for every new menu feature that accepts values. - Keep existing documentation and analysis history intact. If `ANALYSIS.md` or `PROMPT_GUIDE.md` exists, append updates instead of replacing historical context. @@ -62,7 +63,7 @@ Remote script execution is high risk. Do not run this command during development - Before coding, reviewing, or refactoring, check for applicable local skills under `.agents/skills/`. - Repository-local skill packages must be cloned from public GitHub open-source skills. Do not hand-write custom skill packages in this repo. - For every installed repo-local skill, preserve upstream provenance: source URL, commit or tag, and license. -- Current scan on 2026-06-07 found no tracked `.agents/skills/` directory in this repository. +- Current scan on 2026-06-09 found no tracked `.agents/skills/` directory in this repository. - The session-level `karpathy-guidelines` skill exists outside this repo and may be used for disciplined coding behavior, but it is not currently a repo-local cloned skill asset. ## Memory Files diff --git a/MEMORY.md b/MEMORY.md index e629db4..83a2f43 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,6 +1,6 @@ # MEMORY.md -Last updated: 2026-06-07 +Last updated: 2026-06-13 ## User Preferences @@ -17,6 +17,7 @@ Last updated: 2026-06-07 - Repository path: `D:\Projects\WinProjects\LDLWinToolBox` - Git remote: `https://github.com/LoveDoLove/LDLWinToolBox.git` - Current branch at scan time: `lovedolove` +- Latest scanned commit: `816135b Merge branch 'main' into lovedolove` - License: Apache License 2.0 - Primary executable: `LDLWinToolBox.bat` - Primary docs: `README.md`, `AGENTS.md`, `MEMORY.md`, `memory/tasks.md` @@ -24,6 +25,7 @@ Last updated: 2026-06-07 - GitHub metadata: `.github/FUNDING.yml`, `.github/ISSUE_TEMPLATE/bug-report---.md`, `.github/ISSUE_TEMPLATE/feature-request---.md` - Asset: `images/logo.png` - Ignored local template observed: `BLANK_README.md` +- Runtime logs observed under `logs\`; `*.log` is ignored by `.gitignore`. ## Current Repository Logic @@ -40,7 +42,7 @@ Logging behavior: Implemented menu behavior: -1. Advanced System Cleanup: calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, Event Viewer log files, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. +1. Advanced System Cleanup: calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 6 instead of direct file deletion. 2. System Integrity Repair: asks confirmation, runs `sfc /scannow`, then `DISM /Online /Cleanup-Image /RestoreHealth`. 3. Windows Component Store Cleanup: asks confirmation, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup`. 4. Update All Installed Apps: asks confirmation, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements`. @@ -76,8 +78,9 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - The admin-check path issue was fixed by replacing the typo-prone `cacls` path check with `fltmc`. - Cleanup no longer deletes Event Viewer log files directly; option 6 remains the safe `wevtutil` path for clearing logs. - The Kill Browser AI gist content could not be verified from the local environment during implementation; keep the `KILL` confirmation and source warning. +- The remote `kill_ai.ps1` gist was later retrieved on 2026-06-13; it disables Chrome and Edge on-device AI by applying registry policy keys and locking the `OptGuideOnDeviceModel` folders, but it still remains high risk and must not be executed automatically during analysis. - `BLANK_README.md` is present locally but ignored by git and appears to be an unused Best-README-Template source file. -- No tracked `.agents/skills/` directory exists at the 2026-06-07 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. +- No tracked `.agents/skills/` directory exists at the 2026-06-09 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. ## Persistent Working Rules diff --git a/memory/2026-06-09.md b/memory/2026-06-09.md new file mode 100644 index 0000000..4c575d8 --- /dev/null +++ b/memory/2026-06-09.md @@ -0,0 +1,15 @@ +# 2026-06-09 + +## Work Log + +- Scanned the current repository structure, Git state, main Batch script, README, memory files, `.github` metadata, `.gitignore`, and runtime log directory. +- Confirmed current branch is `lovedolove` and latest scanned commit is `816135b Merge branch 'main' into lovedolove`. +- Confirmed `LDLWinToolBox.bat` currently exposes 11 menu options: cleanup, repair, WinSxS cleanup, Winget update, network reset, Event Viewer log clearing, SSD TRIM, BitLocker disable plan, Kill Browser AI, View Log History, and Exit. +- Confirmed structured logging writes to `logs\LDLWinToolBox_yyMMddHHmmss.log`; runtime `.log` files are ignored by `.gitignore`. +- Confirmed no tracked repo-local `.agents/skills/` directory exists. + +## Decisions + +- Updated `AGENTS.md` and `MEMORY.md` to reflect the 2026-06-09 scan. +- Corrected the remembered Advanced System Cleanup description: Event Viewer logs are handled by option 6 through `wevtutil`, not direct file deletion. +- Did not modify application logic in this scan-only update. diff --git a/memory/2026-06-13.md b/memory/2026-06-13.md new file mode 100644 index 0000000..be6963c --- /dev/null +++ b/memory/2026-06-13.md @@ -0,0 +1,14 @@ +# 2026-06-13 + +## Work Log + +- Re-read `AGENTS.md`, `MEMORY.md`, and `C:\Users\LoveDoLove\.codex\RTK.md` to restore the working rules for this session. +- Rescanned the repository structure, `LDLWinToolBox.bat`, `README.md`, `.github/`, and the tracked memory files to re-establish the project state. +- Confirmed the toolbox still centers on the same 11 menu actions, structured logging, and auto-admin elevation flow documented in the existing memory snapshot. +- Retrieved and reviewed the remote `kill_ai.ps1` gist source without executing it. +- Confirmed there is no tracked repo-local `.agents/skills/` directory to install for current work. + +## Decisions + +- Kept the `KILL` confirmation and high-risk remote-source warning unchanged because the gist is verified but still dangerous to execute automatically. +- Did not make any application code changes in this session. diff --git a/memory/tasks.md b/memory/tasks.md index 36344d1..444904b 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -1,11 +1,10 @@ # memory/tasks.md -Last updated: 2026-06-07 +Last updated: 2026-06-13 ## Pending -- [ ] Review the remote `kill_ai.ps1` gist source when it is reachable, and keep the `KILL` confirmation unless a trusted local implementation replaces it. -- [ ] If repo-local skills are needed, clone public GitHub open-source skills into `.agents/skills//` and record URL, commit or tag, and license. +- No open items. ## Completed @@ -17,3 +16,5 @@ Last updated: 2026-06-07 - [x] 2026-06-07: Hardened SSD TRIM drive input validation and stopped direct Event Viewer log deletion from cleanup. - [x] 2026-06-07: Improved logging with `logs\`, session headers, log helper labels, feature sections, user cancellation records, command start/end markers, and exit codes. - [x] 2026-06-07: Added read-only `View Log History` menu option for recent `logs\LDLWinToolBox_*.log` files. +- [x] 2026-06-09: Rescanned current repository logic and updated `AGENTS.md`, `MEMORY.md`, and memory history. +- [x] 2026-06-13: Restored session state, rescanned repository facts, retrieved and reviewed the remote `kill_ai.ps1` gist source, and confirmed no repo-local skills were needed for current work. From 1e4a723f8663ff91566c37d744f8966905d4947c Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sat, 13 Jun 2026 19:52:22 +0800 Subject: [PATCH 12/33] Add feature ideas backlog --- MEMORY.md | 2 ++ memory/feature-ideas.md | 40 ++++++++++++++++++++++++++++++++++++++++ memory/tasks.md | 1 + 3 files changed, 43 insertions(+) create mode 100644 memory/feature-ideas.md diff --git a/MEMORY.md b/MEMORY.md index 83a2f43..8a437be 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -21,6 +21,7 @@ Last updated: 2026-06-13 - License: Apache License 2.0 - Primary executable: `LDLWinToolBox.bat` - Primary docs: `README.md`, `AGENTS.md`, `MEMORY.md`, `memory/tasks.md` +- Backlog notes: `memory/feature-ideas.md` - Prompt/history docs observed as absent at the latest scan: `ANALYSIS.md`, `PROMPT_GUIDE.md` - GitHub metadata: `.github/FUNDING.yml`, `.github/ISSUE_TEMPLATE/bug-report---.md`, `.github/ISSUE_TEMPLATE/feature-request---.md` - Asset: `images/logo.png` @@ -91,3 +92,4 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - Keep console messages readable and route verbose command output into the log. - Add `(Y/N)` confirmation for long-running, destructive, privacy-affecting, or remote-execution operations. - Keep prompt/history updates append-friendly and date-stamped. +- Keep future enhancement ideas in `memory/feature-ideas.md` so they can be reread and prioritized later. diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md new file mode 100644 index 0000000..16f7569 --- /dev/null +++ b/memory/feature-ideas.md @@ -0,0 +1,40 @@ +# Feature Ideas Backlog + +Last updated: 2026-06-13 + +This file is a living backlog of future enhancements and maintenance ideas for `LDLWinToolBox`. +Keep entries concise, append-friendly, and aligned with the Batch-based, menu-driven design. + +## New Features + +- System information summary +- Create a restore point before risky operations +- Windows Update status check +- Driver inventory and version view +- Service health check for common Windows services +- Disk health and SMART summary +- Log export and archive bundle +- Network before/after snapshot +- Defender status check and quick scan entry +- Safe Mode or recovery entry helpers +- Selective cleanup instead of fixed cleanup sets +- Custom exclusion list for cleanup targets +- Exportable report of actions and results +- Version and update check for the toolbox itself + +## Optimizations + +- Standardize confirmation flow for risky actions +- Extract shared helper labels and common routines +- Strengthen input validation for all menu prompts +- Reduce redundant PowerShell calls +- Improve error handling and user-facing failure messages +- Improve log readability and section formatting +- Add preflight checks for external commands +- Make cleanup operations more conservative by default +- Harmonize menu wording and labels +- Add a read-only mode for status checks +- Use clearer section headers in the menu +- Add progress hints for long-running tasks +- Maintain a lightweight verification checklist after changes +- Keep README, memory, and task notes synchronized diff --git a/memory/tasks.md b/memory/tasks.md index 444904b..ddb3a00 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -5,6 +5,7 @@ Last updated: 2026-06-13 ## Pending - No open items. +- Future enhancement ideas are now tracked in `memory/feature-ideas.md`. ## Completed From 88db3d1c9ba8be0b117ea71afdf3e5439142d9eb Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sat, 13 Jun 2026 19:56:45 +0800 Subject: [PATCH 13/33] Add optimization priority roadmap --- MEMORY.md | 1 + memory/feature-ideas.md | 33 +++++++++++++++++++++++++++++++++ memory/tasks.md | 1 + 3 files changed, 35 insertions(+) diff --git a/MEMORY.md b/MEMORY.md index 8a437be..7dbf5c8 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -93,3 +93,4 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - Add `(Y/N)` confirmation for long-running, destructive, privacy-affecting, or remote-execution operations. - Keep prompt/history updates append-friendly and date-stamped. - Keep future enhancement ideas in `memory/feature-ideas.md` so they can be reread and prioritized later. +- Treat the `Suggested Priority Order` section in `memory/feature-ideas.md` as the default implementation roadmap until the user asks to reorder it. diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index 16f7569..5f0b557 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -5,6 +5,39 @@ Last updated: 2026-06-13 This file is a living backlog of future enhancements and maintenance ideas for `LDLWinToolBox`. Keep entries concise, append-friendly, and aligned with the Batch-based, menu-driven design. +## Suggested Priority Order + +### Phase 1: Foundation + +1. Extract shared helper labels and common routines +2. Strengthen input validation for all menu prompts +3. Standardize confirmation flow for risky actions +4. Add preflight checks for external commands +5. Improve error handling and user-facing failure messages + +### Phase 2: Safety And Clarity + +1. Improve log readability and section formatting +2. Harmonize menu wording and labels +3. Use clearer section headers in the menu +4. Add progress hints for long-running tasks +5. Make cleanup operations more conservative by default + +### Phase 3: Efficiency And Maintenance + +1. Reduce redundant PowerShell calls +2. Maintain a lightweight verification checklist after changes +3. Keep README, memory, and task notes synchronized +4. Add a read-only mode for status checks + +### Phase 4: Feature Work Enablers + +1. System information summary +2. Create a restore point before risky operations +3. Log export and archive bundle +4. Exportable report of actions and results +5. Version and update check for the toolbox itself + ## New Features - System information summary diff --git a/memory/tasks.md b/memory/tasks.md index ddb3a00..cfd6314 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -6,6 +6,7 @@ Last updated: 2026-06-13 - No open items. - Future enhancement ideas are now tracked in `memory/feature-ideas.md`. +- The `Suggested Priority Order` in `memory/feature-ideas.md` is the default roadmap for the next optimization pass. ## Completed From ae4ad2e4e14a71c99e8c3310f08cf0b3de37c5aa Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sat, 13 Jun 2026 21:43:46 +0800 Subject: [PATCH 14/33] Rewrite tool as Python uv launcher --- AGENTS.md | 10 +- LDLWinToolBox.bat | 639 +------------------------------------ MEMORY.md | 12 +- README.md | 19 +- ldlwintoolbox.py | 680 ++++++++++++++++++++++++++++++++++++++++ memory/2026-06-13.md | 8 + memory/feature-ideas.md | 2 +- memory/tasks.md | 7 +- pyproject.toml | 10 + 9 files changed, 729 insertions(+), 658 deletions(-) create mode 100644 ldlwintoolbox.py create mode 100644 pyproject.toml diff --git a/AGENTS.md b/AGENTS.md index 256cca4..e142236 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ ## Agent Role -You are the AI maintainer for `LDLWinToolBox`, a standalone Windows Batch utility for administrative cleanup, repair, update, network reset, log clearing, and SSD TRIM workflows. +You are the AI maintainer for `LDLWinToolBox`, a Python-first Windows utility with a thin Batch launcher for administrative cleanup, repair, update, network reset, log clearing, and SSD TRIM workflows. Work from repository facts first. Preserve existing history and project decisions unless the user explicitly asks to replace them. @@ -22,13 +22,13 @@ On every new session: - Follow `C:\Users\LoveDoLove\.codex\RTK.md`: prefix shell commands with `rtk`. - Prefer Windows BAT/Command standard commands through `rtk cmd /c ...`. -- Project implementation must remain centered on `.bat` and standard Windows commands. -- Use PowerShell only as a narrow one-line bridge where native Batch lacks the required Windows capability, matching current patterns such as UAC `RunAs`, timestamp generation, disk free-space queries, or volume enumeration. +- Project implementation must remain centered on `ldlwintoolbox.py` with `LDLWinToolBox.bat` as a thin launcher, using Python standard library code and standard Windows commands where appropriate. +- Use PowerShell only as a narrow one-line bridge where Python or native Windows tooling lacks the required Windows capability, matching current patterns such as UAC `RunAs`, timestamp generation, disk free-space queries, or volume enumeration. - Avoid destructive commands during development unless they are scoped, reviewed, and explicitly requested. ## Project Rules -- Main executable: `LDLWinToolBox.bat`. +- Main executable: `LDLWinToolBox.bat` launcher for `ldlwintoolbox.py`. - Keep the app menu-driven and suitable for Windows 10/11. - The script must auto-check Administrator permission and auto-request elevation with UAC before system-level operations. - Preserve timestamped structured logging under `logs\LDLWinToolBox_yyMMddHHmmss.log`. @@ -41,7 +41,7 @@ On every new session: ## Current Implemented Features -Current `LDLWinToolBox.bat` menu implementation: +Current `ldlwintoolbox.py` menu implementation: 1. Advanced System Cleanup with space calculator. 2. System Integrity Repair using `sfc /scannow` and `DISM /RestoreHealth`. diff --git a/LDLWinToolBox.bat b/LDLWinToolBox.bat index d077547..ec40de6 100644 --- a/LDLWinToolBox.bat +++ b/LDLWinToolBox.bat @@ -1,637 +1,4 @@ @echo off -setlocal EnableDelayedExpansion - -:: --- AUTO ADMIN REQUEST --- -fltmc >nul 2>&1 -if errorlevel 1 ( - echo Requesting administrative privileges... - powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" - exit /B -) -pushd "%CD%" -CD /D "%~dp0" -set "SCRIPT_FILE=%~f0" -set "SCRIPT_DIR=%~dp0" -:: --- END AUTO ADMIN --- - -:: --- INITIALIZE LOGGING --- -for /f "delims=" %%a in ('powershell -Command "Get-Date -Format yyMMddHHmmss"') do set "LOG_TIME=%%a" -set "LOG_DIR=%~dp0logs" -if not exist "!LOG_DIR!" md "!LOG_DIR!" >nul 2>&1 -if not exist "!LOG_DIR!" ( - echo Failed to create logs directory. Using script directory for logs. - set "LOG_DIR=%~dp0" -) -set "LOGFILE=!LOG_DIR!\LDLWinToolBox_!LOG_TIME!.log" -call :init_log - -:main_menu -cls -echo =============================================== -echo LDL Windows ToolBox -echo =============================================== -echo [1] Advanced System Cleanup (with Space Calculator) -echo [2] System Integrity Repair (SFC + DISM) -echo [3] Windows Component Store Cleanup (WinSxS) -echo [4] Update All Installed Apps (Winget) -echo [5] Complete Network Reset -echo [6] Clear Event Viewer Logs -echo [7] Manual SSD TRIM (Optimized for KC3000) -echo [8] Disable BitLocker (Plan) -echo [9] Kill Browser AI -echo [10] View Log History -echo [11] Exit -echo =============================================== -echo Log: !LOGFILE! -echo =============================================== -set /p toolbox_choice="Select an option: " -call :log_only INFO "Menu selection: !toolbox_choice!" - -if "!toolbox_choice!"=="1" goto cleanup -if "!toolbox_choice!"=="2" goto sys_repair -if "!toolbox_choice!"=="3" goto win_sxs -if "!toolbox_choice!"=="4" goto app_update -if "!toolbox_choice!"=="5" goto net_reset -if "!toolbox_choice!"=="6" goto event_logs -if "!toolbox_choice!"=="7" goto ssd_trim -if "!toolbox_choice!"=="8" goto bitlocker_disable -if "!toolbox_choice!"=="9" goto kill_browser_ai -if "!toolbox_choice!"=="10" goto log_history -if "!toolbox_choice!"=="11" ( - call :log INFO "Exiting LDL Windows ToolBox." - exit -) -call :log WARN "Invalid menu selection: !toolbox_choice!" -goto main_menu - -:cleanup -cls -echo =============================================== -echo ADVANCED SYSTEM CLEANUP TOOL -echo =============================================== -echo All operations are being logged to: -echo !LOGFILE! -echo =============================================== -echo. -call :log_section "Advanced System Cleanup" -call :log INFO "Calculating current free space..." -for /f "usebackq" %%a in (`powershell -Command "[math]::Round((Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='%SYSTEMDRIVE%'\").FreeSpace / 1MB)"`) do set "free_before_mb=%%a" -call :log_only INFO "Free space before cleanup: !free_before_mb! MB" - -call :log INFO "[1/4] Stopping background services..." - -call :log INFO "- Stopping Windows Update (wuauserv)..." -call :log_command_start "net stop wuauserv" -net stop wuauserv >> "!LOGFILE!" 2>&1 -call :log_result "net stop wuauserv" !errorlevel! - -call :log INFO "- Stopping Background Intelligent Transfer Service (bits)..." -call :log_command_start "net stop bits" -net stop bits >> "!LOGFILE!" 2>&1 -call :log_result "net stop bits" !errorlevel! - -echo. -call :log INFO "[2/4] Deleting temporary and junk files..." - -for %%f in ( - "%WinDir%\Temp\*.*" - "%WinDir%\Prefetch\*.*" - "%Temp%\*.*" - "%AppData%\Temp\*.*" - "%LocalAppdata%\Temp\*.*" - "%WinDir%\SoftwareDistribution\Download\*.*" -) do ( - call :log INFO "- Cleaning %%~f" - call :log_command_start "del /s /f /q %%~f" - del /s /f /q "%%~f" >> "!LOGFILE!" 2>&1 - call :log_result "del /s /f /q %%~f" !errorlevel! -) - -call :log INFO "- Event Viewer logs are handled by menu option 6 using wevtutil." - -for %%d in ( - "%SYSTEMDRIVE%\AMD" - "%SYSTEMDRIVE%\NVIDIA" - "%SYSTEMDRIVE%\INTEL" -) do ( - if exist "%%~d" ( - call :log INFO "- Removing Directory %%~d" - call :log_command_start "rd /s /q %%~d" - rd /s /q "%%~d" >> "!LOGFILE!" 2>&1 - call :log_result "rd /s /q %%~d" !errorlevel! - ) -) - -echo. -call :log INFO "[3/4] Rebuilding directory structure..." -for %%d in ("%WinDir%\Temp" "%WinDir%\Prefetch" "%Temp%" "%AppData%\Temp" "%LocalAppdata%\Temp") do ( - call :log INFO "- Rebuilding %%~d" - call :log_command_start "rd /s /q %%~d" - rd /s /q "%%~d" >> "!LOGFILE!" 2>&1 - call :log_result "rd /s /q %%~d" !errorlevel! - call :log_command_start "md %%~d" - md "%%~d" >> "!LOGFILE!" 2>&1 - call :log_result "md %%~d" !errorlevel! -) - -echo. -call :log INFO "[4/4] Finalizing optimizations..." - -call :log INFO "- Starting Windows Update (wuauserv)..." -call :log_command_start "net start wuauserv" -net start wuauserv >> "!LOGFILE!" 2>&1 -call :log_result "net start wuauserv" !errorlevel! - -call :log INFO "- Starting Background Intelligent Transfer Service (bits)..." -call :log_command_start "net start bits" -net start bits >> "!LOGFILE!" 2>&1 -call :log_result "net start bits" !errorlevel! - -for /f "usebackq" %%a in (`powershell -Command "[math]::Round((Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='%SYSTEMDRIVE%'\").FreeSpace / 1MB)"`) do set "free_after_mb=%%a" -set /a "space_saved_mb=free_after_mb - free_before_mb" -if !space_saved_mb! LSS 0 set "space_saved_mb=0" -call :log_only INFO "Free space after cleanup: !free_after_mb! MB" - -echo. -call :log INFO "SYSTEM CLEAN UP COMPLETE" -call :log INFO "Total Space Freed: !space_saved_mb! MB" -pause -goto main_menu - -:sys_repair -cls -echo =============================================== -echo SYSTEM INTEGRITY REPAIR (SFC + DISM) -echo =============================================== -echo WARNING: This process can take 15-45 minutes. -echo -^> It CAN be safely interrupted by closing the window. -echo -^> However, it is recommended to let it finish. -echo =============================================== -call :log_section "System Integrity Repair" -set /p confirm="Do you want to proceed? (Y/N): " -if /i "!confirm!" NEQ "Y" ( - call :log INFO "System Integrity Repair cancelled by user." - goto main_menu -) - -echo. -call :log INFO "[1/2] Running System File Checker (SFC)..." -call :log_command_start "sfc /scannow" -sfc /scannow >> "!LOGFILE!" 2>&1 -call :log_result "sfc /scannow" !errorlevel! - -call :log INFO "[2/2] Running DISM RestoreHealth..." -call :log_command_start "DISM /Online /Cleanup-Image /RestoreHealth" -DISM /Online /Cleanup-Image /RestoreHealth >> "!LOGFILE!" 2>&1 -call :log_result "DISM /Online /Cleanup-Image /RestoreHealth" !errorlevel! - -echo. -call :log INFO "SYSTEM INTEGRITY REPAIR COMPLETE" -pause -goto main_menu - -:win_sxs -cls -echo =============================================== -echo WINDOWS COMPONENT STORE CLEANUP (WinSxS) -echo =============================================== -echo WARNING: This deeply cleans old Windows Update files. -echo -^> It can take 10-30 minutes and may appear stuck. -echo -^> DO NOT interrupt this process (can corrupt updates). -echo =============================================== -call :log_section "Windows Component Store Cleanup" -set /p confirm="Do you want to proceed? (Y/N): " -if /i "!confirm!" NEQ "Y" ( - call :log INFO "Windows Component Store Cleanup cancelled by user." - goto main_menu -) - -echo. -call :log INFO "Cleaning Windows Component Store..." -call :log_command_start "DISM.exe /Online /Cleanup-Image /StartComponentCleanup" -DISM.exe /Online /Cleanup-Image /StartComponentCleanup >> "!LOGFILE!" 2>&1 -call :log_result "DISM.exe /Online /Cleanup-Image /StartComponentCleanup" !errorlevel! - -echo. -call :log INFO "WINSXS CLEANUP COMPLETE" -pause -goto main_menu - -:app_update -cls -echo =============================================== -echo UPDATE INSTALLED APPS (WINGET) -echo =============================================== -echo WARNING: Silently updates all apps installed via Winget. -echo -^> May take several minutes. -echo -^> It CAN be safely interrupted. -echo =============================================== -call :log_section "Update Installed Apps" -set /p confirm="Do you want to proceed? (Y/N): " -if /i "!confirm!" NEQ "Y" ( - call :log INFO "Update Installed Apps cancelled by user." - goto main_menu -) - -echo. -call :log INFO "Upgrading all installed applications (this may take a while)..." -call :log_command_start "winget upgrade --all" -winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements >> "!LOGFILE!" 2>&1 -call :log_result "winget upgrade --all" !errorlevel! - -echo. -call :log INFO "APP UPDATE COMPLETE" -pause -goto main_menu - -:net_reset -cls -echo =============================================== -echo COMPLETE NETWORK RESET -echo =============================================== -echo This will reset your network adapters to factory defaults. -echo -^> A system restart will be required afterward. -echo =============================================== -call :log_section "Complete Network Reset" -set /p confirm="Do you want to proceed? (Y/N): " -if /i "!confirm!" NEQ "Y" ( - call :log INFO "Complete Network Reset cancelled by user." - goto main_menu -) - -echo. -call :log INFO "Resetting Winsock..." -call :log_command_start "netsh winsock reset" -netsh winsock reset >> "!LOGFILE!" 2>&1 -call :log_result "netsh winsock reset" !errorlevel! - -call :log INFO "Resetting TCP/IP..." -call :log_command_start "netsh int ip reset" -netsh int ip reset >> "!LOGFILE!" 2>&1 -call :log_result "netsh int ip reset" !errorlevel! - -call :log INFO "Flushing DNS..." -call :log_command_start "ipconfig /flushdns" -ipconfig /flushdns >> "!LOGFILE!" 2>&1 -call :log_result "ipconfig /flushdns" !errorlevel! - -echo. -call :log INFO "NETWORK RESET COMPLETE. Please RESTART your computer." -pause -goto main_menu - -:event_logs -cls -echo =============================================== -echo CLEAR EVENT VIEWER LOGS -echo =============================================== -echo All operations are being logged to: -echo !LOGFILE! -echo =============================================== -echo. -call :log_section "Clear Event Viewer Logs" - -for /F "tokens=*" %%G in ('wevtutil.exe el') DO ( - call :log INFO "- Clearing log: %%G" - call :log_command_start "wevtutil.exe cl %%G" - wevtutil.exe cl "%%G" >> "!LOGFILE!" 2>&1 - call :log_result "wevtutil.exe cl %%G" !errorlevel! -) -echo. -call :log INFO "EVENT LOGS CLEARED" -pause -goto main_menu - -:ssd_trim -cls -echo =============================================== -echo MANUAL SSD TRIM TOOL (KC3000) -echo =============================================== -echo All operations are being logged to: -echo !LOGFILE! -echo =============================================== -echo. -call :log_section "Manual SSD TRIM" -echo Current Drives Connected: -call :log_only INFO "Current drives connected:" -powershell -Command "Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} | ft -AutoSize" -powershell -Command "Get-Volume | Where-Object { $_.DriveLetter -ne $null } | Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} | ft -AutoSize" >> "!LOGFILE!" 2>&1 -echo. -choice /c 0ABCDEFGHIJKLMNOPQRSTUVWXYZ /n /m "Press 0 to return, or drive letter to TRIM (A-Z): " -set "drive_choice=!errorlevel!" -if "!drive_choice!"=="1" ( - call :log INFO "Manual SSD TRIM cancelled by user." - goto main_menu -) -call :set_drive_from_choice !drive_choice! -set "trim_drive=!selected_drive!" -call :log_only INFO "Selected TRIM drive: !trim_drive!:" -if not exist "!trim_drive!:\" ( - call :log ERROR "Drive !trim_drive!: was not found." - pause - goto main_menu -) - -echo. -echo ----------------------------------------------- -echo Optimizing Drive !trim_drive!: ... -echo Optimizing Drive !trim_drive!: ... >> "!LOGFILE!" -echo ----------------------------------------------- -call :log_command_start "defrag !trim_drive!: /L /V" -defrag !trim_drive!: /L /V > "%TEMP%\defrag_out.txt" 2>&1 -set "defrag_rc=!errorlevel!" -type "%TEMP%\defrag_out.txt" -type "%TEMP%\defrag_out.txt" >> "!LOGFILE!" -del /q "%TEMP%\defrag_out.txt" >nul 2>&1 -call :log_result "defrag !trim_drive!: /L /V" !defrag_rc! - -echo. -echo ----------------------------------------------- -call :log INFO "SSD TRIM COMPLETE" -echo [1] Return to Menu -echo [2] Exit -set /p final="Choose an option: " -call :log_only INFO "SSD TRIM final selection: !final!" -if "!final!"=="1" goto main_menu -call :log INFO "Exiting LDL Windows ToolBox." -exit - -:bitlocker_disable -cls -echo =============================================== -echo DISABLE BITLOCKER (PLAN) -echo =============================================== -echo WARNING: This starts BitLocker decryption for the -echo selected drive and turns BitLocker off. -echo -^> Decryption can take a long time. -echo -^> Keep the PC powered on until Windows finishes. -echo -^> Do this only when protection is no longer needed. -echo =============================================== -echo. -call :log_section "Disable BitLocker" -where manage-bde.exe >nul 2>&1 -if errorlevel 1 ( - call :log ERROR "manage-bde.exe was not found on this system." - pause - goto main_menu -) - -echo Current BitLocker status: -call :log_only INFO "Current BitLocker status:" -manage-bde -status -call :log_command_start "manage-bde -status" -manage-bde -status >> "!LOGFILE!" 2>&1 -call :log_result "manage-bde -status" !errorlevel! -echo. -choice /c 0ABCDEFGHIJKLMNOPQRSTUVWXYZ /n /m "Press 0 to return, or drive letter to disable BitLocker (A-Z): " -set "drive_choice=!errorlevel!" -if "!drive_choice!"=="1" ( - call :log INFO "Disable BitLocker cancelled by user." - goto main_menu -) -call :set_drive_from_choice !drive_choice! -set "bitlocker_drive=!selected_drive!" -call :log_only INFO "Selected BitLocker drive: !bitlocker_drive!:" -if not exist "!bitlocker_drive!:\" ( - call :log ERROR "Drive !bitlocker_drive!: was not found." - pause - goto main_menu -) - -echo. -echo Selected drive status: -call :log_only INFO "Selected BitLocker drive status for !bitlocker_drive!:" -manage-bde -status !bitlocker_drive!: -call :log_command_start "manage-bde -status !bitlocker_drive!:" -manage-bde -status !bitlocker_drive!: >> "!LOGFILE!" 2>&1 -call :log_result "manage-bde -status !bitlocker_drive!:" !errorlevel! -echo. -set confirm= -set /p confirm="Type DISABLE to start decryption for !bitlocker_drive!: " -if /i "!confirm!" NEQ "DISABLE" ( - call :log INFO "Disable BitLocker confirmation not provided. Returning to menu." - goto main_menu -) - -echo. -call :log INFO "Starting BitLocker decryption on !bitlocker_drive!: ..." -call :log_command_start "manage-bde -off !bitlocker_drive!:" -manage-bde -off !bitlocker_drive!: >> "!LOGFILE!" 2>&1 -set "bitlocker_rc=!errorlevel!" -call :log_result "manage-bde -off !bitlocker_drive!:" !bitlocker_rc! -if not "!bitlocker_rc!"=="0" ( - call :log ERROR "BITLOCKER DISABLE FAILED. Check !LOGFILE!." -) else ( - call :log INFO "BITLOCKER DECRYPTION STARTED. Check Windows BitLocker status for progress." -) - -echo. -echo Updated status: -call :log_only INFO "Updated BitLocker status for !bitlocker_drive!:" -manage-bde -status !bitlocker_drive!: -call :log_command_start "manage-bde -status !bitlocker_drive!:" -manage-bde -status !bitlocker_drive!: >> "!LOGFILE!" 2>&1 -call :log_result "manage-bde -status !bitlocker_drive!:" !errorlevel! -pause -goto main_menu - -:set_drive_from_choice -set "selected_drive=" -if "%~1"=="2" set "selected_drive=A" -if "%~1"=="3" set "selected_drive=B" -if "%~1"=="4" set "selected_drive=C" -if "%~1"=="5" set "selected_drive=D" -if "%~1"=="6" set "selected_drive=E" -if "%~1"=="7" set "selected_drive=F" -if "%~1"=="8" set "selected_drive=G" -if "%~1"=="9" set "selected_drive=H" -if "%~1"=="10" set "selected_drive=I" -if "%~1"=="11" set "selected_drive=J" -if "%~1"=="12" set "selected_drive=K" -if "%~1"=="13" set "selected_drive=L" -if "%~1"=="14" set "selected_drive=M" -if "%~1"=="15" set "selected_drive=N" -if "%~1"=="16" set "selected_drive=O" -if "%~1"=="17" set "selected_drive=P" -if "%~1"=="18" set "selected_drive=Q" -if "%~1"=="19" set "selected_drive=R" -if "%~1"=="20" set "selected_drive=S" -if "%~1"=="21" set "selected_drive=T" -if "%~1"=="22" set "selected_drive=U" -if "%~1"=="23" set "selected_drive=V" -if "%~1"=="24" set "selected_drive=W" -if "%~1"=="25" set "selected_drive=X" -if "%~1"=="26" set "selected_drive=Y" -if "%~1"=="27" set "selected_drive=Z" -exit /b - -:init_log -> "!LOGFILE!" ( - echo =============================================================================== - echo LDL Windows ToolBox Run Log - echo =============================================================================== - echo Session ID : !LOG_TIME! - echo Started : %DATE% %TIME% - echo Script : !SCRIPT_FILE! - echo Script Dir : !SCRIPT_DIR! - echo Work Dir : %CD% - echo User : %USERDOMAIN%\%USERNAME% - echo Computer : %COMPUTERNAME% - echo OS : %OS% - echo SystemRoot : %SystemRoot% - echo Temp : %TEMP% - echo Log File : !LOGFILE! - echo =============================================================================== - echo. -) -call :log_only INFO "Logging initialized." -exit /b - -:log -set "LOG_LEVEL=%~1" -set "LOG_MESSAGE=%~2" -set "LOG_STAMP=%DATE% %TIME%" ->> "!LOGFILE!" echo [!LOG_STAMP!] [!LOG_LEVEL!] !LOG_MESSAGE! -echo !LOG_MESSAGE! -exit /b - -:log_only -set "LOG_LEVEL=%~1" -set "LOG_MESSAGE=%~2" -set "LOG_STAMP=%DATE% %TIME%" ->> "!LOGFILE!" echo [!LOG_STAMP!] [!LOG_LEVEL!] !LOG_MESSAGE! -exit /b - -:log_section -call :log_only INFO "-------------------------------------------------------------------------------" -call :log INFO "== %~1 ==" -exit /b - -:log_command_start -call :log_only CMD "START %~1" -exit /b - -:log_result -set "LOG_COMMAND=%~1" -set "LOG_CODE=%~2" -if "!LOG_CODE!"=="0" ( - call :log_only OK "END !LOG_COMMAND! exit=!LOG_CODE!" -) else ( - call :log WARN "END !LOG_COMMAND! exit=!LOG_CODE! - check log details." -) -exit /b - -:log_history -cls -echo =============================================== -echo VIEW LOG HISTORY -echo =============================================== -echo Log directory: -echo !LOG_DIR! -echo =============================================== -echo. -call :log_section "View Log History" - -set "LOG_LIST=%TEMP%\LDLWinToolBox_logs_%RANDOM%.tmp" -dir /b /o-d "!LOG_DIR!\LDLWinToolBox_*.log" > "!LOG_LIST!" 2>nul -if errorlevel 1 ( - call :log INFO "No log history found." - if exist "!LOG_LIST!" del /q "!LOG_LIST!" >nul 2>&1 - pause - goto main_menu -) - -set "log_count=0" -for %%N in (1 2 3 4 5 6 7 8 9) do set "log_%%N=" -for /f "usebackq delims=" %%L in ("!LOG_LIST!") do ( - if !log_count! LSS 9 ( - set /a "log_count+=1" - set "log_!log_count!=%%L" - for %%A in ("!LOG_DIR!\%%L") do echo [!log_count!] %%L - %%~zA bytes - %%~tA - ) -) -del /q "!LOG_LIST!" >nul 2>&1 - -if "!log_count!"=="0" ( - call :log INFO "No log history found." - pause - goto main_menu -) - -echo. -echo [0] Return to Menu -choice /c 0123456789 /n /m "Press 0 to return, or 1-9 to view a log: " -set "log_choice=!errorlevel!" -if "!log_choice!"=="1" ( - call :log INFO "View Log History returned to menu." - goto main_menu -) -set /a "log_index=!log_choice!-1" -set "selected_log=" -if "!log_index!"=="1" set "selected_log=!log_1!" -if "!log_index!"=="2" set "selected_log=!log_2!" -if "!log_index!"=="3" set "selected_log=!log_3!" -if "!log_index!"=="4" set "selected_log=!log_4!" -if "!log_index!"=="5" set "selected_log=!log_5!" -if "!log_index!"=="6" set "selected_log=!log_6!" -if "!log_index!"=="7" set "selected_log=!log_7!" -if "!log_index!"=="8" set "selected_log=!log_8!" -if "!log_index!"=="9" set "selected_log=!log_9!" - -if "!selected_log!"=="" ( - call :log WARN "Invalid log history selection: !log_index!" - pause - goto log_history -) - -cls -echo =============================================== -echo Viewing Log: -echo !selected_log! -echo =============================================== -echo Path: !LOG_DIR!\!selected_log! -echo =============================================== -echo. -call :log_only INFO "Viewing log history file: !selected_log!" -more "!LOG_DIR!\!selected_log!" -echo. -pause -goto log_history - -:kill_browser_ai -cls -echo =============================================== -echo KILL BROWSER AI -echo =============================================== -echo WARNING: This downloads and executes a remote -echo PowerShell script from the configured gist URL. -echo -^> It may close browser or AI-related processes. -echo -^> Network access is required. -echo -^> Do not run if you do not trust the source. -echo =============================================== -echo. -echo Source: -echo https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 -echo. -call :log_section "Kill Browser AI" -call :log_only WARN "Remote script source: https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1" -set confirm= -set /p confirm="Type KILL to run Kill Browser AI: " -if /i "!confirm!" NEQ "KILL" ( - call :log INFO "Kill Browser AI cancelled by user." - goto main_menu -) - -echo. -call :log INFO "Running Kill Browser AI..." -call :log_command_start "PowerShell remote kill_ai.ps1" -powershell -NoProfile -ExecutionPolicy Bypass -Command "try { iwr -useb 'https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1' | iex; exit 0 } catch { Write-Error $_; exit 1 }" >> "!LOGFILE!" 2>&1 -set "kill_ai_rc=!errorlevel!" -call :log_result "PowerShell remote kill_ai.ps1" !kill_ai_rc! -if not "!kill_ai_rc!"=="0" ( - call :log ERROR "KILL BROWSER AI FAILED. Check !LOGFILE!." -) else ( - call :log INFO "KILL BROWSER AI COMPLETE." -) -pause -goto main_menu +setlocal +cd /d "%~dp0" +uv run -- python ldlwintoolbox.py diff --git a/MEMORY.md b/MEMORY.md index 7dbf5c8..9859c60 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -6,7 +6,7 @@ Last updated: 2026-06-13 - User prefers Chinese-language collaboration when discussing work, while repository documentation may remain English if that matches the existing files. - Always analyze the current project first, then analyze prompt/history files, then apply future rules while preserving history. -- Use Windows BAT/Command standard commands for this project. +- Use Python as the primary implementation language for this project, with Windows command-line tools where appropriate. - Follow RTK command discipline from `C:\Users\LoveDoLove\.codex\RTK.md`; in this PowerShell environment, use `rtk cmd /c ...` for standard Windows commands. - Keep changes surgical and verifiable. Do not refactor unrelated code. - Skill packages under `.agents/skills/` must be cloned from public GitHub open-source skills, not authored manually in this repository. @@ -19,7 +19,7 @@ Last updated: 2026-06-13 - Current branch at scan time: `lovedolove` - Latest scanned commit: `816135b Merge branch 'main' into lovedolove` - License: Apache License 2.0 -- Primary executable: `LDLWinToolBox.bat` +- Primary executable: `LDLWinToolBox.bat` launcher for `ldlwintoolbox.py` - Primary docs: `README.md`, `AGENTS.md`, `MEMORY.md`, `memory/tasks.md` - Backlog notes: `memory/feature-ideas.md` - Prompt/history docs observed as absent at the latest scan: `ANALYSIS.md`, `PROMPT_GUIDE.md` @@ -30,7 +30,7 @@ Last updated: 2026-06-13 ## Current Repository Logic -`LDLWinToolBox.bat` is a standalone menu-driven Windows Batch script. It initializes delayed expansion, checks for Administrator access, relaunches with UAC through PowerShell `Start-Process -Verb RunAs` when needed, switches to the script directory, and creates a timestamped structured log file named `logs\LDLWinToolBox_yyMMddHHmmss.log`. +`LDLWinToolBox.bat` is now a thin launcher that invokes `uv run -- python ldlwintoolbox.py`. The Python entry point initializes the menu, checks for Administrator access, relaunches with UAC when needed, switches to the script directory, and creates a timestamped structured log file named `logs\LDLWinToolBox_yyMMddHHmmss.log`. Logging behavior: @@ -72,7 +72,7 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - `MEMORY.md` records long-term user preferences, repository facts, current logic, risks, and persistent rules. - `memory/tasks.md` tracks cross-session work. - `ANALYSIS.md` and `PROMPT_GUIDE.md` were not present in the latest working tree scan; if restored later, preserve their history and append updates. -- Existing prompt rules emphasize Batch standard, auto-admin preservation, history preservation, input sanitization, clean verbosity, and long-running process warnings. +- Existing prompt rules emphasize auto-admin preservation, history preservation, input sanitization, clean verbosity, and long-running process warnings. ## Known Gaps And Risks @@ -85,8 +85,8 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal ## Persistent Working Rules -- Preserve the app as a single-file Windows Batch tool unless the user explicitly asks for a different architecture. -- Prefer native Windows commands and Batch syntax for implementation. +- Preserve the app as a Python-first Windows utility with a thin Batch launcher unless the user explicitly asks for a different architecture. +- Prefer Python standard library calls and native Windows commands for implementation. - Keep PowerShell calls minimal, one-line, and justified by Windows capability gaps. - Preserve auto-admin behavior and structured timestamped logs under `logs\`. - Keep console messages readable and route verbose command output into the log. diff --git a/README.md b/README.md index ae7821c..30e25ee 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

LDL Windows ToolBox

- A cohesive, menu-driven Windows Batch utility that safely automates advanced system cleanup, integrity repair, component updates, network repair, BitLocker decryption planning, browser AI cleanup, and NVMe SSD optimizations. + A cohesive, menu-driven Windows utility that safely automates advanced system cleanup, integrity repair, component updates, network repair, BitLocker decryption planning, browser AI cleanup, and NVMe SSD optimizations.
Explore the docs »
@@ -60,7 +60,7 @@ ## About The Project -The LDL Windows ToolBox is a standalone Windows Batch script (`LDLWinToolBox.bat`) that effectively combines administrative privileges checks, system garbage collection, script verifications, network reset, BitLocker decryption planning, browser AI cleanup, and SSD TRIM optimization into a single, cohesive menu-driven interface. +The LDL Windows ToolBox is now a Python-first Windows utility powered by `uv`, with `LDLWinToolBox.bat` acting as a thin launcher for `ldlwintoolbox.py`. It combines administrative privileges checks, system cleanup, repair flows, network reset, BitLocker decryption planning, browser AI cleanup, and SSD TRIM optimization into a single, cohesive menu-driven interface. It safely automates otherwise tedious system administration tasks while maintaining comprehensive, timestamped logs (`logs\LDLWinToolBox_yyMMddHHmmss.log`) of all actions to ensure complete historical records and safety. @@ -68,7 +68,8 @@ It safely automates otherwise tedious system administration tasks while maintain ### Built With -- [![Windows Batch][Batch-shield]][Batch-url] +- [![Python][Python-shield]][Python-url] +- [![uv][uv-shield]][uv-url] - [![PowerShell][PowerShell-shield]][PowerShell-url]

(back to top)

@@ -82,7 +83,7 @@ To get a local copy up and running follow these simple steps. ### Prerequisites - Windows 10 or Windows 11 -- Administrator rights (the script will automatically securely request this using `RunAs` if launched without it) +- Administrator rights (the tool will automatically request this using UAC if launched without it) ### Installation @@ -90,7 +91,7 @@ To get a local copy up and running follow these simple steps. ```sh git clone https://github.com/LoveDoLove/LDLWinToolBox.git ``` -2. Double-click on `LDLWinToolBox.bat` to launch the interactive menu. +2. Double-click `LDLWinToolBox.bat` to launch the interactive menu, or run `uv run -- python ldlwintoolbox.py`.

(back to top)

@@ -168,7 +169,7 @@ Project Link: [https://github.com/LoveDoLove/LDLWinToolBox](https://github.com/L ## Acknowledgments - [Best-README-Template](https://github.com/othneildrew/Best-README-Template) -- [RunAs PowerShell Module](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/start-process?view=powershell-7.2) +- [Windows UAC / ShellExecuteW](https://learn.microsoft.com/windows/win32/api/shellapi/nf-shellapi-shellexecutew) - [Winget Tool](https://docs.microsoft.com/en-us/windows/package-manager/winget/)

(back to top)

@@ -186,7 +187,9 @@ Project Link: [https://github.com/LoveDoLove/LDLWinToolBox](https://github.com/L [issues-url]: https://github.com/LoveDoLove/LDLWinToolBox/issues [license-shield]: https://img.shields.io/github/license/LoveDoLove/LDLWinToolBox.svg?style=for-the-badge [license-url]: https://github.com/LoveDoLove/LDLWinToolBox/blob/master/LICENSE -[Batch-shield]: https://img.shields.io/badge/Windows_Batch-0078D6?style=for-the-badge&logo=windows&logoColor=white -[Batch-url]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands +[Python-shield]: https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white +[Python-url]: https://www.python.org/ +[uv-shield]: https://img.shields.io/badge/uv-111111?style=for-the-badge&logo=python&logoColor=white +[uv-url]: https://docs.astral.sh/uv/ [PowerShell-shield]: https://img.shields.io/badge/PowerShell-5391FE?style=for-the-badge&logo=powershell&logoColor=white [PowerShell-url]: https://docs.microsoft.com/en-us/powershell/ diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py new file mode 100644 index 0000000..e5e7e7c --- /dev/null +++ b/ldlwintoolbox.py @@ -0,0 +1,680 @@ +from __future__ import annotations + +import ctypes +import os +import platform +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + + +MENU_LOGO = "=" * 47 +GIST_URL = "https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1" + + +@dataclass(slots=True) +class CommandResult: + code: int + stdout: str = "" + stderr: str = "" + + +class Logger: + def __init__(self, logfile: Path, script_file: Path, script_dir: Path) -> None: + self.logfile = logfile + self.script_file = script_file + self.script_dir = script_dir + + def _stamp(self) -> str: + now = datetime.now() + return now.strftime("%m/%d/%Y %H:%M:%S") + + def write_raw(self, message: str) -> None: + self.logfile.parent.mkdir(parents=True, exist_ok=True) + with self.logfile.open("a", encoding="utf-8", errors="replace", newline="\n") as handle: + handle.write(message) + if not message.endswith("\n"): + handle.write("\n") + + def log_only(self, level: str, message: str) -> None: + self.write_raw(f"[{self._stamp()}] [{level}] {message}") + + def log(self, level: str, message: str) -> None: + self.log_only(level, message) + print(message) + + def section(self, title: str) -> None: + self.log_only("INFO", "-" * 79) + self.log("INFO", f"== {title} ==") + + def command_start(self, command: str) -> None: + self.log_only("CMD", f"START {command}") + + def command_result(self, command: str, code: int) -> None: + if code == 0: + self.log_only("OK", f"END {command} exit={code}") + else: + self.log("WARN", f"END {command} exit={code} - check log details.") + + +def is_admin() -> bool: + try: + return bool(ctypes.windll.shell32.IsUserAnAdmin()) + except Exception: + return False + + +def relaunch_as_admin() -> None: + script = Path(__file__).resolve() + uv_exe = shutil.which("uv") + if uv_exe: + params = f'run -- python "{script}"' + ctypes.windll.shell32.ShellExecuteW(None, "runas", uv_exe, params, None, 1) + return + params = f'"{script}"' + ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, params, None, 1) + + +def ensure_admin() -> None: + if is_admin(): + return + print("Requesting administrative privileges...") + relaunch_as_admin() + raise SystemExit(0) + + +def clear_screen() -> None: + os.system("cls") + + +def command_exists(command: str) -> bool: + return shutil.which(command) is not None + + +def run_command(command: list[str] | str, *, shell: bool = False, capture: bool = True, check: bool = False) -> CommandResult: + completed = subprocess.run( + command, + shell=shell, + text=True, + capture_output=capture, + check=False, + ) + if check and completed.returncode != 0: + raise subprocess.CalledProcessError(completed.returncode, command, completed.stdout, completed.stderr) + return CommandResult(completed.returncode, completed.stdout or "", completed.stderr or "") + + +def run_and_log(logger: Logger, command: list[str] | str, display: str, *, shell: bool = False, capture_output: bool = True) -> CommandResult: + logger.command_start(display) + result = run_command(command, shell=shell, capture=capture_output) + if result.stdout: + logger.write_raw(result.stdout) + if result.stderr: + logger.write_raw(result.stderr) + logger.command_result(display, result.code) + return result + + +def prompt_yes_no(logger: Logger, prompt: str, context: str) -> bool: + answer = input(prompt).strip() + if answer.upper() == "Y": + return True + logger.log_only("INFO", f"{context} cancelled by user.") + return False + + +def prompt_keyword(logger: Logger, prompt: str, expected: str, context: str) -> bool: + answer = input(prompt).strip() + if answer.upper() == expected.upper(): + return True + logger.log_only("INFO", f"{context} cancelled by user.") + return False + + +def prompt_drive(logger: Logger, prompt: str, context: str) -> str | None: + options = "0ABCDEFGHIJKLMNOPQRSTUVWXYZ" + choice = input(prompt).strip().upper() + if choice == "0": + logger.log_only("INFO", f"{context} cancelled by user.") + return None + if len(choice) != 1 or choice not in options: + logger.log_only("WARN", f"Invalid {context} selection: {choice}") + print("Invalid selection.") + return "" + return choice + + +def write_session_header(logger: Logger, logfile: Path, script_file: Path, script_dir: Path) -> None: + now = datetime.now() + header = [ + "=" * 79, + "LDL Windows ToolBox Run Log", + "=" * 79, + f"Session ID : {now.strftime('%y%m%d%H%M%S')}", + f"Started : {now.strftime('%m/%d/%Y %H:%M:%S')}", + f"Script : {script_file}", + f"Script Dir : {script_dir}", + f"Work Dir : {Path.cwd()}", + f"User : {os.environ.get('USERDOMAIN', '')}\\{os.environ.get('USERNAME', '')}", + f"Computer : {os.environ.get('COMPUTERNAME', '')}", + f"OS : {platform.system()}", + f"SystemRoot : {os.environ.get('SystemRoot', '')}", + f"Temp : {tempfile.gettempdir()}", + f"Log File : {logfile}", + "=" * 79, + "", + ] + logger.logfile.parent.mkdir(parents=True, exist_ok=True) + logger.logfile.write_text("\n".join(header), encoding="utf-8", newline="\n") + logger.log_only("INFO", "Logging initialized.") + + +def get_log_dir(script_dir: Path) -> Path: + log_dir = script_dir / "logs" + try: + log_dir.mkdir(parents=True, exist_ok=True) + return log_dir + except OSError: + print("Failed to create logs directory. Using script directory for logs.") + return script_dir + + +def get_volume_table() -> str: + ps = ( + "Get-Volume | Where-Object { $_.DriveLetter -ne $null } " + "| Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, " + "FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} " + "| Format-Table -AutoSize" + ) + result = run_command(["powershell", "-NoProfile", "-Command", ps], capture=True) + return (result.stdout or "") + (result.stderr or "") + + +def select_existing_drive(logger: Logger, context: str) -> str | None: + choice = prompt_drive(logger, "Press 0 to return, or drive letter to continue (A-Z): ", context) + if choice is None: + return None + if choice == "": + return "" + if not Path(f"{choice}:\\").exists(): + logger.log("ERROR", f"Drive {choice}: was not found.") + return "" + return choice + + +def cleanup(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" ADVANCED SYSTEM CLEANUP TOOL") + print(MENU_LOGO) + print(f"All operations are being logged to:\n{logger.logfile}") + print(MENU_LOGO) + logger.section("Advanced System Cleanup") + + free_before = drive_free_mb() + logger.log_only("INFO", f"Free space before cleanup: {free_before} MB") + + logger.log("INFO", "[1/4] Stopping background services...") + for cmd in (["net", "stop", "wuauserv"], ["net", "stop", "bits"]): + logger.log("INFO", f"- Stopping {cmd[-1]}...") + run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + + print() + logger.log("INFO", "[2/4] Deleting temporary and junk files...") + def env_temp_dir(name: str) -> Path | None: + value = os.environ.get(name) + return Path(value) / "Temp" if value else None + + temp_targets = [ + Path(os.environ["WinDir"]) / "Temp", + Path(os.environ["WinDir"]) / "Prefetch", + Path(os.environ["TEMP"]), + env_temp_dir("AppData"), + env_temp_dir("LocalAppData"), + Path(os.environ["WinDir"]) / "SoftwareDistribution" / "Download", + ] + for target in temp_targets: + if target is None: + continue + logger.log("INFO", f"- Cleaning {target}") + if target.exists(): + for child in target.iterdir(): + try: + if child.is_dir(): + shutil.rmtree(child, ignore_errors=False) + else: + child.unlink(missing_ok=True) + except OSError as exc: + logger.log_only("WARN", f"Failed to remove {child}: {exc}") + + logger.log("INFO", "- Event Viewer logs are handled by menu option 6 using wevtutil.") + system_drive = os.environ.get("SYSTEMDRIVE", "C:") + for root_name in ("AMD", "NVIDIA", "INTEL"): + root = Path(f"{system_drive}\\{root_name}") + if root.exists(): + logger.log("INFO", f"- Removing Directory {root}") + shutil.rmtree(root, ignore_errors=True) + + print() + logger.log("INFO", "[3/4] Rebuilding directory structure...") + for target in temp_targets[:5]: + if target is None: + continue + logger.log("INFO", f"- Rebuilding {target}") + target.mkdir(parents=True, exist_ok=True) + + print() + logger.log("INFO", "[4/4] Finalizing optimizations...") + for cmd in (["net", "start", "wuauserv"], ["net", "start", "bits"]): + logger.log("INFO", f"- Starting {cmd[-1]}...") + run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + + free_after = drive_free_mb() + saved = max(0, free_after - free_before) + logger.log_only("INFO", f"Free space after cleanup: {free_after} MB") + logger.log("INFO", "SYSTEM CLEAN UP COMPLETE") + logger.log("INFO", f"Total Space Freed: {saved} MB") + input("Press Enter to continue...") + + +def drive_free_mb() -> int: + ps = ( + f"[math]::Round((Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='{os.environ.get('SYSTEMDRIVE', 'C:')}'\").FreeSpace / 1MB)" + ) + result = run_command(["powershell", "-Command", ps], capture=True) + text = (result.stdout or "").strip() + try: + return int(float(text)) + except ValueError: + return 0 + + +def sys_repair(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" SYSTEM INTEGRITY REPAIR (SFC + DISM)") + print(MENU_LOGO) + print("WARNING: This process can take 15-45 minutes.") + print("-> It CAN be safely interrupted by closing the window.") + print("-> However, it is recommended to let it finish.") + print(MENU_LOGO) + logger.section("System Integrity Repair") + if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "System Integrity Repair"): + return + for cmd, label in ((["sfc", "/scannow"], "System File Checker"), (["dism", "/Online", "/Cleanup-Image", "/RestoreHealth"], "DISM RestoreHealth")): + if not command_exists(cmd[0]): + logger.log("ERROR", f"{cmd[0]} is required for {label}, but it was not found.") + input("Press Enter to continue...") + return + logger.log("INFO", f"Running {label}...") + run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + logger.log("INFO", "SYSTEM INTEGRITY REPAIR COMPLETE") + input("Press Enter to continue...") + + +def component_store_cleanup(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" WINDOWS COMPONENT STORE CLEANUP (WinSxS)") + print(MENU_LOGO) + print("WARNING: This deeply cleans old Windows Update files.") + print("-> It can take 10-30 minutes and may appear stuck.") + print("-> DO NOT interrupt this process (can corrupt updates).") + print(MENU_LOGO) + logger.section("Windows Component Store Cleanup") + if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "Windows Component Store Cleanup"): + return + if not command_exists("dism"): + logger.log("ERROR", "dism is required for DISM component cleanup, but it was not found.") + input("Press Enter to continue...") + return + logger.log("INFO", "Cleaning Windows Component Store...") + run_and_log(logger, ["dism", "/Online", "/Cleanup-Image", "/StartComponentCleanup"], "DISM.exe /Online /Cleanup-Image /StartComponentCleanup") + logger.log("INFO", "WINSXS CLEANUP COMPLETE") + input("Press Enter to continue...") + + +def app_update(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" UPDATE INSTALLED APPS (WINGET)") + print(MENU_LOGO) + print("WARNING: Silently updates all apps installed via Winget.") + print("-> May take several minutes.") + print("-> It CAN be safely interrupted.") + print(MENU_LOGO) + logger.section("Update Installed Apps") + if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "Update Installed Apps"): + return + if not command_exists("winget"): + logger.log("ERROR", "winget is required for Winget update, but it was not found.") + input("Press Enter to continue...") + return + logger.log("INFO", "Upgrading all installed applications (this may take a while)...") + run_and_log( + logger, + ["winget", "upgrade", "--all", "--include-unknown", "--accept-package-agreements", "--accept-source-agreements"], + "winget upgrade --all", + ) + logger.log("INFO", "APP UPDATE COMPLETE") + input("Press Enter to continue...") + + +def net_reset(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" COMPLETE NETWORK RESET") + print(MENU_LOGO) + print("This will reset your network adapters to factory defaults.") + print("-> A system restart will be required afterward.") + print(MENU_LOGO) + logger.section("Complete Network Reset") + if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "Complete Network Reset"): + return + for cmd, label in ((["netsh", "winsock", "reset"], "netsh winsock reset"), (["netsh", "int", "ip", "reset"], "netsh int ip reset"), (["ipconfig", "/flushdns"], "ipconfig /flushdns")): + if not command_exists(cmd[0]): + logger.log("ERROR", f"{cmd[0]} is required for {label}, but it was not found.") + input("Press Enter to continue...") + return + logger.log("INFO", label.replace("netsh ", "Resetting ").replace("ipconfig ", "Flushing ")) + run_and_log(logger, cmd, label) + logger.log("INFO", "NETWORK RESET COMPLETE. Please RESTART your computer.") + input("Press Enter to continue...") + + +def event_logs(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" CLEAR EVENT VIEWER LOGS") + print(MENU_LOGO) + print(f"All operations are being logged to:\n{logger.logfile}") + print(MENU_LOGO) + logger.section("Clear Event Viewer Logs") + if not command_exists("wevtutil"): + logger.log("ERROR", "wevtutil.exe is required for Event Viewer logs, but it was not found.") + input("Press Enter to continue...") + return + result = run_command(["wevtutil", "el"], capture=True) + logs = [line.strip() for line in result.stdout.splitlines() if line.strip()] + for entry in logs: + logger.log("INFO", f"- Clearing log: {entry}") + run_and_log(logger, ["wevtutil", "cl", entry], f"wevtutil.exe cl {entry}") + logger.log("INFO", "EVENT LOGS CLEARED") + input("Press Enter to continue...") + + +def ssd_trim(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" MANUAL SSD TRIM TOOL (KC3000)") + print(MENU_LOGO) + print(f"All operations are being logged to:\n{logger.logfile}") + print(MENU_LOGO) + logger.section("Manual SSD TRIM") + print("Current Drives Connected:") + logger.log_only("INFO", "Current drives connected:") + if not command_exists("powershell"): + logger.log("ERROR", "powershell.exe is required for Volume enumeration, but it was not found.") + input("Press Enter to continue...") + return + volume_text = get_volume_table() + print(volume_text, end="" if volume_text.endswith("\n") else "\n") + logger.write_raw(volume_text) + print() + drive = select_existing_drive(logger, "Manual SSD TRIM") + if drive is None: + return + if drive == "": + logger.log("ERROR", "No valid drive was selected for Manual SSD TRIM.") + input("Press Enter to continue...") + return + logger.log_only("INFO", f"Selected TRIM drive: {drive}:") + print(f"\nOptimizing Drive {drive}: ...") + logger.write_raw(f"Optimizing Drive {drive}: ...") + print("".join(["-" for _ in range(47)])) + if not command_exists("defrag"): + logger.log("ERROR", "defrag.exe is required for SSD TRIM, but it was not found.") + input("Press Enter to continue...") + return + out_file = Path(tempfile.gettempdir()) / "defrag_out.txt" + result = run_command(["defrag", f"{drive}:", "/L", "/V"], capture=True) + out_file.write_text((result.stdout or "") + (result.stderr or ""), encoding="utf-8", errors="replace") + print(out_file.read_text(encoding="utf-8", errors="replace"), end="") + logger.write_raw(out_file.read_text(encoding="utf-8", errors="replace")) + try: + out_file.unlink() + except OSError: + pass + logger.command_result(f"defrag {drive}: /L /V", result.code) + logger.log("INFO", "SSD TRIM COMPLETE") + print("[1] Return to Menu") + print("[2] Exit") + final = input("Choose an option: ").strip() + logger.log_only("INFO", f"SSD TRIM final selection: {final}") + if final == "2": + raise SystemExit(0) + + +def bitlocker_disable(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" DISABLE BITLOCKER (PLAN)") + print(MENU_LOGO) + print("WARNING: This starts BitLocker decryption for the") + print("selected drive and turns BitLocker off.") + print("-> Decryption can take a long time.") + print("-> Keep the PC powered on until Windows finishes.") + print("-> Do this only when protection is no longer needed.") + print(MENU_LOGO) + logger.section("Disable BitLocker") + if not command_exists("manage-bde"): + logger.log("ERROR", "manage-bde.exe is required for BitLocker management, but it was not found.") + input("Press Enter to continue...") + return + print("Current BitLocker status:") + logger.log_only("INFO", "Current BitLocker status:") + status_result = run_and_log(logger, ["manage-bde", "-status"], "manage-bde -status") + if status_result.stdout: + print(status_result.stdout, end="" if status_result.stdout.endswith("\n") else "\n") + if status_result.stderr: + print(status_result.stderr, end="" if status_result.stderr.endswith("\n") else "\n") + print() + drive = select_existing_drive(logger, "Disable BitLocker") + if drive is None: + return + if drive == "": + logger.log("ERROR", "No valid drive was selected for Disable BitLocker.") + input("Press Enter to continue...") + return + logger.log_only("INFO", f"Selected BitLocker drive: {drive}:") + print("\nSelected drive status:") + logger.log_only("INFO", f"Selected BitLocker drive status for {drive}:") + status_result = run_and_log(logger, ["manage-bde", "-status", f"{drive}:"], f"manage-bde -status {drive}:") + if status_result.stdout: + print(status_result.stdout, end="" if status_result.stdout.endswith("\n") else "\n") + if status_result.stderr: + print(status_result.stderr, end="" if status_result.stderr.endswith("\n") else "\n") + print() + if not prompt_keyword(logger, f"Type DISABLE to start decryption for {drive}: ", "DISABLE", "Disable BitLocker"): + return + logger.log("INFO", f"Starting BitLocker decryption on {drive}: ...") + result = run_and_log(logger, ["manage-bde", "-off", f"{drive}:"], f"manage-bde -off {drive}:") + if result.code != 0: + logger.log("ERROR", "BITLOCKER DISABLE FAILED. Check log.") + else: + logger.log("INFO", "BITLOCKER DECRYPTION STARTED. Check Windows BitLocker status for progress.") + print("\nUpdated status:") + logger.log_only("INFO", f"Updated BitLocker status for {drive}:") + status_result = run_and_log(logger, ["manage-bde", "-status", f"{drive}:"], f"manage-bde -status {drive}:") + if status_result.stdout: + print(status_result.stdout, end="" if status_result.stdout.endswith("\n") else "\n") + if status_result.stderr: + print(status_result.stderr, end="" if status_result.stderr.endswith("\n") else "\n") + input("Press Enter to continue...") + + +def list_log_history(log_dir: Path, logger: Logger) -> list[Path]: + entries = sorted(log_dir.glob("LDLWinToolBox_*.log"), key=lambda path: path.stat().st_mtime, reverse=True) + return entries[:9] + + +def log_history(logger: Logger, log_dir: Path) -> None: + clear_screen() + print(MENU_LOGO) + print(" VIEW LOG HISTORY") + print(MENU_LOGO) + print(f"Log directory:\n{log_dir}") + print(MENU_LOGO) + logger.section("View Log History") + if not command_exists("more"): + logger.log("ERROR", "more.com is required for Log History viewer, but it was not found.") + input("Press Enter to continue...") + return + logs = list_log_history(log_dir, logger) + if not logs: + logger.log("INFO", "No log history found.") + input("Press Enter to continue...") + return + for idx, path in enumerate(logs, start=1): + stat = path.stat() + ts = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M") + print(f"[{idx}] {path.name} - {stat.st_size} bytes - {ts}") + print() + print("[0] Return to Menu") + choice = input("Press 0 to return, or 1-9 to view a log: ").strip() + if choice == "0": + logger.log("INFO", "View Log History returned to menu.") + return + try: + index = int(choice) - 1 + except ValueError: + logger.log("WARN", f"Invalid log history selection: {choice}") + input("Press Enter to continue...") + return + if index < 0 or index >= len(logs): + logger.log("WARN", f"Invalid log history selection: {choice}") + input("Press Enter to continue...") + return + selected = logs[index] + print(MENU_LOGO) + print("Viewing Log:") + print(selected.name) + print(MENU_LOGO) + print(f"Path: {selected}") + print(MENU_LOGO) + logger.log_only("INFO", f"Viewing log history file: {selected.name}") + subprocess.run(["more", str(selected)], shell=False) + input("Press Enter to continue...") + + +def kill_browser_ai(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" KILL BROWSER AI") + print(MENU_LOGO) + print("WARNING: This downloads and executes a remote") + print("PowerShell script from the configured gist URL.") + print("-> It may close browser or AI-related processes.") + print("-> Network access is required.") + print("-> Do not run if you do not trust the source.") + print(MENU_LOGO) + print("Source:") + print(GIST_URL) + print() + logger.section("Kill Browser AI") + logger.log_only("WARN", f"Remote script source: {GIST_URL}") + if not prompt_keyword(logger, "Type KILL to run Kill Browser AI: ", "KILL", "Kill Browser AI"): + return + if not command_exists("powershell"): + logger.log("ERROR", "powershell.exe is required for Kill Browser AI, but it was not found.") + input("Press Enter to continue...") + return + logger.log("INFO", "Running Kill Browser AI...") + cmd = [ + "powershell", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + f"try {{ iwr -useb '{GIST_URL}' | iex; exit 0 }} catch {{ Write-Error $_; exit 1 }}", + ] + result = run_and_log(logger, cmd, "PowerShell remote kill_ai.ps1") + if result.code != 0: + logger.log("ERROR", "KILL BROWSER AI FAILED. Check log.") + else: + logger.log("INFO", "KILL BROWSER AI COMPLETE.") + input("Press Enter to continue...") + + +def main_menu(logger: Logger, log_dir: Path) -> None: + while True: + clear_screen() + print("===============================================") + print(" LDL Windows ToolBox") + print("===============================================") + print("[1] Advanced System Cleanup (with Space Calculator)") + print("[2] System Integrity Repair (SFC + DISM)") + print("[3] Windows Component Store Cleanup (WinSxS)") + print("[4] Update All Installed Apps (Winget)") + print("[5] Complete Network Reset") + print("[6] Clear Event Viewer Logs") + print("[7] Manual SSD TRIM (Optimized for KC3000)") + print("[8] Disable BitLocker (Plan)") + print("[9] Kill Browser AI") + print("[10] View Log History") + print("[11] Exit") + print("===============================================") + print(f"Log: {logger.logfile}") + print("===============================================") + choice = input("Select an option: ").strip() + logger.log_only("INFO", f"Menu selection: {choice}") + + if choice == "1": + cleanup(logger) + elif choice == "2": + sys_repair(logger) + elif choice == "3": + component_store_cleanup(logger) + elif choice == "4": + app_update(logger) + elif choice == "5": + net_reset(logger) + elif choice == "6": + event_logs(logger) + elif choice == "7": + ssd_trim(logger) + elif choice == "8": + bitlocker_disable(logger) + elif choice == "9": + kill_browser_ai(logger) + elif choice == "10": + log_history(logger, log_dir) + elif choice == "11": + logger.log("INFO", "Exiting LDL Windows ToolBox.") + return + else: + logger.log("WARN", f"Invalid menu selection: {choice}") + + +def main() -> None: + ensure_admin() + script_file = Path(__file__).resolve() + script_dir = script_file.parent + os.chdir(script_dir) + log_dir = get_log_dir(script_dir) + log_time = datetime.now().strftime("%y%m%d%H%M%S") + logfile = log_dir / f"LDLWinToolBox_{log_time}.log" + logger = Logger(logfile, script_file, script_dir) + write_session_header(logger, logfile, script_file, script_dir) + try: + main_menu(logger, log_dir) + except KeyboardInterrupt: + logger.log("INFO", "User cancelled the session with Ctrl+C.") + + +if __name__ == "__main__": + main() diff --git a/memory/2026-06-13.md b/memory/2026-06-13.md index be6963c..854e2a1 100644 --- a/memory/2026-06-13.md +++ b/memory/2026-06-13.md @@ -7,6 +7,14 @@ - Confirmed the toolbox still centers on the same 11 menu actions, structured logging, and auto-admin elevation flow documented in the existing memory snapshot. - Retrieved and reviewed the remote `kill_ai.ps1` gist source without executing it. - Confirmed there is no tracked repo-local `.agents/skills/` directory to install for current work. +- Began Phase 1 of the optimization roadmap by adding shared Batch helpers for command preflight checks, confirmation prompts, and drive selection. +- Refactored the main confirmation-driven menu actions to use shared helper labels and added command preflight checks for SFC, DISM, Winget, Network Reset, Event Viewer clearing, SSD TRIM, and BitLocker flows. +- Added preflight checks for `more.com` in Log History and `powershell.exe` in Kill Browser AI. +- The current shell session is not elevated, so live smoke testing of the updated Batch flow still needs an administrator session. +- Fixed the BitLocker and SSD TRIM selection paths so an empty drive value cannot leak into `manage-bde`, which was causing the `":" was not understood` syntax error. +- Shifted the project architecture to Python-first by adding `ldlwintoolbox.py`, a `pyproject.toml` entry, and a thin `LDLWinToolBox.bat` launcher that calls `uv run -- python ldlwintoolbox.py`. +- Updated the README and repository instructions to describe the new Python/uv entry point while preserving the existing 11 menu actions and logging behavior. +- Kept the high-risk remote `kill_ai.ps1` workflow guarded behind the same `KILL` confirmation in the Python implementation. ## Decisions diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index 5f0b557..228614c 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -3,7 +3,7 @@ Last updated: 2026-06-13 This file is a living backlog of future enhancements and maintenance ideas for `LDLWinToolBox`. -Keep entries concise, append-friendly, and aligned with the Batch-based, menu-driven design. +Keep entries concise, append-friendly, and aligned with the Python-first, menu-driven design. ## Suggested Priority Order diff --git a/memory/tasks.md b/memory/tasks.md index cfd6314..92a00fd 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -5,8 +5,6 @@ Last updated: 2026-06-13 ## Pending - No open items. -- Future enhancement ideas are now tracked in `memory/feature-ideas.md`. -- The `Suggested Priority Order` in `memory/feature-ideas.md` is the default roadmap for the next optimization pass. ## Completed @@ -20,3 +18,8 @@ Last updated: 2026-06-13 - [x] 2026-06-07: Added read-only `View Log History` menu option for recent `logs\LDLWinToolBox_*.log` files. - [x] 2026-06-09: Rescanned current repository logic and updated `AGENTS.md`, `MEMORY.md`, and memory history. - [x] 2026-06-13: Restored session state, rescanned repository facts, retrieved and reviewed the remote `kill_ai.ps1` gist source, and confirmed no repo-local skills were needed for current work. +- [x] 2026-06-13: Added shared Batch helpers for command preflight checks, confirmation prompts, and drive selection; refactored confirmation-driven menu actions to use them. +- [x] 2026-06-13: Fixed empty-drive propagation in the BitLocker and SSD TRIM flows after the `":" was not understood` error surfaced. +- [x] 2026-06-13: Reworked the project into a Python-first utility with `ldlwintoolbox.py`, `pyproject.toml`, and a thin `LDLWinToolBox.bat` launcher. +- [x] 2026-06-13: Updated README, AGENTS, MEMORY, and the daily work log to describe the Python/uv entry point. +- [x] 2026-06-13: Replaced the old Batch core with a Python implementation while keeping the launcher, logging, confirmations, and guarded remote command flow. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4cf712b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "ldlwintoolbox" +version = "0.1.0" +description = "LDL Windows ToolBox in Python" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] + +[tool.uv] +package = false From 3b5e50e9e8146b5e37e191b1fe09da5aa2f7fe56 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Tue, 16 Jun 2026 23:33:02 +0800 Subject: [PATCH 15/33] Complete toolbox safety features --- AGENTS.md | 12 ++++++------ MEMORY.md | 18 +++++++++--------- ldlwintoolbox.py | 6 ++---- uv.lock | 8 ++++++++ 4 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 uv.lock diff --git a/AGENTS.md b/AGENTS.md index e142236..0e27d91 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,14 +21,14 @@ On every new session: ## Command Rules - Follow `C:\Users\LoveDoLove\.codex\RTK.md`: prefix shell commands with `rtk`. -- Prefer Windows BAT/Command standard commands through `rtk cmd /c ...`. +- Prefer `rtk cmd /c ...` for standard Windows command-line utilities and keep implementation changes aligned with the Python entry point. - Project implementation must remain centered on `ldlwintoolbox.py` with `LDLWinToolBox.bat` as a thin launcher, using Python standard library code and standard Windows commands where appropriate. - Use PowerShell only as a narrow one-line bridge where Python or native Windows tooling lacks the required Windows capability, matching current patterns such as UAC `RunAs`, timestamp generation, disk free-space queries, or volume enumeration. - Avoid destructive commands during development unless they are scoped, reviewed, and explicitly requested. ## Project Rules -- Main executable: `LDLWinToolBox.bat` launcher for `ldlwintoolbox.py`. +- Main executable: `LDLWinToolBox.bat` thin launcher for `ldlwintoolbox.py` via `uv run -- python`. - Keep the app menu-driven and suitable for Windows 10/11. - The script must auto-check Administrator permission and auto-request elevation with UAC before system-level operations. - Preserve timestamped structured logging under `logs\LDLWinToolBox_yyMMddHHmmss.log`. @@ -43,17 +43,17 @@ On every new session: Current `ldlwintoolbox.py` menu implementation: -1. Advanced System Cleanup with space calculator. +1. Advanced System Cleanup with a free-space calculator, Windows/user temp cleanup, `Prefetch`, `SoftwareDistribution\Download`, and vendor driver root cleanup. 2. System Integrity Repair using `sfc /scannow` and `DISM /RestoreHealth`. 3. Windows Component Store Cleanup using `DISM /StartComponentCleanup`. 4. Update all installed apps using `winget upgrade --all`. 5. Complete Network Reset using Winsock, TCP/IP reset, and DNS flush. 6. Clear Event Viewer Logs using `wevtutil`. 7. Manual SSD TRIM using `defrag /L /V`. -8. Disable BitLocker `(Plan)` using `manage-bde -status` and guarded `manage-bde -off :`. +8. Disable BitLocker `(Plan)` using `manage-bde -status`, drive validation, `DISABLE` confirmation, and guarded `manage-bde -off :`. 9. Kill Browser AI using the user-specified command: `iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex` -10. View Log History using a read-only paged console viewer for recent `logs\LDLWinToolBox_*.log` files. +10. View Log History using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. 11. Exit. Remote script execution is high risk. Do not run this command during development. If it is implemented as a menu feature, add an explicit warning and confirmation before execution. @@ -63,7 +63,7 @@ Remote script execution is high risk. Do not run this command during development - Before coding, reviewing, or refactoring, check for applicable local skills under `.agents/skills/`. - Repository-local skill packages must be cloned from public GitHub open-source skills. Do not hand-write custom skill packages in this repo. - For every installed repo-local skill, preserve upstream provenance: source URL, commit or tag, and license. -- Current scan on 2026-06-09 found no tracked `.agents/skills/` directory in this repository. +- Current scan on 2026-06-16 found no tracked `.agents/skills/` directory in this repository. - The session-level `karpathy-guidelines` skill exists outside this repo and may be used for disciplined coding behavior, but it is not currently a repo-local cloned skill asset. ## Memory Files diff --git a/MEMORY.md b/MEMORY.md index 9859c60..47c0cbb 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,6 +1,6 @@ # MEMORY.md -Last updated: 2026-06-13 +Last updated: 2026-06-16 ## User Preferences @@ -17,9 +17,10 @@ Last updated: 2026-06-13 - Repository path: `D:\Projects\WinProjects\LDLWinToolBox` - Git remote: `https://github.com/LoveDoLove/LDLWinToolBox.git` - Current branch at scan time: `lovedolove` -- Latest scanned commit: `816135b Merge branch 'main' into lovedolove` +- Latest scanned commit: `ae4ad2e Rewrite tool as Python uv launcher` - License: Apache License 2.0 -- Primary executable: `LDLWinToolBox.bat` launcher for `ldlwintoolbox.py` +- Primary executable: `LDLWinToolBox.bat` thin launcher for `ldlwintoolbox.py` via `uv run -- python` +- Packaging metadata: `pyproject.toml`, `uv.lock` - Primary docs: `README.md`, `AGENTS.md`, `MEMORY.md`, `memory/tasks.md` - Backlog notes: `memory/feature-ideas.md` - Prompt/history docs observed as absent at the latest scan: `ANALYSIS.md`, `PROMPT_GUIDE.md` @@ -30,7 +31,7 @@ Last updated: 2026-06-13 ## Current Repository Logic -`LDLWinToolBox.bat` is now a thin launcher that invokes `uv run -- python ldlwintoolbox.py`. The Python entry point initializes the menu, checks for Administrator access, relaunches with UAC when needed, switches to the script directory, and creates a timestamped structured log file named `logs\LDLWinToolBox_yyMMddHHmmss.log`. +`LDLWinToolBox.bat` is now a thin launcher that invokes `uv run -- python ldlwintoolbox.py`. The Python entry point initializes the menu, checks for Administrator access, relaunches with UAC when needed, prefers `uv` when available and falls back to `sys.executable`, switches to the script directory, and creates a timestamped structured log file named `logs\LDLWinToolBox_yyMMddHHmmss.log`. Logging behavior: @@ -39,7 +40,7 @@ Logging behavior: - Uses helper labels for `INFO`, `WARN`, `ERROR`, `CMD`, and `OK` log entries. - Records feature section boundaries, menu selections, user cancellations, key command starts, command exit codes, and major completion messages. - Keeps raw command output in the same log file while keeping console output concise. -- Provides a read-only Log History viewer that lists recent logs newest-first and opens a selected file with `more`. +- Provides a read-only Log History viewer that lists recent logs newest-first, caps the picker at the latest 9 entries, and opens a selected file with `more`. Implemented menu behavior: @@ -76,12 +77,11 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal ## Known Gaps And Risks -- The admin-check path issue was fixed by replacing the typo-prone `cacls` path check with `fltmc`. +- The current Python launcher/elevation flow uses `IsUserAnAdmin()` plus `ShellExecuteW(..., "runas", ...)`; keep both the `uv` and `sys.executable` launch paths working. - Cleanup no longer deletes Event Viewer log files directly; option 6 remains the safe `wevtutil` path for clearing logs. -- The Kill Browser AI gist content could not be verified from the local environment during implementation; keep the `KILL` confirmation and source warning. -- The remote `kill_ai.ps1` gist was later retrieved on 2026-06-13; it disables Chrome and Edge on-device AI by applying registry policy keys and locking the `OptGuideOnDeviceModel` folders, but it still remains high risk and must not be executed automatically during analysis. +- The remote `kill_ai.ps1` gist was retrieved and reviewed on 2026-06-13; it disables Chrome and Edge on-device AI by applying registry policy keys and locking the `OptGuideOnDeviceModel` folders, but it still remains high risk and must not be executed automatically during analysis. - `BLANK_README.md` is present locally but ignored by git and appears to be an unused Best-README-Template source file. -- No tracked `.agents/skills/` directory exists at the 2026-06-09 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. +- No tracked `.agents/skills/` directory exists at the 2026-06-16 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. ## Persistent Working Rules diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index e5e7e7c..e9651a9 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -24,10 +24,8 @@ class CommandResult: class Logger: - def __init__(self, logfile: Path, script_file: Path, script_dir: Path) -> None: + def __init__(self, logfile: Path) -> None: self.logfile = logfile - self.script_file = script_file - self.script_dir = script_dir def _stamp(self) -> str: now = datetime.now() @@ -668,7 +666,7 @@ def main() -> None: log_dir = get_log_dir(script_dir) log_time = datetime.now().strftime("%y%m%d%H%M%S") logfile = log_dir / f"LDLWinToolBox_{log_time}.log" - logger = Logger(logfile, script_file, script_dir) + logger = Logger(logfile) write_session_header(logger, logfile, script_file, script_dir) try: main_menu(logger, log_dir) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..803b19f --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "ldlwintoolbox" +version = "0.1.0" +source = { virtual = "." } From 9881a64abc1ebe3e9d8d0d80830630e5a152fa8e Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Tue, 16 Jun 2026 23:55:56 +0800 Subject: [PATCH 16/33] Refresh repository memory snapshot --- AGENTS.md | 2 +- MEMORY.md | 9 +++++---- memory/2026-06-16.md | 12 ++++++++++++ memory/tasks.md | 3 ++- 4 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 memory/2026-06-16.md diff --git a/AGENTS.md b/AGENTS.md index 0e27d91..42141aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ Current `ldlwintoolbox.py` menu implementation: 7. Manual SSD TRIM using `defrag /L /V`. 8. Disable BitLocker `(Plan)` using `manage-bde -status`, drive validation, `DISABLE` confirmation, and guarded `manage-bde -off :`. 9. Kill Browser AI using the user-specified command: - `iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex` + `powershell -NoProfile -ExecutionPolicy Bypass -Command "try { iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex; exit 0 } catch { Write-Error $_; exit 1 }"` 10. View Log History using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. 11. Exit. diff --git a/MEMORY.md b/MEMORY.md index 47c0cbb..390e248 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -17,7 +17,8 @@ Last updated: 2026-06-16 - Repository path: `D:\Projects\WinProjects\LDLWinToolBox` - Git remote: `https://github.com/LoveDoLove/LDLWinToolBox.git` - Current branch at scan time: `lovedolove` -- Latest scanned commit: `ae4ad2e Rewrite tool as Python uv launcher` +- Latest scanned commit: `3b5e50e Complete toolbox safety features` +- Latest repository scan: `2026-06-16`; the working tree was clean at the start of the documentation refresh. - License: Apache License 2.0 - Primary executable: `LDLWinToolBox.bat` thin launcher for `ldlwintoolbox.py` via `uv run -- python` - Packaging metadata: `pyproject.toml`, `uv.lock` @@ -52,7 +53,7 @@ Implemented menu behavior: 6. Clear Event Viewer Logs: enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. 7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes and validates a single drive letter, confirms the drive exists, runs `defrag : /L /V`, displays output, and appends it to the log. 8. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. -9. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then runs the configured gist command and logs the result. +9. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then launches PowerShell with `-ExecutionPolicy Bypass` and a guarded `try/catch` wrapper around the configured gist command so the result is logged. 10. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. 11. Exit: closes the tool. @@ -62,7 +63,7 @@ The user-listed feature targets below were implemented in `LDLWinToolBox.bat` on - Disable BitLocker `[Plan]` with status display, drive validation, and `DISABLE` confirmation. - Kill Browser AI using: - `iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex` + `powershell -NoProfile -ExecutionPolicy Bypass -Command "try { iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex; exit 0 } catch { Write-Error $_; exit 1 }"` Treat the remote `iwr | iex` command as high risk. Do not execute it during analysis. The menu feature requires a clear warning, `KILL` confirmation, and logging. @@ -79,7 +80,7 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - The current Python launcher/elevation flow uses `IsUserAnAdmin()` plus `ShellExecuteW(..., "runas", ...)`; keep both the `uv` and `sys.executable` launch paths working. - Cleanup no longer deletes Event Viewer log files directly; option 6 remains the safe `wevtutil` path for clearing logs. -- The remote `kill_ai.ps1` gist was retrieved and reviewed on 2026-06-13; it disables Chrome and Edge on-device AI by applying registry policy keys and locking the `OptGuideOnDeviceModel` folders, but it still remains high risk and must not be executed automatically during analysis. +- The remote `kill_ai.ps1` gist was retrieved and reviewed on 2026-06-13; it disables Chrome and Edge on-device AI by applying registry policy keys and locking the `OptGuideOnDeviceModel` folders, but it still remains high risk and is only executed through the guarded PowerShell wrapper after explicit `KILL` confirmation. - `BLANK_README.md` is present locally but ignored by git and appears to be an unused Best-README-Template source file. - No tracked `.agents/skills/` directory exists at the 2026-06-16 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. diff --git a/memory/2026-06-16.md b/memory/2026-06-16.md new file mode 100644 index 0000000..edde62f --- /dev/null +++ b/memory/2026-06-16.md @@ -0,0 +1,12 @@ +# 2026-06-16 + +## Work Log + +- Re-read `AGENTS.md` and `MEMORY.md`, then rescanned the repository facts for the current Python-first layout. +- Confirmed the current branch, latest commit, launcher path, logging behavior, and menu inventory. +- Updated `AGENTS.md`, `MEMORY.md`, and `memory/tasks.md` to reflect the 2026-06-16 scan, the latest commit `3b5e50e`, and the guarded PowerShell wrapper for `Kill Browser AI`. +- Did not change application code or run functional tests. + +## Decisions + +- Kept the remote `kill_ai.ps1` workflow marked high risk and documented only through the explicit confirmation path. diff --git a/memory/tasks.md b/memory/tasks.md index 92a00fd..9071492 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -1,6 +1,6 @@ # memory/tasks.md -Last updated: 2026-06-13 +Last updated: 2026-06-16 ## Pending @@ -8,6 +8,7 @@ Last updated: 2026-06-13 ## Completed +- [x] 2026-06-16: Rescanned the current repository against the Python-first implementation and refreshed AGENTS.md, MEMORY.md, and memory history with the latest commit and guarded remote-script details. - [x] 2026-06-07: Scanned current repository logic, docs, prompt files, Git metadata, and issue templates. - [x] 2026-06-07: Created `AGENTS.md`, `MEMORY.md`, `memory/tasks.md`, and daily work log to restore AI identity and project state in future sessions. - [x] 2026-06-07: Fixed admin privilege check by replacing the malformed `cacls` protected-path check with `fltmc`. From bef04c2ed4bcbee4892862d8b00664630866d866 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Wed, 17 Jun 2026 00:14:23 +0800 Subject: [PATCH 17/33] Complete toolbox safety features Fix Log History pager --- MEMORY.md | 4 ++-- ldlwintoolbox.py | 57 +++++++++++++++++++++++++++++++++++++++----- memory/2026-06-17.md | 13 ++++++++++ memory/tasks.md | 3 ++- 4 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 memory/2026-06-17.md diff --git a/MEMORY.md b/MEMORY.md index 390e248..f42ee1b 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,6 +1,6 @@ # MEMORY.md -Last updated: 2026-06-16 +Last updated: 2026-06-17 ## User Preferences @@ -41,7 +41,7 @@ Logging behavior: - Uses helper labels for `INFO`, `WARN`, `ERROR`, `CMD`, and `OK` log entries. - Records feature section boundaries, menu selections, user cancellations, key command starts, command exit codes, and major completion messages. - Keeps raw command output in the same log file while keeping console output concise. -- Provides a read-only Log History viewer that lists recent logs newest-first, caps the picker at the latest 9 entries, and opens a selected file with `more`. +- Provides a read-only Log History viewer that lists recent logs newest-first, caps the picker at the latest 9 entries, and opens a selected file with an internal paged console viewer. Implemented menu behavior: diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index e9651a9..980978e 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -520,6 +520,55 @@ def list_log_history(log_dir: Path, logger: Logger) -> list[Path]: return entries[:9] +def paginate_log_file(path: Path) -> None: + try: + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError as exc: + print(f"Unable to open log file: {exc}") + input("Press Enter to continue...") + return + + if not lines: + print("(Log file is empty.)") + input("Press Enter to continue...") + return + + terminal_size = shutil.get_terminal_size(fallback=(80, 24)) + page_size = max(10, terminal_size.lines - 6) + page = 0 + + while True: + start = page * page_size + if start >= len(lines): + page = max(0, (len(lines) - 1) // page_size) + start = page * page_size + end = min(start + page_size, len(lines)) + + clear_screen() + print(MENU_LOGO) + print(f"Viewing Log: {path.name}") + print(MENU_LOGO) + print(f"Path: {path}") + print(MENU_LOGO) + print(f"Lines {start + 1}-{end} of {len(lines)}") + print(MENU_LOGO) + for line in lines[start:end]: + print(line) + print(MENU_LOGO) + + if end >= len(lines): + input("End of log. Press Enter to return to the menu...") + return + + choice = input("Press Enter for more, [B]ack, or [Q]uit: ").strip().upper() + if choice == "Q": + return + if choice == "B": + page = max(0, page - 1) + continue + page += 1 + + def log_history(logger: Logger, log_dir: Path) -> None: clear_screen() print(MENU_LOGO) @@ -528,10 +577,6 @@ def log_history(logger: Logger, log_dir: Path) -> None: print(f"Log directory:\n{log_dir}") print(MENU_LOGO) logger.section("View Log History") - if not command_exists("more"): - logger.log("ERROR", "more.com is required for Log History viewer, but it was not found.") - input("Press Enter to continue...") - return logs = list_log_history(log_dir, logger) if not logs: logger.log("INFO", "No log history found.") @@ -565,8 +610,8 @@ def log_history(logger: Logger, log_dir: Path) -> None: print(f"Path: {selected}") print(MENU_LOGO) logger.log_only("INFO", f"Viewing log history file: {selected.name}") - subprocess.run(["more", str(selected)], shell=False) - input("Press Enter to continue...") + paginate_log_file(selected) + logger.log("INFO", "View Log History returned to menu.") def kill_browser_ai(logger: Logger) -> None: diff --git a/memory/2026-06-17.md b/memory/2026-06-17.md new file mode 100644 index 0000000..f3f41fa --- /dev/null +++ b/memory/2026-06-17.md @@ -0,0 +1,13 @@ +# 2026-06-17 + +## Work Log + +- Investigated the `View Log History` flow after the user reported that CMD closed immediately when selecting a log. +- Found that the feature still depended on external `more` execution through `subprocess.run(["more", ...])`. +- Replaced that dependency with an internal paged console viewer in `ldlwintoolbox.py` so the viewer stays in the same CMD session. +- Updated `MEMORY.md` and `memory/tasks.md` to reflect the new log-viewing behavior. + +## Decisions + +- Kept the Log History feature read-only and menu-driven. +- Preferred a Python-native pager over external `more` so the viewer is predictable and easier to maintain. diff --git a/memory/tasks.md b/memory/tasks.md index 9071492..06a9183 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -1,6 +1,6 @@ # memory/tasks.md -Last updated: 2026-06-16 +Last updated: 2026-06-17 ## Pending @@ -8,6 +8,7 @@ Last updated: 2026-06-16 ## Completed +- [x] 2026-06-17: Replaced the Log History `more` viewer with an internal paged console viewer so CMD stays open while browsing logs. - [x] 2026-06-16: Rescanned the current repository against the Python-first implementation and refreshed AGENTS.md, MEMORY.md, and memory history with the latest commit and guarded remote-script details. - [x] 2026-06-07: Scanned current repository logic, docs, prompt files, Git metadata, and issue templates. - [x] 2026-06-07: Created `AGENTS.md`, `MEMORY.md`, `memory/tasks.md`, and daily work log to restore AI identity and project state in future sessions. From fbb270189de0b8342a9ecfc197c08fa79454cfa0 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 14:07:32 +0800 Subject: [PATCH 18/33] Refactor into modular architecture + add Low Latency Mode + reorganize menu - Split monolithic ldlwintoolbox.py into toolbox_base.py + features/ - Added Low Latency Mode (ViVeTool) with arch detection and auto-download - Reorganized main menu into logical groups (Cleanup / Repair / Network / Performance / Security / Tools) - Updated /tools to .gitignore for downloaded binaries - Updated AGENTS.md, MEMORY.md, and memory session files --- .gitignore | 3 +- AGENTS.md | 39 +- MEMORY.md | 83 ++++- features/__init__.py | 0 features/bitlocker_disable.py | 116 ++++++ features/browser_ai_killer.py | 57 +++ features/event_log_clear.py | 36 ++ features/log_viewer.py | 120 ++++++ features/low_latency_mode.py | 268 +++++++++++++ features/network_reset.py | 46 +++ features/ssd_trim.py | 91 +++++ features/system_cleanup.py | 105 ++++++ features/system_repair.py | 44 +++ features/winget_upgrade.py | 51 +++ features/winsxs_cleanup.py | 43 +++ ldlwintoolbox.py | 683 +++------------------------------- memory/2026-07-05.md | 31 ++ memory/tasks.md | 7 +- toolbox_base.py | 186 +++++++++ 19 files changed, 1345 insertions(+), 664 deletions(-) create mode 100644 features/__init__.py create mode 100644 features/bitlocker_disable.py create mode 100644 features/browser_ai_killer.py create mode 100644 features/event_log_clear.py create mode 100644 features/log_viewer.py create mode 100644 features/low_latency_mode.py create mode 100644 features/network_reset.py create mode 100644 features/ssd_trim.py create mode 100644 features/system_cleanup.py create mode 100644 features/system_repair.py create mode 100644 features/winget_upgrade.py create mode 100644 features/winsxs_cleanup.py create mode 100644 memory/2026-07-05.md create mode 100644 toolbox_base.py diff --git a/.gitignore b/.gitignore index a98f73d..033ed24 100644 --- a/.gitignore +++ b/.gitignore @@ -406,4 +406,5 @@ install_rooflow.cmd generate_mcp_yaml.py .vscode *instructions.md -BLANK_README.md \ No newline at end of file +BLANK_README.md +/tools diff --git a/AGENTS.md b/AGENTS.md index 42141aa..f666df1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,20 +41,39 @@ On every new session: ## Current Implemented Features -Current `ldlwintoolbox.py` menu implementation: +Current modular implementation (`ldlwintoolbox.py` + `toolbox_base.py` + `features/`): + +### System Cleanup 1. Advanced System Cleanup with a free-space calculator, Windows/user temp cleanup, `Prefetch`, `SoftwareDistribution\Download`, and vendor driver root cleanup. -2. System Integrity Repair using `sfc /scannow` and `DISM /RestoreHealth`. -3. Windows Component Store Cleanup using `DISM /StartComponentCleanup`. -4. Update all installed apps using `winget upgrade --all`. -5. Complete Network Reset using Winsock, TCP/IP reset, and DNS flush. -6. Clear Event Viewer Logs using `wevtutil`. +2. Windows Component Store Cleanup using `DISM /StartComponentCleanup`. +3. Clear Event Viewer Logs using `wevtutil`. + +### System Repair & Update + +4. System Integrity Repair using `sfc /scannow` and `DISM /RestoreHealth`. +5. Update all installed apps using `winget upgrade --all`. + +### Network + +6. Complete Network Reset using Winsock, TCP/IP reset, and DNS flush. + +### Performance + 7. Manual SSD TRIM using `defrag /L /V`. -8. Disable BitLocker `(Plan)` using `manage-bde -status`, drive validation, `DISABLE` confirmation, and guarded `manage-bde -off :`. -9. Kill Browser AI using the user-specified command: +8. Low Latency Mode in `features/low_latency_mode.py` using ViVeTool (architecture detection, auto-download, sub-menu for query/enable/disable for feature IDs 58989092, 60716524, 61391826). + +### Security & Privacy + +9. Disable BitLocker `(Plan)` using `manage-bde -status`, drive validation, `DISABLE` confirmation, and guarded `manage-bde -off :`. +10. Kill Browser AI using the user-specified command: `powershell -NoProfile -ExecutionPolicy Bypass -Command "try { iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex; exit 0 } catch { Write-Error $_; exit 1 }"` -10. View Log History using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. -11. Exit. + +### Tools + +11. View Log History using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. + +12. Exit. Remote script execution is high risk. Do not run this command during development. If it is implemented as a menu feature, add an explicit warning and confirmation before execution. diff --git a/MEMORY.md b/MEMORY.md index f42ee1b..9fb8618 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,6 +1,6 @@ # MEMORY.md -Last updated: 2026-06-17 +Last updated: 2026-07-05 (modular refactor completed) ## User Preferences @@ -18,7 +18,7 @@ Last updated: 2026-06-17 - Git remote: `https://github.com/LoveDoLove/LDLWinToolBox.git` - Current branch at scan time: `lovedolove` - Latest scanned commit: `3b5e50e Complete toolbox safety features` -- Latest repository scan: `2026-06-16`; the working tree was clean at the start of the documentation refresh. +- Latest repository scan: `2026-07-05`. - License: Apache License 2.0 - Primary executable: `LDLWinToolBox.bat` thin launcher for `ldlwintoolbox.py` via `uv run -- python` - Packaging metadata: `pyproject.toml`, `uv.lock` @@ -34,6 +34,11 @@ Last updated: 2026-06-17 `LDLWinToolBox.bat` is now a thin launcher that invokes `uv run -- python ldlwintoolbox.py`. The Python entry point initializes the menu, checks for Administrator access, relaunches with UAC when needed, prefers `uv` when available and falls back to `sys.executable`, switches to the script directory, and creates a timestamped structured log file named `logs\LDLWinToolBox_yyMMddHHmmss.log`. +The project has been refactored into a modular structure: +- `ldlwintoolbox.py` — thin entry point with admin logic and main menu dispatch +- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run/command/prompt helpers) +- `features/` — one file per feature, each importing only from `toolbox_base` + Logging behavior: - Creates `logs\` automatically and falls back to the script directory if the log directory cannot be created. @@ -43,23 +48,44 @@ Logging behavior: - Keeps raw command output in the same log file while keeping console output concise. - Provides a read-only Log History viewer that lists recent logs newest-first, caps the picker at the latest 9 entries, and opens a selected file with an internal paged console viewer. -Implemented menu behavior: +Implemented menu behavior (each feature in its own `features/*.py` file), grouped into logical categories: + +**System Cleanup (1-3):** + +1. Advanced System Cleanup: calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 3 instead of direct file deletion. +2. Windows Component Store Cleanup: asks confirmation, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup`. +3. Clear Event Viewer Logs: enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. + +**System Repair & Update (4-5):** + +4. System Integrity Repair: asks confirmation, runs `sfc /scannow`, then `DISM /Online /Cleanup-Image /RestoreHealth`. +5. Update All Installed Apps: asks confirmation, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements`. + +**Network (6):** + +6. Complete Network Reset: asks confirmation, runs `netsh winsock reset`, `netsh int ip reset`, and `ipconfig /flushdns`; tells user to restart. + +**Performance (7-8):** -1. Advanced System Cleanup: calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 6 instead of direct file deletion. -2. System Integrity Repair: asks confirmation, runs `sfc /scannow`, then `DISM /Online /Cleanup-Image /RestoreHealth`. -3. Windows Component Store Cleanup: asks confirmation, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup`. -4. Update All Installed Apps: asks confirmation, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements`. -5. Complete Network Reset: asks confirmation, runs `netsh winsock reset`, `netsh int ip reset`, and `ipconfig /flushdns`; tells user to restart. -6. Clear Event Viewer Logs: enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. 7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes and validates a single drive letter, confirms the drive exists, runs `defrag : /L /V`, displays output, and appends it to the log. -8. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. -9. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then launches PowerShell with `-ExecutionPolicy Bypass` and a guarded `try/catch` wrapper around the configured gist command so the result is logged. -10. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. -11. Exit: closes the tool. +8. Low Latency Mode: auto-detects CPU architecture (Intel/AMD x64 or Snapdragon ARM64), fetches the latest ViVeTool release from GitHub via API, downloads and extracts the matching ZIP to `tools/vivetool/`, and provides a sub-menu to query/enable/disable feature IDs 58989092, 60716524, and 61391826. Version caching avoids redundant downloads. + +**Security & Privacy (9-10):** + +9. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. +10. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then launches PowerShell with `-ExecutionPolicy Bypass` and a guarded `try/catch` wrapper around the configured gist command so the result is logged. + +**Tools (11):** + +11. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. + +12. Exit: closes the tool. ## Implemented Feature Targets -The user-listed feature targets below were implemented in `LDLWinToolBox.bat` on 2026-06-07: +The user-listed feature targets below were implemented: + +### 2026-06-07 (Batch) - Disable BitLocker `[Plan]` with status display, drive validation, and `DISABLE` confirmation. - Kill Browser AI using: @@ -67,6 +93,10 @@ The user-listed feature targets below were implemented in `LDLWinToolBox.bat` on Treat the remote `iwr | iex` command as high risk. Do not execute it during analysis. The menu feature requires a clear warning, `KILL` confirmation, and logging. +### 2026-07-05 (Python) + +- Low Latency Mode: auto-detects `platform.machine()` → `IntelAmd` (AMD64/x86) or `SnapdragonArm64` (ARM64), fetches latest ViVe release from `api.github.com/repos/thebookisclosed/ViVe/releases/latest`, downloads matching ZIP via `urllib.request`, extracts with `zipfile` to `tools/vivetool/`, caches version in `version.txt`, provides sub-menu for `/query`, `/enable`, `/disable` on IDs 58989092, 60716524, 61391826. + ## Documentation And Prompt Files - `README.md` describes the app, prerequisites, installation, usage, license, and contact info. @@ -76,10 +106,35 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - `ANALYSIS.md` and `PROMPT_GUIDE.md` were not present in the latest working tree scan; if restored later, preserve their history and append updates. - Existing prompt rules emphasize auto-admin preservation, history preservation, input sanitization, clean verbosity, and long-running process warnings. +## New Feature Details + +### Low Latency Mode (Menu 11) + +**Architecture detection:** +- `platform.machine()` → `AMD64` → Intel/AMD x64 +- `platform.machine()` → `ARM64` → Snapdragon ARM64 + +**ViVeTool management:** +- Stores tool in `tools/vivetool/` under script directory +- Caches version in `version.txt` to skip redundant downloads +- Falls back to cached binary when GitHub API is unavailable + +**Sub-menu:** +1. Check Status — runs `ViVeTool.exe /query /id:58989092,60716524,61391826` +2. Enable — runs `ViVeTool.exe /enable /id:58989092,60716524,61391826` (with Y/N confirmation) +3. Disable — runs `ViVeTool.exe /disable /id:58989092,60716524,61391826` (with Y/N confirmation) +4. Return to Main Menu + +**Risks:** +- Downloads binaries from GitHub; requires internet on first run +- Feature ID changes in future Windows builds may require updates +- Reboot may be required after changing low latency features + ## Known Gaps And Risks - The current Python launcher/elevation flow uses `IsUserAnAdmin()` plus `ShellExecuteW(..., "runas", ...)`; keep both the `uv` and `sys.executable` launch paths working. - Cleanup no longer deletes Event Viewer log files directly; option 6 remains the safe `wevtutil` path for clearing logs. +- No circular dependencies; each feature imports only from `toolbox_base` - The remote `kill_ai.ps1` gist was retrieved and reviewed on 2026-06-13; it disables Chrome and Edge on-device AI by applying registry policy keys and locking the `OptGuideOnDeviceModel` folders, but it still remains high risk and is only executed through the guarded PowerShell wrapper after explicit `KILL` confirmation. - `BLANK_README.md` is present locally but ignored by git and appears to be an unused Best-README-Template source file. - No tracked `.agents/skills/` directory exists at the 2026-06-16 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. diff --git a/features/__init__.py b/features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/features/bitlocker_disable.py b/features/bitlocker_disable.py new file mode 100644 index 0000000..8ddad41 --- /dev/null +++ b/features/bitlocker_disable.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + command_exists, + prompt_keyword, + run_and_log, + select_existing_drive, +) + + +def bitlocker_disable(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" DISABLE BITLOCKER (PLAN)") + print(MENU_LOGO) + print("WARNING: This starts BitLocker decryption for the") + print("selected drive and turns BitLocker off.") + print("-> Decryption can take a long time.") + print("-> Keep the PC powered on until Windows finishes.") + print("-> Do this only when protection is no longer needed.") + print(MENU_LOGO) + logger.section("Disable BitLocker") + if not command_exists("manage-bde"): + logger.log( + "ERROR", + "manage-bde.exe is required for BitLocker management, but it was not found.", + ) + input("Press Enter to continue...") + return + print("Current BitLocker status:") + logger.log_only("INFO", "Current BitLocker status:") + status_result = run_and_log( + logger, ["manage-bde", "-status"], "manage-bde -status" + ) + if status_result.stdout: + print( + status_result.stdout, + end="" if status_result.stdout.endswith("\n") else "\n", + ) + if status_result.stderr: + print( + status_result.stderr, + end="" if status_result.stderr.endswith("\n") else "\n", + ) + print() + drive = select_existing_drive(logger, "Disable BitLocker") + if drive is None: + return + if drive == "": + logger.log( + "ERROR", "No valid drive was selected for Disable BitLocker." + ) + input("Press Enter to continue...") + return + logger.log_only("INFO", f"Selected BitLocker drive: {drive}:") + print("\nSelected drive status:") + logger.log_only( + "INFO", f"Selected BitLocker drive status for {drive}:" + ) + status_result = run_and_log( + logger, + ["manage-bde", "-status", f"{drive}:"], + f"manage-bde -status {drive}:", + ) + if status_result.stdout: + print( + status_result.stdout, + end="" if status_result.stdout.endswith("\n") else "\n", + ) + if status_result.stderr: + print( + status_result.stderr, + end="" if status_result.stderr.endswith("\n") else "\n", + ) + print() + if not prompt_keyword( + logger, + f"Type DISABLE to start decryption for {drive}: ", + "DISABLE", + "Disable BitLocker", + ): + return + logger.log("INFO", f"Starting BitLocker decryption on {drive}: ...") + result = run_and_log( + logger, + ["manage-bde", "-off", f"{drive}:"], + f"manage-bde -off {drive}:", + ) + if result.code != 0: + logger.log("ERROR", "BITLOCKER DISABLE FAILED. Check log.") + else: + logger.log( + "INFO", + "BITLOCKER DECRYPTION STARTED. Check Windows BitLocker status for progress.", + ) + print("\nUpdated status:") + logger.log_only("INFO", f"Updated BitLocker status for {drive}:") + status_result = run_and_log( + logger, + ["manage-bde", "-status", f"{drive}:"], + f"manage-bde -status {drive}:", + ) + if status_result.stdout: + print( + status_result.stdout, + end="" if status_result.stdout.endswith("\n") else "\n", + ) + if status_result.stderr: + print( + status_result.stderr, + end="" if status_result.stderr.endswith("\n") else "\n", + ) + input("Press Enter to continue...") diff --git a/features/browser_ai_killer.py b/features/browser_ai_killer.py new file mode 100644 index 0000000..243fa7a --- /dev/null +++ b/features/browser_ai_killer.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + command_exists, + prompt_keyword, + run_and_log, +) + + +GIST_URL = "https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1" + + +def kill_browser_ai(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" KILL BROWSER AI") + print(MENU_LOGO) + print("WARNING: This downloads and executes a remote") + print("PowerShell script from the configured gist URL.") + print("-> It may close browser or AI-related processes.") + print("-> Network access is required.") + print("-> Do not run if you do not trust the source.") + print(MENU_LOGO) + print("Source:") + print(GIST_URL) + print() + logger.section("Kill Browser AI") + logger.log_only("WARN", f"Remote script source: {GIST_URL}") + if not prompt_keyword( + logger, "Type KILL to run Kill Browser AI: ", "KILL", "Kill Browser AI" + ): + return + if not command_exists("powershell"): + logger.log( + "ERROR", + "powershell.exe is required for Kill Browser AI, but it was not found.", + ) + input("Press Enter to continue...") + return + logger.log("INFO", "Running Kill Browser AI...") + cmd = [ + "powershell", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + f"try {{ iwr -useb '{GIST_URL}' | iex; exit 0 }} catch {{ Write-Error $_; exit 1 }}", + ] + result = run_and_log(logger, cmd, "PowerShell remote kill_ai.ps1") + if result.code != 0: + logger.log("ERROR", "KILL BROWSER AI FAILED. Check log.") + else: + logger.log("INFO", "KILL BROWSER AI COMPLETE.") + input("Press Enter to continue...") diff --git a/features/event_log_clear.py b/features/event_log_clear.py new file mode 100644 index 0000000..2ba0f83 --- /dev/null +++ b/features/event_log_clear.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + command_exists, + run_and_log, + run_command, +) + + +def event_logs(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" CLEAR EVENT VIEWER LOGS") + print(MENU_LOGO) + print(f"All operations are being logged to:\n{logger.logfile}") + print(MENU_LOGO) + logger.section("Clear Event Viewer Logs") + if not command_exists("wevtutil"): + logger.log( + "ERROR", + "wevtutil.exe is required for Event Viewer logs, but it was not found.", + ) + input("Press Enter to continue...") + return + result = run_command(["wevtutil", "el"], capture=True) + logs = [line.strip() for line in result.stdout.splitlines() if line.strip()] + for entry in logs: + logger.log("INFO", f"- Clearing log: {entry}") + run_and_log( + logger, ["wevtutil", "cl", entry], f"wevtutil.exe cl {entry}" + ) + logger.log("INFO", "EVENT LOGS CLEARED") + input("Press Enter to continue...") diff --git a/features/log_viewer.py b/features/log_viewer.py new file mode 100644 index 0000000..864a6df --- /dev/null +++ b/features/log_viewer.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import shutil +from datetime import datetime +from pathlib import Path + +from toolbox_base import MENU_LOGO, Logger, clear_screen + + +def list_log_history(log_dir: Path, logger: Logger) -> list[Path]: + entries = sorted( + log_dir.glob("LDLWinToolBox_*.log"), + key=lambda path: path.stat().st_mtime, + reverse=True, + ) + return entries[:9] + + +def paginate_log_file(path: Path) -> None: + try: + lines = path.read_text( + encoding="utf-8", errors="replace" + ).splitlines() + except OSError as exc: + print(f"Unable to open log file: {exc}") + input("Press Enter to continue...") + return + + if not lines: + print("(Log file is empty.)") + input("Press Enter to continue...") + return + + terminal_size = shutil.get_terminal_size(fallback=(80, 24)) + page_size = max(10, terminal_size.lines - 6) + page = 0 + + while True: + start = page * page_size + if start >= len(lines): + page = max(0, (len(lines) - 1) // page_size) + start = page * page_size + end = min(start + page_size, len(lines)) + + clear_screen() + print(MENU_LOGO) + print(f"Viewing Log: {path.name}") + print(MENU_LOGO) + print(f"Path: {path}") + print(MENU_LOGO) + print(f"Lines {start + 1}-{end} of {len(lines)}") + print(MENU_LOGO) + for line in lines[start:end]: + print(line) + print(MENU_LOGO) + + if end >= len(lines): + input("End of log. Press Enter to return to the menu...") + return + + choice = ( + input("Press Enter for more, [B]ack, or [Q]uit: ").strip().upper() + ) + if choice == "Q": + return + if choice == "B": + page = max(0, page - 1) + continue + page += 1 + + +def log_history(logger: Logger, log_dir: Path) -> None: + clear_screen() + print(MENU_LOGO) + print(" VIEW LOG HISTORY") + print(MENU_LOGO) + print(f"Log directory:\n{log_dir}") + print(MENU_LOGO) + logger.section("View Log History") + logs = list_log_history(log_dir, logger) + if not logs: + logger.log("INFO", "No log history found.") + input("Press Enter to continue...") + return + for idx, path in enumerate(logs, start=1): + stat = path.stat() + ts = datetime.fromtimestamp(stat.st_mtime).strftime( + "%Y-%m-%d %H:%M" + ) + print( + f"[{idx}] {path.name} - {stat.st_size} bytes - {ts}" + ) + print() + print("[0] Return to Menu") + choice = input("Press 0 to return, or 1-9 to view a log: ").strip() + if choice == "0": + logger.log("INFO", "View Log History returned to menu.") + return + try: + index = int(choice) - 1 + except ValueError: + logger.log("WARN", f"Invalid log history selection: {choice}") + input("Press Enter to continue...") + return + if index < 0 or index >= len(logs): + logger.log("WARN", f"Invalid log history selection: {choice}") + input("Press Enter to continue...") + return + selected = logs[index] + print(MENU_LOGO) + print("Viewing Log:") + print(selected.name) + print(MENU_LOGO) + print(f"Path: {selected}") + print(MENU_LOGO) + logger.log_only( + "INFO", f"Viewing log history file: {selected.name}" + ) + paginate_log_file(selected) + logger.log("INFO", "View Log History returned to menu.") diff --git a/features/low_latency_mode.py b/features/low_latency_mode.py new file mode 100644 index 0000000..d3b8224 --- /dev/null +++ b/features/low_latency_mode.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +import json +import platform +import shutil +import urllib.error +import urllib.request +import zipfile +from pathlib import Path + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + prompt_yes_no, + run_and_log, +) + + +VIVE_REPO = "thebookisclosed/ViVe" +VIVE_TOOLS_DIR = ( + Path(__file__).resolve().parent.parent / "tools" / "vivetool" +) +LOW_LATENCY_IDS = ["58989092", "60716524", "61391826"] +LOW_LATENCY_DESC = { + "58989092": "Core Low Latency Profile", + "60716524": "Core low latency mode for background operations", + "61391826": "Optimizes application launch speed", +} + + +def detect_architecture() -> str: + machine = platform.machine().lower() + return "SnapdragonArm64" if machine in ("arm64", "aarch64") else "IntelAmd" + + +def _fetch_json(url: str) -> dict | None: + req = urllib.request.Request( + url, headers={"User-Agent": "LDLWinToolBox/1.0"} + ) + try: + with urllib.request.urlopen(req, timeout=15) as resp: + return json.loads(resp.read().decode("utf-8")) + except ( + urllib.error.URLError, + urllib.error.HTTPError, + OSError, + json.JSONDecodeError, + ): + return None + + +def _download_file(url: str, dest: Path) -> bool: + req = urllib.request.Request( + url, headers={"User-Agent": "LDLWinToolBox/1.0"} + ) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + with dest.open("wb") as f: + shutil.copyfileobj(resp, f) + return True + except (urllib.error.URLError, OSError): + return False + + +def ensure_vivetool(logger: Logger) -> Path | None: + VIVE_TOOLS_DIR.mkdir(parents=True, exist_ok=True) + exe = VIVE_TOOLS_DIR / "ViVeTool.exe" + ver_file = VIVE_TOOLS_DIR / "version.txt" + cached_ver = ( + ver_file.read_text(encoding="utf-8").strip() + if ver_file.exists() + else "" + ) + + arch = detect_architecture() + logger.log_only("INFO", f"Detected architecture: {arch}") + + release = _fetch_json( + f"https://api.github.com/repos/{VIVE_REPO}/releases/latest" + ) + if release is not None: + tag = release["tag_name"] + if tag != cached_ver: + logger.log("INFO", f"Downloading ViVeTool {tag}...") + suffix = f"{arch}.zip" + asset = next( + ( + a + for a in release["assets"] + if a["name"].endswith(suffix) + ), + None, + ) + if asset is None: + logger.log("ERROR", f"No ViVeTool asset for {arch}.") + return exe if exe.exists() else None + zip_path = VIVE_TOOLS_DIR / asset["name"] + logger.log( + "INFO", f" Source: {asset['browser_download_url']}" + ) + if not _download_file(asset["browser_download_url"], zip_path): + logger.log("ERROR", "Download failed.") + zip_path.unlink(missing_ok=True) + return exe if exe.exists() else None + logger.log("INFO", "Extracting...") + try: + with zipfile.ZipFile(zip_path) as zf: + zf.extractall(VIVE_TOOLS_DIR) + except zipfile.BadZipFile: + logger.log("ERROR", "Corrupted download.") + zip_path.unlink(missing_ok=True) + return exe if exe.exists() else None + zip_path.unlink(missing_ok=True) + ver_file.write_text(tag, encoding="utf-8") + logger.log("INFO", f"ViVeTool {tag} ready.") + else: + logger.log("INFO", f"ViVeTool {tag} is up to date.") + elif exe.exists(): + logger.log( + "WARN", "Could not check for updates. Using cached ViVeTool." + ) + else: + logger.log( + "ERROR", + "ViVeTool not found and could not be downloaded.", + ) + return None + return exe if exe.exists() else None + + +def low_latency_mode(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" LOW LATENCY MODE") + print(MENU_LOGO) + print( + "This feature uses ViVeTool to manage Windows low" + ) + print( + "latency feature flags for better system responsiveness." + ) + print() + print("Feature IDs:") + for fid in LOW_LATENCY_IDS: + print(f" {fid} — {LOW_LATENCY_DESC[fid]}") + print(MENU_LOGO) + logger.section("Low Latency Mode") + + arch = detect_architecture() + logger.log_only("INFO", f"Detected architecture: {arch}") + + vivetool = ensure_vivetool(logger) + if vivetool is None: + input("Press Enter to continue...") + return + + while True: + clear_screen() + print(MENU_LOGO) + print(" LOW LATENCY MODE") + print(MENU_LOGO) + print(f"Architecture : {arch}") + print(f"ViVeTool : {vivetool}") + ver_path = VIVE_TOOLS_DIR / "version.txt" + current_ver = ( + ver_path.read_text(encoding="utf-8").strip() + if ver_path.exists() + else "unknown" + ) + print(f"Version : {current_ver}") + print(MENU_LOGO) + print("[1] Check Status") + print("[2] Enable Low Latency Mode") + print("[3] Disable Low Latency Mode") + print("[4] Return to Main Menu") + print(MENU_LOGO) + choice = input("Select an option: ").strip() + logger.log_only( + "INFO", f"Low Latency Mode sub-menu selection: {choice}" + ) + + if choice == "1": + logger.section("Low Latency Mode — Status Check") + result = run_and_log( + logger, + [ + str(vivetool), + "/query", + f"/id:{','.join(LOW_LATENCY_IDS)}", + ], + "vivetool /query", + ) + if result.stdout: + logger.write_raw(result.stdout) + print(result.stdout) + input("Press Enter to continue...") + elif choice == "2": + clear_screen() + print(MENU_LOGO) + print(" LOW LATENCY MODE") + print(MENU_LOGO) + print( + "WARNING: This enables system-wide low latency" + ) + print("features to improve responsiveness.") + print("-> A reboot may be required to take effect.") + print("-> Can be safely reverted by disabling.") + print(MENU_LOGO) + logger.section("Low Latency Mode — Enable") + if not prompt_yes_no( + logger, + "Enable Low Latency Mode? (Y/N): ", + "Low Latency Mode Enable", + ): + continue + result = run_and_log( + logger, + [ + str(vivetool), + "/enable", + f"/id:{','.join(LOW_LATENCY_IDS)}", + ], + "vivetool /enable", + ) + if result.stdout: + logger.write_raw(result.stdout) + print(result.stdout) + input("Press Enter to continue...") + elif choice == "3": + clear_screen() + print(MENU_LOGO) + print(" LOW LATENCY MODE") + print(MENU_LOGO) + print( + "WARNING: This disables low latency features," + ) + print("restoring default system behavior.") + print("-> A reboot may be required to take effect.") + print(MENU_LOGO) + logger.section("Low Latency Mode — Disable") + if not prompt_yes_no( + logger, + "Disable Low Latency Mode? (Y/N): ", + "Low Latency Mode Disable", + ): + continue + result = run_and_log( + logger, + [ + str(vivetool), + "/disable", + f"/id:{','.join(LOW_LATENCY_IDS)}", + ], + "vivetool /disable", + ) + if result.stdout: + logger.write_raw(result.stdout) + print(result.stdout) + input("Press Enter to continue...") + elif choice == "4": + logger.log("INFO", "Low Latency Mode returned to main menu.") + return + else: + logger.log( + "WARN", + f"Invalid Low Latency Mode selection: {choice}", + ) diff --git a/features/network_reset.py b/features/network_reset.py new file mode 100644 index 0000000..3c1ba02 --- /dev/null +++ b/features/network_reset.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + command_exists, + prompt_yes_no, + run_and_log, +) + + +def net_reset(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" COMPLETE NETWORK RESET") + print(MENU_LOGO) + print("This will reset your network adapters to factory defaults.") + print("-> A system restart will be required afterward.") + print(MENU_LOGO) + logger.section("Complete Network Reset") + if not prompt_yes_no( + logger, "Do you want to proceed? (Y/N): ", "Complete Network Reset" + ): + return + for cmd, label in ( + (["netsh", "winsock", "reset"], "netsh winsock reset"), + (["netsh", "int", "ip", "reset"], "netsh int ip reset"), + (["ipconfig", "/flushdns"], "ipconfig /flushdns"), + ): + if not command_exists(cmd[0]): + logger.log( + "ERROR", + f"{cmd[0]} is required for {label}, but it was not found.", + ) + input("Press Enter to continue...") + return + logger.log( + "INFO", + label.replace("netsh ", "Resetting ").replace("ipconfig ", "Flushing "), + ) + run_and_log(logger, cmd, label) + logger.log( + "INFO", "NETWORK RESET COMPLETE. Please RESTART your computer." + ) + input("Press Enter to continue...") diff --git a/features/ssd_trim.py b/features/ssd_trim.py new file mode 100644 index 0000000..56e5e60 --- /dev/null +++ b/features/ssd_trim.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import tempfile +from pathlib import Path + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + command_exists, + run_and_log, + run_command, + select_existing_drive, +) + + +def get_volume_table() -> str: + ps = ( + "Get-Volume | Where-Object { $_.DriveLetter -ne $null } " + "| Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, " + "FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} " + "| Format-Table -AutoSize" + ) + result = run_command( + ["powershell", "-NoProfile", "-Command", ps], capture=True + ) + return (result.stdout or "") + (result.stderr or "") + + +def ssd_trim(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" MANUAL SSD TRIM TOOL (KC3000)") + print(MENU_LOGO) + print(f"All operations are being logged to:\n{logger.logfile}") + print(MENU_LOGO) + logger.section("Manual SSD TRIM") + print("Current Drives Connected:") + logger.log_only("INFO", "Current drives connected:") + if not command_exists("powershell"): + logger.log( + "ERROR", + "powershell.exe is required for Volume enumeration, but it was not found.", + ) + input("Press Enter to continue...") + return + volume_text = get_volume_table() + print(volume_text, end="" if volume_text.endswith("\n") else "\n") + logger.write_raw(volume_text) + print() + drive = select_existing_drive(logger, "Manual SSD TRIM") + if drive is None: + return + if drive == "": + logger.log( + "ERROR", "No valid drive was selected for Manual SSD TRIM." + ) + input("Press Enter to continue...") + return + logger.log_only("INFO", f"Selected TRIM drive: {drive}:") + print(f"\nOptimizing Drive {drive}: ...") + logger.write_raw(f"Optimizing Drive {drive}: ...") + print("".join(["-" for _ in range(47)])) + if not command_exists("defrag"): + logger.log( + "ERROR", + "defrag.exe is required for SSD TRIM, but it was not found.", + ) + input("Press Enter to continue...") + return + out_file = Path(tempfile.gettempdir()) / "defrag_out.txt" + result = run_command(["defrag", f"{drive}:", "/L", "/V"], capture=True) + out_file.write_text( + (result.stdout or "") + (result.stderr or ""), + encoding="utf-8", + errors="replace", + ) + print(out_file.read_text(encoding="utf-8", errors="replace"), end="") + logger.write_raw(out_file.read_text(encoding="utf-8", errors="replace")) + try: + out_file.unlink() + except OSError: + pass + logger.command_result(f"defrag {drive}: /L /V", result.code) + logger.log("INFO", "SSD TRIM COMPLETE") + print("[1] Return to Menu") + print("[2] Exit") + final = input("Choose an option: ").strip() + logger.log_only("INFO", f"SSD TRIM final selection: {final}") + if final == "2": + raise SystemExit(0) diff --git a/features/system_cleanup.py b/features/system_cleanup.py new file mode 100644 index 0000000..496c07f --- /dev/null +++ b/features/system_cleanup.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import os +import shutil +from pathlib import Path + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + run_and_log, + run_command, +) + + +def drive_free_mb() -> int: + ps = ( + f"[math]::Round((Get-CimInstance Win32_LogicalDisk" + f" -Filter \"DeviceID='{os.environ.get('SYSTEMDRIVE', 'C:')}'\").FreeSpace / 1MB)" + ) + result = run_command(["powershell", "-Command", ps], capture=True) + text = (result.stdout or "").strip() + try: + return int(float(text)) + except ValueError: + return 0 + + +def cleanup(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" ADVANCED SYSTEM CLEANUP TOOL") + print(MENU_LOGO) + print(f"All operations are being logged to:\n{logger.logfile}") + print(MENU_LOGO) + logger.section("Advanced System Cleanup") + + free_before = drive_free_mb() + logger.log_only("INFO", f"Free space before cleanup: {free_before} MB") + + logger.log("INFO", "[1/4] Stopping background services...") + for cmd in (["net", "stop", "wuauserv"], ["net", "stop", "bits"]): + logger.log("INFO", f"- Stopping {cmd[-1]}...") + run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + + print() + logger.log("INFO", "[2/4] Deleting temporary and junk files...") + + def env_temp_dir(name: str) -> Path | None: + value = os.environ.get(name) + return Path(value) / "Temp" if value else None + + temp_targets = [ + Path(os.environ["WinDir"]) / "Temp", + Path(os.environ["WinDir"]) / "Prefetch", + Path(os.environ["TEMP"]), + env_temp_dir("AppData"), + env_temp_dir("LocalAppData"), + Path(os.environ["WinDir"]) / "SoftwareDistribution" / "Download", + ] + for target in temp_targets: + if target is None: + continue + logger.log("INFO", f"- Cleaning {target}") + if target.exists(): + for child in target.iterdir(): + try: + if child.is_dir(): + shutil.rmtree(child, ignore_errors=False) + else: + child.unlink(missing_ok=True) + except OSError as exc: + logger.log_only("WARN", f"Failed to remove {child}: {exc}") + + logger.log( + "INFO", + "- Event Viewer logs are handled by menu option 6 using wevtutil.", + ) + system_drive = os.environ.get("SYSTEMDRIVE", "C:") + for root_name in ("AMD", "NVIDIA", "INTEL"): + root = Path(f"{system_drive}\\{root_name}") + if root.exists(): + logger.log("INFO", f"- Removing Directory {root}") + shutil.rmtree(root, ignore_errors=True) + + print() + logger.log("INFO", "[3/4] Rebuilding directory structure...") + for target in temp_targets[:5]: + if target is None: + continue + logger.log("INFO", f"- Rebuilding {target}") + target.mkdir(parents=True, exist_ok=True) + + print() + logger.log("INFO", "[4/4] Finalizing optimizations...") + for cmd in (["net", "start", "wuauserv"], ["net", "start", "bits"]): + logger.log("INFO", f"- Starting {cmd[-1]}...") + run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + + free_after = drive_free_mb() + saved = max(0, free_after - free_before) + logger.log_only("INFO", f"Free space after cleanup: {free_after} MB") + logger.log("INFO", "SYSTEM CLEAN UP COMPLETE") + logger.log("INFO", f"Total Space Freed: {saved} MB") + input("Press Enter to continue...") diff --git a/features/system_repair.py b/features/system_repair.py new file mode 100644 index 0000000..ea7a4ff --- /dev/null +++ b/features/system_repair.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + command_exists, + prompt_yes_no, + run_and_log, +) + + +def sys_repair(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" SYSTEM INTEGRITY REPAIR (SFC + DISM)") + print(MENU_LOGO) + print("WARNING: This process can take 15-45 minutes.") + print("-> It CAN be safely interrupted by closing the window.") + print("-> However, it is recommended to let it finish.") + print(MENU_LOGO) + logger.section("System Integrity Repair") + if not prompt_yes_no( + logger, "Do you want to proceed? (Y/N): ", "System Integrity Repair" + ): + return + for cmd, label in ( + (["sfc", "/scannow"], "System File Checker"), + ( + ["dism", "/Online", "/Cleanup-Image", "/RestoreHealth"], + "DISM RestoreHealth", + ), + ): + if not command_exists(cmd[0]): + logger.log( + "ERROR", + f"{cmd[0]} is required for {label}, but it was not found.", + ) + input("Press Enter to continue...") + return + logger.log("INFO", f"Running {label}...") + run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + logger.log("INFO", "SYSTEM INTEGRITY REPAIR COMPLETE") + input("Press Enter to continue...") diff --git a/features/winget_upgrade.py b/features/winget_upgrade.py new file mode 100644 index 0000000..b25e1f5 --- /dev/null +++ b/features/winget_upgrade.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + command_exists, + prompt_yes_no, + run_and_log, +) + + +def app_update(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" UPDATE INSTALLED APPS (WINGET)") + print(MENU_LOGO) + print("WARNING: Silently updates all apps installed via Winget.") + print("-> May take several minutes.") + print("-> It CAN be safely interrupted.") + print(MENU_LOGO) + logger.section("Update Installed Apps") + if not prompt_yes_no( + logger, "Do you want to proceed? (Y/N): ", "Update Installed Apps" + ): + return + if not command_exists("winget"): + logger.log( + "ERROR", + "winget is required for Winget update, but it was not found.", + ) + input("Press Enter to continue...") + return + logger.log( + "INFO", + "Upgrading all installed applications (this may take a while)...", + ) + run_and_log( + logger, + [ + "winget", + "upgrade", + "--all", + "--include-unknown", + "--accept-package-agreements", + "--accept-source-agreements", + ], + "winget upgrade --all", + ) + logger.log("INFO", "APP UPDATE COMPLETE") + input("Press Enter to continue...") diff --git a/features/winsxs_cleanup.py b/features/winsxs_cleanup.py new file mode 100644 index 0000000..c65a1b4 --- /dev/null +++ b/features/winsxs_cleanup.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from toolbox_base import ( + MENU_LOGO, + Logger, + clear_screen, + command_exists, + prompt_yes_no, + run_and_log, +) + + +def component_store_cleanup(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" WINDOWS COMPONENT STORE CLEANUP (WinSxS)") + print(MENU_LOGO) + print("WARNING: This deeply cleans old Windows Update files.") + print("-> It can take 10-30 minutes and may appear stuck.") + print("-> DO NOT interrupt this process (can corrupt updates).") + print(MENU_LOGO) + logger.section("Windows Component Store Cleanup") + if not prompt_yes_no( + logger, + "Do you want to proceed? (Y/N): ", + "Windows Component Store Cleanup", + ): + return + if not command_exists("dism"): + logger.log( + "ERROR", + "dism is required for DISM component cleanup, but it was not found.", + ) + input("Press Enter to continue...") + return + logger.log("INFO", "Cleaning Windows Component Store...") + run_and_log( + logger, + ["dism", "/Online", "/Cleanup-Image", "/StartComponentCleanup"], + "DISM.exe /Online /Cleanup-Image /StartComponentCleanup", + ) + logger.log("INFO", "WINSXS CLEANUP COMPLETE") + input("Press Enter to continue...") diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index 980978e..f5ad4bb 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -2,61 +2,28 @@ import ctypes import os -import platform import shutil -import subprocess import sys -import tempfile -from dataclasses import dataclass from datetime import datetime from pathlib import Path - -MENU_LOGO = "=" * 47 -GIST_URL = "https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1" - - -@dataclass(slots=True) -class CommandResult: - code: int - stdout: str = "" - stderr: str = "" - - -class Logger: - def __init__(self, logfile: Path) -> None: - self.logfile = logfile - - def _stamp(self) -> str: - now = datetime.now() - return now.strftime("%m/%d/%Y %H:%M:%S") - - def write_raw(self, message: str) -> None: - self.logfile.parent.mkdir(parents=True, exist_ok=True) - with self.logfile.open("a", encoding="utf-8", errors="replace", newline="\n") as handle: - handle.write(message) - if not message.endswith("\n"): - handle.write("\n") - - def log_only(self, level: str, message: str) -> None: - self.write_raw(f"[{self._stamp()}] [{level}] {message}") - - def log(self, level: str, message: str) -> None: - self.log_only(level, message) - print(message) - - def section(self, title: str) -> None: - self.log_only("INFO", "-" * 79) - self.log("INFO", f"== {title} ==") - - def command_start(self, command: str) -> None: - self.log_only("CMD", f"START {command}") - - def command_result(self, command: str, code: int) -> None: - if code == 0: - self.log_only("OK", f"END {command} exit={code}") - else: - self.log("WARN", f"END {command} exit={code} - check log details.") +from toolbox_base import ( + Logger, + clear_screen, + get_log_dir, + write_session_header, +) +from features.bitlocker_disable import bitlocker_disable +from features.browser_ai_killer import kill_browser_ai +from features.event_log_clear import event_logs +from features.log_viewer import log_history +from features.low_latency_mode import low_latency_mode +from features.network_reset import net_reset +from features.ssd_trim import ssd_trim +from features.system_cleanup import cleanup +from features.system_repair import sys_repair +from features.winget_upgrade import app_update +from features.winsxs_cleanup import component_store_cleanup def is_admin() -> bool: @@ -85,591 +52,31 @@ def ensure_admin() -> None: raise SystemExit(0) -def clear_screen() -> None: - os.system("cls") - - -def command_exists(command: str) -> bool: - return shutil.which(command) is not None - - -def run_command(command: list[str] | str, *, shell: bool = False, capture: bool = True, check: bool = False) -> CommandResult: - completed = subprocess.run( - command, - shell=shell, - text=True, - capture_output=capture, - check=False, - ) - if check and completed.returncode != 0: - raise subprocess.CalledProcessError(completed.returncode, command, completed.stdout, completed.stderr) - return CommandResult(completed.returncode, completed.stdout or "", completed.stderr or "") - - -def run_and_log(logger: Logger, command: list[str] | str, display: str, *, shell: bool = False, capture_output: bool = True) -> CommandResult: - logger.command_start(display) - result = run_command(command, shell=shell, capture=capture_output) - if result.stdout: - logger.write_raw(result.stdout) - if result.stderr: - logger.write_raw(result.stderr) - logger.command_result(display, result.code) - return result - - -def prompt_yes_no(logger: Logger, prompt: str, context: str) -> bool: - answer = input(prompt).strip() - if answer.upper() == "Y": - return True - logger.log_only("INFO", f"{context} cancelled by user.") - return False - - -def prompt_keyword(logger: Logger, prompt: str, expected: str, context: str) -> bool: - answer = input(prompt).strip() - if answer.upper() == expected.upper(): - return True - logger.log_only("INFO", f"{context} cancelled by user.") - return False - - -def prompt_drive(logger: Logger, prompt: str, context: str) -> str | None: - options = "0ABCDEFGHIJKLMNOPQRSTUVWXYZ" - choice = input(prompt).strip().upper() - if choice == "0": - logger.log_only("INFO", f"{context} cancelled by user.") - return None - if len(choice) != 1 or choice not in options: - logger.log_only("WARN", f"Invalid {context} selection: {choice}") - print("Invalid selection.") - return "" - return choice - - -def write_session_header(logger: Logger, logfile: Path, script_file: Path, script_dir: Path) -> None: - now = datetime.now() - header = [ - "=" * 79, - "LDL Windows ToolBox Run Log", - "=" * 79, - f"Session ID : {now.strftime('%y%m%d%H%M%S')}", - f"Started : {now.strftime('%m/%d/%Y %H:%M:%S')}", - f"Script : {script_file}", - f"Script Dir : {script_dir}", - f"Work Dir : {Path.cwd()}", - f"User : {os.environ.get('USERDOMAIN', '')}\\{os.environ.get('USERNAME', '')}", - f"Computer : {os.environ.get('COMPUTERNAME', '')}", - f"OS : {platform.system()}", - f"SystemRoot : {os.environ.get('SystemRoot', '')}", - f"Temp : {tempfile.gettempdir()}", - f"Log File : {logfile}", - "=" * 79, - "", - ] - logger.logfile.parent.mkdir(parents=True, exist_ok=True) - logger.logfile.write_text("\n".join(header), encoding="utf-8", newline="\n") - logger.log_only("INFO", "Logging initialized.") - - -def get_log_dir(script_dir: Path) -> Path: - log_dir = script_dir / "logs" - try: - log_dir.mkdir(parents=True, exist_ok=True) - return log_dir - except OSError: - print("Failed to create logs directory. Using script directory for logs.") - return script_dir - - -def get_volume_table() -> str: - ps = ( - "Get-Volume | Where-Object { $_.DriveLetter -ne $null } " - "| Select-Object @{Name='Drive';Expression={$_.DriveLetter + ':'}}, " - "FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} " - "| Format-Table -AutoSize" - ) - result = run_command(["powershell", "-NoProfile", "-Command", ps], capture=True) - return (result.stdout or "") + (result.stderr or "") - - -def select_existing_drive(logger: Logger, context: str) -> str | None: - choice = prompt_drive(logger, "Press 0 to return, or drive letter to continue (A-Z): ", context) - if choice is None: - return None - if choice == "": - return "" - if not Path(f"{choice}:\\").exists(): - logger.log("ERROR", f"Drive {choice}: was not found.") - return "" - return choice - - -def cleanup(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" ADVANCED SYSTEM CLEANUP TOOL") - print(MENU_LOGO) - print(f"All operations are being logged to:\n{logger.logfile}") - print(MENU_LOGO) - logger.section("Advanced System Cleanup") - - free_before = drive_free_mb() - logger.log_only("INFO", f"Free space before cleanup: {free_before} MB") - - logger.log("INFO", "[1/4] Stopping background services...") - for cmd in (["net", "stop", "wuauserv"], ["net", "stop", "bits"]): - logger.log("INFO", f"- Stopping {cmd[-1]}...") - run_and_log(logger, cmd, " ".join(cmd), capture_output=True) - - print() - logger.log("INFO", "[2/4] Deleting temporary and junk files...") - def env_temp_dir(name: str) -> Path | None: - value = os.environ.get(name) - return Path(value) / "Temp" if value else None - - temp_targets = [ - Path(os.environ["WinDir"]) / "Temp", - Path(os.environ["WinDir"]) / "Prefetch", - Path(os.environ["TEMP"]), - env_temp_dir("AppData"), - env_temp_dir("LocalAppData"), - Path(os.environ["WinDir"]) / "SoftwareDistribution" / "Download", - ] - for target in temp_targets: - if target is None: - continue - logger.log("INFO", f"- Cleaning {target}") - if target.exists(): - for child in target.iterdir(): - try: - if child.is_dir(): - shutil.rmtree(child, ignore_errors=False) - else: - child.unlink(missing_ok=True) - except OSError as exc: - logger.log_only("WARN", f"Failed to remove {child}: {exc}") - - logger.log("INFO", "- Event Viewer logs are handled by menu option 6 using wevtutil.") - system_drive = os.environ.get("SYSTEMDRIVE", "C:") - for root_name in ("AMD", "NVIDIA", "INTEL"): - root = Path(f"{system_drive}\\{root_name}") - if root.exists(): - logger.log("INFO", f"- Removing Directory {root}") - shutil.rmtree(root, ignore_errors=True) - - print() - logger.log("INFO", "[3/4] Rebuilding directory structure...") - for target in temp_targets[:5]: - if target is None: - continue - logger.log("INFO", f"- Rebuilding {target}") - target.mkdir(parents=True, exist_ok=True) - - print() - logger.log("INFO", "[4/4] Finalizing optimizations...") - for cmd in (["net", "start", "wuauserv"], ["net", "start", "bits"]): - logger.log("INFO", f"- Starting {cmd[-1]}...") - run_and_log(logger, cmd, " ".join(cmd), capture_output=True) - - free_after = drive_free_mb() - saved = max(0, free_after - free_before) - logger.log_only("INFO", f"Free space after cleanup: {free_after} MB") - logger.log("INFO", "SYSTEM CLEAN UP COMPLETE") - logger.log("INFO", f"Total Space Freed: {saved} MB") - input("Press Enter to continue...") - - -def drive_free_mb() -> int: - ps = ( - f"[math]::Round((Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='{os.environ.get('SYSTEMDRIVE', 'C:')}'\").FreeSpace / 1MB)" - ) - result = run_command(["powershell", "-Command", ps], capture=True) - text = (result.stdout or "").strip() - try: - return int(float(text)) - except ValueError: - return 0 - - -def sys_repair(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" SYSTEM INTEGRITY REPAIR (SFC + DISM)") - print(MENU_LOGO) - print("WARNING: This process can take 15-45 minutes.") - print("-> It CAN be safely interrupted by closing the window.") - print("-> However, it is recommended to let it finish.") - print(MENU_LOGO) - logger.section("System Integrity Repair") - if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "System Integrity Repair"): - return - for cmd, label in ((["sfc", "/scannow"], "System File Checker"), (["dism", "/Online", "/Cleanup-Image", "/RestoreHealth"], "DISM RestoreHealth")): - if not command_exists(cmd[0]): - logger.log("ERROR", f"{cmd[0]} is required for {label}, but it was not found.") - input("Press Enter to continue...") - return - logger.log("INFO", f"Running {label}...") - run_and_log(logger, cmd, " ".join(cmd), capture_output=True) - logger.log("INFO", "SYSTEM INTEGRITY REPAIR COMPLETE") - input("Press Enter to continue...") - - -def component_store_cleanup(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" WINDOWS COMPONENT STORE CLEANUP (WinSxS)") - print(MENU_LOGO) - print("WARNING: This deeply cleans old Windows Update files.") - print("-> It can take 10-30 minutes and may appear stuck.") - print("-> DO NOT interrupt this process (can corrupt updates).") - print(MENU_LOGO) - logger.section("Windows Component Store Cleanup") - if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "Windows Component Store Cleanup"): - return - if not command_exists("dism"): - logger.log("ERROR", "dism is required for DISM component cleanup, but it was not found.") - input("Press Enter to continue...") - return - logger.log("INFO", "Cleaning Windows Component Store...") - run_and_log(logger, ["dism", "/Online", "/Cleanup-Image", "/StartComponentCleanup"], "DISM.exe /Online /Cleanup-Image /StartComponentCleanup") - logger.log("INFO", "WINSXS CLEANUP COMPLETE") - input("Press Enter to continue...") - - -def app_update(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" UPDATE INSTALLED APPS (WINGET)") - print(MENU_LOGO) - print("WARNING: Silently updates all apps installed via Winget.") - print("-> May take several minutes.") - print("-> It CAN be safely interrupted.") - print(MENU_LOGO) - logger.section("Update Installed Apps") - if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "Update Installed Apps"): - return - if not command_exists("winget"): - logger.log("ERROR", "winget is required for Winget update, but it was not found.") - input("Press Enter to continue...") - return - logger.log("INFO", "Upgrading all installed applications (this may take a while)...") - run_and_log( - logger, - ["winget", "upgrade", "--all", "--include-unknown", "--accept-package-agreements", "--accept-source-agreements"], - "winget upgrade --all", - ) - logger.log("INFO", "APP UPDATE COMPLETE") - input("Press Enter to continue...") - - -def net_reset(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" COMPLETE NETWORK RESET") - print(MENU_LOGO) - print("This will reset your network adapters to factory defaults.") - print("-> A system restart will be required afterward.") - print(MENU_LOGO) - logger.section("Complete Network Reset") - if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "Complete Network Reset"): - return - for cmd, label in ((["netsh", "winsock", "reset"], "netsh winsock reset"), (["netsh", "int", "ip", "reset"], "netsh int ip reset"), (["ipconfig", "/flushdns"], "ipconfig /flushdns")): - if not command_exists(cmd[0]): - logger.log("ERROR", f"{cmd[0]} is required for {label}, but it was not found.") - input("Press Enter to continue...") - return - logger.log("INFO", label.replace("netsh ", "Resetting ").replace("ipconfig ", "Flushing ")) - run_and_log(logger, cmd, label) - logger.log("INFO", "NETWORK RESET COMPLETE. Please RESTART your computer.") - input("Press Enter to continue...") - - -def event_logs(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" CLEAR EVENT VIEWER LOGS") - print(MENU_LOGO) - print(f"All operations are being logged to:\n{logger.logfile}") - print(MENU_LOGO) - logger.section("Clear Event Viewer Logs") - if not command_exists("wevtutil"): - logger.log("ERROR", "wevtutil.exe is required for Event Viewer logs, but it was not found.") - input("Press Enter to continue...") - return - result = run_command(["wevtutil", "el"], capture=True) - logs = [line.strip() for line in result.stdout.splitlines() if line.strip()] - for entry in logs: - logger.log("INFO", f"- Clearing log: {entry}") - run_and_log(logger, ["wevtutil", "cl", entry], f"wevtutil.exe cl {entry}") - logger.log("INFO", "EVENT LOGS CLEARED") - input("Press Enter to continue...") - - -def ssd_trim(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" MANUAL SSD TRIM TOOL (KC3000)") - print(MENU_LOGO) - print(f"All operations are being logged to:\n{logger.logfile}") - print(MENU_LOGO) - logger.section("Manual SSD TRIM") - print("Current Drives Connected:") - logger.log_only("INFO", "Current drives connected:") - if not command_exists("powershell"): - logger.log("ERROR", "powershell.exe is required for Volume enumeration, but it was not found.") - input("Press Enter to continue...") - return - volume_text = get_volume_table() - print(volume_text, end="" if volume_text.endswith("\n") else "\n") - logger.write_raw(volume_text) - print() - drive = select_existing_drive(logger, "Manual SSD TRIM") - if drive is None: - return - if drive == "": - logger.log("ERROR", "No valid drive was selected for Manual SSD TRIM.") - input("Press Enter to continue...") - return - logger.log_only("INFO", f"Selected TRIM drive: {drive}:") - print(f"\nOptimizing Drive {drive}: ...") - logger.write_raw(f"Optimizing Drive {drive}: ...") - print("".join(["-" for _ in range(47)])) - if not command_exists("defrag"): - logger.log("ERROR", "defrag.exe is required for SSD TRIM, but it was not found.") - input("Press Enter to continue...") - return - out_file = Path(tempfile.gettempdir()) / "defrag_out.txt" - result = run_command(["defrag", f"{drive}:", "/L", "/V"], capture=True) - out_file.write_text((result.stdout or "") + (result.stderr or ""), encoding="utf-8", errors="replace") - print(out_file.read_text(encoding="utf-8", errors="replace"), end="") - logger.write_raw(out_file.read_text(encoding="utf-8", errors="replace")) - try: - out_file.unlink() - except OSError: - pass - logger.command_result(f"defrag {drive}: /L /V", result.code) - logger.log("INFO", "SSD TRIM COMPLETE") - print("[1] Return to Menu") - print("[2] Exit") - final = input("Choose an option: ").strip() - logger.log_only("INFO", f"SSD TRIM final selection: {final}") - if final == "2": - raise SystemExit(0) - - -def bitlocker_disable(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" DISABLE BITLOCKER (PLAN)") - print(MENU_LOGO) - print("WARNING: This starts BitLocker decryption for the") - print("selected drive and turns BitLocker off.") - print("-> Decryption can take a long time.") - print("-> Keep the PC powered on until Windows finishes.") - print("-> Do this only when protection is no longer needed.") - print(MENU_LOGO) - logger.section("Disable BitLocker") - if not command_exists("manage-bde"): - logger.log("ERROR", "manage-bde.exe is required for BitLocker management, but it was not found.") - input("Press Enter to continue...") - return - print("Current BitLocker status:") - logger.log_only("INFO", "Current BitLocker status:") - status_result = run_and_log(logger, ["manage-bde", "-status"], "manage-bde -status") - if status_result.stdout: - print(status_result.stdout, end="" if status_result.stdout.endswith("\n") else "\n") - if status_result.stderr: - print(status_result.stderr, end="" if status_result.stderr.endswith("\n") else "\n") - print() - drive = select_existing_drive(logger, "Disable BitLocker") - if drive is None: - return - if drive == "": - logger.log("ERROR", "No valid drive was selected for Disable BitLocker.") - input("Press Enter to continue...") - return - logger.log_only("INFO", f"Selected BitLocker drive: {drive}:") - print("\nSelected drive status:") - logger.log_only("INFO", f"Selected BitLocker drive status for {drive}:") - status_result = run_and_log(logger, ["manage-bde", "-status", f"{drive}:"], f"manage-bde -status {drive}:") - if status_result.stdout: - print(status_result.stdout, end="" if status_result.stdout.endswith("\n") else "\n") - if status_result.stderr: - print(status_result.stderr, end="" if status_result.stderr.endswith("\n") else "\n") - print() - if not prompt_keyword(logger, f"Type DISABLE to start decryption for {drive}: ", "DISABLE", "Disable BitLocker"): - return - logger.log("INFO", f"Starting BitLocker decryption on {drive}: ...") - result = run_and_log(logger, ["manage-bde", "-off", f"{drive}:"], f"manage-bde -off {drive}:") - if result.code != 0: - logger.log("ERROR", "BITLOCKER DISABLE FAILED. Check log.") - else: - logger.log("INFO", "BITLOCKER DECRYPTION STARTED. Check Windows BitLocker status for progress.") - print("\nUpdated status:") - logger.log_only("INFO", f"Updated BitLocker status for {drive}:") - status_result = run_and_log(logger, ["manage-bde", "-status", f"{drive}:"], f"manage-bde -status {drive}:") - if status_result.stdout: - print(status_result.stdout, end="" if status_result.stdout.endswith("\n") else "\n") - if status_result.stderr: - print(status_result.stderr, end="" if status_result.stderr.endswith("\n") else "\n") - input("Press Enter to continue...") - - -def list_log_history(log_dir: Path, logger: Logger) -> list[Path]: - entries = sorted(log_dir.glob("LDLWinToolBox_*.log"), key=lambda path: path.stat().st_mtime, reverse=True) - return entries[:9] - - -def paginate_log_file(path: Path) -> None: - try: - lines = path.read_text(encoding="utf-8", errors="replace").splitlines() - except OSError as exc: - print(f"Unable to open log file: {exc}") - input("Press Enter to continue...") - return - - if not lines: - print("(Log file is empty.)") - input("Press Enter to continue...") - return - - terminal_size = shutil.get_terminal_size(fallback=(80, 24)) - page_size = max(10, terminal_size.lines - 6) - page = 0 - - while True: - start = page * page_size - if start >= len(lines): - page = max(0, (len(lines) - 1) // page_size) - start = page * page_size - end = min(start + page_size, len(lines)) - - clear_screen() - print(MENU_LOGO) - print(f"Viewing Log: {path.name}") - print(MENU_LOGO) - print(f"Path: {path}") - print(MENU_LOGO) - print(f"Lines {start + 1}-{end} of {len(lines)}") - print(MENU_LOGO) - for line in lines[start:end]: - print(line) - print(MENU_LOGO) - - if end >= len(lines): - input("End of log. Press Enter to return to the menu...") - return - - choice = input("Press Enter for more, [B]ack, or [Q]uit: ").strip().upper() - if choice == "Q": - return - if choice == "B": - page = max(0, page - 1) - continue - page += 1 - - -def log_history(logger: Logger, log_dir: Path) -> None: - clear_screen() - print(MENU_LOGO) - print(" VIEW LOG HISTORY") - print(MENU_LOGO) - print(f"Log directory:\n{log_dir}") - print(MENU_LOGO) - logger.section("View Log History") - logs = list_log_history(log_dir, logger) - if not logs: - logger.log("INFO", "No log history found.") - input("Press Enter to continue...") - return - for idx, path in enumerate(logs, start=1): - stat = path.stat() - ts = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M") - print(f"[{idx}] {path.name} - {stat.st_size} bytes - {ts}") - print() - print("[0] Return to Menu") - choice = input("Press 0 to return, or 1-9 to view a log: ").strip() - if choice == "0": - logger.log("INFO", "View Log History returned to menu.") - return - try: - index = int(choice) - 1 - except ValueError: - logger.log("WARN", f"Invalid log history selection: {choice}") - input("Press Enter to continue...") - return - if index < 0 or index >= len(logs): - logger.log("WARN", f"Invalid log history selection: {choice}") - input("Press Enter to continue...") - return - selected = logs[index] - print(MENU_LOGO) - print("Viewing Log:") - print(selected.name) - print(MENU_LOGO) - print(f"Path: {selected}") - print(MENU_LOGO) - logger.log_only("INFO", f"Viewing log history file: {selected.name}") - paginate_log_file(selected) - logger.log("INFO", "View Log History returned to menu.") - - -def kill_browser_ai(logger: Logger) -> None: - clear_screen() - print(MENU_LOGO) - print(" KILL BROWSER AI") - print(MENU_LOGO) - print("WARNING: This downloads and executes a remote") - print("PowerShell script from the configured gist URL.") - print("-> It may close browser or AI-related processes.") - print("-> Network access is required.") - print("-> Do not run if you do not trust the source.") - print(MENU_LOGO) - print("Source:") - print(GIST_URL) - print() - logger.section("Kill Browser AI") - logger.log_only("WARN", f"Remote script source: {GIST_URL}") - if not prompt_keyword(logger, "Type KILL to run Kill Browser AI: ", "KILL", "Kill Browser AI"): - return - if not command_exists("powershell"): - logger.log("ERROR", "powershell.exe is required for Kill Browser AI, but it was not found.") - input("Press Enter to continue...") - return - logger.log("INFO", "Running Kill Browser AI...") - cmd = [ - "powershell", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - f"try {{ iwr -useb '{GIST_URL}' | iex; exit 0 }} catch {{ Write-Error $_; exit 1 }}", - ] - result = run_and_log(logger, cmd, "PowerShell remote kill_ai.ps1") - if result.code != 0: - logger.log("ERROR", "KILL BROWSER AI FAILED. Check log.") - else: - logger.log("INFO", "KILL BROWSER AI COMPLETE.") - input("Press Enter to continue...") - - def main_menu(logger: Logger, log_dir: Path) -> None: while True: clear_screen() print("===============================================") print(" LDL Windows ToolBox") print("===============================================") - print("[1] Advanced System Cleanup (with Space Calculator)") - print("[2] System Integrity Repair (SFC + DISM)") - print("[3] Windows Component Store Cleanup (WinSxS)") - print("[4] Update All Installed Apps (Winget)") - print("[5] Complete Network Reset") - print("[6] Clear Event Viewer Logs") - print("[7] Manual SSD TRIM (Optimized for KC3000)") - print("[8] Disable BitLocker (Plan)") - print("[9] Kill Browser AI") - print("[10] View Log History") - print("[11] Exit") + print(" ── System Cleanup ──") + print("[1] Advanced System Cleanup") + print("[2] Windows Component Store Cleanup (WinSxS)") + print("[3] Clear Event Viewer Logs") + print(" ── System Repair & Update ──") + print("[4] System Integrity Repair (SFC + DISM)") + print("[5] Update All Installed Apps (Winget)") + print(" ── Network ──") + print("[6] Complete Network Reset") + print(" ── Performance ──") + print("[7] Manual SSD TRIM") + print("[8] Low Latency Mode (ViVeTool)") + print(" ── Security & Privacy ──") + print("[9] Disable BitLocker (Plan)") + print("[10] Kill Browser AI") + print(" ── Tools ──") + print("[11] View Log History") + print("───────────────────────────────────────────────") + print("[12] Exit") print("===============================================") print(f"Log: {logger.logfile}") print("===============================================") @@ -679,24 +86,26 @@ def main_menu(logger: Logger, log_dir: Path) -> None: if choice == "1": cleanup(logger) elif choice == "2": - sys_repair(logger) - elif choice == "3": component_store_cleanup(logger) + elif choice == "3": + event_logs(logger) elif choice == "4": - app_update(logger) + sys_repair(logger) elif choice == "5": - net_reset(logger) + app_update(logger) elif choice == "6": - event_logs(logger) + net_reset(logger) elif choice == "7": ssd_trim(logger) elif choice == "8": - bitlocker_disable(logger) + low_latency_mode(logger) elif choice == "9": - kill_browser_ai(logger) + bitlocker_disable(logger) elif choice == "10": - log_history(logger, log_dir) + kill_browser_ai(logger) elif choice == "11": + log_history(logger, log_dir) + elif choice == "12": logger.log("INFO", "Exiting LDL Windows ToolBox.") return else: diff --git a/memory/2026-07-05.md b/memory/2026-07-05.md new file mode 100644 index 0000000..d67ee26 --- /dev/null +++ b/memory/2026-07-05.md @@ -0,0 +1,31 @@ +# 2026-07-05 + +## Work Log + +- Refactored monolithic `ldlwintoolbox.py` into modular architecture: + - `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run helpers, prompt utils) + - `features/` — one file per feature + - `ldlwintoolbox.py` — thin entry point with admin logic + main_menu +- All 11 features migrated without functional changes +- Each file imports only from `toolbox_base`; no circular dependencies + +## Decisions + +- `select_existing_drive` lives in `toolbox_base.py` (shared by ssd_trim + bitlocker) +- `drive_free_mb` stays in `system_cleanup.py` (only used there) +- `get_volume_table` stays in `ssd_trim.py` (only used there) +- `detect_architecture`/`ensure_vivetool` stay in `low_latency_mode.py` (only used there) +- `GIST_URL` constant stays in `browser_ai_killer.py` +- All Python stdlib, zero new dependencies + +--- + +*(later same day)* + +## Work Log (cont.) + +- Reorganized main menu into logical groups: + - System Cleanup (1-3), System Repair & Update (4-5), Network (6) + - Performance (7-8), Security & Privacy (9-10), Tools (11), Exit (12) +- Updated `main_menu()` in `ldlwintoolbox.py` with section headers and regrouped options +- Updated `AGENTS.md`, `MEMORY.md`, `memory/tasks.md` to reflect new menu order diff --git a/memory/tasks.md b/memory/tasks.md index 06a9183..fcc24b2 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -1,13 +1,16 @@ # memory/tasks.md -Last updated: 2026-06-17 +Last updated: 2026-07-05 ## Pending -- No open items. +- [ ] Overall user testing and feedback ## Completed +- [x] 2026-07-05: Reorganized main menu into logical groups (Cleanup / Repair & Update / Network / Performance / Security & Privacy / Tools). +- [x] 2026-07-05: Refactored monolithic `ldlwintoolbox.py` into `toolbox_base.py` + `features/` (one file per feature). +- [x] 2026-07-05: Implemented Low Latency Mode with architecture detection, auto ViVeTool download/extraction, and sub-menu (query/enable/disable). - [x] 2026-06-17: Replaced the Log History `more` viewer with an internal paged console viewer so CMD stays open while browsing logs. - [x] 2026-06-16: Rescanned the current repository against the Python-first implementation and refreshed AGENTS.md, MEMORY.md, and memory history with the latest commit and guarded remote-script details. - [x] 2026-06-07: Scanned current repository logic, docs, prompt files, Git metadata, and issue templates. diff --git a/toolbox_base.py b/toolbox_base.py new file mode 100644 index 0000000..7895b78 --- /dev/null +++ b/toolbox_base.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import os +import platform +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + + +MENU_LOGO = "=" * 47 + + +@dataclass(slots=True) +class CommandResult: + code: int + stdout: str = "" + stderr: str = "" + + +class Logger: + def __init__(self, logfile: Path) -> None: + self.logfile = logfile + + def _stamp(self) -> str: + now = datetime.now() + return now.strftime("%m/%d/%Y %H:%M:%S") + + def write_raw(self, message: str) -> None: + self.logfile.parent.mkdir(parents=True, exist_ok=True) + with self.logfile.open("a", encoding="utf-8", errors="replace", newline="\n") as handle: + handle.write(message) + if not message.endswith("\n"): + handle.write("\n") + + def log_only(self, level: str, message: str) -> None: + self.write_raw(f"[{self._stamp()}] [{level}] {message}") + + def log(self, level: str, message: str) -> None: + self.log_only(level, message) + print(message) + + def section(self, title: str) -> None: + self.log_only("INFO", "-" * 79) + self.log("INFO", f"== {title} ==") + + def command_start(self, command: str) -> None: + self.log_only("CMD", f"START {command}") + + def command_result(self, command: str, code: int) -> None: + if code == 0: + self.log_only("OK", f"END {command} exit={code}") + else: + self.log("WARN", f"END {command} exit={code} - check log details.") + + +def clear_screen() -> None: + os.system("cls") + + +def command_exists(command: str) -> bool: + return shutil.which(command) is not None + + +def run_command( + command: list[str] | str, + *, + shell: bool = False, + capture: bool = True, + check: bool = False, +) -> CommandResult: + completed = subprocess.run( + command, + shell=shell, + text=True, + capture_output=capture, + check=False, + ) + if check and completed.returncode != 0: + raise subprocess.CalledProcessError( + completed.returncode, command, completed.stdout, completed.stderr + ) + return CommandResult( + completed.returncode, completed.stdout or "", completed.stderr or "" + ) + + +def run_and_log( + logger: Logger, + command: list[str] | str, + display: str, + *, + shell: bool = False, + capture_output: bool = True, +) -> CommandResult: + logger.command_start(display) + result = run_command(command, shell=shell, capture=capture_output) + if result.stdout: + logger.write_raw(result.stdout) + if result.stderr: + logger.write_raw(result.stderr) + logger.command_result(display, result.code) + return result + + +def prompt_yes_no(logger: Logger, prompt: str, context: str) -> bool: + answer = input(prompt).strip() + if answer.upper() == "Y": + return True + logger.log_only("INFO", f"{context} cancelled by user.") + return False + + +def prompt_keyword(logger: Logger, prompt: str, expected: str, context: str) -> bool: + answer = input(prompt).strip() + if answer.upper() == expected.upper(): + return True + logger.log_only("INFO", f"{context} cancelled by user.") + return False + + +def prompt_drive(logger: Logger, prompt: str, context: str) -> str | None: + options = "0ABCDEFGHIJKLMNOPQRSTUVWXYZ" + choice = input(prompt).strip().upper() + if choice == "0": + logger.log_only("INFO", f"{context} cancelled by user.") + return None + if len(choice) != 1 or choice not in options: + logger.log_only("WARN", f"Invalid {context} selection: {choice}") + print("Invalid selection.") + return "" + return choice + + +def select_existing_drive(logger: Logger, context: str) -> str | None: + choice = prompt_drive( + logger, "Press 0 to return, or drive letter to continue (A-Z): ", context + ) + if choice is None: + return None + if choice == "": + return "" + if not Path(f"{choice}:\\").exists(): + logger.log("ERROR", f"Drive {choice}: was not found.") + return "" + return choice + + +def write_session_header( + logger: Logger, logfile: Path, script_file: Path, script_dir: Path +) -> None: + now = datetime.now() + header = [ + "=" * 79, + "LDL Windows ToolBox Run Log", + "=" * 79, + f"Session ID : {now.strftime('%y%m%d%H%M%S')}", + f"Started : {now.strftime('%m/%d/%Y %H:%M:%S')}", + f"Script : {script_file}", + f"Script Dir : {script_dir}", + f"Work Dir : {Path.cwd()}", + f"User : {os.environ.get('USERDOMAIN', '')}\\{os.environ.get('USERNAME', '')}", + f"Computer : {os.environ.get('COMPUTERNAME', '')}", + f"OS : {platform.system()}", + f"SystemRoot : {os.environ.get('SystemRoot', '')}", + f"Temp : {tempfile.gettempdir()}", + f"Log File : {logfile}", + "=" * 79, + "", + ] + logger.logfile.parent.mkdir(parents=True, exist_ok=True) + logger.logfile.write_text("\n".join(header), encoding="utf-8", newline="\n") + logger.log_only("INFO", "Logging initialized.") + + +def get_log_dir(script_dir: Path) -> Path: + log_dir = script_dir / "logs" + try: + log_dir.mkdir(parents=True, exist_ok=True) + return log_dir + except OSError: + print("Failed to create logs directory. Using script directory for logs.") + return script_dir From 1a3a8b32c69f0afb5bc4bfaa4b375b4a7c8b0921 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 14:12:35 +0800 Subject: [PATCH 19/33] Update AGENTS.md, MEMORY.md, and README.md for current project state - AGENTS.md: added Project Architecture section, feature file references, Python Logger rule - MEMORY.md: updated commit hash, menu numbering, Low Latency menu position - README.md: rewritten using BLANK_README.md template reflecting modular architecture and grouped menu --- AGENTS.md | 49 +++++++++++++++++++++++++------------------- MEMORY.md | 12 ++++++----- README.md | 61 +++++++++++++++++++++++++++++++++++++------------------ 3 files changed, 76 insertions(+), 46 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f666df1..271f977 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ ## Agent Role -You are the AI maintainer for `LDLWinToolBox`, a Python-first Windows utility with a thin Batch launcher for administrative cleanup, repair, update, network reset, log clearing, and SSD TRIM workflows. +You are the AI maintainer for `LDLWinToolBox`, a Python-first Windows utility with a thin Batch launcher for administrative cleanup, repair, update, network reset, log clearing, SSD TRIM, and low-latency configuration workflows. Work from repository facts first. Preserve existing history and project decisions unless the user explicitly asks to replace them. @@ -32,46 +32,53 @@ On every new session: - Keep the app menu-driven and suitable for Windows 10/11. - The script must auto-check Administrator permission and auto-request elevation with UAC before system-level operations. - Preserve timestamped structured logging under `logs\LDLWinToolBox_yyMMddHHmmss.log`. -- Console output should stay concise and user-readable; raw command output should go to `!LOGFILE!`. +- Console output should stay concise and user-readable; raw command output is routed through the Logger to the structured log file. - Logs should include a session header, feature sections, user cancellation notes, command start/end markers, and exit codes for key system commands. - Runtime logs are ignored by git through the existing `*.log` ignore rule. - Long-running or risky operations must warn the user, explain interrupt safety, and ask for `(Y/N)` confirmation. - Sanitize user input for every new menu feature that accepts values. - Keep existing documentation and analysis history intact. If `ANALYSIS.md` or `PROMPT_GUIDE.md` exists, append updates instead of replacing historical context. -## Current Implemented Features +## Project Architecture + +The project follows a modular file-per-feature architecture: -Current modular implementation (`ldlwintoolbox.py` + `toolbox_base.py` + `features/`): +- `ldlwintoolbox.py` — thin entry point with admin logic and main menu dispatch +- `LDLWinToolBox.bat` — thin launcher invoking `uv run -- python ldlwintoolbox.py` +- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run/command/prompt helpers) +- `features/` — one file per feature, each importing only from `toolbox_base` +- Zero external dependencies; all imports from Python stdlib + +## Current Implemented Features -### System Cleanup +### System Cleanup (1-3) -1. Advanced System Cleanup with a free-space calculator, Windows/user temp cleanup, `Prefetch`, `SoftwareDistribution\Download`, and vendor driver root cleanup. -2. Windows Component Store Cleanup using `DISM /StartComponentCleanup`. -3. Clear Event Viewer Logs using `wevtutil`. +1. Advanced System Cleanup in `features/system_cleanup.py` with free-space calculator, Windows/user temp cleanup, `Prefetch`, `SoftwareDistribution\Download`, and vendor driver root cleanup. +2. Windows Component Store Cleanup in `features/winsxs_cleanup.py` using `DISM /StartComponentCleanup`. +3. Clear Event Viewer Logs in `features/event_log_clear.py` using `wevtutil`. -### System Repair & Update +### System Repair & Update (4-5) -4. System Integrity Repair using `sfc /scannow` and `DISM /RestoreHealth`. -5. Update all installed apps using `winget upgrade --all`. +4. System Integrity Repair in `features/system_repair.py` using `sfc /scannow` and `DISM /RestoreHealth`. +5. Update all installed apps in `features/winget_upgrade.py` using `winget upgrade --all`. -### Network +### Network (6) -6. Complete Network Reset using Winsock, TCP/IP reset, and DNS flush. +6. Complete Network Reset in `features/network_reset.py` using Winsock, TCP/IP reset, and DNS flush. -### Performance +### Performance (7-8) -7. Manual SSD TRIM using `defrag /L /V`. +7. Manual SSD TRIM in `features/ssd_trim.py` using `defrag /L /V`. 8. Low Latency Mode in `features/low_latency_mode.py` using ViVeTool (architecture detection, auto-download, sub-menu for query/enable/disable for feature IDs 58989092, 60716524, 61391826). -### Security & Privacy +### Security & Privacy (9-10) -9. Disable BitLocker `(Plan)` using `manage-bde -status`, drive validation, `DISABLE` confirmation, and guarded `manage-bde -off :`. -10. Kill Browser AI using the user-specified command: - `powershell -NoProfile -ExecutionPolicy Bypass -Command "try { iwr -useb https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1 | iex; exit 0 } catch { Write-Error $_; exit 1 }"` +9. Disable BitLocker in `features/bitlocker_disable.py` using `manage-bde -status`, drive validation, `DISABLE` confirmation, and guarded `manage-bde -off :`. +10. Kill Browser AI in `features/browser_ai_killer.py` using the configured remote PowerShell script. -### Tools +### Tools (11) -11. View Log History using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. +11. View Log History in `features/log_viewer.py` using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. 12. Exit. diff --git a/MEMORY.md b/MEMORY.md index 9fb8618..feb5d94 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,6 +1,6 @@ # MEMORY.md -Last updated: 2026-07-05 (modular refactor completed) +Last updated: 2026-07-05 ## User Preferences @@ -17,7 +17,7 @@ Last updated: 2026-07-05 (modular refactor completed) - Repository path: `D:\Projects\WinProjects\LDLWinToolBox` - Git remote: `https://github.com/LoveDoLove/LDLWinToolBox.git` - Current branch at scan time: `lovedolove` -- Latest scanned commit: `3b5e50e Complete toolbox safety features` +- Latest scanned commit: `fbb2701 Refactor into modular architecture + add Low Latency Mode + reorganize menu` - Latest repository scan: `2026-07-05`. - License: Apache License 2.0 - Primary executable: `LDLWinToolBox.bat` thin launcher for `ldlwintoolbox.py` via `uv run -- python` @@ -28,7 +28,7 @@ Last updated: 2026-07-05 (modular refactor completed) - GitHub metadata: `.github/FUNDING.yml`, `.github/ISSUE_TEMPLATE/bug-report---.md`, `.github/ISSUE_TEMPLATE/feature-request---.md` - Asset: `images/logo.png` - Ignored local template observed: `BLANK_README.md` -- Runtime logs observed under `logs\`; `*.log` is ignored by `.gitignore`. +- Runtime logs observed under `logs\`; `*.log` is ignored by `.gitignore`. Downloaded binaries in `tools/` are also git-ignored. ## Current Repository Logic @@ -95,7 +95,9 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal ### 2026-07-05 (Python) +- Modular refactor: split monolithic `ldlwintoolbox.py` into `toolbox_base.py` + `features/` (one file per feature). - Low Latency Mode: auto-detects `platform.machine()` → `IntelAmd` (AMD64/x86) or `SnapdragonArm64` (ARM64), fetches latest ViVe release from `api.github.com/repos/thebookisclosed/ViVe/releases/latest`, downloads matching ZIP via `urllib.request`, extracts with `zipfile` to `tools/vivetool/`, caches version in `version.txt`, provides sub-menu for `/query`, `/enable`, `/disable` on IDs 58989092, 60716524, 61391826. +- Menu reorganization into logical groups: System Cleanup, Repair & Update, Network, Performance, Security & Privacy, Tools. ## Documentation And Prompt Files @@ -108,7 +110,7 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal ## New Feature Details -### Low Latency Mode (Menu 11) +### Low Latency Mode (Menu 8) **Architecture detection:** - `platform.machine()` → `AMD64` → Intel/AMD x64 @@ -133,7 +135,7 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal ## Known Gaps And Risks - The current Python launcher/elevation flow uses `IsUserAnAdmin()` plus `ShellExecuteW(..., "runas", ...)`; keep both the `uv` and `sys.executable` launch paths working. -- Cleanup no longer deletes Event Viewer log files directly; option 6 remains the safe `wevtutil` path for clearing logs. +- Cleanup no longer deletes Event Viewer log files directly; option 3 (Clear Event Viewer Logs) remains the safe `wevtutil` path for clearing logs. - No circular dependencies; each feature imports only from `toolbox_base` - The remote `kill_ai.ps1` gist was retrieved and reviewed on 2026-06-13; it disables Chrome and Edge on-device AI by applying registry policy keys and locking the `OptGuideOnDeviceModel` folders, but it still remains high risk and is only executed through the guarded PowerShell wrapper after explicit `KILL` confirmation. - `BLANK_README.md` is present locally but ignored by git and appears to be an unused Best-README-Template source file. diff --git a/README.md b/README.md index 30e25ee..d01c0f5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

LDL Windows ToolBox

- A cohesive, menu-driven Windows utility that safely automates advanced system cleanup, integrity repair, component updates, network repair, BitLocker decryption planning, browser AI cleanup, and NVMe SSD optimizations. + A cohesive, menu-driven Windows utility that safely automates system cleanup, integrity repair, component updates, network reset, BitLocker decryption planning, browser AI cleanup, SSD TRIM, and low-latency configuration workflows.
Explore the docs »
@@ -60,9 +60,14 @@ ## About The Project -The LDL Windows ToolBox is now a Python-first Windows utility powered by `uv`, with `LDLWinToolBox.bat` acting as a thin launcher for `ldlwintoolbox.py`. It combines administrative privileges checks, system cleanup, repair flows, network reset, BitLocker decryption planning, browser AI cleanup, and SSD TRIM optimization into a single, cohesive menu-driven interface. +The LDL Windows ToolBox is a Python-first Windows utility powered by `uv`, with `LDLWinToolBox.bat` acting as a thin launcher for `ldlwintoolbox.py`. It combines administrative privilege elevation, system cleanup, repair flows, network reset, BitLocker decryption planning, browser AI cleanup, SSD TRIM optimization, and low-latency configuration into a single cohesive menu-driven interface. -It safely automates otherwise tedious system administration tasks while maintaining comprehensive, timestamped logs (`logs\LDLWinToolBox_yyMMddHHmmss.log`) of all actions to ensure complete historical records and safety. +The project follows a modular architecture: +- `ldlwintoolbox.py` — thin entry point with admin logic and main menu dispatch +- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run/command/prompt helpers) +- `features/` — one file per feature, each importing only from `toolbox_base` + +All operations are safely logged with comprehensive timestamped records under `logs\LDLWinToolBox_yyMMddHHmmss.log`. The tool uses only Python standard library and built-in Windows commands; zero external dependencies are required.

(back to top)

@@ -83,7 +88,8 @@ To get a local copy up and running follow these simple steps. ### Prerequisites - Windows 10 or Windows 11 -- Administrator rights (the tool will automatically request this using UAC if launched without it) +- Administrator rights (the tool automatically requests elevation via UAC if launched without them) +- [uv](https://docs.astral.sh/uv/) (recommended) — the launcher falls back to `python` if uv is not available ### Installation @@ -91,7 +97,10 @@ To get a local copy up and running follow these simple steps. ```sh git clone https://github.com/LoveDoLove/LDLWinToolBox.git ``` -2. Double-click `LDLWinToolBox.bat` to launch the interactive menu, or run `uv run -- python ldlwintoolbox.py`. +2. Double-click `LDLWinToolBox.bat` to launch the interactive menu, or run: + ```sh + uv run -- python ldlwintoolbox.py + ```

(back to top)

@@ -99,23 +108,35 @@ To get a local copy up and running follow these simple steps. ## Usage -Upon launching, the interactive menu provides numerical options (1-11) to execute tools: +Upon launching, the interactive menu provides numbered options organized into logical groups: + +**System Cleanup** +- **[1] Advanced System Cleanup**: Deeply cleans temporary system data, prefetch, SoftwareDistribution downloads, vendor driver roots; calculates space freed (MB). +- **[2] Windows Component Store Cleanup (WinSxS)**: Removes superseded Windows Update install files using DISM. +- **[3] Clear Event Viewer Logs**: Flushes system, security, and application logs via wevtutil. + +**System Repair & Update** +- **[4] System Integrity Repair (SFC + DISM)**: Scans and repairs corrupt OS files with SFC and DISM RestoreHealth. +- **[5] Update All Installed Apps**: Silently updates all winget-installed applications. + +**Network** +- **[6] Complete Network Reset**: Resets Winsock, TCP/IP stack, and DNS cache entirely. + +**Performance** +- **[7] Manual SSD TRIM**: Triggers manual SSD re-trim using the Windows defrag utility. +- **[8] Low Latency Mode (ViVeTool)**: Auto-detects CPU architecture (Intel/AMD or Snapdragon ARM64), downloads ViVeTool, and manages Windows low-latency feature flags (IDs 58989092, 60716524, 61391826) with query/enable/disable sub-menu. + +**Security & Privacy** +- **[9] Disable BitLocker (Plan)**: Shows BitLocker status, validates a selected drive, then starts decryption after typing `DISABLE`. +- **[10] Kill Browser AI**: Executes a configured remote PowerShell cleanup command to disable on-device browser AI features after typing `KILL`. -- **[1] Advanced System Cleanup**: Deeply cleans temporary system data, calculates Space Freed (MB). -- **[2] System Integrity Repair**: Executes `SFC` and `DISM` to scan and repair corrupt OS files. -- **[3] Windows Component Store Cleanup**: Removes superseded Windows Update install files (WinSxS). -- **[4] Update All Installed Apps**: Silently updates all `winget`-installed apps. -- **[5] Complete Network Reset**: Resets Winsock, TCP/IP, and DNS cache entirely. -- **[6] Clear Event Viewer Logs**: Flushes system, security, and application logs. -- **[7] Manual SSD TRIM**: Optimized for NVMe drives, triggers manual SSD re-trim using Windows defrag. -- **[8] Disable BitLocker (Plan)**: Shows BitLocker status, validates a selected drive letter, then starts `manage-bde -off` only after typing `DISABLE`. -- **[9] Kill Browser AI**: Runs the configured remote PowerShell cleanup command only after typing `KILL`. -- **[10] View Log History**: Lists recent toolbox logs and opens the selected log with paged console viewing. -- **[11] Exit**: Closes the toolbox. +**Tools** +- **[11] View Log History**: Lists recent toolbox logs and opens the selected log with paged console viewing. +- **[12] Exit**: Closes the toolbox. -Each run writes a structured log under `logs\` with a session header, environment summary, section markers, user cancellations, command start/end markers, and exit codes for key system commands. The Log History viewer shows the newest logs first and does not delete or modify old log files. +Each run writes a structured log under `logs\` with a session header, environment summary, section markers, user cancellations, command start/end markers, and exit codes for key system commands. -_For AI maintenance context and persistent project rules, please refer to [AGENTS.md](AGENTS.md), [MEMORY.md](MEMORY.md), and [memory/tasks.md](memory/tasks.md)._ +_For AI maintenance context and persistent project rules, refer to [AGENTS.md](AGENTS.md), [MEMORY.md](MEMORY.md), and [memory/tasks.md](memory/tasks.md)._

(back to top)

@@ -169,13 +190,13 @@ Project Link: [https://github.com/LoveDoLove/LDLWinToolBox](https://github.com/L ## Acknowledgments - [Best-README-Template](https://github.com/othneildrew/Best-README-Template) +- [ViVeTool](https://github.com/thebookisclosed/ViVe) by thebookisclosed - [Windows UAC / ShellExecuteW](https://learn.microsoft.com/windows/win32/api/shellapi/nf-shellapi-shellexecutew) - [Winget Tool](https://docs.microsoft.com/en-us/windows/package-manager/winget/)

(back to top)

- [contributors-shield]: https://img.shields.io/github/contributors/LoveDoLove/LDLWinToolBox.svg?style=for-the-badge [contributors-url]: https://github.com/LoveDoLove/LDLWinToolBox/graphs/contributors From 1fdeba286ca657a9f108d8c2f4a85e6263e8c8fc Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 14:44:02 +0800 Subject: [PATCH 20/33] Add Y/N confirmation to features 1, 3, 7, and 12 - Advanced System Cleanup, Clear Event Viewer Logs, Manual SSD TRIM, and Exit now all require Y/N confirmation before executing. - This ensures every destructive/system-changing operation is guarded by user confirmation, fixing a critical safety gap. --- AGENTS.md | 8 ++++---- MEMORY.md | 8 ++++---- features/event_log_clear.py | 7 +++++++ features/ssd_trim.py | 7 +++++++ features/system_cleanup.py | 7 +++++++ ldlwintoolbox.py | 7 +++++++ memory/tasks.md | 1 + 7 files changed, 37 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 271f977..984815e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,9 +53,9 @@ The project follows a modular file-per-feature architecture: ### System Cleanup (1-3) -1. Advanced System Cleanup in `features/system_cleanup.py` with free-space calculator, Windows/user temp cleanup, `Prefetch`, `SoftwareDistribution\Download`, and vendor driver root cleanup. +1. Advanced System Cleanup in `features/system_cleanup.py` with Y/N confirmation, free-space calculator, Windows/user temp cleanup, `Prefetch`, `SoftwareDistribution\Download`, and vendor driver root cleanup. 2. Windows Component Store Cleanup in `features/winsxs_cleanup.py` using `DISM /StartComponentCleanup`. -3. Clear Event Viewer Logs in `features/event_log_clear.py` using `wevtutil`. +3. Clear Event Viewer Logs in `features/event_log_clear.py` with Y/N confirmation using `wevtutil`. ### System Repair & Update (4-5) @@ -68,7 +68,7 @@ The project follows a modular file-per-feature architecture: ### Performance (7-8) -7. Manual SSD TRIM in `features/ssd_trim.py` using `defrag /L /V`. +7. Manual SSD TRIM in `features/ssd_trim.py` with Y/N confirmation after drive selection using `defrag /L /V`. 8. Low Latency Mode in `features/low_latency_mode.py` using ViVeTool (architecture detection, auto-download, sub-menu for query/enable/disable for feature IDs 58989092, 60716524, 61391826). ### Security & Privacy (9-10) @@ -80,7 +80,7 @@ The project follows a modular file-per-feature architecture: 11. View Log History in `features/log_viewer.py` using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. -12. Exit. +12. Exit with Y/N confirmation. Remote script execution is high risk. Do not run this command during development. If it is implemented as a menu feature, add an explicit warning and confirmation before execution. diff --git a/MEMORY.md b/MEMORY.md index feb5d94..14226b9 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -52,9 +52,9 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe **System Cleanup (1-3):** -1. Advanced System Cleanup: calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 3 instead of direct file deletion. +1. Advanced System Cleanup: asks Y/N confirmation; calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 3 instead of direct file deletion. 2. Windows Component Store Cleanup: asks confirmation, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup`. -3. Clear Event Viewer Logs: enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. +3. Clear Event Viewer Logs: asks Y/N confirmation; enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. **System Repair & Update (4-5):** @@ -67,7 +67,7 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe **Performance (7-8):** -7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes and validates a single drive letter, confirms the drive exists, runs `defrag : /L /V`, displays output, and appends it to the log. +7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes and validates a single drive letter, confirms the drive exists, asks Y/N confirmation, runs `defrag : /L /V`, displays output, and appends it to the log. 8. Low Latency Mode: auto-detects CPU architecture (Intel/AMD x64 or Snapdragon ARM64), fetches the latest ViVeTool release from GitHub via API, downloads and extracts the matching ZIP to `tools/vivetool/`, and provides a sub-menu to query/enable/disable feature IDs 58989092, 60716524, and 61391826. Version caching avoids redundant downloads. **Security & Privacy (9-10):** @@ -79,7 +79,7 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe 11. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. -12. Exit: closes the tool. +12. Exit: asks Y/N confirmation, then closes the tool. ## Implemented Feature Targets diff --git a/features/event_log_clear.py b/features/event_log_clear.py index 2ba0f83..8261786 100644 --- a/features/event_log_clear.py +++ b/features/event_log_clear.py @@ -5,6 +5,7 @@ Logger, clear_screen, command_exists, + prompt_yes_no, run_and_log, run_command, ) @@ -25,6 +26,12 @@ def event_logs(logger: Logger) -> None: ) input("Press Enter to continue...") return + if not prompt_yes_no( + logger, + "Clear all Event Viewer logs? (Y/N): ", + "Clear Event Viewer Logs", + ): + return result = run_command(["wevtutil", "el"], capture=True) logs = [line.strip() for line in result.stdout.splitlines() if line.strip()] for entry in logs: diff --git a/features/ssd_trim.py b/features/ssd_trim.py index 56e5e60..9323b7d 100644 --- a/features/ssd_trim.py +++ b/features/ssd_trim.py @@ -8,6 +8,7 @@ Logger, clear_screen, command_exists, + prompt_yes_no, run_and_log, run_command, select_existing_drive, @@ -58,6 +59,12 @@ def ssd_trim(logger: Logger) -> None: input("Press Enter to continue...") return logger.log_only("INFO", f"Selected TRIM drive: {drive}:") + if not prompt_yes_no( + logger, + f"Run SSD TRIM on drive {drive}:? (Y/N): ", + "Manual SSD TRIM", + ): + return print(f"\nOptimizing Drive {drive}: ...") logger.write_raw(f"Optimizing Drive {drive}: ...") print("".join(["-" for _ in range(47)])) diff --git a/features/system_cleanup.py b/features/system_cleanup.py index 496c07f..7df2fb8 100644 --- a/features/system_cleanup.py +++ b/features/system_cleanup.py @@ -8,6 +8,7 @@ MENU_LOGO, Logger, clear_screen, + prompt_yes_no, run_and_log, run_command, ) @@ -34,6 +35,12 @@ def cleanup(logger: Logger) -> None: print(f"All operations are being logged to:\n{logger.logfile}") print(MENU_LOGO) logger.section("Advanced System Cleanup") + if not prompt_yes_no( + logger, + "Do you want to proceed? (Y/N): ", + "Advanced System Cleanup", + ): + return free_before = drive_free_mb() logger.log_only("INFO", f"Free space before cleanup: {free_before} MB") diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index f5ad4bb..94529fa 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -11,6 +11,7 @@ Logger, clear_screen, get_log_dir, + prompt_yes_no, write_session_header, ) from features.bitlocker_disable import bitlocker_disable @@ -106,6 +107,12 @@ def main_menu(logger: Logger, log_dir: Path) -> None: elif choice == "11": log_history(logger, log_dir) elif choice == "12": + if not prompt_yes_no( + logger, + "Are you sure you want to exit? (Y/N): ", + "Exit", + ): + continue logger.log("INFO", "Exiting LDL Windows ToolBox.") return else: diff --git a/memory/tasks.md b/memory/tasks.md index fcc24b2..25a033e 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -8,6 +8,7 @@ Last updated: 2026-07-05 ## Completed +- [x] 2026-07-05: Added missing Y/N confirmation prompts to features 1 (Advanced System Cleanup), 3 (Clear Event Viewer Logs), 7 (Manual SSD TRIM), and 12 (Exit). - [x] 2026-07-05: Reorganized main menu into logical groups (Cleanup / Repair & Update / Network / Performance / Security & Privacy / Tools). - [x] 2026-07-05: Refactored monolithic `ldlwintoolbox.py` into `toolbox_base.py` + `features/` (one file per feature). - [x] 2026-07-05: Implemented Low Latency Mode with architecture detection, auto ViVeTool download/extraction, and sub-menu (query/enable/disable). From 9c71adfe6be49f1f27680b8959959732bd14b6d5 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 14:51:22 +0800 Subject: [PATCH 21/33] Add optional restore point before destructive operations - create_restore_point() in toolbox_base.py uses Checkpoint-Computer via PowerShell; logs success/failure without blocking the feature - 8 features (1-7, 9) now offer (Y/N) to create a restore point before executing the destructive operation - Error 0x80070422 (System Restore disabled) detected with helpful message - Updated AGENTS.md, MEMORY.md, memory/tasks.md, memory/feature-ideas.md with completed Phase 1 roadmap items --- AGENTS.md | 16 ++++----- MEMORY.md | 26 +++++++++----- features/bitlocker_disable.py | 8 +++++ features/event_log_clear.py | 7 ++++ features/network_reset.py | 7 ++++ features/ssd_trim.py | 7 ++++ features/system_cleanup.py | 7 ++++ features/system_repair.py | 7 ++++ features/winget_upgrade.py | 7 ++++ features/winsxs_cleanup.py | 7 ++++ memory/feature-ideas.md | 66 ++++++++++++++++++----------------- memory/tasks.md | 5 +-- toolbox_base.py | 33 ++++++++++++++++++ 13 files changed, 153 insertions(+), 50 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 984815e..edf4866 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,27 +53,27 @@ The project follows a modular file-per-feature architecture: ### System Cleanup (1-3) -1. Advanced System Cleanup in `features/system_cleanup.py` with Y/N confirmation, free-space calculator, Windows/user temp cleanup, `Prefetch`, `SoftwareDistribution\Download`, and vendor driver root cleanup. -2. Windows Component Store Cleanup in `features/winsxs_cleanup.py` using `DISM /StartComponentCleanup`. -3. Clear Event Viewer Logs in `features/event_log_clear.py` with Y/N confirmation using `wevtutil`. +1. Advanced System Cleanup in `features/system_cleanup.py` with Y/N confirmation, optional restore point, free-space calculator, Windows/user temp cleanup, `Prefetch`, `SoftwareDistribution\Download`, and vendor driver root cleanup. +2. Windows Component Store Cleanup in `features/winsxs_cleanup.py` with Y/N confirmation and optional restore point using `DISM /StartComponentCleanup`. +3. Clear Event Viewer Logs in `features/event_log_clear.py` with Y/N confirmation and optional restore point using `wevtutil`. ### System Repair & Update (4-5) -4. System Integrity Repair in `features/system_repair.py` using `sfc /scannow` and `DISM /RestoreHealth`. -5. Update all installed apps in `features/winget_upgrade.py` using `winget upgrade --all`. +4. System Integrity Repair in `features/system_repair.py` with Y/N confirmation and optional restore point using `sfc /scannow` and `DISM /RestoreHealth`. +5. Update all installed apps in `features/winget_upgrade.py` with Y/N confirmation and optional restore point using `winget upgrade --all`. ### Network (6) -6. Complete Network Reset in `features/network_reset.py` using Winsock, TCP/IP reset, and DNS flush. +6. Complete Network Reset in `features/network_reset.py` with Y/N confirmation and optional restore point using Winsock, TCP/IP reset, and DNS flush. ### Performance (7-8) -7. Manual SSD TRIM in `features/ssd_trim.py` with Y/N confirmation after drive selection using `defrag /L /V`. +7. Manual SSD TRIM in `features/ssd_trim.py` with Y/N confirmation after drive selection and optional restore point using `defrag /L /V`. 8. Low Latency Mode in `features/low_latency_mode.py` using ViVeTool (architecture detection, auto-download, sub-menu for query/enable/disable for feature IDs 58989092, 60716524, 61391826). ### Security & Privacy (9-10) -9. Disable BitLocker in `features/bitlocker_disable.py` using `manage-bde -status`, drive validation, `DISABLE` confirmation, and guarded `manage-bde -off :`. +9. Disable BitLocker in `features/bitlocker_disable.py` using `manage-bde -status`, drive validation, optional restore point, `DISABLE` confirmation, and guarded `manage-bde -off :`. 10. Kill Browser AI in `features/browser_ai_killer.py` using the configured remote PowerShell script. ### Tools (11) diff --git a/MEMORY.md b/MEMORY.md index 14226b9..a9a4eba 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -52,27 +52,27 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe **System Cleanup (1-3):** -1. Advanced System Cleanup: asks Y/N confirmation; calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 3 instead of direct file deletion. -2. Windows Component Store Cleanup: asks confirmation, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup`. -3. Clear Event Viewer Logs: asks Y/N confirmation; enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. +1. Advanced System Cleanup: asks Y/N confirmation, optional restore point; calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 3 instead of direct file deletion. +2. Windows Component Store Cleanup: asks Y/N confirmation and optional restore point, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup`. +3. Clear Event Viewer Logs: asks Y/N confirmation and optional restore point; enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. **System Repair & Update (4-5):** -4. System Integrity Repair: asks confirmation, runs `sfc /scannow`, then `DISM /Online /Cleanup-Image /RestoreHealth`. -5. Update All Installed Apps: asks confirmation, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements`. +4. System Integrity Repair: asks confirmation and optional restore point, runs `sfc /scannow`, then `DISM /Online /Cleanup-Image /RestoreHealth`. +5. Update All Installed Apps: asks confirmation and optional restore point, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements`. **Network (6):** -6. Complete Network Reset: asks confirmation, runs `netsh winsock reset`, `netsh int ip reset`, and `ipconfig /flushdns`; tells user to restart. +6. Complete Network Reset: asks confirmation and optional restore point, runs `netsh winsock reset`, `netsh int ip reset`, and `ipconfig /flushdns`; tells user to restart. **Performance (7-8):** -7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes and validates a single drive letter, confirms the drive exists, asks Y/N confirmation, runs `defrag : /L /V`, displays output, and appends it to the log. +7. Manual SSD TRIM: lists volumes with PowerShell `Get-Volume`, sanitizes and validates a single drive letter, confirms the drive exists, asks Y/N confirmation and optional restore point, runs `defrag : /L /V`, displays output, and appends it to the log. 8. Low Latency Mode: auto-detects CPU architecture (Intel/AMD x64 or Snapdragon ARM64), fetches the latest ViVeTool release from GitHub via API, downloads and extracts the matching ZIP to `tools/vivetool/`, and provides a sub-menu to query/enable/disable feature IDs 58989092, 60716524, and 61391826. Version caching avoids redundant downloads. **Security & Privacy (9-10):** -9. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. +9. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, optional restore point, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. 10. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then launches PowerShell with `-ExecutionPolicy Bypass` and a guarded `try/catch` wrapper around the configured gist command so the result is logged. **Tools (11):** @@ -132,6 +132,16 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - Feature ID changes in future Windows builds may require updates - Reboot may be required after changing low latency features +## Restore Point Feature + +`create_restore_point(logger, description)` in `toolbox_base.py` creates a system restore point before destructive operations. Key behaviors: + +- Uses `Checkpoint-Computer` via single-line PowerShell +- Asks user `(Y/N)` before attempting +- Failure (e.g. System Restore disabled) logs WARN and continues — never blocks the feature +- Known error `0x80070422` detected and shown with a helpful message +- Integrated into features 1-7 and 9 (all except read-only/remote features) + ## Known Gaps And Risks - The current Python launcher/elevation flow uses `IsUserAnAdmin()` plus `ShellExecuteW(..., "runas", ...)`; keep both the `uv` and `sys.executable` launch paths working. diff --git a/features/bitlocker_disable.py b/features/bitlocker_disable.py index 8ddad41..35f8e79 100644 --- a/features/bitlocker_disable.py +++ b/features/bitlocker_disable.py @@ -5,7 +5,9 @@ Logger, clear_screen, command_exists, + create_restore_point, prompt_keyword, + prompt_yes_no, run_and_log, select_existing_drive, ) @@ -76,6 +78,12 @@ def bitlocker_disable(logger: Logger) -> None: end="" if status_result.stderr.endswith("\n") else "\n", ) print() + if prompt_yes_no( + logger, + "Create a system restore point before proceeding? (Y/N): ", + "Restore Point - Disable BitLocker", + ): + create_restore_point(logger, "Before Disabling BitLocker") if not prompt_keyword( logger, f"Type DISABLE to start decryption for {drive}: ", diff --git a/features/event_log_clear.py b/features/event_log_clear.py index 8261786..0480b54 100644 --- a/features/event_log_clear.py +++ b/features/event_log_clear.py @@ -5,6 +5,7 @@ Logger, clear_screen, command_exists, + create_restore_point, prompt_yes_no, run_and_log, run_command, @@ -26,6 +27,12 @@ def event_logs(logger: Logger) -> None: ) input("Press Enter to continue...") return + if prompt_yes_no( + logger, + "Create a system restore point before proceeding? (Y/N): ", + "Restore Point - Clear Event Viewer Logs", + ): + create_restore_point(logger, "Before Clearing Event Viewer Logs") if not prompt_yes_no( logger, "Clear all Event Viewer logs? (Y/N): ", diff --git a/features/network_reset.py b/features/network_reset.py index 3c1ba02..017d107 100644 --- a/features/network_reset.py +++ b/features/network_reset.py @@ -5,6 +5,7 @@ Logger, clear_screen, command_exists, + create_restore_point, prompt_yes_no, run_and_log, ) @@ -19,6 +20,12 @@ def net_reset(logger: Logger) -> None: print("-> A system restart will be required afterward.") print(MENU_LOGO) logger.section("Complete Network Reset") + if prompt_yes_no( + logger, + "Create a system restore point before proceeding? (Y/N): ", + "Restore Point - Complete Network Reset", + ): + create_restore_point(logger, "Before Network Reset") if not prompt_yes_no( logger, "Do you want to proceed? (Y/N): ", "Complete Network Reset" ): diff --git a/features/ssd_trim.py b/features/ssd_trim.py index 9323b7d..c5079be 100644 --- a/features/ssd_trim.py +++ b/features/ssd_trim.py @@ -8,6 +8,7 @@ Logger, clear_screen, command_exists, + create_restore_point, prompt_yes_no, run_and_log, run_command, @@ -59,6 +60,12 @@ def ssd_trim(logger: Logger) -> None: input("Press Enter to continue...") return logger.log_only("INFO", f"Selected TRIM drive: {drive}:") + if prompt_yes_no( + logger, + "Create a system restore point before proceeding? (Y/N): ", + "Restore Point - Manual SSD TRIM", + ): + create_restore_point(logger, "Before SSD TRIM") if not prompt_yes_no( logger, f"Run SSD TRIM on drive {drive}:? (Y/N): ", diff --git a/features/system_cleanup.py b/features/system_cleanup.py index 7df2fb8..c112a93 100644 --- a/features/system_cleanup.py +++ b/features/system_cleanup.py @@ -8,6 +8,7 @@ MENU_LOGO, Logger, clear_screen, + create_restore_point, prompt_yes_no, run_and_log, run_command, @@ -35,6 +36,12 @@ def cleanup(logger: Logger) -> None: print(f"All operations are being logged to:\n{logger.logfile}") print(MENU_LOGO) logger.section("Advanced System Cleanup") + if prompt_yes_no( + logger, + "Create a system restore point before proceeding? (Y/N): ", + "Restore Point - Advanced System Cleanup", + ): + create_restore_point(logger, "Before Advanced System Cleanup") if not prompt_yes_no( logger, "Do you want to proceed? (Y/N): ", diff --git a/features/system_repair.py b/features/system_repair.py index ea7a4ff..89ad281 100644 --- a/features/system_repair.py +++ b/features/system_repair.py @@ -5,6 +5,7 @@ Logger, clear_screen, command_exists, + create_restore_point, prompt_yes_no, run_and_log, ) @@ -20,6 +21,12 @@ def sys_repair(logger: Logger) -> None: print("-> However, it is recommended to let it finish.") print(MENU_LOGO) logger.section("System Integrity Repair") + if prompt_yes_no( + logger, + "Create a system restore point before proceeding? (Y/N): ", + "Restore Point - System Integrity Repair", + ): + create_restore_point(logger, "Before System Integrity Repair") if not prompt_yes_no( logger, "Do you want to proceed? (Y/N): ", "System Integrity Repair" ): diff --git a/features/winget_upgrade.py b/features/winget_upgrade.py index b25e1f5..66749c2 100644 --- a/features/winget_upgrade.py +++ b/features/winget_upgrade.py @@ -5,6 +5,7 @@ Logger, clear_screen, command_exists, + create_restore_point, prompt_yes_no, run_and_log, ) @@ -20,6 +21,12 @@ def app_update(logger: Logger) -> None: print("-> It CAN be safely interrupted.") print(MENU_LOGO) logger.section("Update Installed Apps") + if prompt_yes_no( + logger, + "Create a system restore point before proceeding? (Y/N): ", + "Restore Point - Update Installed Apps", + ): + create_restore_point(logger, "Before Winget App Upgrade") if not prompt_yes_no( logger, "Do you want to proceed? (Y/N): ", "Update Installed Apps" ): diff --git a/features/winsxs_cleanup.py b/features/winsxs_cleanup.py index c65a1b4..e5ae9d9 100644 --- a/features/winsxs_cleanup.py +++ b/features/winsxs_cleanup.py @@ -5,6 +5,7 @@ Logger, clear_screen, command_exists, + create_restore_point, prompt_yes_no, run_and_log, ) @@ -20,6 +21,12 @@ def component_store_cleanup(logger: Logger) -> None: print("-> DO NOT interrupt this process (can corrupt updates).") print(MENU_LOGO) logger.section("Windows Component Store Cleanup") + if prompt_yes_no( + logger, + "Create a system restore point before proceeding? (Y/N): ", + "Restore Point - Windows Component Store Cleanup", + ): + create_restore_point(logger, "Before WinSxS Component Store Cleanup") if not prompt_yes_no( logger, "Do you want to proceed? (Y/N): ", diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index 228614c..91b7e72 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -1,47 +1,56 @@ # Feature Ideas Backlog -Last updated: 2026-06-13 +Last updated: 2026-07-05 This file is a living backlog of future enhancements and maintenance ideas for `LDLWinToolBox`. Keep entries concise, append-friendly, and aligned with the Python-first, menu-driven design. ## Suggested Priority Order -### Phase 1: Foundation +### Phase 1: Foundation ✅ (Complete) -1. Extract shared helper labels and common routines -2. Strengthen input validation for all menu prompts -3. Standardize confirmation flow for risky actions -4. Add preflight checks for external commands -5. Improve error handling and user-facing failure messages +- [x] Extract shared helper labels and common routines (toolbox_base.py) +- [x] Strengthen input validation for all menu prompts (prompt_drive, select_existing_drive) +- [x] Standardize confirmation flow for risky actions (prompt_yes_no, prompt_keyword) +- [x] Add preflight checks for external commands (command_exists) +- [x] Improve error handling and user-facing failure messages +- [x] Create restore point before risky operations (create_restore_point) -### Phase 2: Safety And Clarity +### Phase 2: Safety & Clarity (Next) -1. Improve log readability and section formatting -2. Harmonize menu wording and labels -3. Use clearer section headers in the menu -4. Add progress hints for long-running tasks -5. Make cleanup operations more conservative by default +1. Add progress hints for long-running tasks (`[1/N]` markers where missing) +2. Make cleanup operations more conservative by default -### Phase 3: Efficiency And Maintenance +### Phase 3: New Feature Modules + +1. System information summary +2. Windows Update status check +3. Defender status check and quick scan entry +4. Service health check for common Windows services + +### Phase 4: Diagnostics & Reporting + +1. Disk health and SMART summary +2. Driver inventory and version view +3. Network before/after snapshot +4. Log export and archive bundle +5. Exportable report of actions and results + +### Phase 5: Efficiency & Maintenance 1. Reduce redundant PowerShell calls -2. Maintain a lightweight verification checklist after changes -3. Keep README, memory, and task notes synchronized -4. Add a read-only mode for status checks +2. Add a read-only mode for status checks +3. Version and update check for the toolbox itself -### Phase 4: Feature Work Enablers +### Phase 6: Advanced Features -1. System information summary -2. Create a restore point before risky operations -3. Log export and archive bundle -4. Exportable report of actions and results -5. Version and update check for the toolbox itself +1. Selective cleanup instead of fixed cleanup sets +2. Custom exclusion list for cleanup targets +3. Safe Mode or recovery entry helpers ## New Features - System information summary -- Create a restore point before risky operations - Windows Update status check - Driver inventory and version view - Service health check for common Windows services @@ -57,17 +66,10 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d ## Optimizations -- Standardize confirmation flow for risky actions -- Extract shared helper labels and common routines -- Strengthen input validation for all menu prompts - Reduce redundant PowerShell calls - Improve error handling and user-facing failure messages -- Improve log readability and section formatting -- Add preflight checks for external commands +- Add progress hints for long-running tasks - Make cleanup operations more conservative by default -- Harmonize menu wording and labels - Add a read-only mode for status checks -- Use clearer section headers in the menu -- Add progress hints for long-running tasks - Maintain a lightweight verification checklist after changes - Keep README, memory, and task notes synchronized diff --git a/memory/tasks.md b/memory/tasks.md index 25a033e..ab27246 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -4,11 +4,12 @@ Last updated: 2026-07-05 ## Pending -- [ ] Overall user testing and feedback +- [ ] Phase 2+: System information summary, progress hints, Windows Update check, Defender check, service health, SMART summary, driver inventory, network snapshot, reduced PS calls, export report, self-update, selective cleanup, safe mode, read-only mode ## Completed -- [x] 2026-07-05: Added missing Y/N confirmation prompts to features 1 (Advanced System Cleanup), 3 (Clear Event Viewer Logs), 7 (Manual SSD TRIM), and 12 (Exit). +- [x] 2026-07-05: Added optional restore point via `create_restore_point()` to features 1-7 and 9. +- [x] 2026-07-05: Added missing Y/N confirmation prompts to features 1, 3, 7, and 12. - [x] 2026-07-05: Reorganized main menu into logical groups (Cleanup / Repair & Update / Network / Performance / Security & Privacy / Tools). - [x] 2026-07-05: Refactored monolithic `ldlwintoolbox.py` into `toolbox_base.py` + `features/` (one file per feature). - [x] 2026-07-05: Implemented Low Latency Mode with architecture detection, auto ViVeTool download/extraction, and sub-menu (query/enable/disable). diff --git a/toolbox_base.py b/toolbox_base.py index 7895b78..3ec2241 100644 --- a/toolbox_base.py +++ b/toolbox_base.py @@ -149,6 +149,39 @@ def select_existing_drive(logger: Logger, context: str) -> str | None: return choice +def create_restore_point(logger: Logger, description: str) -> bool: + """Create a system restore point. Returns True on success, False on failure. + + If System Restore is disabled or PowerShell is unavailable, logs a warning + and returns False without blocking the caller. + """ + if not command_exists("powershell"): + logger.log_only( + "WARN", + "Cannot create restore point: PowerShell is not available.", + ) + return False + ps = ( + "Checkpoint-Computer -Description 'LDLWinToolBox - " + + description.replace("'", "''") + + "' -RestorePointType MODIFY_SETTINGS" + ) + logger.log_only("INFO", f"Creating system restore point: {description} ...") + result = run_command(["powershell", "-NoProfile", "-Command", ps], capture=True) + rc = result.code + if rc == 0: + logger.log_only("OK", f"Restore point created: {description}") + print(">>> System restore point created successfully.") + return True + stderr = result.stderr.strip() + logger.log_only("WARN", f"Restore point failed (exit={rc}): {stderr or 'unknown error'}") + if "0x80070422" in stderr: + print(">>> System Restore may be disabled. Enable it in System Properties to use this feature.") + else: + print(f">>> Restore point creation failed (exit={rc}). Continuing anyway.") + return False + + def write_session_header( logger: Logger, logfile: Path, script_file: Path, script_dir: Path ) -> None: From 74c759d159ba1cee96b09eb9ae1a5490438b495e Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 14:53:43 +0800 Subject: [PATCH 22/33] Phase 2: Progress hints and conservative cleanup - Add [1/2][2/2] progress markers to System Integrity Repair - Add [1/1] progress markers to WinSxS cleanup and Winget upgrade - Vendor driver root deletion (AMD/NVIDIA/INTEL) now opt-in (Y/N) instead of automatic, making system cleanup more conservative - Updated memory files with Phase 2 completion status --- MEMORY.md | 8 ++++---- features/system_cleanup.py | 17 +++++++++++------ features/system_repair.py | 7 ++++--- features/winget_upgrade.py | 2 +- features/winsxs_cleanup.py | 2 +- memory/feature-ideas.md | 6 +++--- memory/tasks.md | 1 + 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/MEMORY.md b/MEMORY.md index a9a4eba..2c767c8 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -52,14 +52,14 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe **System Cleanup (1-3):** -1. Advanced System Cleanup: asks Y/N confirmation, optional restore point; calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads, and root driver folders such as `AMD`, `NVIDIA`, and `INTEL`; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 3 instead of direct file deletion. -2. Windows Component Store Cleanup: asks Y/N confirmation and optional restore point, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup`. +1. Advanced System Cleanup: asks Y/N confirmation, optional restore point; calculates free space before and after cleanup, stops `wuauserv` and `bits`, deletes Windows/user temp files, Prefetch, SoftwareDistribution downloads; **optionally** removes vendor driver roots (`AMD`, `NVIDIA`, `INTEL`) via separate Y/N prompt; rebuilds temp directories; restarts services; reports MB freed. Event Viewer logs are intentionally handled by option 3 instead of direct file deletion. +2. Windows Component Store Cleanup: asks Y/N confirmation and optional restore point, runs `DISM.exe /Online /Cleanup-Image /StartComponentCleanup` with `[1/1]` progress hint. 3. Clear Event Viewer Logs: asks Y/N confirmation and optional restore point; enumerates all logs with `wevtutil.exe el` and clears each one with `wevtutil.exe cl`. **System Repair & Update (4-5):** -4. System Integrity Repair: asks confirmation and optional restore point, runs `sfc /scannow`, then `DISM /Online /Cleanup-Image /RestoreHealth`. -5. Update All Installed Apps: asks confirmation and optional restore point, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements`. +4. System Integrity Repair: asks confirmation and optional restore point, runs `sfc /scannow` then `DISM /Online /Cleanup-Image /RestoreHealth` with `[1/2] [2/2]` progress hints. +5. Update All Installed Apps: asks confirmation and optional restore point, runs `winget upgrade --all --include-unknown --accept-package-agreements --accept-source-agreements` with `[1/1]` progress hint. **Network (6):** diff --git a/features/system_cleanup.py b/features/system_cleanup.py index c112a93..2814e32 100644 --- a/features/system_cleanup.py +++ b/features/system_cleanup.py @@ -90,12 +90,17 @@ def env_temp_dir(name: str) -> Path | None: "INFO", "- Event Viewer logs are handled by menu option 6 using wevtutil.", ) - system_drive = os.environ.get("SYSTEMDRIVE", "C:") - for root_name in ("AMD", "NVIDIA", "INTEL"): - root = Path(f"{system_drive}\\{root_name}") - if root.exists(): - logger.log("INFO", f"- Removing Directory {root}") - shutil.rmtree(root, ignore_errors=True) + if prompt_yes_no( + logger, + "Also remove vendor driver directories (AMD, NVIDIA, INTEL) on system drive? (Y/N): ", + "Vendor Driver Cleanup", + ): + system_drive = os.environ.get("SYSTEMDRIVE", "C:") + for root_name in ("AMD", "NVIDIA", "INTEL"): + root = Path(f"{system_drive}\\{root_name}") + if root.exists(): + logger.log("INFO", f"- Removing Directory {root}") + shutil.rmtree(root, ignore_errors=True) print() logger.log("INFO", "[3/4] Rebuilding directory structure...") diff --git a/features/system_repair.py b/features/system_repair.py index 89ad281..52c8245 100644 --- a/features/system_repair.py +++ b/features/system_repair.py @@ -31,13 +31,14 @@ def sys_repair(logger: Logger) -> None: logger, "Do you want to proceed? (Y/N): ", "System Integrity Repair" ): return - for cmd, label in ( + steps = [ (["sfc", "/scannow"], "System File Checker"), ( ["dism", "/Online", "/Cleanup-Image", "/RestoreHealth"], "DISM RestoreHealth", ), - ): + ] + for i, (cmd, label) in enumerate(steps, 1): if not command_exists(cmd[0]): logger.log( "ERROR", @@ -45,7 +46,7 @@ def sys_repair(logger: Logger) -> None: ) input("Press Enter to continue...") return - logger.log("INFO", f"Running {label}...") + logger.log("INFO", f"[{i}/{len(steps)}] Running {label}...") run_and_log(logger, cmd, " ".join(cmd), capture_output=True) logger.log("INFO", "SYSTEM INTEGRITY REPAIR COMPLETE") input("Press Enter to continue...") diff --git a/features/winget_upgrade.py b/features/winget_upgrade.py index 66749c2..2213832 100644 --- a/features/winget_upgrade.py +++ b/features/winget_upgrade.py @@ -40,7 +40,7 @@ def app_update(logger: Logger) -> None: return logger.log( "INFO", - "Upgrading all installed applications (this may take a while)...", + "[1/1] Upgrading all installed applications (this may take a while)...", ) run_and_log( logger, diff --git a/features/winsxs_cleanup.py b/features/winsxs_cleanup.py index e5ae9d9..be9535d 100644 --- a/features/winsxs_cleanup.py +++ b/features/winsxs_cleanup.py @@ -40,7 +40,7 @@ def component_store_cleanup(logger: Logger) -> None: ) input("Press Enter to continue...") return - logger.log("INFO", "Cleaning Windows Component Store...") + logger.log("INFO", "[1/1] Cleaning Windows Component Store...") run_and_log( logger, ["dism", "/Online", "/Cleanup-Image", "/StartComponentCleanup"], diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index 91b7e72..a21a7fa 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -16,10 +16,10 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - [x] Improve error handling and user-facing failure messages - [x] Create restore point before risky operations (create_restore_point) -### Phase 2: Safety & Clarity (Next) +### Phase 2: Safety & Clarity ✅ (Complete) -1. Add progress hints for long-running tasks (`[1/N]` markers where missing) -2. Make cleanup operations more conservative by default +- [x] Add progress hints for long-running tasks (`[1/N]` markers where missing) +- [x] Make cleanup operations more conservative by default (vendor driver roots now opt-in) ### Phase 3: New Feature Modules diff --git a/memory/tasks.md b/memory/tasks.md index ab27246..f665495 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -8,6 +8,7 @@ Last updated: 2026-07-05 ## Completed +- [x] 2026-07-05: Phase 2: Added progress hints `[1/1]` to WinSxS / Winget, `[1/2][2/2]` to SFC+DISM; made vendor driver root deletion opt-in (Y/N) for conservative cleanup. - [x] 2026-07-05: Added optional restore point via `create_restore_point()` to features 1-7 and 9. - [x] 2026-07-05: Added missing Y/N confirmation prompts to features 1, 3, 7, and 12. - [x] 2026-07-05: Reorganized main menu into logical groups (Cleanup / Repair & Update / Network / Performance / Security & Privacy / Tools). From 8b604288a279eac3be3342e0856edefa17b8ce0b Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 14:56:53 +0800 Subject: [PATCH 23/33] Phase 3: Diagnostics section with 4 new features - System Information (menu 11): OS/CPU/RAM/disk/uptime via stdlib+ctypes+winreg - Windows Update Status (menu 12): service query, registry config, UsoClient scan - Defender Status & Scan (menu 13): Get-MpComputerStatus, signature update, quick scan - Service Health Check (menu 14): 20 critical services via PowerShell Get-Service - Menu renumbered: Diagnostics (11-14), Tools (15), Exit (16) - Updated memory files with Phase 3 completion --- MEMORY.md | 13 ++- features/defender_tools.py | 123 ++++++++++++++++++++++++++++ features/service_health.py | 126 +++++++++++++++++++++++++++++ features/system_info.py | 148 +++++++++++++++++++++++++++++++++ features/windows_update.py | 162 +++++++++++++++++++++++++++++++++++++ ldlwintoolbox.py | 23 +++++- memory/feature-ideas.md | 10 +-- memory/tasks.md | 1 + 8 files changed, 595 insertions(+), 11 deletions(-) create mode 100644 features/defender_tools.py create mode 100644 features/service_health.py create mode 100644 features/system_info.py create mode 100644 features/windows_update.py diff --git a/MEMORY.md b/MEMORY.md index 2c767c8..a6497e1 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -75,11 +75,18 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe 9. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, optional restore point, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. 10. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then launches PowerShell with `-ExecutionPolicy Bypass` and a guarded `try/catch` wrapper around the configured gist command so the result is logged. -**Tools (11):** +**Tools (15):** -11. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. +15. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. -12. Exit: asks Y/N confirmation, then closes the tool. +16. Exit: asks Y/N confirmation, then closes the tool. + +**Diagnostics (11-14):** + +11. System Information: read-only summary of OS, CPU, RAM, disk, uptime using stdlib + ctypes + winreg. +12. Windows Update Status: queries wuauserv, Auto Update registry config, last install/search dates; runs UsoClient scan. +13. Defender Status & Quick Scan: displays Get-MpComputerStatus fields, optional MpCmdRun signature update, optional Start-MpQuickScan. +14. Service Health Check: checks 20 critical services via PowerShell Get-Service, shows Running/Stopped summary. ## Implemented Feature Targets diff --git a/features/defender_tools.py b/features/defender_tools.py new file mode 100644 index 0000000..fbad3bc --- /dev/null +++ b/features/defender_tools.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import subprocess + +from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, prompt_yes_no, run_command + + +def _ps_get(cmdlet: str) -> str: + if not command_exists("powershell"): + return "PowerShell not available" + result = run_command( + ["powershell", "-NoProfile", "-Command", cmdlet], + capture=True, + ) + if result.code != 0: + return "" + return result.stdout.strip() + + +def _show_defender_status(logger: Logger) -> None: + script = ( + "$s=Get-MpComputerStatus; " + "Write-Output $s.AntivirusEnabled; " + "Write-Output $s.AMServiceEnabled; " + "Write-Output $s.AMProductVersion; " + "Write-Output $s.AntispywareEnabled; " + "Write-Output $s.RealTimeProtectionEnabled; " + "Write-Output $s.NISEnabled; " + "Write-Output $s.QuickScanAge; " + "Write-Output $s.FullScanAge; " + "Write-Output $s.DefinitionAge; " + "Write-Output $s.DefinitionVersion; " + "Write-Output $s.LastQuickScanSource; " + "Write-Output $s.LastFullScanSource" + ) + raw = _ps_get(script) + lines = raw.splitlines() + fields = [ + "Antivirus Enabled", + "AMService Enabled", + "AM Product Version", + "Antispyware Enabled", + "Real-Time Protection", + "Network Inspection System", + "Quick Scan Age (days)", + "Full Scan Age (days)", + "Definition Age (days)", + "Definition Version", + "Last Quick Scan Source", + "Last Full Scan Source", + ] + print(f" {'Status Field':<30} {'Value':<20}") + print(f" {'-'*30} {'-'*20}") + for field, val in zip(fields, lines): + display = val if val else "N/A" + print(f" {field:<30} {display:<20}") + print() + + for field, val in zip(fields, lines): + logger.log_only("INFO", f"{field}: {val}") + + age_ranges = { + "Quick Scan Age (days)": (lines[6] if len(lines) > 6 else ""), + "Full Scan Age (days)": (lines[7] if len(lines) > 7 else ""), + "Definition Age (days)": (lines[8] if len(lines) > 8 else ""), + } + for label, age_str in age_ranges.items(): + try: + age = int(age_str) + if age > 7: + print(f" >>> {label} is {age} days. A scan is recommended.") + except (ValueError, TypeError): + pass + + +def defender_tools(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" WINDOWS DEFENDER STATUS & SCAN") + print(MENU_LOGO) + logger.section("Defender Status") + + if not command_exists("powershell"): + logger.log("ERROR", "PowerShell is required for Defender status.") + input("Press Enter to continue...") + return + + _show_defender_status(logger) + + if command_exists("MpCmdRun.exe"): + logger.section("Signature Update via MpCmdRun") + print("Checking for signature updates...") + result = run_command( + [ + "MpCmdRun.exe", + "-SignatureUpdate", + ], + capture=True, + ) + if result.code == 0: + print("Signatures are up to date.") + else: + print(f"Signature update returned exit code {result.code}.") + logger.log_only("INFO", f"MpCmdRun -SignatureUpdate exit={result.code}") + + logger.section("Quick Scan") + if prompt_yes_no(logger, "Run a Windows Defender Quick Scan? (Y/N): ", "Quick Scan"): + script = "Start-MpQuickScan" + print("Running Quick Scan (this may take several minutes)...") + logger.log_only("CMD", "START-MPQUICKSCAN") + result = run_command( + ["powershell", "-NoProfile", "-Command", script], + capture=True, + ) + rc = result.code + logger.command_result("Start-MpQuickScan", rc) + if rc == 0: + print("Quick Scan completed successfully.") + else: + print(f"Quick Scan returned exit code {rc}.") + + logger.log_only("INFO", "DEFENDER STATUS CHECK COMPLETE") + input("Press Enter to continue...") diff --git a/features/service_health.py b/features/service_health.py new file mode 100644 index 0000000..dd1cbea --- /dev/null +++ b/features/service_health.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import subprocess + +from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, run_command + + +_CRITICAL_SERVICES = [ + ("wuauserv", "Windows Update"), + ("BITS", "Background Intelligent Transfer"), + ("TrustedInstaller", "Windows Modules Installer"), + ("sppsvc", "Software Protection"), + ("winmgmt", "WMI"), + ("Dnscache", "DNS Client"), + ("Dhcp", "DHCP Client"), + ("NlaSvc", "Network Location Awareness"), + ("EventLog", "Windows Event Log"), + ("Audiosrv", "Windows Audio"), + ("Themes", "Themes"), + ("Spooler", "Print Spooler"), + ("WSearch", "Windows Search"), + ("MpsSvc", "Windows Defender Firewall"), + ("BFE", "Base Filtering Engine"), + ("LanmanWorkstation", "Workstation"), + ("LanmanServer", "Server"), + ("WlanSvc", "WLAN AutoConfig"), + ("RpcSs", "Remote Procedure Call (RPC)"), + ("DcomLaunch", "DCOM Server Process Launcher"), +] + + +def _get_services_ps(service_names: list[str]) -> dict[str, str | None]: + if not command_exists("powershell"): + return {} + names = ",".join(f"'{n}'" for n in service_names) + cmd = ( + f"Get-Service -Name {names} -ErrorAction SilentlyContinue " + "| ForEach-Object { $_.Name + '|' + $_.Status + '|' + $_.DisplayName }" + ) + result = run_command( + ["powershell", "-NoProfile", "-Command", cmd], + capture=True, + ) + if result.code != 0: + return {} + out: dict[str, str | None] = {} + for line in result.stdout.splitlines(): + parts = line.strip().split("|", 2) + if len(parts) >= 2: + out[parts[0]] = parts[1] + return out + + +def _sc_status(name: str) -> str | None: + if not command_exists("sc"): + return None + result = run_command(["sc", "query", name], capture=True) + if result.code != 0: + return None + for line in result.stdout.splitlines(): + line = line.strip() + if line.upper().startswith("STATE"): + parts = line.split() + if len(parts) >= 4: + codes = { + 1: "STOPPED", + 2: "START_PENDING", + 3: "STOP_PENDING", + 4: "RUNNING", + 5: "CONTINUE_PENDING", + 6: "PAUSE_PENDING", + 7: "PAUSED", + } + return codes.get(int(parts[2]), parts[2]) + return None + + +def service_health(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" SERVICE HEALTH CHECK") + print(MENU_LOGO) + logger.section("Service Health Check") + + names = [s[0] for s in _CRITICAL_SERVICES] + ps_status = _get_services_ps(names) + + print(f" {'Status':<15} {'Service Name':<20} {'Display Name':<40}") + print(f" {'-'*15} {'-'*20} {'-'*40}") + + running = 0 + stopped = 0 + unknown = 0 + + for name, display_name in _CRITICAL_SERVICES: + status = ps_status.get(name) or _sc_status(name) or "UNKNOWN" + status_upper = status.upper() + if status_upper in ("RUNNING", "START_PENDING"): + running += 1 + elif status_upper in ("STOPPED", "STOP_PENDING", "PAUSED", "PAUSE_PENDING"): + stopped += 1 + else: + unknown += 1 + + status_display = status[:14] if len(status) > 14 else status + print(f" {status_display:<15} {name:<20} {display_name:<40}") + + print() + total = len(_CRITICAL_SERVICES) + print(f" Running: {running} / Stopped: {stopped} / Unknown: {unknown} / Total: {total}") + if stopped: + print(" >>> Some critical services are stopped. Check manually if issues persist.") + print() + + logger.log_only( + "INFO", + f"Services: {running} running, {stopped} stopped, {unknown} unknown of {total}", + ) + + for name, _ in _CRITICAL_SERVICES: + status = ps_status.get(name) + if status: + logger.log_only("INFO", f"{name}: {status}") + + logger.log_only("INFO", "SERVICE HEALTH CHECK COMPLETE") + input("Press Enter to continue...") diff --git a/features/system_info.py b/features/system_info.py new file mode 100644 index 0000000..872bcff --- /dev/null +++ b/features/system_info.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import ctypes +import os +import platform +import shutil +import subprocess +import winreg +from datetime import datetime + +from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, run_command + + +class _MEMORYSTATUSEX(ctypes.Structure): + _fields_ = [ + ("dwLength", ctypes.c_ulong), + ("dwMemoryLoad", ctypes.c_ulong), + ("ullTotalPhys", ctypes.c_ulonglong), + ("ullAvailPhys", ctypes.c_ulonglong), + ("ullTotalPageFile", ctypes.c_ulonglong), + ("ullAvailPageFile", ctypes.c_ulonglong), + ("ullTotalVirtual", ctypes.c_ulonglong), + ("ullAvailVirtual", ctypes.c_ulonglong), + ("ullAvailExtendedVirtual", ctypes.c_ulonglong), + ] + + +def _fmt_bytes(n: int) -> str: + for unit in ("B", "KB", "MB", "GB", "TB"): + if n < 1024: + return f"{n:.1f} {unit}" + n /= 1024 + return f"{n:.1f} PB" + + +def _fmt_uptime(ms: int) -> str: + days = ms // 86400000 + rem = ms % 86400000 + hours = rem // 3600000 + rem = rem % 3600000 + minutes = rem // 60000 + seconds = rem % 60000 // 1000 + if days: + return f"{days}d {hours}h {minutes}m {seconds}s" + if hours: + return f"{hours}h {minutes}m {seconds}s" + return f"{minutes}m {seconds}s" + + +def _reg_str(key: int, subkey: str, value: str) -> str | None: + try: + with winreg.OpenKey(key, subkey) as k: + data, _ = winreg.QueryValueEx(k, value) + return str(data) + except OSError: + return None + + +def _reg_dword(key: int, subkey: str, value: str) -> int | None: + try: + with winreg.OpenKey(key, subkey) as k: + data, _ = winreg.QueryValueEx(k, value) + return int(data) + except OSError: + return None + + +def system_info(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" SYSTEM INFORMATION") + print(MENU_LOGO) + logger.section("System Information") + + kernel32 = ctypes.windll.kernel32 + + os_edition = _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows NT\CurrentVersion", + "ProductName", + ) or platform.system() + os_build = _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows NT\CurrentVersion", + "CurrentBuild", + ) or platform.version() + os_display = f"{os_edition} (Build {os_build})" + + cpu_name = _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"HARDWARE\DESCRIPTION\System\CentralProcessor\0", + "ProcessorNameString", + ) or platform.processor() + cpu_cores = os.cpu_count() or 0 + cpu_display = f"{cpu_name} ({cpu_cores} logical cores)" + + memory_status = _MEMORYSTATUSEX() + memory_status.dwLength = ctypes.sizeof(_MEMORYSTATUSEX) + kernel32.GlobalMemoryStatusEx(ctypes.byref(memory_status)) + total_ram = memory_status.ullTotalPhys + avail_ram = memory_status.ullAvailPhys + used_ram = total_ram - avail_ram + ram_pct = memory_status.dwMemoryLoad + ram_display = f"{_fmt_bytes(used_ram)} / {_fmt_bytes(total_ram)} ({ram_pct}% used)" + + sys_drive = os.environ.get("SystemDrive", "C:") + du = shutil.disk_usage(f"{sys_drive}\\") + disk_total = du.total + disk_free = du.free + disk_used = du.total - du.free + disk_pct = du.used * 100 // du.total + disk_display = f"{_fmt_bytes(disk_used)} / {_fmt_bytes(disk_total)} ({disk_pct}% used), {_fmt_bytes(disk_free)} free" + + uptime_ms = kernel32.GetTickCount64() + uptime_display = _fmt_uptime(uptime_ms) + + computer = os.environ.get("COMPUTERNAME", "N/A") + user = f"{os.environ.get('USERDOMAIN', '')}\\{os.environ.get('USERNAME', '')}" + boot_time = _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Control\Session Manager\Power", + "LastBootTime", + ) + boot_display = boot_time if boot_time else "N/A" + + print(f" Computer Name : {computer}") + print(f" User : {user}") + print(f" OS : {os_display}") + print(f" CPU : {cpu_display}") + print(f" Memory : {ram_display}") + print(f" System Drive : {sys_drive}") + print(f" Disk : {disk_display}") + print(f" Uptime : {uptime_display}") + print() + + for line in ( + f"Computer : {computer}", + f"User : {user}", + f"OS : {os_display}", + f"CPU : {cpu_display}", + f"Memory : {ram_display}", + f"Disk ({sys_drive}): {disk_display}", + f"Uptime : {uptime_display}", + f"LastBoot : {boot_display}", + ): + logger.log_only("INFO", line) + + input("Press Enter to continue...") diff --git a/features/windows_update.py b/features/windows_update.py new file mode 100644 index 0000000..d1ca0aa --- /dev/null +++ b/features/windows_update.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +import subprocess +import winreg + +from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, run_command + + +def _reg_str(key: int, subkey: str, value: str) -> str | None: + try: + with winreg.OpenKey(key, subkey) as k: + data, _ = winreg.QueryValueEx(k, value) + return str(data) + except OSError: + return None + + +def _reg_dword(key: int, subkey: str, value: str) -> int | None: + try: + with winreg.OpenKey(key, subkey) as k: + data, _ = winreg.QueryValueEx(k, value) + return int(data) + except OSError: + return None + + +def _sc_query(service: str) -> str | None: + if not command_exists("sc"): + return None + result = run_command(["sc", "query", service], capture=True) + if result.code != 0: + return None + for line in result.stdout.splitlines(): + line = line.strip() + if line.upper().startswith("STATE"): + parts = line.split() + if len(parts) >= 4: + codes = { + 1: "STOPPED", + 2: "START_PENDING", + 3: "STOP_PENDING", + 4: "RUNNING", + 5: "CONTINUE_PENDING", + 6: "PAUSE_PENDING", + 7: "PAUSED", + } + return codes.get(int(parts[2]), parts[2]) + return None + + +_AU_STATES = { + 1: "Disabled", + 2: "Not configured", + 3: "Enabled - notify before download", + 4: "Enabled - auto download, notify to install", + 5: "Enabled - auto download and install on schedule", +} + + +def windows_update(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" WINDOWS UPDATE STATUS") + print(MENU_LOGO) + logger.section("Windows Update Status") + + svc = _sc_query("wuauserv") + if svc: + print(f" Service (wuauserv) : {svc}") + else: + print(" Service (wuauserv) : Unable to query") + + au_state = _reg_dword( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update", + "AUState", + ) + if au_state is not None: + label = _AU_STATES.get(au_state, f"Unknown ({au_state})") + print(f" Auto Update Config : {label}") + else: + print(" Auto Update Config : N/A (registry key not found)") + + last_install = _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Install", + "LastSuccessTime", + ) + if last_install: + print(f" Last Install : {last_install}") + else: + print(" Last Install : No record found") + + last_search = _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Search", + "LastSuccessTime", + ) + if last_search: + print(f" Last Search : {last_search}") + else: + print(" Last Search : No record found") + + notify = _reg_dword( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update", + "IncludeRecommendedUpdates", + ) + if notify is not None: + print(f" Recommended Updates : {'Included' if notify else 'Not included'}") + + detection = _reg_dword( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update", + "DetectionState", + ) + if detection is not None: + print(f" Detection State : {detection}") + + deferred = _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\WindowsUpdate\UpdatePolicy\Policy", + "DeferQualityUpdates", + ) + if deferred: + print(f" Quality Updates : Deferred") + else: + print(f" Quality Updates : Not deferred") + + if svc == "RUNNING" and last_install: + from datetime import datetime, timedelta + + try: + dt = datetime.strptime(last_install, "%Y-%m-%d %H:%M:%S") + days_ago = (datetime.now() - dt).days + if days_ago > 30: + print(f" >>> Last update was {days_ago} days ago. Consider running [5] Winget upgrade.") + except ValueError: + pass + + print() + for line in ( + f"Service (wuauserv): {svc or 'N/A'}", + f"AutoUpdate state: {au_state}", + f"Last install: {last_install or 'N/A'}", + f"Last search: {last_search or 'N/A'}", + ): + logger.log_only("INFO", line) + + logger.section("Check for updates via UsoClient") + if command_exists("usoclient"): + print("Running UsoClient ScanInstallWait (this may take a moment)...") + result = run_command(["usoclient", "StartScan"], capture=True) + if result.code == 0: + print("UsoClient completed successfully.") + else: + print(f"UsoClient returned exit code {result.code}.") + else: + print("UsoClient not found (Windows 10 1809+ required).") + + logger.log_only("INFO", "WINDOWS UPDATE STATUS CHECK COMPLETE") + input("Press Enter to continue...") diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index 94529fa..b7de53f 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -16,13 +16,17 @@ ) from features.bitlocker_disable import bitlocker_disable from features.browser_ai_killer import kill_browser_ai +from features.defender_tools import defender_tools from features.event_log_clear import event_logs from features.log_viewer import log_history from features.low_latency_mode import low_latency_mode from features.network_reset import net_reset +from features.service_health import service_health from features.ssd_trim import ssd_trim from features.system_cleanup import cleanup +from features.system_info import system_info from features.system_repair import sys_repair +from features.windows_update import windows_update from features.winget_upgrade import app_update from features.winsxs_cleanup import component_store_cleanup @@ -74,10 +78,15 @@ def main_menu(logger: Logger, log_dir: Path) -> None: print(" ── Security & Privacy ──") print("[9] Disable BitLocker (Plan)") print("[10] Kill Browser AI") + print(" ── Diagnostics ──") + print("[11] System Information") + print("[12] Windows Update Status") + print("[13] Defender Status & Quick Scan") + print("[14] Service Health Check") print(" ── Tools ──") - print("[11] View Log History") + print("[15] View Log History") print("───────────────────────────────────────────────") - print("[12] Exit") + print("[16] Exit") print("===============================================") print(f"Log: {logger.logfile}") print("===============================================") @@ -105,8 +114,16 @@ def main_menu(logger: Logger, log_dir: Path) -> None: elif choice == "10": kill_browser_ai(logger) elif choice == "11": - log_history(logger, log_dir) + system_info(logger) elif choice == "12": + windows_update(logger) + elif choice == "13": + defender_tools(logger) + elif choice == "14": + service_health(logger) + elif choice == "15": + log_history(logger, log_dir) + elif choice == "16": if not prompt_yes_no( logger, "Are you sure you want to exit? (Y/N): ", diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index a21a7fa..335c754 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -21,12 +21,12 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - [x] Add progress hints for long-running tasks (`[1/N]` markers where missing) - [x] Make cleanup operations more conservative by default (vendor driver roots now opt-in) -### Phase 3: New Feature Modules +### Phase 3: New Feature Modules ✅ (Complete) -1. System information summary -2. Windows Update status check -3. Defender status check and quick scan entry -4. Service health check for common Windows services +- [x] System information summary (OS, CPU, RAM, disk, uptime via ctypes+winreg) +- [x] Windows Update status check (service, registry config, UsoClient scan) +- [x] Defender status check and quick scan entry (Get-MpComputerStatus, Start-MpQuickScan) +- [x] Service health check for common Windows services (20 critical services) ### Phase 4: Diagnostics & Reporting diff --git a/memory/tasks.md b/memory/tasks.md index f665495..48df557 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -8,6 +8,7 @@ Last updated: 2026-07-05 ## Completed +- [x] 2026-07-05: Phase 3: Created 4 diagnostic features (system_info, windows_update, defender_tools, service_health) with read-only info + optional scan actions. Renumbered menu (Exit: 12→16). - [x] 2026-07-05: Phase 2: Added progress hints `[1/1]` to WinSxS / Winget, `[1/2][2/2]` to SFC+DISM; made vendor driver root deletion opt-in (Y/N) for conservative cleanup. - [x] 2026-07-05: Added optional restore point via `create_restore_point()` to features 1-7 and 9. - [x] 2026-07-05: Added missing Y/N confirmation prompts to features 1, 3, 7, and 12. From bf8bca5f8a4d596aeffb076a99b9fbeb77cb0045 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 14:59:05 +0800 Subject: [PATCH 24/33] Phase 4: Reporting & Export section with 4 new features - Disk Health & SMART (menu 15): Get-PhysicalDisk, Get-StorageReliabilityCounter - Driver Inventory (menu 16): driverquery /FO CSV parsing with type/date summary - Network Snapshot (menu 17): ipconfig/route/netsh/netstat capture + file save + diff - Export Logs & Report (menu 18): session report generation + ZIP archive of all logs - Menu renumbered: Diagnostics (11-18), Tools (19), Exit (20) - Updated memory files with Phase 4 completion --- MEMORY.md | 8 +- features/disk_health.py | 122 +++++++++++++++++++++++++ features/driver_inventory.py | 82 +++++++++++++++++ features/export_report.py | 170 +++++++++++++++++++++++++++++++++++ features/network_snapshot.py | 122 +++++++++++++++++++++++++ ldlwintoolbox.py | 22 ++++- memory/feature-ideas.md | 35 ++++---- memory/tasks.md | 1 + 8 files changed, 539 insertions(+), 23 deletions(-) create mode 100644 features/disk_health.py create mode 100644 features/driver_inventory.py create mode 100644 features/export_report.py create mode 100644 features/network_snapshot.py diff --git a/MEMORY.md b/MEMORY.md index a6497e1..4bbf56a 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -79,14 +79,18 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe 15. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. -16. Exit: asks Y/N confirmation, then closes the tool. +20. Exit: asks Y/N confirmation, then closes the tool. -**Diagnostics (11-14):** +**Diagnostics (11-18):** 11. System Information: read-only summary of OS, CPU, RAM, disk, uptime using stdlib + ctypes + winreg. 12. Windows Update Status: queries wuauserv, Auto Update registry config, last install/search dates; runs UsoClient scan. 13. Defender Status & Quick Scan: displays Get-MpComputerStatus fields, optional MpCmdRun signature update, optional Start-MpQuickScan. 14. Service Health Check: checks 20 critical services via PowerShell Get-Service, shows Running/Stopped summary. +15. Disk Health & SMART: Get-PhysicalDisk + Get-StorageReliabilityCounter for health, wear, temp, errors; volume summary. +16. Driver Inventory: parses driverquery /FO CSV output, shows all drivers with type/date summary. +17. Network Snapshot: captures ipconfig/route/netsh/netstat state to file and log; optional diff against previous snapshot. +18. Export Logs & Report: generates a plain-text session report (features run, commands, warnings) and archives all logs to ZIP. ## Implemented Feature Targets diff --git a/features/disk_health.py b/features/disk_health.py new file mode 100644 index 0000000..483115e --- /dev/null +++ b/features/disk_health.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, run_command + + +def _run_ps(script: str) -> str: + if not command_exists("powershell"): + return "" + result = run_command( + ["powershell", "-NoProfile", "-Command", script], + capture=True, + ) + if result.code != 0: + return "" + return result.stdout.strip() + + +def _fmt_bytes(n: int | float) -> str: + for unit in ("B", "KB", "MB", "GB", "TB"): + if n < 1024: + return f"{n:.1f} {unit}" + n /= 1024 + return f"{n:.1f} PB" + + +def disk_health(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" DISK HEALTH & SMART SUMMARY") + print(MENU_LOGO) + logger.section("Disk Health & SMART") + + ps = ( + "$d=Get-PhysicalDisk | ForEach-Object {" + "$f=$_;" + "$rel=$_ | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue;" + "$w=if($rel -and $rel.WearPercentage -ge 0){$rel.WearPercentage.ToString()+'%'}else{'N/A'};" + "$t=if($rel -and $rel.Temperature -ne 0){$rel.Temperature.ToString()+'C'}else{'N/A'};" + "$re=if($rel){$rel.ReadErrorsTotal}else{'N/A'};" + "$we=if($rel){$rel.WriteErrorsTotal}else{'N/A'};" + "Write-Output ($f.FriendlyName+'|'+$f.MediaType+'|'+$f.HealthStatus+'|'+" + "[math]::Round($f.Size/1GB,1).ToString()+'GB|'+$f.OperationalStatus+'|'+$f.BusType+'|'+" + "$t+'|'+$w+'|'+$re+'|'+$we)" + "};" + "if(-not $d){Write-Output 'NO_DATA'}" + ) + + raw = _run_ps(ps) + if not raw or raw.strip() == "NO_DATA": + logger.log("INFO", "No physical disk data available via PowerShell.") + print("No physical disk data returned. Try running as Administrator.") + print() + print(" Fallback: Volume-level info from Get-PSDrive:") + ps2 = ( + "Get-PSDrive -PSProvider FileSystem " + "| Where-Object {$_.Root -match '^[A-Z]:\\\\$'} " + "| ForEach-Object {Write-Output ($_.Root+'|'+[math]::Round($_.Used/1GB,1).ToString()+'/'+" + "[math]::Round(($_.Used+$_.Free)/1GB,1).ToString()+'GB|'+[math]::Round($_.Free/1GB,1).ToString()+'GB')}" + ) + raw2 = _run_ps(ps2) + if raw2: + print(f" {'Drive':<8} {'Used/Total':<22} {'Free':<10}") + print(f" {'-'*8} {'-'*22} {'-'*10}") + for line in raw2.splitlines(): + parts = line.strip().split("|") + if len(parts) >= 3: + print(f" {parts[0]:<8} {parts[1]:<22} {parts[2]:<10}") + logger.log_only("INFO", f"Disk: {parts[0]} {parts[1]} free={parts[2]}") + input("Press Enter to continue...") + return + + print(f" {'Name':<30} {'Type':<12} {'Health':<12} {'Size':<10} {'Status':<14} {'Bus':<10} {'Temp':<8} {'Wear':<8} {'ReadErr':<8} {'WriteErr':<8}") + print(f" {'-'*30} {'-'*12} {'-'*12} {'-'*10} {'-'*14} {'-'*10} {'-'*8} {'-'*8} {'-'*8} {'-'*8}") + + for line in raw.splitlines(): + parts = line.strip().split("|") + if len(parts) >= 10: + name, media, health, size, op_status, bus, temp, wear, re, we = parts[:10] + print(f" {name:<30} {media:<12} {health:<12} {size:<10} {op_status:<14} {bus:<10} {temp:<8} {wear:<8} {re:<8} {we:<8}") + logger.log_only("INFO", f"Disk: {name} health={health} wear={wear} temp={temp}") + + print() + logger.section("Volume Summary") + ps_vol = ( + "Get-Volume | Where-Object {$_.DriveType -eq 'Fixed' -and $_.DriveLetter} " + "| ForEach-Object {Write-Output ($_.DriveLetter+':|'+$_.FileSystem+'|'+$_.HealthStatus+'|'+$_.SizeRemaining+'|'+$_.Size)}" + ) + raw_vol = _run_ps(ps_vol) + if raw_vol: + print(f" {'Volume':<8} {'FS':<8} {'Health':<12} {'Free':<12} {'Total':<12}") + print(f" {'-'*8} {'-'*8} {'-'*12} {'-'*12} {'-'*12}") + for line in raw_vol.splitlines(): + parts = line.strip().split("|") + if len(parts) >= 5: + vol, fs, h, free_s, total_s = parts[:5] + free_b = int(free_s) if free_s.isdigit() else 0 + total_b = int(total_s) if total_s.isdigit() else 0 + free_fmt = _fmt_bytes(free_b) if free_b else free_s + total_fmt = _fmt_bytes(total_b) if total_b else total_s + print(f" {vol:<8} {fs:<8} {h:<12} {free_fmt:<12} {total_fmt:<12}") + logger.log_only("INFO", f"Volume: {vol} {h} free={free_fmt} of {total_fmt}") + + print() + logger.section("SMART Reliability Counters") + ps_smart = ( + "Get-PhysicalDisk | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue " + "| ForEach-Object {Write-Output ($_.DeviceId+'|'+$_.Temperature+'|'+" + "$_.WearPercentage+'|'+$_.ReadErrorsTotal+'|'+$_.WriteErrorsTotal+'|'+" + "$_.ReadLatencyMax+'|'+$_.WriteLatencyMax+'|'+$_.FlushLatencyMax)}" + ) + raw_smart = _run_ps(ps_smart) + if raw_smart: + print(f" {'Disk#':<8} {'Temp(C)':<10} {'Wear%':<8} {'ReadErr':<10} {'WriteErr':<10} {'RdLat(ms)':<12} {'WrLat(ms)':<12} {'FlLat(ms)':<12}") + print(f" {'-'*8} {'-'*10} {'-'*8} {'-'*10} {'-'*10} {'-'*12} {'-'*12} {'-'*12}") + for line in raw_smart.splitlines(): + parts = line.strip().split("|") + if len(parts) >= 8: + did, temp, wear, re, we, rl, wl, fl = parts[:8] + print(f" {did:<8} {temp:<10} {wear:<8} {re:<10} {we:<10} {rl:<12} {wl:<12} {fl:<12}") + + logger.log_only("INFO", "DISK HEALTH CHECK COMPLETE") + input("Press Enter to continue...") diff --git a/features/driver_inventory.py b/features/driver_inventory.py new file mode 100644 index 0000000..85b9ddc --- /dev/null +++ b/features/driver_inventory.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, run_command + + +def driver_inventory(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" DRIVER INVENTORY") + print(MENU_LOGO) + logger.section("Driver Inventory") + + if not command_exists("driverquery"): + logger.log("ERROR", "driverquery.exe is required but not found.") + input("Press Enter to continue...") + return + + logger.log_only("INFO", "Querying drivers via driverquery /FO CSV /V ...") + result = run_command( + ["driverquery", "/FO", "CSV", "/V"], + capture=True, + ) + if result.code != 0: + logger.log("ERROR", "driverquery failed.") + input("Press Enter to continue...") + return + + lines = result.stdout.splitlines() + if len(lines) < 2: + logger.log("INFO", "No driver data returned.") + input("Press Enter to continue...") + return + + header = lines[0] + rows = lines[1:] + + import csv + import io + reader = csv.reader(io.StringIO(result.stdout)) + all_rows = list(reader) + if len(all_rows) < 2: + logger.log("INFO", "No driver data parsed.") + input("Press Enter to continue...") + return + + headers = all_rows[0] + data_rows = all_rows[1:] + + try: + name_idx = headers.index("Module Name") + type_idx = headers.index("Driver Type") + date_idx = headers.index("Link Date") + except ValueError: + name_idx = headers.index("Module Name") if "Module Name" in headers else 0 + type_idx = 3 if len(headers) > 3 else 1 + date_idx = 4 if len(headers) > 4 else 2 + + total = len(data_rows) + type_counts: dict[str, int] = {} + print() + print(f" Total drivers: {total}") + print() + print(f" {'Driver Name':<35} {'Type':<18} {'Date':<20}") + print(f" {'-'*35} {'-'*18} {'-'*20}") + + for row in data_rows: + name = row[name_idx] if len(row) > name_idx else "?" + d_type = row[type_idx] if len(row) > type_idx else "?" + date = row[date_idx] if len(row) > date_idx else "?" + type_counts[d_type] = type_counts.get(d_type, 0) + 1 + print(f" {name:<35} {d_type:<18} {date:<20}") + logger.log_only("INFO", f"Driver: {name} type={d_type} date={date}") + + print() + print(" --- Driver Type Summary ---") + for d_type, count in sorted(type_counts.items(), key=lambda x: -x[1]): + print(f" {d_type:<25} {count:>5}") + print(f" {'TOTAL':<25} {total:>5}") + + logger.log_only("INFO", f"Drivers: {total} total, {len(type_counts)} types") + logger.log_only("INFO", "DRIVER INVENTORY COMPLETE") + input("Press Enter to continue...") diff --git a/features/export_report.py b/features/export_report.py new file mode 100644 index 0000000..2e03839 --- /dev/null +++ b/features/export_report.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import os +import re +import zipfile +from datetime import datetime +from pathlib import Path + +from toolbox_base import MENU_LOGO, Logger, clear_screen, prompt_yes_no + + +def _gather_logs(log_dir: Path) -> list[Path]: + if not log_dir.exists(): + return [] + return sorted(log_dir.glob("LDLWinToolBox_*.log"), key=os.path.getmtime, reverse=True) + + +def _generate_report(log_dir: Path, log_text: str) -> str: + now = datetime.now() + lines: list[str] = [] + lines.append("=" * 60) + lines.append("LDL Windows ToolBox - Session Report") + lines.append(f"Generated: {now.strftime('%m/%d/%Y %H:%M:%S')}") + lines.append("=" * 60) + lines.append("") + + session_match = re.search(r"Session ID\s*:\s*(\d+)", log_text) + if session_match: + lines.append(f"Session : {session_match.group(1)}") + + start_match = re.search(r"Started\s*:\s*(.+)", log_text) + if start_match: + lines.append(f"Started : {start_match.group(1).strip()}") + + user_match = re.search(r"User\s*:\s*(.+)", log_text) + if user_match: + lines.append(f"User : {user_match.group(1).strip()}") + + comp_match = re.search(r"Computer\s*:\s*(.+)", log_text) + if comp_match: + lines.append(f"Computer : {comp_match.group(1).strip()}") + + os_match = re.search(r"OS\s*:\s*(.+)", log_text) + if os_match: + lines.append(f"OS : {os_match.group(1).strip()}") + + log_match = re.search(r"Log File\s*:\s*(.+)", log_text) + if log_match: + lines.append(f"Log File : {log_match.group(1).strip()}") + + lines.append("") + + sections = re.findall(r"==\s*(.+?)\s*==", log_text) + if sections: + lines.append("Features Run:") + for s in sections: + lines.append(f" - {s}") + lines.append("") + + cmd_lines = re.findall(r"\[CMD\]\s+START\s+(.+)", log_text) + if cmd_lines: + lines.append("Commands Executed:") + for cmd in cmd_lines: + lines.append(f" - {cmd}") + lines.append("") + + warn_lines = re.findall(r"\[(WARN|ERROR)\]\s+(.+)", log_text) + if warn_lines: + lines.append("Warnings / Errors:") + for level, msg in warn_lines: + lines.append(f" [{level}] {msg}") + lines.append("") + + menu_lines = re.findall(r"\[INFO\] Menu selection:\s*(.+)", log_text) + if menu_lines: + lines.append("Menu Selections:") + for m in menu_lines: + lines.append(f" - {m}") + lines.append("") + + ok_lines = re.findall(r"\[OK\]\s+END\s+(.+?)\s+exit=0", log_text) + fail_lines = re.findall(r"\[WARN\]\s+END\s+(.+?)\s+exit=(\d+)", log_text) + if ok_lines or fail_lines: + lines.append("Command Results:") + for cmd in ok_lines: + lines.append(f" [OK] {cmd}") + for cmd, code in fail_lines: + lines.append(f" [FAIL]({code}) {cmd}") + lines.append("") + + lines.append("=" * 60) + lines.append("Report End") + lines.append("=" * 60) + return "\n".join(lines) + + +def export_report(logger: Logger, log_dir: Path) -> None: + clear_screen() + print(MENU_LOGO) + print(" EXPORT LOGS & REPORT") + print(MENU_LOGO) + logger.section("Export Logs & Report") + + export_dir = Path(log_dir.parent) / "exports" + export_dir.mkdir(parents=True, exist_ok=True) + + logs = _gather_logs(log_dir) + if not logs: + logger.log("INFO", "No log files found to export.") + input("Press Enter to continue...") + return + + print(f" Found {len(logs)} log file(s).") + + current_log = str(logger.logfile) + + timestamp = datetime.now().strftime("%y%m%d%H%M%S") + + report_text = "" + for log_file in logs: + if str(log_file) == current_log: + try: + log_text = log_file.read_text(encoding="utf-8", errors="replace") + report_text = _generate_report(log_dir, log_text) + except OSError: + pass + break + + if report_text: + report_name = f"report_{timestamp}.txt" + report_path = export_dir / report_name + try: + report_path.write_text(report_text, encoding="utf-8", newline="\n") + print(f" Report file: {report_path}") + logger.log_only("INFO", f"Report saved to {report_path}") + except OSError as e: + logger.log("ERROR", f"Failed to write report: {e}") + + if prompt_yes_no( + logger, + f"Archive all {len(logs)} log(s) to a ZIP file? (Y/N): ", + "Log Archive", + ): + zip_name = f"LDLWinToolBox_logs_{timestamp}.zip" + zip_path = export_dir / zip_name + try: + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + for log_file in logs: + arcname = log_file.name + zf.write(log_file, arcname) + if report_text: + zf.writestr(report_name, report_text) + print(f" Archive created: {zip_path} ({_fmt_zip_size(zip_path)})") + logger.log_only("INFO", f"Log archive saved to {zip_path}") + except OSError as e: + logger.log("ERROR", f"Failed to create archive: {e}") + else: + logger.log_only("INFO", "Log archive skipped by user.") + + logger.log_only("INFO", "EXPORT LOGS & REPORT COMPLETE") + input("Press Enter to continue...") + + +def _fmt_zip_size(zip_path: Path) -> str: + size = zip_path.stat().st_size + for unit in ("B", "KB", "MB", "GB"): + if size < 1024: + return f"{size:.1f} {unit}" + size /= 1024 + return f"{size:.1f} TB" diff --git a/features/network_snapshot.py b/features/network_snapshot.py new file mode 100644 index 0000000..1b37020 --- /dev/null +++ b/features/network_snapshot.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import os +from datetime import datetime +from pathlib import Path + +from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, prompt_yes_no, run_command + + +def _capture_cmd(logger: Logger, cmd: list[str], label: str, lines: list[str]) -> None: + if not command_exists(cmd[0]): + lines.append(f"[SKIP] {label}: {cmd[0]} not found") + return + result = run_command(cmd, capture=True) + lines.append(f"=== {label} ===") + if result.code == 0: + lines.append(result.stdout.strip()) + else: + lines.append(f"(exit={result.code})") + lines.append("") + + +_SNAPSHOT_DIR: str | None = None + + +def _get_snapshot_dir(script_dir_str: str | None = None) -> Path: + global _SNAPSHOT_DIR + if _SNAPSHOT_DIR is not None: + return Path(_SNAPSHOT_DIR) + base = Path(script_dir_str) if script_dir_str else Path.cwd() + snap_dir = base / "exports" / "network_snapshots" + snap_dir.mkdir(parents=True, exist_ok=True) + _SNAPSHOT_DIR = str(snap_dir) + return snap_dir + + +def _list_snapshots(script_path_str: str | None = None) -> list[Path]: + snap_dir = _get_snapshot_dir(script_path_str) + if not snap_dir.exists(): + return [] + files = sorted(snap_dir.glob("network_snapshot_*.txt"), reverse=True) + return files + + +def network_snapshot(logger: Logger, script_dir: Path | None = None) -> None: + clear_screen() + print(MENU_LOGO) + print(" NETWORK SNAPSHOT") + print(MENU_LOGO) + logger.section("Network Snapshot") + + lines: list[str] = [] + ts = datetime.now().strftime("%m/%d/%Y %H:%M:%S") + lines.append(f"Network Snapshot captured at {ts}") + lines.append("=" * 60) + lines.append("") + + _capture_cmd(logger, ["ipconfig", "/all"], "IPCONFIG /ALL", lines) + _capture_cmd(logger, ["route", "print"], "ROUTE PRINT", lines) + _capture_cmd( + logger, + ["netsh", "interface", "show", "interface"], + "NETSH INTERFACE SHOW", + lines, + ) + _capture_cmd(logger, ["netsh", "wlan", "show", "interfaces"], "WLAN INTERFACES", lines) + _capture_cmd(logger, ["netstat", "-ano"], "NETSTAT -ANO", lines) + + output = "\n".join(lines) + + print(" Current Network State:") + print() + ip_lines = [l for l in lines if "IPv4 Address" in l or "Default Gateway" in l or "DNS Servers" in l] + for l in ip_lines[:10]: + print(f" {l.strip()}") + print() + print(f" Full snapshot saved to log ({len(lines)} lines).") + + logger.write_raw(output) + logger.log_only("INFO", "Network snapshot written to log file.") + + snap_dir = _get_snapshot_dir() + snap_file = snap_dir / f"network_snapshot_{datetime.now():%y%m%d%H%M%S}.txt" + with open(snap_file, "w", encoding="utf-8", newline="\n") as f: + f.write(output) + print(f" Snapshot file: {snap_file}") + + logger.log_only("INFO", f"Snapshot saved to {snap_file}") + + existing = _list_snapshots() + if len(existing) >= 2: + prev_snap = existing[1] + if prompt_yes_no( + logger, + f"Compare with previous snapshot ({prev_snap.name})? (Y/N): ", + "Compare Snapshots", + ): + logger.log_only("INFO", f"Comparing with {prev_snap.name}") + prev_text = prev_snap.read_text(encoding="utf-8", errors="replace") + curr_text = output + import difflib + diff = list(difflib.unified_diff( + prev_text.splitlines(), + curr_text.splitlines(), + fromfile=prev_snap.name, + tofile=snap_file.name, + lineterm="", + )) + if diff: + print() + print(" --- Differences from previous snapshot ---") + for d in diff[:60]: + print(f" {d}") + if len(diff) > 60: + print(f" ... ({len(diff) - 60} more lines)") + logger.write_raw("\n".join(diff)) + else: + print(" No differences detected.") + logger.log_only("INFO", "Snapshot comparison complete.") + + logger.log_only("INFO", "NETWORK SNAPSHOT COMPLETE") + input("Press Enter to continue...") diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index b7de53f..d4aff12 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -17,10 +17,14 @@ from features.bitlocker_disable import bitlocker_disable from features.browser_ai_killer import kill_browser_ai from features.defender_tools import defender_tools +from features.disk_health import disk_health +from features.driver_inventory import driver_inventory from features.event_log_clear import event_logs +from features.export_report import export_report from features.log_viewer import log_history from features.low_latency_mode import low_latency_mode from features.network_reset import net_reset +from features.network_snapshot import network_snapshot from features.service_health import service_health from features.ssd_trim import ssd_trim from features.system_cleanup import cleanup @@ -83,10 +87,14 @@ def main_menu(logger: Logger, log_dir: Path) -> None: print("[12] Windows Update Status") print("[13] Defender Status & Quick Scan") print("[14] Service Health Check") + print("[15] Disk Health & SMART Summary") + print("[16] Driver Inventory") + print("[17] Network Snapshot") + print("[18] Export Logs & Report") print(" ── Tools ──") - print("[15] View Log History") + print("[19] View Log History") print("───────────────────────────────────────────────") - print("[16] Exit") + print("[20] Exit") print("===============================================") print(f"Log: {logger.logfile}") print("===============================================") @@ -122,8 +130,16 @@ def main_menu(logger: Logger, log_dir: Path) -> None: elif choice == "14": service_health(logger) elif choice == "15": - log_history(logger, log_dir) + disk_health(logger) elif choice == "16": + driver_inventory(logger) + elif choice == "17": + network_snapshot(logger, script_dir) + elif choice == "18": + export_report(logger, log_dir) + elif choice == "19": + log_history(logger, log_dir) + elif choice == "20": if not prompt_yes_no( logger, "Are you sure you want to exit? (Y/N): ", diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index 335c754..782f40b 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -28,13 +28,12 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - [x] Defender status check and quick scan entry (Get-MpComputerStatus, Start-MpQuickScan) - [x] Service health check for common Windows services (20 critical services) -### Phase 4: Diagnostics & Reporting +### Phase 4: Reporting & Export ✅ (Complete) -1. Disk health and SMART summary -2. Driver inventory and version view -3. Network before/after snapshot -4. Log export and archive bundle -5. Exportable report of actions and results +- [x] Disk health and SMART summary (Get-PhysicalDisk + Get-StorageReliabilityCounter) +- [x] Driver inventory and version view (driverquery /FO CSV parsing) +- [x] Network before/after snapshot (ipconfig/route/netsh/netstat capture + diff) +- [x] Log export and archive bundle (#4+#5 combined: report generation + ZIP archive) ### Phase 5: Efficiency & Maintenance @@ -50,14 +49,14 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d ## New Features -- System information summary -- Windows Update status check -- Driver inventory and version view -- Service health check for common Windows services -- Disk health and SMART summary -- Log export and archive bundle -- Network before/after snapshot -- Defender status check and quick scan entry +- [x] System information summary +- [x] Windows Update status check +- [x] Driver inventory and version view +- [x] Service health check for common Windows services +- [x] Disk health and SMART summary +- [x] Log export and archive bundle +- [x] Network before/after snapshot +- [x] Defender status check and quick scan entry - Safe Mode or recovery entry helpers - Selective cleanup instead of fixed cleanup sets - Custom exclusion list for cleanup targets @@ -66,10 +65,10 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d ## Optimizations -- Reduce redundant PowerShell calls -- Improve error handling and user-facing failure messages -- Add progress hints for long-running tasks -- Make cleanup operations more conservative by default +- [x] Reduce redundant PowerShell calls +- [x] Improve error handling and user-facing failure messages +- [x] Add progress hints for long-running tasks +- [x] Make cleanup operations more conservative by default - Add a read-only mode for status checks - Maintain a lightweight verification checklist after changes - Keep README, memory, and task notes synchronized diff --git a/memory/tasks.md b/memory/tasks.md index 48df557..b83eb2f 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -8,6 +8,7 @@ Last updated: 2026-07-05 ## Completed +- [x] 2026-07-05: Phase 4: Created 4 reporting features (disk_health, driver_inventory, network_snapshot, export_report). Renumbered menu (Exit: 16→20). - [x] 2026-07-05: Phase 3: Created 4 diagnostic features (system_info, windows_update, defender_tools, service_health) with read-only info + optional scan actions. Renumbered menu (Exit: 12→16). - [x] 2026-07-05: Phase 2: Added progress hints `[1/1]` to WinSxS / Winget, `[1/2][2/2]` to SFC+DISM; made vendor driver root deletion opt-in (Y/N) for conservative cleanup. - [x] 2026-07-05: Added optional restore point via `create_restore_point()` to features 1-7 and 9. From 474a0b50bf3886f850ada8d7e0c5b6be8e8a7ed0 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:00:20 +0800 Subject: [PATCH 25/33] Phase 5: Self-update check via GitHub releases API - New feature: Check for Updates (menu 20) queries GitHub Releases API, compares remote tag with local version (1.0.3), opens browser on newer - Version bumped to 1.0.3 in pyproject.toml and toolbox_base.py - Added TOOLBOX_VERSION constant to toolbox_base.py - Menu renumbered: Tools (19-20), Exit (21) - Updated memory files with Phase 5 completion --- MEMORY.md | 7 +-- features/self_update.py | 113 ++++++++++++++++++++++++++++++++++++++++ ldlwintoolbox.py | 6 ++- memory/feature-ideas.md | 10 ++-- memory/tasks.md | 1 + pyproject.toml | 2 +- toolbox_base.py | 1 + uv.lock | 2 +- 8 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 features/self_update.py diff --git a/MEMORY.md b/MEMORY.md index 4bbf56a..ad75a28 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -75,11 +75,12 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe 9. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, optional restore point, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. 10. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then launches PowerShell with `-ExecutionPolicy Bypass` and a guarded `try/catch` wrapper around the configured gist command so the result is logged. -**Tools (15):** +**Tools (19-20):** -15. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. +19. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. +20. Check for Updates: queries GitHub releases API, compares with local version (1.0.3), opens browser for download if newer. -20. Exit: asks Y/N confirmation, then closes the tool. +21. Exit: asks Y/N confirmation, then closes the tool. **Diagnostics (11-18):** diff --git a/features/self_update.py b/features/self_update.py new file mode 100644 index 0000000..064e999 --- /dev/null +++ b/features/self_update.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import json +import re +import urllib.error +import urllib.request + +from toolbox_base import MENU_LOGO, TOOLBOX_VERSION, Logger, clear_screen, run_command + + +def _parse_tag(tag: str) -> tuple[int, ...]: + clean = tag.lstrip("v").lstrip("V") + parts = re.split(r"[._\-]", clean) + result: list[int] = [] + for p in parts: + try: + result.append(int(p)) + except ValueError: + break + return tuple(result) + + +def _is_newer(remote_tag: str, local_ver: str) -> bool: + remote_parts = _parse_tag(remote_tag) + local_parts = _parse_tag(local_ver) + if not remote_parts: + return False + if not local_parts: + return True + return remote_parts > local_parts + + +def self_update(logger: Logger) -> None: + clear_screen() + print(MENU_LOGO) + print(" CHECK FOR UPDATES") + print(MENU_LOGO) + logger.section("Check for Updates") + + print(f" Local version : {TOOLBOX_VERSION}") + logger.log_only("INFO", f"Local version: {TOOLBOX_VERSION}") + + remote_url = "https://api.github.com/repos/LoveDoLove/LDLWinToolBox/releases/latest" + print(f" Checking : {remote_url}") + logger.log_only("INFO", f"Querying GitHub API: {remote_url}") + + try: + req = urllib.request.Request( + remote_url, + headers={"User-Agent": "LDLWinToolBox/1.0", "Accept": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=15) as resp: + data = json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + logger.log("WARN", f"GitHub API returned HTTP {e.code}") + print(f" GitHub API error: HTTP {e.code}") + input("Press Enter to continue...") + return + except (urllib.error.URLError, OSError, ValueError) as e: + logger.log("WARN", f"Failed to reach GitHub: {e}") + print(f" Network error: {e}") + input("Press Enter to continue...") + return + + remote_tag = data.get("tag_name", "") + remote_name = data.get("name", "") + remote_body = data.get("body", "") + remote_url_page = data.get("html_url", "") + + if not remote_tag: + logger.log("WARN", "No tag_name found in response.") + input("Press Enter to continue...") + return + + print(f" Remote version: {remote_tag}") + logger.log_only("INFO", f"Remote version: {remote_tag} ({remote_name})") + + if _is_newer(remote_tag, TOOLBOX_VERSION): + print() + print(f" >>> A new version is available: {remote_tag}") + print(f" >>> {remote_url_page}") + print() + if remote_body: + short = remote_body.strip()[:500] + print(f" Release notes:") + for line in short.splitlines()[:10]: + print(f" {line}") + print() + from urllib.request import urlopen + try: + req_check = urllib.request.Request( + remote_url_page, + method="HEAD", + ) + with urllib.request.urlopen(req_check, timeout=5): + pass + if prompt_yes_no( + logger, + "Open download page in browser? (Y/N): ", + "Open Download Page", + ): + import webbrowser + webbrowser.open(remote_url_page) + logger.log_only("INFO", f"Browser opened to {remote_url_page}") + except Exception: + print(f" Download: {remote_url_page}") + else: + print() + print(" >>> You are on the latest version.") + logger.log_only("INFO", "Up to date.") + + logger.log_only("INFO", "UPDATE CHECK COMPLETE") + input("Press Enter to continue...") diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index d4aff12..ecc9ec4 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -25,6 +25,7 @@ from features.low_latency_mode import low_latency_mode from features.network_reset import net_reset from features.network_snapshot import network_snapshot +from features.self_update import self_update from features.service_health import service_health from features.ssd_trim import ssd_trim from features.system_cleanup import cleanup @@ -93,8 +94,9 @@ def main_menu(logger: Logger, log_dir: Path) -> None: print("[18] Export Logs & Report") print(" ── Tools ──") print("[19] View Log History") + print("[20] Check for Updates") print("───────────────────────────────────────────────") - print("[20] Exit") + print("[21] Exit") print("===============================================") print(f"Log: {logger.logfile}") print("===============================================") @@ -140,6 +142,8 @@ def main_menu(logger: Logger, log_dir: Path) -> None: elif choice == "19": log_history(logger, log_dir) elif choice == "20": + self_update(logger) + elif choice == "21": if not prompt_yes_no( logger, "Are you sure you want to exit? (Y/N): ", diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index 782f40b..b361e70 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -35,11 +35,11 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - [x] Network before/after snapshot (ipconfig/route/netsh/netstat capture + diff) - [x] Log export and archive bundle (#4+#5 combined: report generation + ZIP archive) -### Phase 5: Efficiency & Maintenance +### Phase 5: Efficiency & Maintenance ✅ (Partial) -1. Reduce redundant PowerShell calls -2. Add a read-only mode for status checks -3. Version and update check for the toolbox itself +- [x] Version and update check for the toolbox itself (GitHub releases API) +- [ ] Reduce redundant PowerShell calls (deferred — low impact) +- [ ] Add a read-only mode for status checks (deferred — needs launch-flow change) ### Phase 6: Advanced Features @@ -61,7 +61,7 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - Selective cleanup instead of fixed cleanup sets - Custom exclusion list for cleanup targets - Exportable report of actions and results -- Version and update check for the toolbox itself +- [x] Version and update check for the toolbox itself ## Optimizations diff --git a/memory/tasks.md b/memory/tasks.md index b83eb2f..5265f0b 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -8,6 +8,7 @@ Last updated: 2026-07-05 ## Completed +- [x] 2026-07-05: Phase 5: Self-update check via GitHub releases API (menu 20). Bumped version to 1.0.3. Renumbered Exit to 21. - [x] 2026-07-05: Phase 4: Created 4 reporting features (disk_health, driver_inventory, network_snapshot, export_report). Renumbered menu (Exit: 16→20). - [x] 2026-07-05: Phase 3: Created 4 diagnostic features (system_info, windows_update, defender_tools, service_health) with read-only info + optional scan actions. Renumbered menu (Exit: 12→16). - [x] 2026-07-05: Phase 2: Added progress hints `[1/1]` to WinSxS / Winget, `[1/2][2/2]` to SFC+DISM; made vendor driver root deletion opt-in (Y/N) for conservative cleanup. diff --git a/pyproject.toml b/pyproject.toml index 4cf712b..d5ec478 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldlwintoolbox" -version = "0.1.0" +version = "1.0.3" description = "LDL Windows ToolBox in Python" readme = "README.md" requires-python = ">=3.11" diff --git a/toolbox_base.py b/toolbox_base.py index 3ec2241..7e7535b 100644 --- a/toolbox_base.py +++ b/toolbox_base.py @@ -12,6 +12,7 @@ MENU_LOGO = "=" * 47 +TOOLBOX_VERSION = "1.0.3" @dataclass(slots=True) diff --git a/uv.lock b/uv.lock index 803b19f..4adac55 100644 --- a/uv.lock +++ b/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.11" [[package]] name = "ldlwintoolbox" -version = "0.1.0" +version = "1.0.3" source = { virtual = "." } From 0dd207a4fd0bf87c1647e5e4de4ab0a050b99c53 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:00:43 +0800 Subject: [PATCH 26/33] Fix unused imports in self_update and add missing prompt_yes_no import --- features/self_update.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/self_update.py b/features/self_update.py index 064e999..2d39a49 100644 --- a/features/self_update.py +++ b/features/self_update.py @@ -5,7 +5,7 @@ import urllib.error import urllib.request -from toolbox_base import MENU_LOGO, TOOLBOX_VERSION, Logger, clear_screen, run_command +from toolbox_base import MENU_LOGO, TOOLBOX_VERSION, Logger, clear_screen, prompt_yes_no def _parse_tag(tag: str) -> tuple[int, ...]: @@ -86,7 +86,6 @@ def self_update(logger: Logger) -> None: for line in short.splitlines()[:10]: print(f" {line}") print() - from urllib.request import urlopen try: req_check = urllib.request.Request( remote_url_page, From 11d615bcc85deec3deaff76a7f652e9d5cc3b7ea Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:03:22 +0800 Subject: [PATCH 27/33] Phase 6: Advanced Features - Recovery tools, selective cleanup, exclusion list - Recovery & Safe Mode Tools (menu 11): bcdedit/reagentc/shutdown sub-menu for boot config, safe mode (minimal/networking/cmd-prompt), WinRE, normal boot restore - Selective cleanup: system_cleanup.py refactored with target sub-menu (Windows Temp, User Temp, Prefetch, SoftDist, Vendor Roots) - Cleanup Exclusion List (menu 22): JSON config in config/exclusions.json, paths checked before deletion during cleanup - Exclusion checks integrated into system_cleanup via cleanup_config.is_excluded - Menu renumbered: Recovery (11), Diagnostics (12-19), Tools (20-22), Exit (23) --- MEMORY.md | 14 ++- features/cleanup_config.py | 170 +++++++++++++++++++++++++++++++ features/recovery_tools.py | 146 +++++++++++++++++++++++++++ features/system_cleanup.py | 199 ++++++++++++++++++++++++++----------- ldlwintoolbox.py | 51 ++++++---- memory/feature-ideas.md | 14 +-- memory/tasks.md | 1 + 7 files changed, 506 insertions(+), 89 deletions(-) create mode 100644 features/cleanup_config.py create mode 100644 features/recovery_tools.py diff --git a/MEMORY.md b/MEMORY.md index ad75a28..d2d4c4b 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -75,12 +75,17 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe 9. Disable BitLocker `(Plan)`: shows current BitLocker status, validates a selected drive letter, displays selected drive status, optional restore point, requires typing `DISABLE`, then starts `manage-bde -off :` and logs updated status. 10. Kill Browser AI: warns that it downloads and executes a remote PowerShell script, requires typing `KILL`, then launches PowerShell with `-ExecutionPolicy Bypass` and a guarded `try/catch` wrapper around the configured gist command so the result is logged. -**Tools (19-20):** +**Recovery (11):** -19. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. -20. Check for Updates: queries GitHub releases API, compares with local version (1.0.3), opens browser for download if newer. +11. Recovery & Safe Mode Tools: sub-menu for bcdedit boot config, safe mode (minimal/networking/cmd-prompt), WinRE status/enable/disable, restore normal boot, restart to recovery. -21. Exit: asks Y/N confirmation, then closes the tool. +**Tools (20-22):** + +20. View Log History: lists the newest toolbox logs in `logs\`, lets the user choose one of the latest 9 entries, and opens it with paged console viewing. +21. Check for Updates: queries GitHub releases API, compares with local version (1.0.3), opens browser for download if newer. +22. Cleanup Exclusion List: manage JSON-based exclusion list in `config/exclusions.json`; paths matching exclusions are skipped during cleanup. + +23. Exit: asks Y/N confirmation, then closes the tool. **Diagnostics (11-18):** @@ -92,6 +97,7 @@ Implemented menu behavior (each feature in its own `features/*.py` file), groupe 16. Driver Inventory: parses driverquery /FO CSV output, shows all drivers with type/date summary. 17. Network Snapshot: captures ipconfig/route/netsh/netstat state to file and log; optional diff against previous snapshot. 18. Export Logs & Report: generates a plain-text session report (features run, commands, warnings) and archives all logs to ZIP. +19. System Cleanup: now supports selective target sub-menu (Windows Temp, User Temp, Prefetch, SoftDist, Vendor Roots) with exclusion list integration. ## Implemented Feature Targets diff --git a/features/cleanup_config.py b/features/cleanup_config.py new file mode 100644 index 0000000..83158c1 --- /dev/null +++ b/features/cleanup_config.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path + +from toolbox_base import MENU_LOGO, Logger, clear_screen + + +_CONFIG_DIR: str | None = None + + +def _get_config_dir() -> Path: + global _CONFIG_DIR + if _CONFIG_DIR is not None: + return Path(_CONFIG_DIR) + p = Path.cwd() / "config" + p.mkdir(parents=True, exist_ok=True) + _CONFIG_DIR = str(p) + return p + + +_EXCLUSIONS_FILE: str | None = None + + +def _get_exclusions_path() -> Path: + global _EXCLUSIONS_FILE + if _EXCLUSIONS_FILE is not None: + return Path(_EXCLUSIONS_FILE) + p = _get_config_dir() / "exclusions.json" + _EXCLUSIONS_FILE = str(p) + return p + + +def _load_exclusions() -> list[str]: + path = _get_exclusions_path() + if not path.exists(): + return [] + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, list) else [] + except (OSError, json.JSONDecodeError): + return [] + + +def _save_exclusions(exclusions: list[str]) -> bool: + path = _get_exclusions_path() + try: + path.write_text( + json.dumps(exclusions, indent=2, ensure_ascii=False), + encoding="utf-8", + newline="\n", + ) + return True + except OSError: + return False + + +def _path_resolve(entry: str) -> str: + expanded = os.path.expandvars(entry) + return str(Path(expanded).resolve()) if expanded else entry + + +def _matches_exclusion(target: Path) -> bool: + resolved = str(target.resolve()).lower() + for entry in _load_exclusions(): + excl = os.path.expandvars(entry) + excl_res = str(Path(excl).resolve()).lower() if excl else entry.lower() + if resolved.startswith(excl_res): + return True + return False + + +def get_exclusions() -> list[str]: + return list(_load_exclusions()) + + +def is_excluded(target: Path) -> bool: + return _matches_exclusion(target) + + +def cleanup_config(logger: Logger) -> None: + while True: + exclusions = _load_exclusions() + total = len(exclusions) + clear_screen() + print(MENU_LOGO) + print(" CLEANUP EXCLUSION LIST") + print(MENU_LOGO) + if exclusions: + print(f" Exclusions ({total}):") + for i, entry in enumerate(exclusions, 1): + print(f" {i}. {entry}") + else: + print(" No exclusions configured.") + print(MENU_LOGO) + print("[1] Add Exclusion Path") + print("[2] Remove Exclusion by Number") + print("[3] Clear All Exclusions") + print("[0] Return to Main Menu") + print(MENU_LOGO) + print(" Tip: Use %WinDir%, %TEMP%, %SystemDrive% variables.") + print(MENU_LOGO) + choice = input("Select an option: ").strip() + logger.log_only("INFO", f"Cleanup Config sub-menu selection: {choice}") + + if choice == "1": + logger.section("Add Exclusion") + path = input("Enter path to exclude: ").strip() + if not path: + print("Empty path ignored.") + input("Press Enter to continue...") + continue + resolved = _path_resolve(path) + if any(e.lower() == path.lower() or _path_resolve(e).lower() == resolved.lower() for e in exclusions): + print(f"'{path}' is already in the exclusion list.") + input("Press Enter to continue...") + continue + exclusions.append(path) + if _save_exclusions(exclusions): + logger.log("INFO", f"Exclusion added: {path} (resolved: {resolved})") + print(f"Added: {path}") + else: + logger.log("ERROR", "Failed to write exclusions file.") + input("Press Enter to continue...") + + elif choice == "2": + if not exclusions: + print("No exclusions to remove.") + input("Press Enter to continue...") + continue + logger.section("Remove Exclusion") + try: + idx_str = input(f"Enter number to remove (1-{total}): ").strip() + idx = int(idx_str) - 1 + if idx < 0 or idx >= total: + print(f"Invalid number. Enter 1-{total}.") + input("Press Enter to continue...") + continue + removed = exclusions.pop(idx) + if _save_exclusions(exclusions): + logger.log("INFO", f"Exclusion removed: {removed}") + print(f"Removed: {removed}") + else: + logger.log("ERROR", "Failed to write exclusions file.") + except ValueError: + print("Invalid input.") + input("Press Enter to continue...") + + elif choice == "3": + if not exclusions: + print("No exclusions to clear.") + input("Press Enter to continue...") + continue + logger.section("Clear All Exclusions") + from toolbox_base import prompt_yes_no + if prompt_yes_no(logger, "Clear all exclusions? (Y/N): ", "Clear Exclusions"): + if _save_exclusions([]): + logger.log("INFO", "All exclusions cleared.") + print("All exclusions removed.") + else: + logger.log("ERROR", "Failed to write exclusions file.") + input("Press Enter to continue...") + + elif choice == "0": + logger.log("INFO", "Cleanup Config returned to main menu.") + return + + else: + logger.log("WARN", f"Invalid Cleanup Config selection: {choice}") diff --git a/features/recovery_tools.py b/features/recovery_tools.py new file mode 100644 index 0000000..0251550 --- /dev/null +++ b/features/recovery_tools.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, prompt_yes_no, run_command + + +def _bcdedit(args: list[str]) -> str: + if not command_exists("bcdedit"): + return "" + result = run_command(["bcdedit"] + args, capture=True) + return result.stdout.strip() if result.code == 0 else f"(exit={result.code})" + + +def _reagentc(args: list[str]) -> str: + if not command_exists("reagentc"): + return "" + result = run_command(["reagentc"] + args, capture=True) + return result.stdout.strip() if result.code == 0 else f"(exit={result.code})" + + +def _has_safeboot() -> bool: + output = _bcdedit(["/enum", "{current}"]) + return "safeboot" in output.lower() + + +def recovery_tools(logger: Logger) -> None: + while True: + has_sb = _has_safeboot() + clear_screen() + print(MENU_LOGO) + print(" RECOVERY & SAFE MODE TOOLS") + print(MENU_LOGO) + print(f" Safe Mode active : {'Yes' if has_sb else 'No'}") + print(MENU_LOGO) + print("[1] View Boot Configuration") + print("[2] Boot to Safe Mode (Minimal)") + print("[3] Boot to Safe Mode (Networking)") + print("[4] Boot to Safe Mode (Command Prompt)") + print("[5] Restore Normal Boot") + print("[6] Boot to Recovery Environment (WinRE)") + print("[7] Check WinRE Status") + if has_sb: + print("[8] Boot Normally on Next Restart") + print("[0] Return to Main Menu") + print(MENU_LOGO) + choice = input("Select an option: ").strip() + logger.log_only("INFO", f"Recovery Tools sub-menu selection: {choice}") + + if choice == "1": + logger.section("Boot Configuration") + output = _bcdedit(["/enum"]) + print(output if output else "Unable to read boot configuration.") + logger.write_raw(output) + input("Press Enter to continue...") + + elif choice == "2": + logger.section("Safe Mode — Minimal") + print("WARNING: This will set Safe Mode boot on next restart.") + if prompt_yes_no(logger, "Set Safe Mode (Minimal)? (Y/N): ", "Safe Mode Minimal"): + output = _bcdedit(["/set", "{current}", "safeboot", "minimal"]) + print(output) + logger.write_raw(output) + if prompt_yes_no(logger, "Restart now? (Y/N): ", "Restart"): + run_command(["shutdown", "/r", "/t", "5"], capture=False) + logger.log_only("INFO", "System restart initiated.") + input("Press Enter to continue...") + + elif choice == "3": + logger.section("Safe Mode — Networking") + print("WARNING: This will set Safe Mode with Networking on next restart.") + if prompt_yes_no(logger, "Set Safe Mode (Networking)? (Y/N): ", "Safe Mode Networking"): + output = _bcdedit(["/set", "{current}", "safeboot", "network"]) + print(output) + logger.write_raw(output) + if prompt_yes_no(logger, "Restart now? (Y/N): ", "Restart"): + run_command(["shutdown", "/r", "/t", "5"], capture=False) + logger.log_only("INFO", "System restart initiated.") + input("Press Enter to continue...") + + elif choice == "4": + logger.section("Safe Mode — Command Prompt") + print("WARNING: This will set Safe Mode with Command Prompt on next restart.") + if prompt_yes_no(logger, "Set Safe Mode (Command Prompt)? (Y/N): ", "Safe Mode CmdPrompt"): + output = _bcdedit(["/set", "{current}", "safeboot", "minimal"]) + print(output) + out2 = _bcdedit(["/set", "{current}", "safebootalternateshell", "yes"]) + print(out2) + logger.write_raw(output + "\n" + out2) + if prompt_yes_no(logger, "Restart now? (Y/N): ", "Restart"): + run_command(["shutdown", "/r", "/t", "5"], capture=False) + logger.log_only("INFO", "System restart initiated.") + input("Press Enter to continue...") + + elif choice == "5": + logger.section("Restore Normal Boot") + print("WARNING: This will remove Safe Mode and restore normal boot.") + if prompt_yes_no(logger, "Restore normal boot? (Y/N): ", "Restore Normal Boot"): + out1 = _bcdedit(["/deletevalue", "{current}", "safeboot"]) + print(out1) + out2 = _bcdedit(["/deletevalue", "{current}", "safebootalternateshell"]) + print(out2) + logger.write_raw(out1 + "\n" + out2) + print("Normal boot restored. Reboot is recommended.") + input("Press Enter to continue...") + + elif choice == "6": + logger.section("Boot to Recovery Environment") + print("WARNING: This will restart into Windows Recovery Environment.") + if prompt_yes_no(logger, "Restart to WinRE now? (Y/N): ", "Boot to WinRE"): + run_command(["shutdown", "/r", "/o", "/t", "5"], capture=False) + logger.log_only("INFO", "System restart to WinRE initiated.") + input("Press Enter to continue...") + + elif choice == "7": + logger.section("WinRE Status") + output = _reagentc(["/info"]) + print(output if output else "Unable to query WinRE status.") + logger.write_raw(output) + print() + if command_exists("reagentc"): + logger.section("Enable / Disable WinRE") + if prompt_yes_no(logger, "Enable Windows Recovery Environment? (Y/N): ", "Enable WinRE"): + out = _reagentc(["/enable"]) + print(out) + logger.write_raw(out) + if prompt_yes_no(logger, "Disable Windows Recovery Environment? (Y/N): ", "Disable WinRE"): + out = _reagentc(["/disable"]) + print(out) + logger.write_raw(out) + input("Press Enter to continue...") + + elif choice == "8" and has_sb: + logger.section("Boot Normally on Next Restart") + out1 = _bcdedit(["/deletevalue", "{current}", "safeboot"]) + print(out1) + out2 = _bcdedit(["/deletevalue", "{current}", "safebootalternateshell"]) + print(out2) + logger.write_raw(out1 + "\n" + out2) + print("Safe Mode cleared. System will boot normally on next restart.") + input("Press Enter to continue...") + + elif choice == "0": + logger.log("INFO", "Recovery Tools returned to main menu.") + return + + else: + logger.log("WARN", f"Invalid Recovery Tools selection: {choice}") diff --git a/features/system_cleanup.py b/features/system_cleanup.py index 2814e32..389f312 100644 --- a/features/system_cleanup.py +++ b/features/system_cleanup.py @@ -28,7 +28,115 @@ def drive_free_mb() -> int: return 0 +def _is_excluded(target: Path) -> bool: + try: + from features.cleanup_config import is_excluded + return is_excluded(target) + except Exception: + return False + + +def _clean_dir(target: Path, logger: Logger) -> None: + if not target.exists(): + logger.log_only("INFO", f"Skipping (not found): {target}") + return + if _is_excluded(target): + logger.log("INFO", f"- Skipping (excluded): {target}") + return + logger.log("INFO", f"- Cleaning {target}") + for child in target.iterdir(): + if _is_excluded(child): + continue + try: + if child.is_dir(): + shutil.rmtree(child, ignore_errors=False) + else: + child.unlink(missing_ok=True) + except OSError as exc: + logger.log_only("WARN", f"Failed to remove {child}: {exc}") + + +def _rebuild_dir(target: Path, logger: Logger) -> None: + if _is_excluded(target): + return + logger.log("INFO", f"- Rebuilding {target}") + target.mkdir(parents=True, exist_ok=True) + + +def _stop_services(logger: Logger) -> None: + logger.log("INFO", "[1/4] Stopping background services...") + for cmd in (["net", "stop", "wuauserv"], ["net", "stop", "bits"]): + logger.log("INFO", f"- Stopping {cmd[-1]}...") + run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + + +def _start_services(logger: Logger) -> None: + logger.log("INFO", "[4/4] Finalizing optimizations...") + for cmd in (["net", "start", "wuauserv"], ["net", "start", "bits"]): + logger.log("INFO", f"- Starting {cmd[-1]}...") + run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + + +def _get_targets() -> dict[str, list[Path]]: + system_drive = os.environ.get("SYSTEMDRIVE", "C:") + win_dir = Path(os.environ["WinDir"]) + + def env_temp(name: str) -> Path | None: + val = os.environ.get(name) + return Path(val) / "Temp" if val else None + + return { + "WindowsTemp": [win_dir / "Temp"], + "UserTemp": [ + env_temp("TEMP"), + env_temp("AppData"), + env_temp("LocalAppData"), + ], + "Prefetch": [win_dir / "Prefetch"], + "SoftwareDistribution": [win_dir / "SoftwareDistribution" / "Download"], + "VendorRoots": [Path(f"{system_drive}\\{n}") for n in ("AMD", "NVIDIA", "INTEL")], + } + + +def _select_targets(logger: Logger) -> str | None: + while True: + clear_screen() + print(MENU_LOGO) + print(" ADVANCED SYSTEM CLEANUP") + print(MENU_LOGO) + print("Select cleanup mode:") + print("[1] All (full cleanup)") + print("[2] Windows Temp only") + print("[3] User Temp only") + print("[4] Prefetch only") + print("[5] SoftwareDistribution Downloads only") + print("[6] Vendor Driver Roots only") + print("[0] Cancel") + print(MENU_LOGO) + choice = input("Select an option: ").strip() + logger.log_only("INFO", f"Cleanup target selection: {choice}") + if choice == "0": + return None + if choice in ("1", "2", "3", "4", "5", "6"): + return choice + logger.log_only("WARN", f"Invalid target selection: {choice}") + + def cleanup(logger: Logger) -> None: + target_choice = _select_targets(logger) + if target_choice is None: + return + + is_all = target_choice == "1" + targets = _get_targets() + + if is_all: + selected_names = list(targets.keys()) + else: + name_map = {"2": "WindowsTemp", "3": "UserTemp", "4": "Prefetch", + "5": "SoftwareDistribution", "6": "VendorRoots"} + selected_names = [name_map[target_choice]] + clear_screen() print(MENU_LOGO) print(" ADVANCED SYSTEM CLEANUP TOOL") @@ -52,69 +160,46 @@ def cleanup(logger: Logger) -> None: free_before = drive_free_mb() logger.log_only("INFO", f"Free space before cleanup: {free_before} MB") - logger.log("INFO", "[1/4] Stopping background services...") - for cmd in (["net", "stop", "wuauserv"], ["net", "stop", "bits"]): - logger.log("INFO", f"- Stopping {cmd[-1]}...") - run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + if is_all or any(n in selected_names for n in ("WindowsTemp", "UserTemp", "SoftwareDistribution")): + _stop_services(logger) print() logger.log("INFO", "[2/4] Deleting temporary and junk files...") - def env_temp_dir(name: str) -> Path | None: - value = os.environ.get(name) - return Path(value) / "Temp" if value else None - - temp_targets = [ - Path(os.environ["WinDir"]) / "Temp", - Path(os.environ["WinDir"]) / "Prefetch", - Path(os.environ["TEMP"]), - env_temp_dir("AppData"), - env_temp_dir("LocalAppData"), - Path(os.environ["WinDir"]) / "SoftwareDistribution" / "Download", - ] - for target in temp_targets: - if target is None: - continue - logger.log("INFO", f"- Cleaning {target}") - if target.exists(): - for child in target.iterdir(): - try: - if child.is_dir(): - shutil.rmtree(child, ignore_errors=False) - else: - child.unlink(missing_ok=True) - except OSError as exc: - logger.log_only("WARN", f"Failed to remove {child}: {exc}") - - logger.log( - "INFO", - "- Event Viewer logs are handled by menu option 6 using wevtutil.", - ) - if prompt_yes_no( - logger, - "Also remove vendor driver directories (AMD, NVIDIA, INTEL) on system drive? (Y/N): ", - "Vendor Driver Cleanup", - ): - system_drive = os.environ.get("SYSTEMDRIVE", "C:") - for root_name in ("AMD", "NVIDIA", "INTEL"): - root = Path(f"{system_drive}\\{root_name}") - if root.exists(): - logger.log("INFO", f"- Removing Directory {root}") - shutil.rmtree(root, ignore_errors=True) + clean_dirs: list[Path] = [] + for name in selected_names: + if name == "VendorRoots": + if is_all: + if prompt_yes_no(logger, "Remove vendor driver directories (AMD, NVIDIA, INTEL)? (Y/N): ", "Vendor Driver Cleanup"): + clean_dirs.extend(targets["VendorRoots"]) + else: + clean_dirs.extend(targets["VendorRoots"]) + else: + clean_dirs.extend(d for d in targets[name] if d is not None) - print() - logger.log("INFO", "[3/4] Rebuilding directory structure...") - for target in temp_targets[:5]: - if target is None: - continue - logger.log("INFO", f"- Rebuilding {target}") - target.mkdir(parents=True, exist_ok=True) + for d in clean_dirs: + _clean_dir(d, logger) - print() - logger.log("INFO", "[4/4] Finalizing optimizations...") - for cmd in (["net", "start", "wuauserv"], ["net", "start", "bits"]): - logger.log("INFO", f"- Starting {cmd[-1]}...") - run_and_log(logger, cmd, " ".join(cmd), capture_output=True) + if target_choice == "6": + _rebuild_clean_dir = False + else: + _rebuild_clean_dir = True + + if is_all: + logger.log("INFO", "- Event Viewer logs are handled by menu option 6 using wevtutil.") + + if is_all or any(n in selected_names for n in ("WindowsTemp", "UserTemp", "Prefetch")): + print() + logger.log("INFO", "[3/4] Rebuilding directory structure...") + for name in selected_names: + if name != "VendorRoots": + for d in targets[name]: + if d is not None: + _rebuild_dir(d, logger) + + if is_all or any(n in selected_names for n in ("WindowsTemp", "UserTemp", "SoftwareDistribution")): + print() + _start_services(logger) free_after = drive_free_mb() saved = max(0, free_after - free_before) diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index ecc9ec4..3ef950d 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -16,6 +16,7 @@ ) from features.bitlocker_disable import bitlocker_disable from features.browser_ai_killer import kill_browser_ai +from features.cleanup_config import cleanup_config from features.defender_tools import defender_tools from features.disk_health import disk_health from features.driver_inventory import driver_inventory @@ -25,6 +26,7 @@ from features.low_latency_mode import low_latency_mode from features.network_reset import net_reset from features.network_snapshot import network_snapshot +from features.recovery_tools import recovery_tools from features.self_update import self_update from features.service_health import service_health from features.ssd_trim import ssd_trim @@ -83,20 +85,23 @@ def main_menu(logger: Logger, log_dir: Path) -> None: print(" ── Security & Privacy ──") print("[9] Disable BitLocker (Plan)") print("[10] Kill Browser AI") + print(" ── Recovery ──") + print("[11] Recovery & Safe Mode Tools") print(" ── Diagnostics ──") - print("[11] System Information") - print("[12] Windows Update Status") - print("[13] Defender Status & Quick Scan") - print("[14] Service Health Check") - print("[15] Disk Health & SMART Summary") - print("[16] Driver Inventory") - print("[17] Network Snapshot") - print("[18] Export Logs & Report") + print("[12] System Information") + print("[13] Windows Update Status") + print("[14] Defender Status & Quick Scan") + print("[15] Service Health Check") + print("[16] Disk Health & SMART Summary") + print("[17] Driver Inventory") + print("[18] Network Snapshot") + print("[19] Export Logs & Report") print(" ── Tools ──") - print("[19] View Log History") - print("[20] Check for Updates") + print("[20] View Log History") + print("[21] Check for Updates") + print("[22] Cleanup Exclusion List") print("───────────────────────────────────────────────") - print("[21] Exit") + print("[23] Exit") print("===============================================") print(f"Log: {logger.logfile}") print("===============================================") @@ -124,26 +129,30 @@ def main_menu(logger: Logger, log_dir: Path) -> None: elif choice == "10": kill_browser_ai(logger) elif choice == "11": - system_info(logger) + recovery_tools(logger) elif choice == "12": - windows_update(logger) + system_info(logger) elif choice == "13": - defender_tools(logger) + windows_update(logger) elif choice == "14": - service_health(logger) + defender_tools(logger) elif choice == "15": - disk_health(logger) + service_health(logger) elif choice == "16": - driver_inventory(logger) + disk_health(logger) elif choice == "17": - network_snapshot(logger, script_dir) + driver_inventory(logger) elif choice == "18": - export_report(logger, log_dir) + network_snapshot(logger, script_dir) elif choice == "19": - log_history(logger, log_dir) + export_report(logger, log_dir) elif choice == "20": - self_update(logger) + log_history(logger, log_dir) elif choice == "21": + self_update(logger) + elif choice == "22": + cleanup_config(logger) + elif choice == "23": if not prompt_yes_no( logger, "Are you sure you want to exit? (Y/N): ", diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index b361e70..ecafe4e 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -41,11 +41,11 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - [ ] Reduce redundant PowerShell calls (deferred — low impact) - [ ] Add a read-only mode for status checks (deferred — needs launch-flow change) -### Phase 6: Advanced Features +### Phase 6: Advanced Features ✅ (Complete) -1. Selective cleanup instead of fixed cleanup sets -2. Custom exclusion list for cleanup targets -3. Safe Mode or recovery entry helpers +- [x] Selective cleanup instead of fixed cleanup sets (sub-menu in system_cleanup) +- [x] Custom exclusion list for cleanup targets (config/exclusions.json manager) +- [x] Safe Mode or recovery entry helpers (bcdedit/reagentc/shutdown sub-menu) ## New Features @@ -57,9 +57,9 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - [x] Log export and archive bundle - [x] Network before/after snapshot - [x] Defender status check and quick scan entry -- Safe Mode or recovery entry helpers -- Selective cleanup instead of fixed cleanup sets -- Custom exclusion list for cleanup targets +- [x] Safe Mode or recovery entry helpers +- [x] Selective cleanup instead of fixed cleanup sets +- [x] Custom exclusion list for cleanup targets - Exportable report of actions and results - [x] Version and update check for the toolbox itself diff --git a/memory/tasks.md b/memory/tasks.md index 5265f0b..c260f58 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -8,6 +8,7 @@ Last updated: 2026-07-05 ## Completed +- [x] 2026-07-05: Phase 6: Recovery tools (bcdedit safe mode/WinRE), selective cleanup sub-menu, exclusion list manager (config/exclusions.json). Menu renumbered Exit to 23. - [x] 2026-07-05: Phase 5: Self-update check via GitHub releases API (menu 20). Bumped version to 1.0.3. Renumbered Exit to 21. - [x] 2026-07-05: Phase 4: Created 4 reporting features (disk_health, driver_inventory, network_snapshot, export_report). Renumbered menu (Exit: 16→20). - [x] 2026-07-05: Phase 3: Created 4 diagnostic features (system_info, windows_update, defender_tools, service_health) with read-only info + optional scan actions. Renumbered menu (Exit: 12→16). From 417d00c8c1959e38a28697fc4cf3697b4c76cbd8 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:06:08 +0800 Subject: [PATCH 28/33] Phase 5 completion: read-only mode + PowerShell batching - Read-only mode: diagnostics (12-22) now run without admin privileges; admin features (1-11) hidden with [R] restart-as-admin shortcut - PowerShell optimization: disk_health.py combined 3 calls into 1 using section markers (# DISKS / # VOLUMES / # SMART) - Menu system refactored: script_dir and is_admin passed as parameters - All 6 roadmap phases now complete - Updated memory files with final Phase 5 status --- features/disk_health.py | 106 ++++++++++++++++++++++++---------------- ldlwintoolbox.py | 71 ++++++++++++++++----------- memory/feature-ideas.md | 8 +-- memory/tasks.md | 1 + 4 files changed, 111 insertions(+), 75 deletions(-) diff --git a/features/disk_health.py b/features/disk_health.py index 483115e..99d41ba 100644 --- a/features/disk_health.py +++ b/features/disk_health.py @@ -30,7 +30,8 @@ def disk_health(logger: Logger) -> None: print(MENU_LOGO) logger.section("Disk Health & SMART") - ps = ( + combined_ps = ( + "# DISKS\n" "$d=Get-PhysicalDisk | ForEach-Object {" "$f=$_;" "$rel=$_ | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue;" @@ -40,28 +41,35 @@ def disk_health(logger: Logger) -> None: "$we=if($rel){$rel.WriteErrorsTotal}else{'N/A'};" "Write-Output ($f.FriendlyName+'|'+$f.MediaType+'|'+$f.HealthStatus+'|'+" "[math]::Round($f.Size/1GB,1).ToString()+'GB|'+$f.OperationalStatus+'|'+$f.BusType+'|'+" - "$t+'|'+$w+'|'+$re+'|'+$we)" - "};" - "if(-not $d){Write-Output 'NO_DATA'}" + "$t+'|'+$w+'|'+$re+'|'+$we)};" + "if(-not $d){Write-Output 'NO_DATA'}\n" + "# VOLUMES\n" + "Get-Volume | Where-Object {$_.DriveType -eq 'Fixed' -and $_.DriveLetter} " + "| ForEach-Object {Write-Output ($_.DriveLetter+':|'+$_.FileSystem+'|'+$_.HealthStatus+'|'+$_.SizeRemaining+'|'+$_.Size)}\n" + "# SMART\n" + "Get-PhysicalDisk | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue " + "| ForEach-Object {Write-Output ($_.DeviceId+'|'+$_.Temperature+'|'+" + "$_.WearPercentage+'|'+$_.ReadErrorsTotal+'|'+$_.WriteErrorsTotal+'|'+" + "$_.ReadLatencyMax+'|'+$_.WriteLatencyMax+'|'+$_.FlushLatencyMax)}" ) - raw = _run_ps(ps) - if not raw or raw.strip() == "NO_DATA": - logger.log("INFO", "No physical disk data available via PowerShell.") - print("No physical disk data returned. Try running as Administrator.") + raw = _run_ps(combined_ps) + if not raw: + logger.log("INFO", "No disk data available via PowerShell.") + print("No disk data returned. Try running as Administrator.") print() print(" Fallback: Volume-level info from Get-PSDrive:") - ps2 = ( + ps_fb = ( "Get-PSDrive -PSProvider FileSystem " "| Where-Object {$_.Root -match '^[A-Z]:\\\\$'} " "| ForEach-Object {Write-Output ($_.Root+'|'+[math]::Round($_.Used/1GB,1).ToString()+'/'+" "[math]::Round(($_.Used+$_.Free)/1GB,1).ToString()+'GB|'+[math]::Round($_.Free/1GB,1).ToString()+'GB')}" ) - raw2 = _run_ps(ps2) - if raw2: + fb_raw = _run_ps(ps_fb) + if fb_raw: print(f" {'Drive':<8} {'Used/Total':<22} {'Free':<10}") print(f" {'-'*8} {'-'*22} {'-'*10}") - for line in raw2.splitlines(): + for line in fb_raw.splitlines(): parts = line.strip().split("|") if len(parts) >= 3: print(f" {parts[0]:<8} {parts[1]:<22} {parts[2]:<10}") @@ -69,28 +77,46 @@ def disk_health(logger: Logger) -> None: input("Press Enter to continue...") return - print(f" {'Name':<30} {'Type':<12} {'Health':<12} {'Size':<10} {'Status':<14} {'Bus':<10} {'Temp':<8} {'Wear':<8} {'ReadErr':<8} {'WriteErr':<8}") - print(f" {'-'*30} {'-'*12} {'-'*12} {'-'*10} {'-'*14} {'-'*10} {'-'*8} {'-'*8} {'-'*8} {'-'*8}") - + sections: dict[str, list[str]] = {} + current_section = "DISKS" for line in raw.splitlines(): - parts = line.strip().split("|") - if len(parts) >= 10: - name, media, health, size, op_status, bus, temp, wear, re, we = parts[:10] - print(f" {name:<30} {media:<12} {health:<12} {size:<10} {op_status:<14} {bus:<10} {temp:<8} {wear:<8} {re:<8} {we:<8}") - logger.log_only("INFO", f"Disk: {name} health={health} wear={wear} temp={temp}") - - print() - logger.section("Volume Summary") - ps_vol = ( - "Get-Volume | Where-Object {$_.DriveType -eq 'Fixed' -and $_.DriveLetter} " - "| ForEach-Object {Write-Output ($_.DriveLetter+':|'+$_.FileSystem+'|'+$_.HealthStatus+'|'+$_.SizeRemaining+'|'+$_.Size)}" - ) - raw_vol = _run_ps(ps_vol) - if raw_vol: + stripped = line.strip() + if stripped == "# DISKS": + current_section = "DISKS" + elif stripped == "# VOLUMES": + current_section = "VOLUMES" + elif stripped == "# SMART": + current_section = "SMART" + else: + sections.setdefault(current_section, []).append(stripped) + + disk_lines = sections.get("DISKS", []) + has_no_data = len(disk_lines) == 1 and disk_lines[0] == "NO_DATA" + + if has_no_data: + logger.log("INFO", "No physical disk data available via PowerShell.") + print("No physical disk data returned. Try running as Administrator.") + input("Press Enter to continue...") + return + + if disk_lines: + print(f" {'Name':<30} {'Type':<12} {'Health':<12} {'Size':<10} {'Status':<14} {'Bus':<10} {'Temp':<8} {'Wear':<8} {'ReadErr':<8} {'WriteErr':<8}") + print(f" {'-'*30} {'-'*12} {'-'*12} {'-'*10} {'-'*14} {'-'*10} {'-'*8} {'-'*8} {'-'*8} {'-'*8}") + for line in disk_lines: + parts = line.split("|") + if len(parts) >= 10: + name, media, health, size, op_status, bus, temp, wear, re, we = parts[:10] + print(f" {name:<30} {media:<12} {health:<12} {size:<10} {op_status:<14} {bus:<10} {temp:<8} {wear:<8} {re:<8} {we:<8}") + logger.log_only("INFO", f"Disk: {name} health={health} wear={wear} temp={temp}") + + vol_lines = sections.get("VOLUMES", []) + if vol_lines: + print() + logger.section("Volume Summary") print(f" {'Volume':<8} {'FS':<8} {'Health':<12} {'Free':<12} {'Total':<12}") print(f" {'-'*8} {'-'*8} {'-'*12} {'-'*12} {'-'*12}") - for line in raw_vol.splitlines(): - parts = line.strip().split("|") + for line in vol_lines: + parts = line.split("|") if len(parts) >= 5: vol, fs, h, free_s, total_s = parts[:5] free_b = int(free_s) if free_s.isdigit() else 0 @@ -100,20 +126,14 @@ def disk_health(logger: Logger) -> None: print(f" {vol:<8} {fs:<8} {h:<12} {free_fmt:<12} {total_fmt:<12}") logger.log_only("INFO", f"Volume: {vol} {h} free={free_fmt} of {total_fmt}") - print() - logger.section("SMART Reliability Counters") - ps_smart = ( - "Get-PhysicalDisk | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue " - "| ForEach-Object {Write-Output ($_.DeviceId+'|'+$_.Temperature+'|'+" - "$_.WearPercentage+'|'+$_.ReadErrorsTotal+'|'+$_.WriteErrorsTotal+'|'+" - "$_.ReadLatencyMax+'|'+$_.WriteLatencyMax+'|'+$_.FlushLatencyMax)}" - ) - raw_smart = _run_ps(ps_smart) - if raw_smart: + smart_lines = sections.get("SMART", []) + if smart_lines: + print() + logger.section("SMART Reliability Counters") print(f" {'Disk#':<8} {'Temp(C)':<10} {'Wear%':<8} {'ReadErr':<10} {'WriteErr':<10} {'RdLat(ms)':<12} {'WrLat(ms)':<12} {'FlLat(ms)':<12}") print(f" {'-'*8} {'-'*10} {'-'*8} {'-'*10} {'-'*10} {'-'*12} {'-'*12} {'-'*12}") - for line in raw_smart.splitlines(): - parts = line.strip().split("|") + for line in smart_lines: + parts = line.split("|") if len(parts) >= 8: did, temp, wear, re, we, rl, wl, fl = parts[:8] print(f" {did:<8} {temp:<10} {wear:<8} {re:<10} {we:<10} {rl:<12} {wl:<12} {fl:<12}") diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index 3ef950d..2540120 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -56,37 +56,34 @@ def relaunch_as_admin() -> None: ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, params, None, 1) -def ensure_admin() -> None: - if is_admin(): - return - print("Requesting administrative privileges...") - relaunch_as_admin() - raise SystemExit(0) - - -def main_menu(logger: Logger, log_dir: Path) -> None: +def main_menu(logger: Logger, log_dir: Path, script_dir: Path, is_admin_user: bool) -> None: while True: clear_screen() print("===============================================") print(" LDL Windows ToolBox") + if not is_admin_user: + print(" *** READ-ONLY MODE ***") print("===============================================") - print(" ── System Cleanup ──") - print("[1] Advanced System Cleanup") - print("[2] Windows Component Store Cleanup (WinSxS)") - print("[3] Clear Event Viewer Logs") - print(" ── System Repair & Update ──") - print("[4] System Integrity Repair (SFC + DISM)") - print("[5] Update All Installed Apps (Winget)") - print(" ── Network ──") - print("[6] Complete Network Reset") - print(" ── Performance ──") - print("[7] Manual SSD TRIM") - print("[8] Low Latency Mode (ViVeTool)") - print(" ── Security & Privacy ──") - print("[9] Disable BitLocker (Plan)") - print("[10] Kill Browser AI") - print(" ── Recovery ──") - print("[11] Recovery & Safe Mode Tools") + if is_admin_user: + print(" ── System Cleanup ──") + print("[1] Advanced System Cleanup") + print("[2] Windows Component Store Cleanup (WinSxS)") + print("[3] Clear Event Viewer Logs") + print(" ── System Repair & Update ──") + print("[4] System Integrity Repair (SFC + DISM)") + print("[5] Update All Installed Apps (Winget)") + print(" ── Network ──") + print("[6] Complete Network Reset") + print(" ── Performance ──") + print("[7] Manual SSD TRIM") + print("[8] Low Latency Mode (ViVeTool)") + print(" ── Security & Privacy ──") + print("[9] Disable BitLocker (Plan)") + print("[10] Kill Browser AI") + print(" ── Recovery ──") + print("[11] Recovery & Safe Mode Tools") + else: + print(" (Admin features hidden. Press [R] to restart as admin.)") print(" ── Diagnostics ──") print("[12] System Information") print("[13] Windows Update Status") @@ -101,6 +98,8 @@ def main_menu(logger: Logger, log_dir: Path) -> None: print("[21] Check for Updates") print("[22] Cleanup Exclusion List") print("───────────────────────────────────────────────") + if not is_admin_user: + print("[R] Restart as Administrator") print("[23] Exit") print("===============================================") print(f"Log: {logger.logfile}") @@ -108,6 +107,17 @@ def main_menu(logger: Logger, log_dir: Path) -> None: choice = input("Select an option: ").strip() logger.log_only("INFO", f"Menu selection: {choice}") + if not is_admin_user and choice.upper() == "R": + logger.log("INFO", "User requested admin restart.") + relaunch_as_admin() + continue + + if not is_admin_user and choice in ("1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"): + logger.log("WARN", f"Admin feature {choice} blocked in read-only mode.") + print("This feature requires administrator privileges.") + input("Press Enter to continue...") + continue + if choice == "1": cleanup(logger) elif choice == "2": @@ -166,7 +176,6 @@ def main_menu(logger: Logger, log_dir: Path) -> None: def main() -> None: - ensure_admin() script_file = Path(__file__).resolve() script_dir = script_file.parent os.chdir(script_dir) @@ -175,8 +184,14 @@ def main() -> None: logfile = log_dir / f"LDLWinToolBox_{log_time}.log" logger = Logger(logfile) write_session_header(logger, logfile, script_file, script_dir) + admin_user = is_admin() + if not admin_user: + logger.log_only("WARN", "Started without admin privileges - read-only mode.") + print("Running in read-only mode. Admin features (1-11) are hidden.") + print("Press Enter to continue...") + input() try: - main_menu(logger, log_dir) + main_menu(logger, log_dir, script_dir, admin_user) except KeyboardInterrupt: logger.log("INFO", "User cancelled the session with Ctrl+C.") diff --git a/memory/feature-ideas.md b/memory/feature-ideas.md index ecafe4e..fab57e3 100644 --- a/memory/feature-ideas.md +++ b/memory/feature-ideas.md @@ -35,11 +35,11 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - [x] Network before/after snapshot (ipconfig/route/netsh/netstat capture + diff) - [x] Log export and archive bundle (#4+#5 combined: report generation + ZIP archive) -### Phase 5: Efficiency & Maintenance ✅ (Partial) +### Phase 5: Efficiency & Maintenance ✅ (Complete) - [x] Version and update check for the toolbox itself (GitHub releases API) -- [ ] Reduce redundant PowerShell calls (deferred — low impact) -- [ ] Add a read-only mode for status checks (deferred — needs launch-flow change) +- [x] Reduce redundant PowerShell calls (disk_health: 3→1 call; remaining calls are non-redundant) +- [x] Add a read-only mode for status checks (diagnostics 12–22 run without admin, 1–11 hidden with [R] restart option) ### Phase 6: Advanced Features ✅ (Complete) @@ -69,6 +69,6 @@ Keep entries concise, append-friendly, and aligned with the Python-first, menu-d - [x] Improve error handling and user-facing failure messages - [x] Add progress hints for long-running tasks - [x] Make cleanup operations more conservative by default -- Add a read-only mode for status checks +- [x] Add a read-only mode for status checks - Maintain a lightweight verification checklist after changes - Keep README, memory, and task notes synchronized diff --git a/memory/tasks.md b/memory/tasks.md index c260f58..e098c6c 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -8,6 +8,7 @@ Last updated: 2026-07-05 ## Completed +- [x] 2026-07-05: Phase 5 complete: Self-update check, read-only mode (diagnostics without admin), PowerShell calls batched (disk_health: 3→1). - [x] 2026-07-05: Phase 6: Recovery tools (bcdedit safe mode/WinRE), selective cleanup sub-menu, exclusion list manager (config/exclusions.json). Menu renumbered Exit to 23. - [x] 2026-07-05: Phase 5: Self-update check via GitHub releases API (menu 20). Bumped version to 1.0.3. Renumbered Exit to 21. - [x] 2026-07-05: Phase 4: Created 4 reporting features (disk_health, driver_inventory, network_snapshot, export_report). Renumbered menu (Exit: 16→20). From f2002e515bf5bbd7c528ae49af99d9f7b3826bae Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:08:33 +0800 Subject: [PATCH 29/33] Update README.md with full 23-item menu, read-only mode, and 6-phase roadmap --- README.md | 101 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d01c0f5..cf2fc69 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,23 @@ + + + + + [![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] @@ -18,7 +35,7 @@

LDL Windows ToolBox

- A cohesive, menu-driven Windows utility that safely automates system cleanup, integrity repair, component updates, network reset, BitLocker decryption planning, browser AI cleanup, SSD TRIM, and low-latency configuration workflows. + A cohesive, menu-driven Windows utility for system cleanup, repair, network reset, performance tuning, security management, diagnostics, recovery, and reporting — all in a single Python-first toolbox.
Explore the docs »
@@ -49,6 +66,7 @@

  • Usage
  • +
  • Roadmap
  • Contributing
  • License
  • Contact
  • @@ -60,14 +78,17 @@ ## About The Project -The LDL Windows ToolBox is a Python-first Windows utility powered by `uv`, with `LDLWinToolBox.bat` acting as a thin launcher for `ldlwintoolbox.py`. It combines administrative privilege elevation, system cleanup, repair flows, network reset, BitLocker decryption planning, browser AI cleanup, SSD TRIM optimization, and low-latency configuration into a single cohesive menu-driven interface. +The LDL Windows ToolBox is a Python-first Windows utility powered by `uv`, with `LDLWinToolBox.bat` as a thin launcher for `ldlwintoolbox.py`. It combines administrative privilege elevation, system cleanup, repair flows, network reset, BitLocker planning, browser AI cleanup, SSD TRIM, low-latency configuration, recovery tools, diagnostics, and reporting into a single cohesive menu-driven interface. The project follows a modular architecture: -- `ldlwintoolbox.py` — thin entry point with admin logic and main menu dispatch -- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run/command/prompt helpers) +- `LDLWinToolBox.bat` — thin Batch launcher for the Python entry point +- `ldlwintoolbox.py` — entry point with admin detection, read-only mode, and main menu dispatch +- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run/command/prompt/restore-point helpers) - `features/` — one file per feature, each importing only from `toolbox_base` +- `config/exclusions.json` — user-managed exclusion list for cleanup operations +- `logs/` — structured timestamped session logs -All operations are safely logged with comprehensive timestamped records under `logs\LDLWinToolBox_yyMMddHHmmss.log`. The tool uses only Python standard library and built-in Windows commands; zero external dependencies are required. +The tool runs with zero external dependencies (Python standard library + built-in Windows commands). When launched without administrator privileges, it automatically enters **read-only mode**, hiding destructive features and allowing safe inspection of system information, diagnostics, and logs.

    (back to top)

    @@ -88,7 +109,7 @@ To get a local copy up and running follow these simple steps. ### Prerequisites - Windows 10 or Windows 11 -- Administrator rights (the tool automatically requests elevation via UAC if launched without them) +- Administrator rights (optional — the tool supports read-only mode without elevation; destructive features auto-request UAC if needed) - [uv](https://docs.astral.sh/uv/) (recommended) — the launcher falls back to `python` if uv is not available ### Installation @@ -108,39 +129,66 @@ To get a local copy up and running follow these simple steps. ## Usage -Upon launching, the interactive menu provides numbered options organized into logical groups: +Upon launching, the interactive menu provides numbered options organized into logical groups. When running without administrator privileges, the tool enters **read-only mode** — admin features (1–11) are hidden and a "[R] Restart as Administrator" shortcut is provided. -**System Cleanup** -- **[1] Advanced System Cleanup**: Deeply cleans temporary system data, prefetch, SoftwareDistribution downloads, vendor driver roots; calculates space freed (MB). +### System Cleanup (1–3) +- **[1] Advanced System Cleanup**: Deeply cleans temporary system/user data, Prefetch, SoftwareDistribution downloads; selective target sub-menu with exclusion list integration; calculates space freed (MB). - **[2] Windows Component Store Cleanup (WinSxS)**: Removes superseded Windows Update install files using DISM. -- **[3] Clear Event Viewer Logs**: Flushes system, security, and application logs via wevtutil. +- **[3] Clear Event Viewer Logs**: Flushes all Windows event logs via wevtutil. -**System Repair & Update** -- **[4] System Integrity Repair (SFC + DISM)**: Scans and repairs corrupt OS files with SFC and DISM RestoreHealth. +### System Repair & Update (4–5) +- **[4] System Integrity Repair (SFC + DISM)**: Scans and repairs corrupt OS files with SFC /scannow and DISM /RestoreHealth; shows [1/2] [2/2] progress hints. - **[5] Update All Installed Apps**: Silently updates all winget-installed applications. -**Network** -- **[6] Complete Network Reset**: Resets Winsock, TCP/IP stack, and DNS cache entirely. +### Network (6) +- **[6] Complete Network Reset**: Resets Winsock, TCP/IP stack, and DNS cache; requires restart. + +### Performance (7–8) +- **[7] Manual SSD TRIM**: Triggers manual SSD re-trim with `defrag /L /V` on a user-selected volume. +- **[8] Low Latency Mode (ViVeTool)**: Auto-detects CPU architecture (Intel/AMD x64 or Snapdragon ARM64), downloads ViVeTool, and manages Windows feature flags (IDs 58989092, 60716524, 61391826) with a query/enable/disable sub-menu. + +### Security & Privacy (9–10) +- **[9] Disable BitLocker (Plan)**: Shows status for all drives, validates selection, then starts `manage-bde -off` after `DISABLE` confirmation. +- **[10] Kill Browser AI**: Executes a remote PowerShell cleanup script to disable on-device browser AI features after `KILL` confirmation. -**Performance** -- **[7] Manual SSD TRIM**: Triggers manual SSD re-trim using the Windows defrag utility. -- **[8] Low Latency Mode (ViVeTool)**: Auto-detects CPU architecture (Intel/AMD or Snapdragon ARM64), downloads ViVeTool, and manages Windows low-latency feature flags (IDs 58989092, 60716524, 61391826) with query/enable/disable sub-menu. +### Recovery (11) +- **[11] Recovery & Safe Mode Tools**: Sub-menu for boot configuration (bcdedit), Safe Mode (minimal / networking / command prompt), WinRE status & enable/disable, and restore normal boot. -**Security & Privacy** -- **[9] Disable BitLocker (Plan)**: Shows BitLocker status, validates a selected drive, then starts decryption after typing `DISABLE`. -- **[10] Kill Browser AI**: Executes a configured remote PowerShell cleanup command to disable on-device browser AI features after typing `KILL`. +### Diagnostics (12–19) +- **[12] System Information**: OS edition/build, CPU name/logical cores, RAM usage, system drive usage, uptime via ctypes + winreg. +- **[13] Windows Update Status**: wuauserv service state, Auto Update registry config, last install/search dates, runs UsoClient scan. +- **[14] Defender Status & Quick Scan**: Get-MpComputerStatus fields, optional MpCmdRun signature update, optional Start-MpQuickScan. +- **[15] Service Health Check**: Status of 20 critical Windows services (wuauserv, BITS, EventLog, Dhcp, etc.). +- **[16] Disk Health & SMART Summary**: Get-PhysicalDisk with StorageReliabilityCounter (wear, temperature, errors), volume summary, SMART counters. +- **[17] Driver Inventory**: Parses `driverquery /FO CSV` with type/date summary. +- **[18] Network Snapshot**: Captures `ipconfig /all`, `route print`, `netsh interface`, `netstat -ano` to file and log; optional diff with previous snapshot. +- **[19] Export Logs & Report**: Generates a plain-text session report (features run, commands, warnings) and archives all logs to ZIP under `exports/`. -**Tools** -- **[11] View Log History**: Lists recent toolbox logs and opens the selected log with paged console viewing. -- **[12] Exit**: Closes the toolbox. +### Tools (20–22) +- **[20] View Log History**: Lists recent toolbox logs and opens the selected file with a paged console viewer. +- **[21] Check for Updates**: Queries the GitHub Releases API, compares with local version (1.0.3), optionally opens browser for download. +- **[22] Cleanup Exclusion List**: Manages a JSON-based exclusion list (`config/exclusions.json`); paths matching exclusions are skipped during cleanup. -Each run writes a structured log under `logs\` with a session header, environment summary, section markers, user cancellations, command start/end markers, and exit codes for key system commands. +Each run writes a structured log under `logs/` with a session header, environment summary, section markers, user cancellations, command start/end markers, and exit codes. Long-running or destructive operations display warnings and require explicit (Y/N) confirmation. Optional system restore points can be created before destructive features. _For AI maintenance context and persistent project rules, refer to [AGENTS.md](AGENTS.md), [MEMORY.md](MEMORY.md), and [memory/tasks.md](memory/tasks.md)._

    (back to top)

    -See the [open issues](https://github.com/LoveDoLove/LDLWinToolBox/issues) for a full list of proposed features (and known issues). + + +## Roadmap + +The project has completed all 6 planned development phases: + +- **Phase 1**: Foundation — shared helpers, input validation, confirmation flow, restore points +- **Phase 2**: Safety & Clarity — progress hints, conservative cleanup defaults +- **Phase 3**: New Feature Modules — system info, Windows Update, Defender, service health +- **Phase 4**: Reporting & Export — disk SMART, driver inventory, network snapshot, log export +- **Phase 5**: Efficiency & Maintenance — self-update check, read-only mode, PowerShell batching +- **Phase 6**: Advanced Features — recovery tools, selective cleanup, exclusion list + +Future improvements are tracked as [open issues](https://github.com/LoveDoLove/LDLWinToolBox/issues) and in [memory/feature-ideas.md](memory/feature-ideas.md).

    (back to top)

    @@ -193,10 +241,13 @@ Project Link: [https://github.com/LoveDoLove/LDLWinToolBox](https://github.com/L - [ViVeTool](https://github.com/thebookisclosed/ViVe) by thebookisclosed - [Windows UAC / ShellExecuteW](https://learn.microsoft.com/windows/win32/api/shellapi/nf-shellapi-shellexecutew) - [Winget Tool](https://docs.microsoft.com/en-us/windows/package-manager/winget/) +- [Python](https://www.python.org/) +- [uv](https://docs.astral.sh/uv/)

    (back to top)

    + [contributors-shield]: https://img.shields.io/github/contributors/LoveDoLove/LDLWinToolBox.svg?style=for-the-badge [contributors-url]: https://github.com/LoveDoLove/LDLWinToolBox/graphs/contributors From 12d873c15afa00abbd3df858787ec5fc83aac4e2 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:25:18 +0800 Subject: [PATCH 30/33] Production Version: ruff CI, PyInstaller packaging, ANSI UX, release workflow - Phase A: ruff lint/format/isort config in pyproject.toml, scripts/check.ps1, CI workflow - Phase B: Color class + cprint() + Spinner, colored menu with version display - Phase C: LDLWinToolBox.spec, release.yml auto-build on tag, TOOLBOX_VERSION from pyproject.toml - README.md rewritten using Best-README-Template format - Reviewer fixes: remove unused G variable, fix check.ps1 comment, clean up ANSI wrapping --- .github/workflows/ci.yml | 23 ++++++++ .github/workflows/release.yml | 31 ++++++++++ AGENTS.md | 46 ++++++++++++--- LDLWinToolBox.spec | 49 ++++++++++++++++ MEMORY.md | 32 +++++++++++ README.md | 41 +++++--------- ldlwintoolbox.py | 103 ++++++++++++++++++++-------------- memory/2026-07-05.md | 36 +++++++++++- memory/tasks.md | 17 +++++- pyproject.toml | 24 ++++++++ scripts/check.ps1 | 42 ++++++++++++++ toolbox_base.py | 70 ++++++++++++++++++++++- 12 files changed, 435 insertions(+), 79 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 LDLWinToolBox.spec create mode 100644 scripts/check.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4ba6261 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [main, lovedolove] + paths-ignore: ["**.md", "images/**", ".github/**"] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - uses: astral-sh/ruff-action@v3 + with: + args: format --check + - uses: astral-sh/ruff-action@v3 + with: + args: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8855de3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: write + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install PyInstaller + run: pip install pyinstaller + - name: Build EXE + run: pyinstaller LDLWinToolBox.spec --noconfirm + - name: Upload EXE artifact + uses: actions/upload-artifact@v4 + with: + name: LDLWinToolBox.exe + path: dist/LDLWinToolBox.exe + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: dist/LDLWinToolBox.exe + generate_release_notes: true diff --git a/AGENTS.md b/AGENTS.md index edf4866..834b3b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,15 +39,21 @@ On every new session: - Sanitize user input for every new menu feature that accepts values. - Keep existing documentation and analysis history intact. If `ANALYSIS.md` or `PROMPT_GUIDE.md` exists, append updates instead of replacing historical context. -## Project Architecture +## Project Architecture (Production) The project follows a modular file-per-feature architecture: -- `ldlwintoolbox.py` — thin entry point with admin logic and main menu dispatch +- `ldlwintoolbox.py` — thin entry point with admin logic and colored main menu dispatch - `LDLWinToolBox.bat` — thin launcher invoking `uv run -- python ldlwintoolbox.py` -- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run/command/prompt helpers) +- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, Color, cprint, Spinner, run/command/prompt helpers) - `features/` — one file per feature, each importing only from `toolbox_base` -- Zero external dependencies; all imports from Python stdlib +- `scripts/check.ps1` — unified ruff lint + format check runner +- `LDLWinToolBox.spec` — PyInstaller spec for EXE packaging +- `.github/workflows/ci.yml` — CI (ruff on push/PR) +- `.github/workflows/release.yml` — Release (PyInstaller build on tag) +- `README.md` — project documentation written using the `BLANK_README.md` (Best-README-Template) format, covering all 23 menu features, architecture, and production build info +- Zero external dependencies; all imports from Python stdlib; ANSI colors for UX +- `TOOLBOX_VERSION` read dynamically from `pyproject.toml` via `tomllib` ## Current Implemented Features @@ -76,11 +82,37 @@ The project follows a modular file-per-feature architecture: 9. Disable BitLocker in `features/bitlocker_disable.py` using `manage-bde -status`, drive validation, optional restore point, `DISABLE` confirmation, and guarded `manage-bde -off :`. 10. Kill Browser AI in `features/browser_ai_killer.py` using the configured remote PowerShell script. -### Tools (11) +### Recovery (11) -11. View Log History in `features/log_viewer.py` using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. +11. Recovery & Safe Mode Tools in `features/recovery_tools.py` with bcdedit boot config, safe mode (minimal/networking/cmd-prompt), WinRE status/enable/disable, restore normal boot. -12. Exit with Y/N confirmation. +### Diagnostics (12-19) + +12. System Information in `features/system_info.py` (OS, CPU, RAM, disk, uptime via ctypes+winreg). +13. Windows Update Status in `features/windows_update.py` (service state, registry config, UsoClient scan). +14. Defender Status & Quick Scan in `features/defender_tools.py` (Get-MpComputerStatus, MpCmdRun update, Start-MpQuickScan). +15. Service Health Check in `features/service_health.py` (20 critical services via Get-Service/sc query). +16. Disk Health & SMART Summary in `features/disk_health.py` (Get-PhysicalDisk + Get-StorageReliabilityCounter). +17. Driver Inventory in `features/driver_inventory.py` (driverquery /FO CSV parsing). +18. Network Snapshot in `features/network_snapshot.py` (ipconfig/route/netsh/netstat capture + diff). +19. Export Logs & Report in `features/export_report.py` (session report + log ZIP archive). + +### Tools (20-22) + +20. View Log History in `features/log_viewer.py` using a read-only paged console viewer for the newest `logs\LDLWinToolBox_*.log` files. +21. Check for Updates in `features/self_update.py` (GitHub releases API comparison). +22. Cleanup Exclusion List in `features/cleanup_config.py` (JSON-based exclusion manager). + +23. Exit with Y/N confirmation. + +### Production Version Additions +- `Color` class + `cprint()` for ANSI colored console output (zero deps) +- `Spinner` context manager for long-running task progress indication (thread-based, zero deps) +- `TOOLBOX_VERSION` dynamically read from `pyproject.toml` via `tomllib` +- `scripts/check.ps1` — unified ruff linter + formatter runner +- `LDLWinToolBox.spec` — PyInstaller spec for EXE packaging +- `.github/workflows/ci.yml` — CI workflow (ruff on push/PR) +- `.github/workflows/release.yml` — Release workflow (PyInstaller build on version tag) Remote script execution is high risk. Do not run this command during development. If it is implemented as a menu feature, add an explicit warning and confirmation before execution. diff --git a/LDLWinToolBox.spec b/LDLWinToolBox.spec new file mode 100644 index 0000000..7f62f0a --- /dev/null +++ b/LDLWinToolBox.spec @@ -0,0 +1,49 @@ +# -*- mode: python ; coding: utf-8 -*- +# PyInstaller spec for LDLWinToolBox + +import tomllib +from pathlib import Path + +here = Path(__file__).parent +pyproject = here / "pyproject.toml" +data = tomllib.loads(pyproject.read_text(encoding="utf-8")) +app_ver = data["project"]["version"] + +a = Analysis( + ["ldlwintoolbox.py"], + pathex=[], + binaries=[], + datas=[("features", "features"), ("pyproject.toml", ".")], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[ + "tkinter", "idlelib", "turtle", "test", "distutils", + "unittest", "pdb", "pyparsing", "matplotlib", + ], + noarchive=False, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name="LDLWinToolBox", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + version=app_ver, +) diff --git a/MEMORY.md b/MEMORY.md index d2d4c4b..6be6dea 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -169,6 +169,18 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - `BLANK_README.md` is present locally but ignored by git and appears to be an unused Best-README-Template source file. - No tracked `.agents/skills/` directory exists at the 2026-06-16 scan; any future repo-local skill installation must clone a public GitHub source and record provenance. +## New / Changed Files (Production Version) + +- `pyproject.toml` — added `[tool.ruff]` section for lint/format/isort +- `toolbox_base.py` — added `Color`, `cprint()`, `Spinner`, dynamic `TOOLBOX_VERSION` from pyproject.toml +- `ldlwintoolbox.py` — colored main menu with version display, box-drawing chars +- `scripts/check.ps1` — unified ruff lint + format + import check runner +- `.github/workflows/ci.yml` — CI workflow (Windows + ruff-action) +- `.github/workflows/release.yml` — Release workflow (PyInstaller build + GitHub Release) +- `LDLWinToolBox.spec` — PyInstaller spec for EXE packaging +- `README.md` — rewritten using `BLANK_README.md` (Best-README-Template) format, covering all 23 menu features, architecture, and production build info +- `memory/2026-07-05.md` — updated with Production Version work log + ## Persistent Working Rules - Preserve the app as a Python-first Windows utility with a thin Batch launcher unless the user explicitly asks for a different architecture. @@ -180,3 +192,23 @@ Treat the remote `iwr | iex` command as high risk. Do not execute it during anal - Keep prompt/history updates append-friendly and date-stamped. - Keep future enhancement ideas in `memory/feature-ideas.md` so they can be reread and prioritized later. - Treat the `Suggested Priority Order` section in `memory/feature-ideas.md` as the default implementation roadmap until the user asks to reorder it. + +## Production Version Plan (2026-07-05) + +Four-phase plan to move from feature-complete to production-ready. All phases completed. + +### Phase A: Code Hardening +- A1: Ruff lint + format + isort in `pyproject.toml` +- A2: Full type annotations in all `features/*.py` (already complete) +- A3: `scripts/check.ps1` — unified lint/format runner +- A4: `.github/workflows/ci.yml` — run check on every push/PR + +### Phase B: UX Polish +- B1: ANSI color constants (`Color` class) + `cprint()` in `toolbox_base.py`, zero dependencies +- B2: `Spinner` context manager for long-running tasks (thread-based, zero deps) +- B3: Colored menu with box-drawing chars, group headers, version display, dimmed log path + +### Phase C: Packaging & Release +- C1: PyInstaller `.spec` at project root +- C2: `.github/workflows/release.yml` — auto-build + release on version tag +- C3: Version sourced from `pyproject.toml` via `tomllib` at runtime (`TOOLBOX_VERSION`) diff --git a/README.md b/README.md index cf2fc69..7f10653 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@

    LDL Windows ToolBox

    - A cohesive, menu-driven Windows utility for system cleanup, repair, network reset, performance tuning, security management, diagnostics, recovery, and reporting — all in a single Python-first toolbox. + A cohesive, menu-driven Windows utility for system cleanup, repair, network reset, performance tuning, security management, diagnostics, recovery, and reporting.
    Explore the docs »
    @@ -66,7 +66,6 @@

  • Usage
  • -
  • Roadmap
  • Contributing
  • License
  • Contact
  • @@ -78,17 +77,22 @@ ## About The Project -The LDL Windows ToolBox is a Python-first Windows utility powered by `uv`, with `LDLWinToolBox.bat` as a thin launcher for `ldlwintoolbox.py`. It combines administrative privilege elevation, system cleanup, repair flows, network reset, BitLocker planning, browser AI cleanup, SSD TRIM, low-latency configuration, recovery tools, diagnostics, and reporting into a single cohesive menu-driven interface. +The LDL Windows ToolBox is a Python-first Windows utility that combines administrative privilege elevation, system cleanup, repair flows, network reset, BitLocker management, browser AI cleanup, SSD TRIM, low-latency configuration, recovery tools, diagnostics, and reporting into a single cohesive menu-driven interface. -The project follows a modular architecture: -- `LDLWinToolBox.bat` — thin Batch launcher for the Python entry point -- `ldlwintoolbox.py` — entry point with admin detection, read-only mode, and main menu dispatch -- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run/command/prompt/restore-point helpers) +The project follows a modular file-per-feature architecture: + +- `LDLWinToolBox.bat` — thin Batch launcher invoking `uv run -- python ldlwintoolbox.py` +- `ldlwintoolbox.py` — entry point with admin detection, read-only mode, and colored main menu dispatch +- `toolbox_base.py` — shared infrastructure (Logger, CommandResult, ANSI Color, Spinner, run/command/prompt helpers) - `features/` — one file per feature, each importing only from `toolbox_base` - `config/exclusions.json` — user-managed exclusion list for cleanup operations - `logs/` — structured timestamped session logs +- `scripts/check.ps1` — ruff lint + format check runner +- `LDLWinToolBox.spec` — PyInstaller spec for EXE packaging +- `.github/workflows/ci.yml` — CI workflow (ruff on push/PR) +- `.github/workflows/release.yml` — Release workflow (PyInstaller build on version tag) -The tool runs with zero external dependencies (Python standard library + built-in Windows commands). When launched without administrator privileges, it automatically enters **read-only mode**, hiding destructive features and allowing safe inspection of system information, diagnostics, and logs. +The tool runs with zero external dependencies (Python standard library + built-in Windows commands + ANSI escape codes). When launched without administrator privileges, it automatically enters **read-only mode**, hiding destructive features and allowing safe inspection of system information, diagnostics, and logs.

    (back to top)

    @@ -137,7 +141,7 @@ Upon launching, the interactive menu provides numbered options organized into lo - **[3] Clear Event Viewer Logs**: Flushes all Windows event logs via wevtutil. ### System Repair & Update (4–5) -- **[4] System Integrity Repair (SFC + DISM)**: Scans and repairs corrupt OS files with SFC /scannow and DISM /RestoreHealth; shows [1/2] [2/2] progress hints. +- **[4] System Integrity Repair (SFC + DISM)**: Scans and repairs corrupt OS files with SFC /scannow and DISM /RestoreHealth. - **[5] Update All Installed Apps**: Silently updates all winget-installed applications. ### Network (6) @@ -166,29 +170,14 @@ Upon launching, the interactive menu provides numbered options organized into lo ### Tools (20–22) - **[20] View Log History**: Lists recent toolbox logs and opens the selected file with a paged console viewer. -- **[21] Check for Updates**: Queries the GitHub Releases API, compares with local version (1.0.3), optionally opens browser for download. +- **[21] Check for Updates**: Queries the GitHub Releases API, compares with local version, optionally opens browser for download. - **[22] Cleanup Exclusion List**: Manages a JSON-based exclusion list (`config/exclusions.json`); paths matching exclusions are skipped during cleanup. Each run writes a structured log under `logs/` with a session header, environment summary, section markers, user cancellations, command start/end markers, and exit codes. Long-running or destructive operations display warnings and require explicit (Y/N) confirmation. Optional system restore points can be created before destructive features. -_For AI maintenance context and persistent project rules, refer to [AGENTS.md](AGENTS.md), [MEMORY.md](MEMORY.md), and [memory/tasks.md](memory/tasks.md)._ -

    (back to top)

    - - -## Roadmap - -The project has completed all 6 planned development phases: - -- **Phase 1**: Foundation — shared helpers, input validation, confirmation flow, restore points -- **Phase 2**: Safety & Clarity — progress hints, conservative cleanup defaults -- **Phase 3**: New Feature Modules — system info, Windows Update, Defender, service health -- **Phase 4**: Reporting & Export — disk SMART, driver inventory, network snapshot, log export -- **Phase 5**: Efficiency & Maintenance — self-update check, read-only mode, PowerShell batching -- **Phase 6**: Advanced Features — recovery tools, selective cleanup, exclusion list - -Future improvements are tracked as [open issues](https://github.com/LoveDoLove/LDLWinToolBox/issues) and in [memory/feature-ideas.md](memory/feature-ideas.md). +See the [open issues](https://github.com/LoveDoLove/LDLWinToolBox/issues) for a full list of proposed features (and known issues).

    (back to top)

    diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index 2540120..3d1c6ad 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -8,8 +8,11 @@ from pathlib import Path from toolbox_base import ( + Color, Logger, + TOOLBOX_VERSION, clear_screen, + cprint, get_log_dir, prompt_yes_no, write_session_header, @@ -56,54 +59,70 @@ def relaunch_as_admin() -> None: ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, params, None, 1) +def _print_header(is_admin_user: bool) -> None: + B = Color.BOLD + C = Color.CYAN + Y = Color.YELLOW + cprint("─" * 47, Color.DIM) + cprint(" ⚙ LDL Windows ToolBox", B, C) + cprint(f" v{TOOLBOX_VERSION}", Color.DIM) + if not is_admin_user: + cprint(" ★ READ-ONLY MODE ★", B, Y) + cprint("─" * 47, Color.DIM) + + +def _print_section(title: str) -> None: + cprint(f" ── {title} ──", Color.BOLD, Color.GREEN) + + +def _print_item(key: str, desc: str) -> None: + cprint(f" [{key}] {desc}", Color.WHITE) + + def main_menu(logger: Logger, log_dir: Path, script_dir: Path, is_admin_user: bool) -> None: while True: clear_screen() - print("===============================================") - print(" LDL Windows ToolBox") - if not is_admin_user: - print(" *** READ-ONLY MODE ***") - print("===============================================") + _print_header(is_admin_user) if is_admin_user: - print(" ── System Cleanup ──") - print("[1] Advanced System Cleanup") - print("[2] Windows Component Store Cleanup (WinSxS)") - print("[3] Clear Event Viewer Logs") - print(" ── System Repair & Update ──") - print("[4] System Integrity Repair (SFC + DISM)") - print("[5] Update All Installed Apps (Winget)") - print(" ── Network ──") - print("[6] Complete Network Reset") - print(" ── Performance ──") - print("[7] Manual SSD TRIM") - print("[8] Low Latency Mode (ViVeTool)") - print(" ── Security & Privacy ──") - print("[9] Disable BitLocker (Plan)") - print("[10] Kill Browser AI") - print(" ── Recovery ──") - print("[11] Recovery & Safe Mode Tools") + _print_section("System Cleanup") + _print_item("1", "Advanced System Cleanup") + _print_item("2", "Windows Component Store Cleanup (WinSxS)") + _print_item("3", "Clear Event Viewer Logs") + _print_section("System Repair & Update") + _print_item("4", "System Integrity Repair (SFC + DISM)") + _print_item("5", "Update All Installed Apps (Winget)") + _print_section("Network") + _print_item("6", "Complete Network Reset") + _print_section("Performance") + _print_item("7", "Manual SSD TRIM") + _print_item("8", "Low Latency Mode (ViVeTool)") + _print_section("Security & Privacy") + _print_item("9", "Disable BitLocker (Plan)") + _print_item("10", "Kill Browser AI") + _print_section("Recovery") + _print_item("11", "Recovery & Safe Mode Tools") else: - print(" (Admin features hidden. Press [R] to restart as admin.)") - print(" ── Diagnostics ──") - print("[12] System Information") - print("[13] Windows Update Status") - print("[14] Defender Status & Quick Scan") - print("[15] Service Health Check") - print("[16] Disk Health & SMART Summary") - print("[17] Driver Inventory") - print("[18] Network Snapshot") - print("[19] Export Logs & Report") - print(" ── Tools ──") - print("[20] View Log History") - print("[21] Check for Updates") - print("[22] Cleanup Exclusion List") - print("───────────────────────────────────────────────") + cprint(" (Admin features hidden. Press [R] to restart as admin.)", Color.DIM) + _print_section("Diagnostics") + _print_item("12", "System Information") + _print_item("13", "Windows Update Status") + _print_item("14", "Defender Status & Quick Scan") + _print_item("15", "Service Health Check") + _print_item("16", "Disk Health & SMART Summary") + _print_item("17", "Driver Inventory") + _print_item("18", "Network Snapshot") + _print_item("19", "Export Logs & Report") + _print_section("Tools") + _print_item("20", "View Log History") + _print_item("21", "Check for Updates") + _print_item("22", "Cleanup Exclusion List") + cprint(Color.DIM + "───────────────────────────────────────────────" + Color.RESET) if not is_admin_user: - print("[R] Restart as Administrator") - print("[23] Exit") - print("===============================================") - print(f"Log: {logger.logfile}") - print("===============================================") + cprint(" [R] Restart as Administrator", Color.YELLOW) + cprint(" [23] Exit", Color.RED) + cprint(Color.DIM + "═" * 47 + Color.RESET) + cprint(f" Log: {logger.logfile}", Color.DIM) + cprint(Color.DIM + "═" * 47 + Color.RESET) choice = input("Select an option: ").strip() logger.log_only("INFO", f"Menu selection: {choice}") diff --git a/memory/2026-07-05.md b/memory/2026-07-05.md index d67ee26..7dc2bc0 100644 --- a/memory/2026-07-05.md +++ b/memory/2026-07-05.md @@ -1,6 +1,6 @@ # 2026-07-05 -## Work Log +## Work Log (Morning) - Refactored monolithic `ldlwintoolbox.py` into modular architecture: - `toolbox_base.py` — shared infrastructure (Logger, CommandResult, run helpers, prompt utils) @@ -29,3 +29,37 @@ - Performance (7-8), Security & Privacy (9-10), Tools (11), Exit (12) - Updated `main_menu()` in `ldlwintoolbox.py` with section headers and regrouped options - Updated `AGENTS.md`, `MEMORY.md`, `memory/tasks.md` to reflect new menu order + +--- + +## Production Version — 2026-07-05 + +Implemented the Production Version plan covering 4 phases: + +### Phase A: Code Hardening +- A1: Added Ruff config (lint + format + isort) to `pyproject.toml` +- A2: All feature files already had complete type annotations — verified, no changes needed +- A3: Created `scripts/check.ps1` — unified ruff lint/format/import check runner +- A4: Created `.github/workflows/ci.yml` — runs ruff on every push/PR + +### Phase B: UX Polish +- B1: Added `Color` class (ANSI escape constants), `cprint()` function in `toolbox_base.py` +- B2: Added `Spinner` context manager for long-running tasks (thread-based, stdlib only) +- B3: Redesigned `ldlwintoolbox.py` main menu with: + - Colored header (Cyan title, bold) + - Version display from pyproject.toml + - Green section headers with box-drawing chars + - White menu items, Dim separators, Red exit button + - Read-only mode highlighted in Yellow + +### Phase C: Packaging & Release +- C1: Created `LDLWinToolBox.spec` for PyInstaller packaging +- C2: Created `.github/workflows/release.yml` — auto-build on tag push +- C3: Version now sourced from `pyproject.toml` via `tomllib` at import time + +### Key Design Decisions +- `Color.cprint()` uses ANSI escape codes only — zero external dependencies +- `Spinner` uses stdlib `threading` + `time` — zero external dependencies +- Version reads from `pyproject.toml` at load time, falls back to `0.0.0` +- PyInstaller spec includes `features/` directory and `pyproject.toml` +- CI and Release workflows are GitHub Actions native with `ruff-action` diff --git a/memory/tasks.md b/memory/tasks.md index e098c6c..2b65c29 100644 --- a/memory/tasks.md +++ b/memory/tasks.md @@ -2,12 +2,25 @@ Last updated: 2026-07-05 -## Pending +## Pending (Production Version) -- [ ] Phase 2+: System information summary, progress hints, Windows Update check, Defender check, service health, SMART summary, driver inventory, network snapshot, reduced PS calls, export report, self-update, selective cleanup, safe mode, read-only mode +- [ ] Phase C1: PyInstaller spec 打包腳本 +- [ ] Phase C2: GitHub Release workflow (自動 build exe + upload) +- [ ] Phase C3: 版本號統一從 pyproject.toml 讀取 ## Completed +- [x] 2026-07-05: Phase B3: 選單美化 + ANSI 彩色 + box-drawing + 版本號顯示 +- [x] 2026-07-05: Phase B2: 自製進度指示器 (spinner for long tasks) +- [x] 2026-07-05: Phase B1: ANSI 彩色輸出基礎設施 (toolbox_base.py 色彩常數 + cprint) +- [x] 2026-07-05: Phase A4: 建立 GitHub Actions CI workflow (.github/workflows/ci.yml) +- [x] 2026-07-05: Phase A3: 建立 scripts/check.ps1 統一檢查腳本 +- [x] 2026-07-05: Phase A2: 補全所有 features/*.py 型別註解 +- [x] 2026-07-05: Phase A1: pyproject.toml 加入 Ruff 設定 (lint + format + isort) +- [x] 2026-07-05: 寫入生產版本 (Production Version) 計劃到 MEMORY.md 與 tasks.md + +## Completed (Pre-Production) + - [x] 2026-07-05: Phase 5 complete: Self-update check, read-only mode (diagnostics without admin), PowerShell calls batched (disk_health: 3→1). - [x] 2026-07-05: Phase 6: Recovery tools (bcdedit safe mode/WinRE), selective cleanup sub-menu, exclusion list manager (config/exclusions.json). Menu renumbered Exit to 23. - [x] 2026-07-05: Phase 5: Self-update check via GitHub releases API (menu 20). Bumped version to 1.0.3. Renumbered Exit to 21. diff --git a/pyproject.toml b/pyproject.toml index d5ec478..8b4c52d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,27 @@ dependencies = [] [tool.uv] package = false + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "YTT", # flake8-2020 + "RUF", # ruff-specific +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" diff --git a/scripts/check.ps1 b/scripts/check.ps1 new file mode 100644 index 0000000..f0d5737 --- /dev/null +++ b/scripts/check.ps1 @@ -0,0 +1,42 @@ +# LDLWinToolBox -- unified lint & format check +# Requires: uv +# Usage: .\scripts\check.ps1 [-Fix] + +param( + [switch]$Fix = $false +) + +$Root = Split-Path -Parent $PSScriptRoot +Set-Location $Root +$global:ExitCode = 0 + +Write-Host "=== ruff format check ===" -ForegroundColor Cyan +if ($Fix) { + uv run -- ruff format . +} else { + uv run -- ruff format --check . +} +if (-not $?) { $global:ExitCode = 1 } + +Write-Host "=== ruff lint check ===" -ForegroundColor Cyan +if ($Fix) { + uv run -- ruff check --fix . +} else { + uv run -- ruff check . +} +if (-not $?) { $global:ExitCode = 1 } + +Write-Host "=== ruff isort check ===" -ForegroundColor Cyan +if ($Fix) { + uv run -- ruff check --select I --fix . +} else { + uv run -- ruff check --select I . +} +if (-not $?) { $global:ExitCode = 1 } + +if ($global:ExitCode) { + Write-Host "FAILED - run with -Fix to auto-fix" -ForegroundColor Red +} else { + Write-Host "ALL CHECKS PASSED" -ForegroundColor Green +} +exit $global:ExitCode diff --git a/toolbox_base.py b/toolbox_base.py index 7e7535b..c779d8f 100644 --- a/toolbox_base.py +++ b/toolbox_base.py @@ -6,13 +6,81 @@ import subprocess import sys import tempfile +import threading +import time from dataclasses import dataclass from datetime import datetime from pathlib import Path MENU_LOGO = "=" * 47 -TOOLBOX_VERSION = "1.0.3" + +class Color: + RESET = "\033[0m" + BOLD = "\033[1m" + DIM = "\033[2m" + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + MAGENTA = "\033[95m" + CYAN = "\033[96m" + WHITE = "\033[97m" + HEADER = "\033[105m\033[97m" + + +def cprint(text: str, *styles: str, indent: int = 0) -> None: + prefix = " " * indent + joined = "".join(styles) + if joined: + print(f"{prefix}{joined}{text}{Color.RESET}") + else: + print(f"{prefix}{text}") + + +class Spinner: + def __init__(self, message: str = "", delay: float = 0.15) -> None: + self._message = message + self._delay = delay + self._running = False + self._thread: threading.Thread | None = None + + def __enter__(self) -> Spinner: + self._running = True + self._thread = threading.Thread(target=self._spin, daemon=True) + self._thread.start() + return self + + def __exit__(self, *exc: object) -> None: + self._running = False + if self._thread: + self._thread.join() + sys.stdout.write("\r" + " " * (len(self._message) + 4) + "\r") + sys.stdout.flush() + + def _spin(self) -> None: + chars = "|/-\\" + i = 0 + while self._running: + sys.stdout.write(f"\r{chars[i % len(chars)]} {self._message}") + sys.stdout.flush() + time.sleep(self._delay) + i += 1 + + +def _read_version() -> str: + try: + import tomllib + pyproject = Path(__file__).resolve().parent / "pyproject.toml" + if pyproject.exists(): + data = tomllib.loads(pyproject.read_text(encoding="utf-8")) + return data.get("project", {}).get("version", "0.0.0") + except Exception: + pass + return "0.0.0" + + +TOOLBOX_VERSION: str = _read_version() @dataclass(slots=True) From 47c768d7ebe032db2993f028b9eb65c1122a2ca7 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:29:05 +0800 Subject: [PATCH 31/33] CI fix: ruff format + lint compliance - Run uff format on all 22 files to match ruff 0.15.20 formatting - Fix E501 long lines in disk_health.py, system_info.py, windows_update.py, toolbox_base.py - Fix E741 ambiguous name l -> line in network_snapshot.py - Fix F841 unused header/ ows in driver_inventory.py - Fix N806 uppercase locals in ldlwintoolbox.py (_print_header) --- features/bitlocker_disable.py | 12 ++----- features/browser_ai_killer.py | 5 +-- features/cleanup_config.py | 7 ++-- features/defender_tools.py | 4 +-- features/disk_health.py | 42 +++++++++++++++++------- features/driver_inventory.py | 6 ++-- features/event_log_clear.py | 4 +-- features/log_viewer.py | 20 +++--------- features/low_latency_mode.py | 61 ++++++++--------------------------- features/network_reset.py | 8 ++--- features/network_snapshot.py | 28 +++++++++------- features/recovery_tools.py | 16 ++++++--- features/self_update.py | 3 +- features/service_health.py | 5 +-- features/ssd_trim.py | 9 ++---- features/system_cleanup.py | 24 +++++++++++--- features/system_info.py | 48 ++++++++++++++++----------- features/system_repair.py | 4 +-- features/windows_update.py | 10 +++--- features/winget_upgrade.py | 4 +-- ldlwintoolbox.py | 44 ++++++++++++++++--------- toolbox_base.py | 16 ++++----- 22 files changed, 189 insertions(+), 191 deletions(-) diff --git a/features/bitlocker_disable.py b/features/bitlocker_disable.py index 35f8e79..3fbd2c1 100644 --- a/features/bitlocker_disable.py +++ b/features/bitlocker_disable.py @@ -34,9 +34,7 @@ def bitlocker_disable(logger: Logger) -> None: return print("Current BitLocker status:") logger.log_only("INFO", "Current BitLocker status:") - status_result = run_and_log( - logger, ["manage-bde", "-status"], "manage-bde -status" - ) + status_result = run_and_log(logger, ["manage-bde", "-status"], "manage-bde -status") if status_result.stdout: print( status_result.stdout, @@ -52,16 +50,12 @@ def bitlocker_disable(logger: Logger) -> None: if drive is None: return if drive == "": - logger.log( - "ERROR", "No valid drive was selected for Disable BitLocker." - ) + logger.log("ERROR", "No valid drive was selected for Disable BitLocker.") input("Press Enter to continue...") return logger.log_only("INFO", f"Selected BitLocker drive: {drive}:") print("\nSelected drive status:") - logger.log_only( - "INFO", f"Selected BitLocker drive status for {drive}:" - ) + logger.log_only("INFO", f"Selected BitLocker drive status for {drive}:") status_result = run_and_log( logger, ["manage-bde", "-status", f"{drive}:"], diff --git a/features/browser_ai_killer.py b/features/browser_ai_killer.py index 243fa7a..4a172fd 100644 --- a/features/browser_ai_killer.py +++ b/features/browser_ai_killer.py @@ -9,7 +9,6 @@ run_and_log, ) - GIST_URL = "https://gist.githubusercontent.com/raw/d08347a1f1083e4e3d29daf17f86223c/kill_ai.ps1" @@ -29,9 +28,7 @@ def kill_browser_ai(logger: Logger) -> None: print() logger.section("Kill Browser AI") logger.log_only("WARN", f"Remote script source: {GIST_URL}") - if not prompt_keyword( - logger, "Type KILL to run Kill Browser AI: ", "KILL", "Kill Browser AI" - ): + if not prompt_keyword(logger, "Type KILL to run Kill Browser AI: ", "KILL", "Kill Browser AI"): return if not command_exists("powershell"): logger.log( diff --git a/features/cleanup_config.py b/features/cleanup_config.py index 83158c1..d195d67 100644 --- a/features/cleanup_config.py +++ b/features/cleanup_config.py @@ -6,7 +6,6 @@ from toolbox_base import MENU_LOGO, Logger, clear_screen - _CONFIG_DIR: str | None = None @@ -112,7 +111,10 @@ def cleanup_config(logger: Logger) -> None: input("Press Enter to continue...") continue resolved = _path_resolve(path) - if any(e.lower() == path.lower() or _path_resolve(e).lower() == resolved.lower() for e in exclusions): + if any( + e.lower() == path.lower() or _path_resolve(e).lower() == resolved.lower() + for e in exclusions + ): print(f"'{path}' is already in the exclusion list.") input("Press Enter to continue...") continue @@ -154,6 +156,7 @@ def cleanup_config(logger: Logger) -> None: continue logger.section("Clear All Exclusions") from toolbox_base import prompt_yes_no + if prompt_yes_no(logger, "Clear all exclusions? (Y/N): ", "Clear Exclusions"): if _save_exclusions([]): logger.log("INFO", "All exclusions cleared.") diff --git a/features/defender_tools.py b/features/defender_tools.py index fbad3bc..4d4049b 100644 --- a/features/defender_tools.py +++ b/features/defender_tools.py @@ -1,7 +1,5 @@ from __future__ import annotations -import subprocess - from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, prompt_yes_no, run_command @@ -50,7 +48,7 @@ def _show_defender_status(logger: Logger) -> None: "Last Full Scan Source", ] print(f" {'Status Field':<30} {'Value':<20}") - print(f" {'-'*30} {'-'*20}") + print(f" {'-' * 30} {'-' * 20}") for field, val in zip(fields, lines): display = val if val else "N/A" print(f" {field:<30} {display:<20}") diff --git a/features/disk_health.py b/features/disk_health.py index 99d41ba..1105af8 100644 --- a/features/disk_health.py +++ b/features/disk_health.py @@ -45,7 +45,8 @@ def disk_health(logger: Logger) -> None: "if(-not $d){Write-Output 'NO_DATA'}\n" "# VOLUMES\n" "Get-Volume | Where-Object {$_.DriveType -eq 'Fixed' -and $_.DriveLetter} " - "| ForEach-Object {Write-Output ($_.DriveLetter+':|'+$_.FileSystem+'|'+$_.HealthStatus+'|'+$_.SizeRemaining+'|'+$_.Size)}\n" + "| ForEach-Object {Write-Output ($_.DriveLetter+':|'+$_.FileSystem+'|'+" + "$_.HealthStatus+'|'+$_.SizeRemaining+'|'+$_.Size)}\n" "# SMART\n" "Get-PhysicalDisk | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue " "| ForEach-Object {Write-Output ($_.DeviceId+'|'+$_.Temperature+'|'+" @@ -62,13 +63,15 @@ def disk_health(logger: Logger) -> None: ps_fb = ( "Get-PSDrive -PSProvider FileSystem " "| Where-Object {$_.Root -match '^[A-Z]:\\\\$'} " - "| ForEach-Object {Write-Output ($_.Root+'|'+[math]::Round($_.Used/1GB,1).ToString()+'/'+" - "[math]::Round(($_.Used+$_.Free)/1GB,1).ToString()+'GB|'+[math]::Round($_.Free/1GB,1).ToString()+'GB')}" + "| ForEach-Object {Write-Output ($_.Root+'|'+" + "[math]::Round($_.Used/1GB,1).ToString()+'/'+" + "[math]::Round(($_.Used+$_.Free)/1GB,1).ToString()+'GB|'+" + "[math]::Round($_.Free/1GB,1).ToString()+'GB')}" ) fb_raw = _run_ps(ps_fb) if fb_raw: print(f" {'Drive':<8} {'Used/Total':<22} {'Free':<10}") - print(f" {'-'*8} {'-'*22} {'-'*10}") + print(f" {'-' * 8} {'-' * 22} {'-' * 10}") for line in fb_raw.splitlines(): parts = line.strip().split("|") if len(parts) >= 3: @@ -100,13 +103,22 @@ def disk_health(logger: Logger) -> None: return if disk_lines: - print(f" {'Name':<30} {'Type':<12} {'Health':<12} {'Size':<10} {'Status':<14} {'Bus':<10} {'Temp':<8} {'Wear':<8} {'ReadErr':<8} {'WriteErr':<8}") - print(f" {'-'*30} {'-'*12} {'-'*12} {'-'*10} {'-'*14} {'-'*10} {'-'*8} {'-'*8} {'-'*8} {'-'*8}") + print( + f" {'Name':<30} {'Type':<12} {'Health':<12} {'Size':<10} {'Status':<14} " + f"{'Bus':<10} {'Temp':<8} {'Wear':<8} {'ReadErr':<8} {'WriteErr':<8}" + ) + print( + f" {'-' * 30} {'-' * 12} {'-' * 12} {'-' * 10} {'-' * 14} " + f"{'-' * 10} {'-' * 8} {'-' * 8} {'-' * 8} {'-' * 8}" + ) for line in disk_lines: parts = line.split("|") if len(parts) >= 10: name, media, health, size, op_status, bus, temp, wear, re, we = parts[:10] - print(f" {name:<30} {media:<12} {health:<12} {size:<10} {op_status:<14} {bus:<10} {temp:<8} {wear:<8} {re:<8} {we:<8}") + print( + f" {name:<30} {media:<12} {health:<12} {size:<10} {op_status:<14} " + f"{bus:<10} {temp:<8} {wear:<8} {re:<8} {we:<8}" + ) logger.log_only("INFO", f"Disk: {name} health={health} wear={wear} temp={temp}") vol_lines = sections.get("VOLUMES", []) @@ -114,7 +126,7 @@ def disk_health(logger: Logger) -> None: print() logger.section("Volume Summary") print(f" {'Volume':<8} {'FS':<8} {'Health':<12} {'Free':<12} {'Total':<12}") - print(f" {'-'*8} {'-'*8} {'-'*12} {'-'*12} {'-'*12}") + print(f" {'-' * 8} {'-' * 8} {'-' * 12} {'-' * 12} {'-' * 12}") for line in vol_lines: parts = line.split("|") if len(parts) >= 5: @@ -130,13 +142,21 @@ def disk_health(logger: Logger) -> None: if smart_lines: print() logger.section("SMART Reliability Counters") - print(f" {'Disk#':<8} {'Temp(C)':<10} {'Wear%':<8} {'ReadErr':<10} {'WriteErr':<10} {'RdLat(ms)':<12} {'WrLat(ms)':<12} {'FlLat(ms)':<12}") - print(f" {'-'*8} {'-'*10} {'-'*8} {'-'*10} {'-'*10} {'-'*12} {'-'*12} {'-'*12}") + print( + f" {'Disk#':<8} {'Temp(C)':<10} {'Wear%':<8} {'ReadErr':<10} " + f"{'WriteErr':<10} {'RdLat(ms)':<12} {'WrLat(ms)':<12} {'FlLat(ms)':<12}" + ) + print( + f" {'-' * 8} {'-' * 10} {'-' * 8} {'-' * 10} {'-' * 10} " + f"{'-' * 12} {'-' * 12} {'-' * 12}" + ) for line in smart_lines: parts = line.split("|") if len(parts) >= 8: did, temp, wear, re, we, rl, wl, fl = parts[:8] - print(f" {did:<8} {temp:<10} {wear:<8} {re:<10} {we:<10} {rl:<12} {wl:<12} {fl:<12}") + print( + f" {did:<8} {temp:<10} {wear:<8} {re:<10} {we:<10} {rl:<12} {wl:<12} {fl:<12}" + ) logger.log_only("INFO", "DISK HEALTH CHECK COMPLETE") input("Press Enter to continue...") diff --git a/features/driver_inventory.py b/features/driver_inventory.py index 85b9ddc..02e1155 100644 --- a/features/driver_inventory.py +++ b/features/driver_inventory.py @@ -31,11 +31,9 @@ def driver_inventory(logger: Logger) -> None: input("Press Enter to continue...") return - header = lines[0] - rows = lines[1:] - import csv import io + reader = csv.reader(io.StringIO(result.stdout)) all_rows = list(reader) if len(all_rows) < 2: @@ -61,7 +59,7 @@ def driver_inventory(logger: Logger) -> None: print(f" Total drivers: {total}") print() print(f" {'Driver Name':<35} {'Type':<18} {'Date':<20}") - print(f" {'-'*35} {'-'*18} {'-'*20}") + print(f" {'-' * 35} {'-' * 18} {'-' * 20}") for row in data_rows: name = row[name_idx] if len(row) > name_idx else "?" diff --git a/features/event_log_clear.py b/features/event_log_clear.py index 0480b54..14dca79 100644 --- a/features/event_log_clear.py +++ b/features/event_log_clear.py @@ -43,8 +43,6 @@ def event_logs(logger: Logger) -> None: logs = [line.strip() for line in result.stdout.splitlines() if line.strip()] for entry in logs: logger.log("INFO", f"- Clearing log: {entry}") - run_and_log( - logger, ["wevtutil", "cl", entry], f"wevtutil.exe cl {entry}" - ) + run_and_log(logger, ["wevtutil", "cl", entry], f"wevtutil.exe cl {entry}") logger.log("INFO", "EVENT LOGS CLEARED") input("Press Enter to continue...") diff --git a/features/log_viewer.py b/features/log_viewer.py index 864a6df..aeac142 100644 --- a/features/log_viewer.py +++ b/features/log_viewer.py @@ -18,9 +18,7 @@ def list_log_history(log_dir: Path, logger: Logger) -> list[Path]: def paginate_log_file(path: Path) -> None: try: - lines = path.read_text( - encoding="utf-8", errors="replace" - ).splitlines() + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() except OSError as exc: print(f"Unable to open log file: {exc}") input("Press Enter to continue...") @@ -58,9 +56,7 @@ def paginate_log_file(path: Path) -> None: input("End of log. Press Enter to return to the menu...") return - choice = ( - input("Press Enter for more, [B]ack, or [Q]uit: ").strip().upper() - ) + choice = input("Press Enter for more, [B]ack, or [Q]uit: ").strip().upper() if choice == "Q": return if choice == "B": @@ -84,12 +80,8 @@ def log_history(logger: Logger, log_dir: Path) -> None: return for idx, path in enumerate(logs, start=1): stat = path.stat() - ts = datetime.fromtimestamp(stat.st_mtime).strftime( - "%Y-%m-%d %H:%M" - ) - print( - f"[{idx}] {path.name} - {stat.st_size} bytes - {ts}" - ) + ts = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M") + print(f"[{idx}] {path.name} - {stat.st_size} bytes - {ts}") print() print("[0] Return to Menu") choice = input("Press 0 to return, or 1-9 to view a log: ").strip() @@ -113,8 +105,6 @@ def log_history(logger: Logger, log_dir: Path) -> None: print(MENU_LOGO) print(f"Path: {selected}") print(MENU_LOGO) - logger.log_only( - "INFO", f"Viewing log history file: {selected.name}" - ) + logger.log_only("INFO", f"Viewing log history file: {selected.name}") paginate_log_file(selected) logger.log("INFO", "View Log History returned to menu.") diff --git a/features/low_latency_mode.py b/features/low_latency_mode.py index d3b8224..c72abb2 100644 --- a/features/low_latency_mode.py +++ b/features/low_latency_mode.py @@ -16,11 +16,8 @@ run_and_log, ) - VIVE_REPO = "thebookisclosed/ViVe" -VIVE_TOOLS_DIR = ( - Path(__file__).resolve().parent.parent / "tools" / "vivetool" -) +VIVE_TOOLS_DIR = Path(__file__).resolve().parent.parent / "tools" / "vivetool" LOW_LATENCY_IDS = ["58989092", "60716524", "61391826"] LOW_LATENCY_DESC = { "58989092": "Core Low Latency Profile", @@ -35,9 +32,7 @@ def detect_architecture() -> str: def _fetch_json(url: str) -> dict | None: - req = urllib.request.Request( - url, headers={"User-Agent": "LDLWinToolBox/1.0"} - ) + req = urllib.request.Request(url, headers={"User-Agent": "LDLWinToolBox/1.0"}) try: with urllib.request.urlopen(req, timeout=15) as resp: return json.loads(resp.read().decode("utf-8")) @@ -51,9 +46,7 @@ def _fetch_json(url: str) -> dict | None: def _download_file(url: str, dest: Path) -> bool: - req = urllib.request.Request( - url, headers={"User-Agent": "LDLWinToolBox/1.0"} - ) + req = urllib.request.Request(url, headers={"User-Agent": "LDLWinToolBox/1.0"}) try: with urllib.request.urlopen(req, timeout=30) as resp: with dest.open("wb") as f: @@ -67,38 +60,26 @@ def ensure_vivetool(logger: Logger) -> Path | None: VIVE_TOOLS_DIR.mkdir(parents=True, exist_ok=True) exe = VIVE_TOOLS_DIR / "ViVeTool.exe" ver_file = VIVE_TOOLS_DIR / "version.txt" - cached_ver = ( - ver_file.read_text(encoding="utf-8").strip() - if ver_file.exists() - else "" - ) + cached_ver = ver_file.read_text(encoding="utf-8").strip() if ver_file.exists() else "" arch = detect_architecture() logger.log_only("INFO", f"Detected architecture: {arch}") - release = _fetch_json( - f"https://api.github.com/repos/{VIVE_REPO}/releases/latest" - ) + release = _fetch_json(f"https://api.github.com/repos/{VIVE_REPO}/releases/latest") if release is not None: tag = release["tag_name"] if tag != cached_ver: logger.log("INFO", f"Downloading ViVeTool {tag}...") suffix = f"{arch}.zip" asset = next( - ( - a - for a in release["assets"] - if a["name"].endswith(suffix) - ), + (a for a in release["assets"] if a["name"].endswith(suffix)), None, ) if asset is None: logger.log("ERROR", f"No ViVeTool asset for {arch}.") return exe if exe.exists() else None zip_path = VIVE_TOOLS_DIR / asset["name"] - logger.log( - "INFO", f" Source: {asset['browser_download_url']}" - ) + logger.log("INFO", f" Source: {asset['browser_download_url']}") if not _download_file(asset["browser_download_url"], zip_path): logger.log("ERROR", "Download failed.") zip_path.unlink(missing_ok=True) @@ -117,9 +98,7 @@ def ensure_vivetool(logger: Logger) -> Path | None: else: logger.log("INFO", f"ViVeTool {tag} is up to date.") elif exe.exists(): - logger.log( - "WARN", "Could not check for updates. Using cached ViVeTool." - ) + logger.log("WARN", "Could not check for updates. Using cached ViVeTool.") else: logger.log( "ERROR", @@ -134,12 +113,8 @@ def low_latency_mode(logger: Logger) -> None: print(MENU_LOGO) print(" LOW LATENCY MODE") print(MENU_LOGO) - print( - "This feature uses ViVeTool to manage Windows low" - ) - print( - "latency feature flags for better system responsiveness." - ) + print("This feature uses ViVeTool to manage Windows low") + print("latency feature flags for better system responsiveness.") print() print("Feature IDs:") for fid in LOW_LATENCY_IDS: @@ -164,9 +139,7 @@ def low_latency_mode(logger: Logger) -> None: print(f"ViVeTool : {vivetool}") ver_path = VIVE_TOOLS_DIR / "version.txt" current_ver = ( - ver_path.read_text(encoding="utf-8").strip() - if ver_path.exists() - else "unknown" + ver_path.read_text(encoding="utf-8").strip() if ver_path.exists() else "unknown" ) print(f"Version : {current_ver}") print(MENU_LOGO) @@ -176,9 +149,7 @@ def low_latency_mode(logger: Logger) -> None: print("[4] Return to Main Menu") print(MENU_LOGO) choice = input("Select an option: ").strip() - logger.log_only( - "INFO", f"Low Latency Mode sub-menu selection: {choice}" - ) + logger.log_only("INFO", f"Low Latency Mode sub-menu selection: {choice}") if choice == "1": logger.section("Low Latency Mode — Status Check") @@ -200,9 +171,7 @@ def low_latency_mode(logger: Logger) -> None: print(MENU_LOGO) print(" LOW LATENCY MODE") print(MENU_LOGO) - print( - "WARNING: This enables system-wide low latency" - ) + print("WARNING: This enables system-wide low latency") print("features to improve responsiveness.") print("-> A reboot may be required to take effect.") print("-> Can be safely reverted by disabling.") @@ -232,9 +201,7 @@ def low_latency_mode(logger: Logger) -> None: print(MENU_LOGO) print(" LOW LATENCY MODE") print(MENU_LOGO) - print( - "WARNING: This disables low latency features," - ) + print("WARNING: This disables low latency features,") print("restoring default system behavior.") print("-> A reboot may be required to take effect.") print(MENU_LOGO) diff --git a/features/network_reset.py b/features/network_reset.py index 017d107..e709aad 100644 --- a/features/network_reset.py +++ b/features/network_reset.py @@ -26,9 +26,7 @@ def net_reset(logger: Logger) -> None: "Restore Point - Complete Network Reset", ): create_restore_point(logger, "Before Network Reset") - if not prompt_yes_no( - logger, "Do you want to proceed? (Y/N): ", "Complete Network Reset" - ): + if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "Complete Network Reset"): return for cmd, label in ( (["netsh", "winsock", "reset"], "netsh winsock reset"), @@ -47,7 +45,5 @@ def net_reset(logger: Logger) -> None: label.replace("netsh ", "Resetting ").replace("ipconfig ", "Flushing "), ) run_and_log(logger, cmd, label) - logger.log( - "INFO", "NETWORK RESET COMPLETE. Please RESTART your computer." - ) + logger.log("INFO", "NETWORK RESET COMPLETE. Please RESTART your computer.") input("Press Enter to continue...") diff --git a/features/network_snapshot.py b/features/network_snapshot.py index 1b37020..db13cad 100644 --- a/features/network_snapshot.py +++ b/features/network_snapshot.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os from datetime import datetime from pathlib import Path @@ -70,9 +69,13 @@ def network_snapshot(logger: Logger, script_dir: Path | None = None) -> None: print(" Current Network State:") print() - ip_lines = [l for l in lines if "IPv4 Address" in l or "Default Gateway" in l or "DNS Servers" in l] - for l in ip_lines[:10]: - print(f" {l.strip()}") + ip_lines = [ + line + for line in lines + if "IPv4 Address" in line or "Default Gateway" in line or "DNS Servers" in line + ] + for line in ip_lines[:10]: + print(f" {line.strip()}") print() print(f" Full snapshot saved to log ({len(lines)} lines).") @@ -99,13 +102,16 @@ def network_snapshot(logger: Logger, script_dir: Path | None = None) -> None: prev_text = prev_snap.read_text(encoding="utf-8", errors="replace") curr_text = output import difflib - diff = list(difflib.unified_diff( - prev_text.splitlines(), - curr_text.splitlines(), - fromfile=prev_snap.name, - tofile=snap_file.name, - lineterm="", - )) + + diff = list( + difflib.unified_diff( + prev_text.splitlines(), + curr_text.splitlines(), + fromfile=prev_snap.name, + tofile=snap_file.name, + lineterm="", + ) + ) if diff: print() print(" --- Differences from previous snapshot ---") diff --git a/features/recovery_tools.py b/features/recovery_tools.py index 0251550..f3c361e 100644 --- a/features/recovery_tools.py +++ b/features/recovery_tools.py @@ -6,14 +6,14 @@ def _bcdedit(args: list[str]) -> str: if not command_exists("bcdedit"): return "" - result = run_command(["bcdedit"] + args, capture=True) + result = run_command(["bcdedit", *args], capture=True) return result.stdout.strip() if result.code == 0 else f"(exit={result.code})" def _reagentc(args: list[str]) -> str: if not command_exists("reagentc"): return "" - result = run_command(["reagentc"] + args, capture=True) + result = run_command(["reagentc", *args], capture=True) return result.stdout.strip() if result.code == 0 else f"(exit={result.code})" @@ -79,7 +79,9 @@ def recovery_tools(logger: Logger) -> None: elif choice == "4": logger.section("Safe Mode — Command Prompt") print("WARNING: This will set Safe Mode with Command Prompt on next restart.") - if prompt_yes_no(logger, "Set Safe Mode (Command Prompt)? (Y/N): ", "Safe Mode CmdPrompt"): + if prompt_yes_no( + logger, "Set Safe Mode (Command Prompt)? (Y/N): ", "Safe Mode CmdPrompt" + ): output = _bcdedit(["/set", "{current}", "safeboot", "minimal"]) print(output) out2 = _bcdedit(["/set", "{current}", "safebootalternateshell", "yes"]) @@ -118,11 +120,15 @@ def recovery_tools(logger: Logger) -> None: print() if command_exists("reagentc"): logger.section("Enable / Disable WinRE") - if prompt_yes_no(logger, "Enable Windows Recovery Environment? (Y/N): ", "Enable WinRE"): + if prompt_yes_no( + logger, "Enable Windows Recovery Environment? (Y/N): ", "Enable WinRE" + ): out = _reagentc(["/enable"]) print(out) logger.write_raw(out) - if prompt_yes_no(logger, "Disable Windows Recovery Environment? (Y/N): ", "Disable WinRE"): + if prompt_yes_no( + logger, "Disable Windows Recovery Environment? (Y/N): ", "Disable WinRE" + ): out = _reagentc(["/disable"]) print(out) logger.write_raw(out) diff --git a/features/self_update.py b/features/self_update.py index 2d39a49..1444df2 100644 --- a/features/self_update.py +++ b/features/self_update.py @@ -82,7 +82,7 @@ def self_update(logger: Logger) -> None: print() if remote_body: short = remote_body.strip()[:500] - print(f" Release notes:") + print(" Release notes:") for line in short.splitlines()[:10]: print(f" {line}") print() @@ -99,6 +99,7 @@ def self_update(logger: Logger) -> None: "Open Download Page", ): import webbrowser + webbrowser.open(remote_url_page) logger.log_only("INFO", f"Browser opened to {remote_url_page}") except Exception: diff --git a/features/service_health.py b/features/service_health.py index dd1cbea..e60ed35 100644 --- a/features/service_health.py +++ b/features/service_health.py @@ -1,10 +1,7 @@ from __future__ import annotations -import subprocess - from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, run_command - _CRITICAL_SERVICES = [ ("wuauserv", "Windows Update"), ("BITS", "Background Intelligent Transfer"), @@ -86,7 +83,7 @@ def service_health(logger: Logger) -> None: ps_status = _get_services_ps(names) print(f" {'Status':<15} {'Service Name':<20} {'Display Name':<40}") - print(f" {'-'*15} {'-'*20} {'-'*40}") + print(f" {'-' * 15} {'-' * 20} {'-' * 40}") running = 0 stopped = 0 diff --git a/features/ssd_trim.py b/features/ssd_trim.py index c5079be..913a29d 100644 --- a/features/ssd_trim.py +++ b/features/ssd_trim.py @@ -10,7 +10,6 @@ command_exists, create_restore_point, prompt_yes_no, - run_and_log, run_command, select_existing_drive, ) @@ -23,9 +22,7 @@ def get_volume_table() -> str: "FileSystemLabel, @{Name='Size(GB)';Expression={[math]::round($_.Size / 1GB, 2)}} " "| Format-Table -AutoSize" ) - result = run_command( - ["powershell", "-NoProfile", "-Command", ps], capture=True - ) + result = run_command(["powershell", "-NoProfile", "-Command", ps], capture=True) return (result.stdout or "") + (result.stderr or "") @@ -54,9 +51,7 @@ def ssd_trim(logger: Logger) -> None: if drive is None: return if drive == "": - logger.log( - "ERROR", "No valid drive was selected for Manual SSD TRIM." - ) + logger.log("ERROR", "No valid drive was selected for Manual SSD TRIM.") input("Press Enter to continue...") return logger.log_only("INFO", f"Selected TRIM drive: {drive}:") diff --git a/features/system_cleanup.py b/features/system_cleanup.py index 389f312..e9fa31c 100644 --- a/features/system_cleanup.py +++ b/features/system_cleanup.py @@ -31,6 +31,7 @@ def drive_free_mb() -> int: def _is_excluded(target: Path) -> bool: try: from features.cleanup_config import is_excluded + return is_excluded(target) except Exception: return False @@ -133,8 +134,13 @@ def cleanup(logger: Logger) -> None: if is_all: selected_names = list(targets.keys()) else: - name_map = {"2": "WindowsTemp", "3": "UserTemp", "4": "Prefetch", - "5": "SoftwareDistribution", "6": "VendorRoots"} + name_map = { + "2": "WindowsTemp", + "3": "UserTemp", + "4": "Prefetch", + "5": "SoftwareDistribution", + "6": "VendorRoots", + } selected_names = [name_map[target_choice]] clear_screen() @@ -160,7 +166,9 @@ def cleanup(logger: Logger) -> None: free_before = drive_free_mb() logger.log_only("INFO", f"Free space before cleanup: {free_before} MB") - if is_all or any(n in selected_names for n in ("WindowsTemp", "UserTemp", "SoftwareDistribution")): + if is_all or any( + n in selected_names for n in ("WindowsTemp", "UserTemp", "SoftwareDistribution") + ): _stop_services(logger) print() @@ -170,7 +178,11 @@ def cleanup(logger: Logger) -> None: for name in selected_names: if name == "VendorRoots": if is_all: - if prompt_yes_no(logger, "Remove vendor driver directories (AMD, NVIDIA, INTEL)? (Y/N): ", "Vendor Driver Cleanup"): + if prompt_yes_no( + logger, + "Remove vendor driver directories (AMD, NVIDIA, INTEL)? (Y/N): ", + "Vendor Driver Cleanup", + ): clean_dirs.extend(targets["VendorRoots"]) else: clean_dirs.extend(targets["VendorRoots"]) @@ -197,7 +209,9 @@ def cleanup(logger: Logger) -> None: if d is not None: _rebuild_dir(d, logger) - if is_all or any(n in selected_names for n in ("WindowsTemp", "UserTemp", "SoftwareDistribution")): + if is_all or any( + n in selected_names for n in ("WindowsTemp", "UserTemp", "SoftwareDistribution") + ): print() _start_services(logger) diff --git a/features/system_info.py b/features/system_info.py index 872bcff..33b33d3 100644 --- a/features/system_info.py +++ b/features/system_info.py @@ -4,11 +4,9 @@ import os import platform import shutil -import subprocess import winreg -from datetime import datetime -from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, run_command +from toolbox_base import MENU_LOGO, Logger, clear_screen class _MEMORYSTATUSEX(ctypes.Structure): @@ -74,23 +72,32 @@ def system_info(logger: Logger) -> None: kernel32 = ctypes.windll.kernel32 - os_edition = _reg_str( - winreg.HKEY_LOCAL_MACHINE, - r"SOFTWARE\Microsoft\Windows NT\CurrentVersion", - "ProductName", - ) or platform.system() - os_build = _reg_str( - winreg.HKEY_LOCAL_MACHINE, - r"SOFTWARE\Microsoft\Windows NT\CurrentVersion", - "CurrentBuild", - ) or platform.version() + os_edition = ( + _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows NT\CurrentVersion", + "ProductName", + ) + or platform.system() + ) + os_build = ( + _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows NT\CurrentVersion", + "CurrentBuild", + ) + or platform.version() + ) os_display = f"{os_edition} (Build {os_build})" - cpu_name = _reg_str( - winreg.HKEY_LOCAL_MACHINE, - r"HARDWARE\DESCRIPTION\System\CentralProcessor\0", - "ProcessorNameString", - ) or platform.processor() + cpu_name = ( + _reg_str( + winreg.HKEY_LOCAL_MACHINE, + r"HARDWARE\DESCRIPTION\System\CentralProcessor\0", + "ProcessorNameString", + ) + or platform.processor() + ) cpu_cores = os.cpu_count() or 0 cpu_display = f"{cpu_name} ({cpu_cores} logical cores)" @@ -109,7 +116,10 @@ def system_info(logger: Logger) -> None: disk_free = du.free disk_used = du.total - du.free disk_pct = du.used * 100 // du.total - disk_display = f"{_fmt_bytes(disk_used)} / {_fmt_bytes(disk_total)} ({disk_pct}% used), {_fmt_bytes(disk_free)} free" + disk_display = ( + f"{_fmt_bytes(disk_used)} / {_fmt_bytes(disk_total)} " + f"({disk_pct}% used), {_fmt_bytes(disk_free)} free" + ) uptime_ms = kernel32.GetTickCount64() uptime_display = _fmt_uptime(uptime_ms) diff --git a/features/system_repair.py b/features/system_repair.py index 52c8245..40ad0b5 100644 --- a/features/system_repair.py +++ b/features/system_repair.py @@ -27,9 +27,7 @@ def sys_repair(logger: Logger) -> None: "Restore Point - System Integrity Repair", ): create_restore_point(logger, "Before System Integrity Repair") - if not prompt_yes_no( - logger, "Do you want to proceed? (Y/N): ", "System Integrity Repair" - ): + if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "System Integrity Repair"): return steps = [ (["sfc", "/scannow"], "System File Checker"), diff --git a/features/windows_update.py b/features/windows_update.py index d1ca0aa..a5f6f00 100644 --- a/features/windows_update.py +++ b/features/windows_update.py @@ -1,6 +1,5 @@ from __future__ import annotations -import subprocess import winreg from toolbox_base import MENU_LOGO, Logger, clear_screen, command_exists, run_command @@ -123,18 +122,19 @@ def windows_update(logger: Logger) -> None: "DeferQualityUpdates", ) if deferred: - print(f" Quality Updates : Deferred") + print(" Quality Updates : Deferred") else: - print(f" Quality Updates : Not deferred") + print(" Quality Updates : Not deferred") if svc == "RUNNING" and last_install: - from datetime import datetime, timedelta + from datetime import datetime try: dt = datetime.strptime(last_install, "%Y-%m-%d %H:%M:%S") days_ago = (datetime.now() - dt).days if days_ago > 30: - print(f" >>> Last update was {days_ago} days ago. Consider running [5] Winget upgrade.") + print(f" >>> Last update was {days_ago} days ago.") + print(" Consider running [5] Winget upgrade.") except ValueError: pass diff --git a/features/winget_upgrade.py b/features/winget_upgrade.py index 2213832..6d56fef 100644 --- a/features/winget_upgrade.py +++ b/features/winget_upgrade.py @@ -27,9 +27,7 @@ def app_update(logger: Logger) -> None: "Restore Point - Update Installed Apps", ): create_restore_point(logger, "Before Winget App Upgrade") - if not prompt_yes_no( - logger, "Do you want to proceed? (Y/N): ", "Update Installed Apps" - ): + if not prompt_yes_no(logger, "Do you want to proceed? (Y/N): ", "Update Installed Apps"): return if not command_exists("winget"): logger.log( diff --git a/ldlwintoolbox.py b/ldlwintoolbox.py index 3d1c6ad..b04849f 100644 --- a/ldlwintoolbox.py +++ b/ldlwintoolbox.py @@ -7,16 +7,6 @@ from datetime import datetime from pathlib import Path -from toolbox_base import ( - Color, - Logger, - TOOLBOX_VERSION, - clear_screen, - cprint, - get_log_dir, - prompt_yes_no, - write_session_header, -) from features.bitlocker_disable import bitlocker_disable from features.browser_ai_killer import kill_browser_ai from features.cleanup_config import cleanup_config @@ -39,6 +29,16 @@ from features.windows_update import windows_update from features.winget_upgrade import app_update from features.winsxs_cleanup import component_store_cleanup +from toolbox_base import ( + TOOLBOX_VERSION, + Color, + Logger, + clear_screen, + cprint, + get_log_dir, + prompt_yes_no, + write_session_header, +) def is_admin() -> bool: @@ -60,14 +60,14 @@ def relaunch_as_admin() -> None: def _print_header(is_admin_user: bool) -> None: - B = Color.BOLD - C = Color.CYAN - Y = Color.YELLOW + bold = Color.BOLD + cyan = Color.CYAN + yellow = Color.YELLOW cprint("─" * 47, Color.DIM) - cprint(" ⚙ LDL Windows ToolBox", B, C) + cprint(" ⚙ LDL Windows ToolBox", bold, cyan) cprint(f" v{TOOLBOX_VERSION}", Color.DIM) if not is_admin_user: - cprint(" ★ READ-ONLY MODE ★", B, Y) + cprint(" ★ READ-ONLY MODE ★", bold, yellow) cprint("─" * 47, Color.DIM) @@ -131,7 +131,19 @@ def main_menu(logger: Logger, log_dir: Path, script_dir: Path, is_admin_user: bo relaunch_as_admin() continue - if not is_admin_user and choice in ("1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"): + if not is_admin_user and choice in ( + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + ): logger.log("WARN", f"Admin feature {choice} blocked in read-only mode.") print("This feature requires administrator privileges.") input("Press Enter to continue...") diff --git a/toolbox_base.py b/toolbox_base.py index c779d8f..8dbb8df 100644 --- a/toolbox_base.py +++ b/toolbox_base.py @@ -12,9 +12,9 @@ from datetime import datetime from pathlib import Path - MENU_LOGO = "=" * 47 + class Color: RESET = "\033[0m" BOLD = "\033[1m" @@ -71,6 +71,7 @@ def _spin(self) -> None: def _read_version() -> str: try: import tomllib + pyproject = Path(__file__).resolve().parent / "pyproject.toml" if pyproject.exists(): data = tomllib.loads(pyproject.read_text(encoding="utf-8")) @@ -152,9 +153,7 @@ def run_command( raise subprocess.CalledProcessError( completed.returncode, command, completed.stdout, completed.stderr ) - return CommandResult( - completed.returncode, completed.stdout or "", completed.stderr or "" - ) + return CommandResult(completed.returncode, completed.stdout or "", completed.stderr or "") def run_and_log( @@ -205,9 +204,7 @@ def prompt_drive(logger: Logger, prompt: str, context: str) -> str | None: def select_existing_drive(logger: Logger, context: str) -> str | None: - choice = prompt_drive( - logger, "Press 0 to return, or drive letter to continue (A-Z): ", context - ) + choice = prompt_drive(logger, "Press 0 to return, or drive letter to continue (A-Z): ", context) if choice is None: return None if choice == "": @@ -245,7 +242,10 @@ def create_restore_point(logger: Logger, description: str) -> bool: stderr = result.stderr.strip() logger.log_only("WARN", f"Restore point failed (exit={rc}): {stderr or 'unknown error'}") if "0x80070422" in stderr: - print(">>> System Restore may be disabled. Enable it in System Properties to use this feature.") + print( + ">>> System Restore may be disabled. Enable it in System Properties" + " to use this feature." + ) else: print(f">>> Restore point creation failed (exit={rc}). Continuing anyway.") return False From f256e9ae964b49b99d9acad355c2f4cfdd057d32 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:33:23 +0800 Subject: [PATCH 32/33] Fix PyInstaller spec: __file__ not defined in spec namespace, use Path.cwd() --- LDLWinToolBox.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LDLWinToolBox.spec b/LDLWinToolBox.spec index 7f62f0a..d376064 100644 --- a/LDLWinToolBox.spec +++ b/LDLWinToolBox.spec @@ -4,7 +4,7 @@ import tomllib from pathlib import Path -here = Path(__file__).parent +here = Path.cwd() pyproject = here / "pyproject.toml" data = tomllib.loads(pyproject.read_text(encoding="utf-8")) app_ver = data["project"]["version"] From 052c3dc522954a8910ee95e22fc1ea4bd3deccc0 Mon Sep 17 00:00:00 2001 From: LoveDoLove Date: Sun, 5 Jul 2026 15:34:34 +0800 Subject: [PATCH 33/33] Bump version to 1.0.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8b4c52d..c528a4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldlwintoolbox" -version = "1.0.3" +version = "1.0.5" description = "LDL Windows ToolBox in Python" readme = "README.md" requires-python = ">=3.11"