diff --git a/AppLens-Tune.ps1 b/AppLens-Tune.ps1 index 50a62ab..584a20f 100644 --- a/AppLens-Tune.ps1 +++ b/AppLens-Tune.ps1 @@ -5,7 +5,7 @@ .DESCRIPTION Captures a targeted workstation snapshot focused on startup load, background services, local AI tooling, storage hotspots, and repo placement. The - script does not change the machine. It only writes a plain-text report. + script does not change the machine. It only writes a Markdown report. #> function Get-DesktopFilePath { @@ -14,7 +14,10 @@ function Get-DesktopFilePath { [string]$FileName ) - $desktopPath = [Environment]::GetFolderPath('Desktop') + $desktopPath = $env:APPLENS_OUTPUT_DIR + if ([string]::IsNullOrWhiteSpace($desktopPath)) { + $desktopPath = [Environment]::GetFolderPath('Desktop') + } if ([string]::IsNullOrWhiteSpace($desktopPath)) { $desktopPath = Join-Path $env:USERPROFILE 'Desktop' } @@ -207,7 +210,8 @@ function New-Section { $section = @() $section += '' - $section += $Title + $heading = $Title -replace '^\s*---\s*', '' -replace '\s*---\s*$', '' + $section += "## $heading" if (-not $Lines -or $Lines.Count -eq 0) { $section += '(none)' } else { @@ -216,7 +220,7 @@ function New-Section { return $section } -$OutputPath = Get-DesktopFilePath -FileName "AppLens_Tune_Results_$env:COMPUTERNAME.txt" +$OutputPath = Get-DesktopFilePath -FileName "AppLens_Tune_Results_$env:COMPUTERNAME.md" $computerSystem = Get-CimInstance Win32_ComputerSystem $operatingSystem = Get-CimInstance Win32_OperatingSystem @@ -407,18 +411,18 @@ if ($cDrive -and $cDrive.Free -lt 100GB) { } $summaryLines = @( - "Machine: $($computerSystem.Manufacturer) $($computerSystem.Model)", - "OS: $($operatingSystem.Caption) ($($operatingSystem.Version))", - "RAM: $('{0:N1} GB' -f ($computerSystem.TotalPhysicalMemory / 1GB))", - "C: Free: $(Format-Size $cDrive.Free)" + "- **Machine:** $($computerSystem.Manufacturer) $($computerSystem.Model)", + "- **OS:** $($operatingSystem.Caption) ($($operatingSystem.Version))", + "- **RAM:** $('{0:N1} GB' -f ($computerSystem.TotalPhysicalMemory / 1GB))", + "- **C: Free:** $(Format-Size $cDrive.Free)" ) $output = @() -$output += '=== AppLens-Tune Audit Results ===' -$output += "Computer: $env:COMPUTERNAME" -$output += "User: $env:USERNAME" -$output += "Scan Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -$output += 'Mode: Audit (read-only)' +$output += '# AppLens-Tune Audit Results' +$output += "- **Computer:** $env:COMPUTERNAME" +$output += "- **User:** $env:USERNAME" +$output += "- **Scan Date:** $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +$output += '- **Mode:** Audit (read-only)' $output += '' $output += $summaryLines $output += New-Section -Title '--- Stability Checks ---' -Lines $stableFindings diff --git a/AppLens-Tune.py b/AppLens-Tune.py index bc41c23..ca74984 100644 --- a/AppLens-Tune.py +++ b/AppLens-Tune.py @@ -120,7 +120,8 @@ def table(rows: list[dict[str, object]], columns: list[str]) -> list[str]: def section(title: str, lines: list[str]) -> list[str]: - return ["", title, *(lines if lines else ["(none)"])] + heading = title.strip().strip("-").strip() + return ["", f"## {heading}", *(lines if lines else ["(none)"])] def total_ram_bytes() -> int | None: @@ -638,16 +639,16 @@ def build_report() -> str: stable, review, optional = build_findings(startup_rows, service_rows, storage_rows, repo_rows, llm_review, llm_optional) lines: list[str] = [] - lines.append("=== AppLens-Tune Audit Results ===") - lines.append(f"Computer: {socket.gethostname()}") - lines.append(f"User: {getpass.getuser()}") - lines.append(f"Scan Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - lines.append("Mode: Audit (read-only)") + lines.append("# AppLens-Tune Audit Results") + lines.append(f"- **Computer:** {socket.gethostname()}") + lines.append(f"- **User:** {getpass.getuser()}") + lines.append(f"- **Scan Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + lines.append("- **Mode:** Audit (read-only)") lines.append("") - lines.append(f"Machine: {platform.machine()}") - lines.append(f"OS: {platform.platform()}") - lines.append(f"RAM: {format_size(total_ram_bytes())}") - lines.append(f"Root Free: {format_size(disk.free)}") + lines.append(f"- **Machine:** {platform.machine()}") + lines.append(f"- **OS:** {platform.platform()}") + lines.append(f"- **RAM:** {format_size(total_ram_bytes())}") + lines.append(f"- **Root Free:** {format_size(disk.free)}") lines.extend(section("--- Stability Checks ---", stable)) lines.extend(section("--- Review Items ---", review)) lines.extend(section("--- Optional Improvements ---", optional)) @@ -670,7 +671,7 @@ def build_report() -> str: def main() -> int: report = build_report() - path = output_path(f"AppLens_Tune_Results_{socket.gethostname()}.txt") + path = output_path(f"AppLens_Tune_Results_{socket.gethostname()}.md") path.write_text(report, encoding="utf-8") print(report, end="") print("") diff --git a/AppLens.ps1 b/AppLens.ps1 index 67c9e7b..83a49fe 100644 --- a/AppLens.ps1 +++ b/AppLens.ps1 @@ -4,7 +4,7 @@ AppLens — Pre-Audit App Scanner for CSI AI Workflow Audits .DESCRIPTION Scans installed desktop apps (Win32) and Microsoft Store apps without - requiring admin rights. Outputs a clean, categorized text file suitable + requiring admin rights. Outputs a clean, categorized Markdown file suitable for pasting into an AI prompt alongside a workflow audit transcript. #> @@ -16,7 +16,10 @@ function Get-DesktopFilePath { [string]$FileName ) - $desktopPath = [Environment]::GetFolderPath('Desktop') + $desktopPath = $env:APPLENS_OUTPUT_DIR + if ([string]::IsNullOrWhiteSpace($desktopPath)) { + $desktopPath = [Environment]::GetFolderPath('Desktop') + } if ([string]::IsNullOrWhiteSpace($desktopPath)) { $desktopPath = Join-Path $env:USERPROFILE 'Desktop' } @@ -28,7 +31,7 @@ function Get-DesktopFilePath { return Join-Path $desktopPath $FileName } -$OutputPath = Get-DesktopFilePath -FileName "AppLens_Results_$env:COMPUTERNAME.txt" +$OutputPath = Get-DesktopFilePath -FileName "AppLens_Results_$env:COMPUTERNAME.md" # Patterns for apps that should be filtered into the "Runtimes & Frameworks" section $RuntimePatterns = @( @@ -425,17 +428,17 @@ $storeApps = Get-StoreApps # Build output $output = @() -$output += "=== AppLens Scan Results ===" -$output += "Computer: $env:COMPUTERNAME" -$output += "User: $env:USERNAME" -$output += "Scan Date: $(Get-Date -Format 'yyyy-MM-dd')" +$output += "# AppLens Scan Results" +$output += "- **Computer:** $env:COMPUTERNAME" +$output += "- **User:** $env:USERNAME" +$output += "- **Scan Date:** $(Get-Date -Format 'yyyy-MM-dd')" $output += "" -$output += "--- Desktop Applications ---" +$output += "## Desktop Applications" # Microsoft 365 group if ($m365Apps.Count -gt 0) { $m365Apps = $m365Apps | Sort-Object Name - $output += "Microsoft 365 (Office)" + $output += "### Microsoft 365 (Office)" foreach ($app in $m365Apps) { $shortName = $app.Name $output += Format-AppLine -Name $shortName -Version $app.Version -UserInstalled $app.UserInstalled -Indent ' - ' @@ -450,7 +453,7 @@ foreach ($app in $desktopApps) { # Store apps $output += "" -$output += "--- Microsoft Store Apps ---" +$output += "## Microsoft Store Apps" if ($storeApps.Count -eq 0) { $output += "(none detected)" } else { @@ -461,7 +464,7 @@ if ($storeApps.Count -eq 0) { # Runtimes $output += "" -$output += "--- Runtimes & Frameworks (for reference) ---" +$output += "## Runtimes & Frameworks (for reference)" if ($runtimes.Count -eq 0) { $output += "(none detected)" } else { diff --git a/AppLens.py b/AppLens.py index d0e3595..92ccd51 100644 --- a/AppLens.py +++ b/AppLens.py @@ -2,7 +2,7 @@ """ AppLens app inventory scanner for macOS and Linux. -Read-only. Writes a categorized plain-text report to the user's Desktop when +Read-only. Writes a categorized Markdown report to the user's Desktop when available, or to the home directory when no Desktop folder exists. """ @@ -264,13 +264,13 @@ def build_report() -> str: package_apps = [item for item in package_apps if not is_runtime(item["name"])] lines: list[str] = [] - lines.append("=== AppLens Scan Results ===") - lines.append(f"Computer: {socket.gethostname()}") - lines.append(f"User: {getpass.getuser()}") - lines.append(f"OS: {platform.platform()}") - lines.append(f"Scan Date: {datetime.now().strftime('%Y-%m-%d')}") + lines.append("# AppLens Scan Results") + lines.append(f"- **Computer:** {socket.gethostname()}") + lines.append(f"- **User:** {getpass.getuser()}") + lines.append(f"- **OS:** {platform.platform()}") + lines.append(f"- **Scan Date:** {datetime.now().strftime('%Y-%m-%d')}") lines.append("") - lines.append("--- Desktop Applications ---") + lines.append("## Desktop Applications") desktop_apps = dedupe(desktop_apps) if desktop_apps: lines.extend(format_app(**item) for item in desktop_apps) @@ -278,7 +278,7 @@ def build_report() -> str: lines.append("(none detected)") lines.append("") - lines.append(package_title) + lines.append(f"## {package_title.strip('- ')}") package_apps = dedupe(package_apps) if package_apps: lines.extend(format_app(**item) for item in package_apps) @@ -286,14 +286,14 @@ def build_report() -> str: lines.append("(none detected)") lines.append("") - lines.append("--- Developer/CLI Tools (detected) ---") + lines.append("## Developer/CLI Tools (detected)") if developer_tools: lines.extend(format_app(**item) for item in developer_tools) else: lines.append("(none detected)") lines.append("") - lines.append("--- Runtimes & Frameworks (for reference) ---") + lines.append("## Runtimes & Frameworks (for reference)") runtimes = dedupe(runtimes) if runtimes: lines.extend(format_app(**item) for item in runtimes) @@ -305,7 +305,7 @@ def build_report() -> str: def main() -> int: report = build_report() - path = output_path(f"AppLens_Results_{socket.gethostname()}.txt") + path = output_path(f"AppLens_Results_{socket.gethostname()}.md") path.write_text(report, encoding="utf-8") print(report, end="") print("") diff --git a/README.md b/README.md index 1f38602..98f9d1e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,14 @@ More detail is in [docs/AppLensDesktop-Build.md](docs/AppLensDesktop-Build.md), Double-click: +```text +Run-AppLens-Capture.bat +``` + +This writes both reports and logs to a Desktop folder named `AppLens-Capture-`. + +Individual scripts: + ```text Run-AppLens.bat Run-AppLens-Tune.bat @@ -84,9 +92,16 @@ powershell -ExecutionPolicy Bypass -File AppLens-Tune.ps1 ### macOS and Linux ```sh -chmod +x Run-AppLens.sh Run-AppLens-Tune.sh -./Run-AppLens.sh -./Run-AppLens-Tune.sh +sh Run-AppLens-Capture.sh +``` + +This writes both reports and logs to a Desktop folder named `AppLens-Capture-`. + +Individual scripts: + +```sh +sh Run-AppLens.sh +sh Run-AppLens-Tune.sh ``` Or run Python directly: @@ -100,8 +115,8 @@ python3 AppLens-Tune.py Script reports are written to the user's Desktop: -- `AppLens_Results_.txt` -- `AppLens_Tune_Results_.txt` +- `AppLens_Results_.md` +- `AppLens_Tune_Results_.md` The desktop app exports: diff --git a/Run-AppLens-Capture.bat b/Run-AppLens-Capture.bat new file mode 100644 index 0000000..4a02bd9 --- /dev/null +++ b/Run-AppLens-Capture.bat @@ -0,0 +1,69 @@ +@echo off +setlocal EnableExtensions + +set APPLENS_INTERACTIVE=0 +set "CAPTURE_DIR=%USERPROFILE%\Desktop\AppLens-Capture-%COMPUTERNAME%" +set "APPLENS_OUTPUT_DIR=%CAPTURE_DIR%" +set "CAPTURE_EXIT=0" + +if not exist "%CAPTURE_DIR%" mkdir "%CAPTURE_DIR%" +if exist "%CAPTURE_DIR%\AppLens_Results_%COMPUTERNAME%.txt" del /q "%CAPTURE_DIR%\AppLens_Results_%COMPUTERNAME%.txt" +if exist "%CAPTURE_DIR%\AppLens_Tune_Results_%COMPUTERNAME%.txt" del /q "%CAPTURE_DIR%\AppLens_Tune_Results_%COMPUTERNAME%.txt" +if exist "%CAPTURE_DIR%\README-What-To-Send.txt" del /q "%CAPTURE_DIR%\README-What-To-Send.txt" + +echo AppLens Capture +echo. +echo This will run AppLens and AppLens-Tune, then open the output folder. +echo Output folder: +echo %CAPTURE_DIR% +echo. + +call :run_script "AppLens" "AppLens.ps1" "AppLens_Run_Log.txt" +call :run_script "AppLens-Tune" "AppLens-Tune.ps1" "AppLens_Tune_Run_Log.txt" + +> "%CAPTURE_DIR%\README-What-To-Send.md" echo # AppLens Capture +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo. +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo Send this entire folder back for AppLens intake. +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo. +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo ## Expected report files +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo. +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo - AppLens_Results_%COMPUTERNAME%.md +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo - AppLens_Tune_Results_%COMPUTERNAME%.md +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo. +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo Include the log files if either report is missing. +>>"%CAPTURE_DIR%\README-What-To-Send.md" echo Do not include serial numbers or UUIDs unless explicitly requested. + +echo. +if "%CAPTURE_EXIT%"=="0" ( + echo Capture finished. +) else ( + echo Capture finished with at least one issue. Include the log files. +) +echo. +echo Opening output folder... +start "" "%CAPTURE_DIR%" +echo. +echo Press any key to close. +pause >nul +exit /b %CAPTURE_EXIT% + +:run_script +set "APP_NAME=%~1" +set "SCRIPT_NAME=%~2" +set "LOG_NAME=%~3" +set "LOG_PATH=%CAPTURE_DIR%\%LOG_NAME%" + +echo Running %APP_NAME%... +echo [%DATE% %TIME%] Running %SCRIPT_NAME%>"%LOG_PATH%" +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0%SCRIPT_NAME%" >>"%LOG_PATH%" 2>&1 +set "SCRIPT_EXIT=0" +if errorlevel 1 set "SCRIPT_EXIT=1" +>>"%LOG_PATH%" echo [%DATE% %TIME%] Exit code: %SCRIPT_EXIT% + +if "%SCRIPT_EXIT%"=="0" ( + echo %APP_NAME% finished. +) else ( + echo %APP_NAME% had an issue. See %LOG_PATH% + set "CAPTURE_EXIT=1" +) +exit /b 0 diff --git a/Run-AppLens-Capture.sh b/Run-AppLens-Capture.sh new file mode 100644 index 0000000..662706d --- /dev/null +++ b/Run-AppLens-Capture.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env sh +set -u + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +PYTHON_BIN=${PYTHON:-} + +if [ -z "$PYTHON_BIN" ]; then + for candidate in python3 python; do + if command -v "$candidate" >/dev/null 2>&1 && + "$candidate" -c 'import sys; raise SystemExit(0 if sys.version_info[0] >= 3 else 1)' >/dev/null 2>&1; then + PYTHON_BIN=$candidate + break + fi + done +fi + +if [ -z "$PYTHON_BIN" ]; then + echo "Python 3 is required to run AppLens on macOS/Linux." >&2 + exit 1 +fi + +HOST_NAME=$(hostname 2>/dev/null || echo "unknown") +DESKTOP_DIR="$HOME/Desktop" +if [ ! -d "$DESKTOP_DIR" ]; then + DESKTOP_DIR="$HOME" +fi + +CAPTURE_DIR="$DESKTOP_DIR/AppLens-Capture-$HOST_NAME" +mkdir -p "$CAPTURE_DIR" +rm -f \ + "$CAPTURE_DIR/AppLens_Results_$HOST_NAME.txt" \ + "$CAPTURE_DIR/AppLens_Tune_Results_$HOST_NAME.txt" \ + "$CAPTURE_DIR/README-What-To-Send.txt" +export APPLENS_OUTPUT_DIR="$CAPTURE_DIR" + +CAPTURE_EXIT=0 + +run_script() { + app_name=$1 + script_name=$2 + log_name=$3 + log_path="$CAPTURE_DIR/$log_name" + + echo "Running $app_name..." + echo "[$(date)] Running $script_name" >"$log_path" + if "$PYTHON_BIN" "$SCRIPT_DIR/$script_name" >>"$log_path" 2>&1; then + echo "[$(date)] Exit code: 0" >>"$log_path" + echo "$app_name finished." + else + echo "[$(date)] Exit code: 1" >>"$log_path" + echo "$app_name had an issue. See $log_path" + CAPTURE_EXIT=1 + fi +} + +echo "AppLens Capture" +echo +echo "Output folder:" +echo "$CAPTURE_DIR" +echo + +run_script "AppLens" "AppLens.py" "AppLens_Run_Log.txt" +run_script "AppLens-Tune" "AppLens-Tune.py" "AppLens_Tune_Run_Log.txt" + +cat >"$CAPTURE_DIR/README-What-To-Send.md" </dev/null 2>&1; then + open "$CAPTURE_DIR" >/dev/null 2>&1 || true +elif command -v xdg-open >/dev/null 2>&1; then + xdg-open "$CAPTURE_DIR" >/dev/null 2>&1 || true +fi + +echo +if [ "$CAPTURE_EXIT" -eq 0 ]; then + echo "Capture finished." +else + echo "Capture finished with at least one issue. Include the log files." +fi +echo "Folder: $CAPTURE_DIR" +exit "$CAPTURE_EXIT" diff --git a/Run-AppLens-Tune.bat b/Run-AppLens-Tune.bat index 4cef2f5..f690622 100644 --- a/Run-AppLens-Tune.bat +++ b/Run-AppLens-Tune.bat @@ -1,3 +1,24 @@ @echo off -set APPLENS_INTERACTIVE=1 -powershell -ExecutionPolicy Bypass -File "%~dp0AppLens-Tune.ps1" +setlocal EnableExtensions +set APPLENS_INTERACTIVE=0 +set "APPLENS_LOG=%USERPROFILE%\Desktop\AppLens_Tune_Run_Log.txt" + +echo AppLens-Tune is running. This can take a few minutes on busy machines. +echo [%DATE% %TIME%] Running AppLens-Tune.ps1>"%APPLENS_LOG%" + +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0AppLens-Tune.ps1" >>"%APPLENS_LOG%" 2>&1 +set "APPLENS_EXIT=0" +if errorlevel 1 set "APPLENS_EXIT=1" +>>"%APPLENS_LOG%" echo [%DATE% %TIME%] Exit code: %APPLENS_EXIT% + +echo. +if "%APPLENS_EXIT%"=="0" ( + echo AppLens-Tune finished. Check your Desktop for AppLens_Tune_Results_*.md. +) else ( + echo AppLens-Tune did not finish successfully. +) +echo Log: %APPLENS_LOG% +echo. +echo Press any key to close. +pause >nul +exit /b %APPLENS_EXIT% diff --git a/Run-AppLens.bat b/Run-AppLens.bat index 1961c76..5fd097a 100644 --- a/Run-AppLens.bat +++ b/Run-AppLens.bat @@ -1,3 +1,24 @@ @echo off -set APPLENS_INTERACTIVE=1 -powershell -ExecutionPolicy Bypass -File "%~dp0AppLens.ps1" +setlocal EnableExtensions +set APPLENS_INTERACTIVE=0 +set "APPLENS_LOG=%USERPROFILE%\Desktop\AppLens_Run_Log.txt" + +echo AppLens is running. This usually takes less than a minute. +echo [%DATE% %TIME%] Running AppLens.ps1>"%APPLENS_LOG%" + +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0AppLens.ps1" >>"%APPLENS_LOG%" 2>&1 +set "APPLENS_EXIT=0" +if errorlevel 1 set "APPLENS_EXIT=1" +>>"%APPLENS_LOG%" echo [%DATE% %TIME%] Exit code: %APPLENS_EXIT% + +echo. +if "%APPLENS_EXIT%"=="0" ( + echo AppLens finished. Check your Desktop for AppLens_Results_*.md. +) else ( + echo AppLens did not finish successfully. +) +echo Log: %APPLENS_LOG% +echo. +echo Press any key to close. +pause >nul +exit /b %APPLENS_EXIT%