diff --git a/.gitattributes b/.gitattributes index 8ba9a84c..66d5fa21 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,9 @@ # "Solution Items" contains a space, which .gitattributes cannot express in a path.) usedComponents.json text eol=lf +# Git executes hooks with its shell, including on Windows. +.githooks/* text eol=lf + # Line-ending test fixtures (src/LogExpert.Tests/TestData). Their whole point is # byte-exact terminators (bare \r, mixed \n/\r\n/\r), so git must NOT normalize # EOLs on them — otherwise core.autocrlf would rewrite \r\n<->\n and corrupt the diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100644 index 00000000..2359126f --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,24 @@ +# Commit message cleanup + +Enable the hook once per clone, from the repository root: + +```sh +git config --local core.hooksPath .githooks +``` + +On Linux or macOS, also run `chmod +x .githooks/commit-msg` if needed. +Git for Windows includes the shell and utilities used by this hook. + +Before a commit is created, `commit-msg` removes: + +- All `Co-authored-by:` lines, regardless of capitalization or author. +- Entire lines containing recognized AI conversation/session URLs for ChatGPT, + Codex, Claude, Cursor, Gemini, Microsoft Copilot, or GitHub Copilot. + +The URL patterns live in `commit-msg`; add patterns there for other providers +or URL formats. Ordinary links, including GitHub issues and pull requests, stay. +If a session URL shares a line with other text, that entire line is removed. + +This applies to future local commits, including amendments; it does not rewrite +existing history. `git commit --no-verify` skips this hook. Hooks are not installed +automatically when someone clones the repository. diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100644 index 00000000..bcbb4a1a --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,30 @@ +#!/bin/sh +# Git passes the commit-message filename as the first argument. +set -eu + +# PowerShell/GUI clients may not put Git for Windows utilities on PATH. +PATH="$PATH:/usr/bin:/bin" +export PATH + +message=$1 +temporary=$(mktemp "${message}.clean.XXXXXX") +trap 'rm -f "$temporary"' EXIT +trap 'exit 1' HUP INT TERM + +# Match specific conversation/session URL paths, keeping ordinary project links. +# Remove the entire matching line, including any Markdown label or trailer key. +awk ' +{ + line = tolower($0) + if (line ~ /^[[:space:]]*co-authored-by[[:space:]]*:/) next + if (line ~ /https?:\/\/(www\.)?(chatgpt\.com|chat\.openai\.com)\/(c|share|codex\/tasks)\//) next + if (line ~ /https?:\/\/(www\.)?claude\.ai\/(chat|share|code)\//) next + if (line ~ /https?:\/\/(www\.)?cursor\.com\/(agents|background-agent)\//) next + if (line ~ /https?:\/\/(www\.)?gemini\.google\.com\/(app|share)\//) next + if (line ~ /https?:\/\/copilot\.microsoft\.com\/(chats|shares)\//) next + if (line ~ /https?:\/\/github\.com\/[^/[:space:]]+\/[^/[:space:]]+\/(sessions|copilot\/tasks)\//) next + print +} +' "$message" > "$temporary" + +cat "$temporary" > "$message" diff --git a/.gitignore b/.gitignore index 656cf6de..b4f1bde9 100644 --- a/.gitignore +++ b/.gitignore @@ -719,3 +719,10 @@ http-client.private.env.json ######## END JET BRAINS git ignore https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore /AgentFiles/* /.claude/* + +# Local scratch notes (tickets, checklists, working docs) +.scratch/ + +# graphify knowledge-graph outputs and extraction cache. Unanchored so it also +# catches src/graphify-out/, where the AST/semantic cache lands. +graphify-out/ diff --git a/tools/winget-sandbox/LogExpert.wsb b/tools/winget-sandbox/LogExpert.wsb new file mode 100644 index 00000000..25a5ad8b --- /dev/null +++ b/tools/winget-sandbox/LogExpert.wsb @@ -0,0 +1,18 @@ + + Enable + + + G:\Github\LogExpert\winget + C:\Manifest + true + + + G:\Github\LogExpert\tools\winget-sandbox + C:\SandboxTools + true + + + + powershell.exe -NoProfile -ExecutionPolicy Bypass -NoExit -File C:\SandboxTools\Test-Manifest.ps1 + + diff --git a/tools/winget-sandbox/README.md b/tools/winget-sandbox/README.md new file mode 100644 index 00000000..80d61efa --- /dev/null +++ b/tools/winget-sandbox/README.md @@ -0,0 +1,45 @@ +# Test LogExpert's WinGet manifests + +Double-click `LogExpert.wsb` on the host. The paths in that file assume this checkout is +`G:\Github\LogExpert`; update its two `HostFolder` entries if you move the checkout. +Windows Sandbox must be enabled and able to access the internet. + +The guest maps only the manifests and test tools, both read-only. It installs WinGet +using [Microsoft's Sandbox bootstrap instructions](https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget-on-windows-sandbox), +enables local manifests inside the guest, validates the YAML, and silently installs +LogExpert with its declared .NET dependency. It checks the registered version and +x64 Desktop Runtime, then opens a sample log for your manual check. + +1. Confirm the application opens, both sample lines appear, filtering for `ERROR` + works, and the columnizer menu and Help/About work without exceptions. +2. Close LogExpert, then click **Yes** in the **LogExpert application check** dialog. + Click **No** if any application check failed. The script tests + silent uninstall and checks that the executable and uninstall entry disappear. +3. Copy `Desktop\LogExpert-test-results` from the guest to the host before closing + Sandbox. Files inside the guest are discarded when Sandbox closes. + +If installation succeeded but a later check failed, keep that Sandbox open and +open `C:\SandboxTools` in the guest's File Explorer and double-click +`Resume-Test.cmd`. This opens PowerShell and resumes the checks. Alternatively: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\SandboxTools\Test-Manifest.ps1 -Resume +``` + +Resume skips WinGet setup and package installation, then checks the installed +application and tests removal. Its results go in a timestamped `resume-*` subfolder +of `Desktop\LogExpert-test-results`, preserving the original installation logs. +If the Sandbox has already been closed, open `LogExpert.wsb` for a fresh test. + +If startup fails, click **No** in the check dialog and keep the guest open to inspect the exception and +logs. Successful manifest validation or installation does not establish that the +application works. To uninstall manually inside the guest: + +```powershell +winget uninstall --product-code '{9C6E17B8-912C-45F9-9E7F-49CEAD8D6D7A}_is1' --silent --disable-interactivity +``` + +This is a clean installation test, not an upgrade test. Once the first version is +accepted into `microsoft/winget-pkgs`, also test upgrading from it before relying on +the release automation in PR #614. Submit only the three YAML files from `winget` +under `manifests/l/LogExperts/LogExpert/1.40.3/` in the community repository. diff --git a/tools/winget-sandbox/Resume-Test.cmd b/tools/winget-sandbox/Resume-Test.cmd new file mode 100644 index 00000000..5cae304a --- /dev/null +++ b/tools/winget-sandbox/Resume-Test.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -NoExit -File "%~dp0Test-Manifest.ps1" -Resume diff --git a/tools/winget-sandbox/Test-Manifest.ps1 b/tools/winget-sandbox/Test-Manifest.ps1 new file mode 100644 index 00000000..380da12d --- /dev/null +++ b/tools/winget-sandbox/Test-Manifest.ps1 @@ -0,0 +1,131 @@ +# Run only through LogExpert.wsb. All installations and logs stay in the guest. +param([switch]$Resume) + +$ErrorActionPreference = 'Stop' +if ($env:USERNAME -ne 'WDAGUtilityAccount' -or -not (Test-Path C:\Manifest)) { + throw 'Run this script inside Windows Sandbox using LogExpert.wsb.' +} + +$results = Join-Path $env:USERPROFILE 'Desktop\LogExpert-test-results' +if ($Resume) { + $results = Join-Path $results ('resume-' + (Get-Date -Format 'yyyyMMdd-HHmmss')) +} +New-Item -ItemType Directory -Path $results -Force | Out-Null +Start-Transcript -Path (Join-Path $results 'transcript.txt') -Force + +function Invoke-WinGet { + Write-Host "Running: winget $args" + # Piping native output makes it visible to PowerShell's transcript as well as + # saving a separate command log (direct console output can be absent there). + & $script:winget @args | Tee-Object -FilePath (Join-Path $results 'winget-output.txt') -Append | Out-Host + $commandExitCode = $LASTEXITCODE + Write-Host ('WinGet exit code: {0} (0x{1:X8})' -f $commandExitCode, ($commandExitCode -band 0xffffffffL)) + if ($commandExitCode -ne 0) { + throw "WinGet failed (exit code $commandExitCode): $args" + } +} + +function Get-LogExpertRegistration { + $key = '{9C6E17B8-912C-45F9-9E7F-49CEAD8D6D7A}_is1' + foreach ($root in @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' + )) { + Get-ItemProperty -LiteralPath "$root\$key" -ErrorAction SilentlyContinue + } +} + +try { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $ProgressPreference = 'SilentlyContinue' + if (-not $Resume) { + Write-Host 'Installing WinGet in Windows Sandbox...' + Install-PackageProvider -Name NuGet -Force | Out-Null + Install-Module -Name Microsoft.WinGet.Client -Repository PSGallery -Force -Scope AllUsers + Import-Module Microsoft.WinGet.Client + Repair-WinGetPackageManager -AllUsers + } + + $script:winget = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\winget.exe' + if (-not (Test-Path $script:winget)) { + $script:winget = Join-Path (Get-AppxPackage Microsoft.DesktopAppInstaller).InstallLocation 'winget.exe' + } + Invoke-WinGet --version + Invoke-WinGet --info + if (-not $Resume) { + Invoke-WinGet settings --enable LocalManifestFiles + Invoke-WinGet validate --manifest C:\Manifest + Invoke-WinGet show --id Microsoft.DotNet.DesktopRuntime.10 --exact --source winget --accept-source-agreements --disable-interactivity + # Let the manifest install its dependency; do not preinstall .NET. + Invoke-WinGet install --manifest C:\Manifest --dependency-source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity --verbose-logs + } + + $registration = @(Get-LogExpertRegistration) + if ($registration.Count -ne 1) { + throw "Expected one machine-wide LogExpert uninstall entry, found $($registration.Count)." + } + $registration | Select-Object DisplayName, DisplayVersion, Publisher, InstallLocation, PSChildName | + Format-List | Out-String | Tee-Object -FilePath (Join-Path $results 'registration.txt') | Write-Host + $expectedVersion = (Select-String -Path C:\Manifest\LogExperts.LogExpert.yaml -Pattern '^PackageVersion:\s*(.+)$').Matches.Groups[1].Value.Trim() + if ($registration[0].DisplayVersion -ne $expectedVersion) { + throw "Installed version does not match manifest version $expectedVersion." + } + # Inno registers a versioned display name. Query installed applications without + # requiring an exact unversioned name or membership in the published catalog. + # The registry checks above verify the precise product code and version. + Invoke-WinGet list --name LogExpert --accept-source-agreements --disable-interactivity + $dotnet = Join-Path $env:ProgramFiles 'dotnet\dotnet.exe' + $runtimes = & $dotnet --list-runtimes + $runtimes | Tee-Object -FilePath (Join-Path $results 'runtimes.txt') | Write-Host + if ($LASTEXITCODE -ne 0 -or -not ($runtimes -match '^Microsoft.WindowsDesktop.App 10\.')) { + throw 'The x64 .NET 10 Desktop Runtime is missing.' + } + $exe = Join-Path $registration[0].InstallLocation 'LogExpert.exe' + $sample = Join-Path $results 'sample.log' + @('2026-09-06 INFO Sandbox installation completed', '2026-09-06 ERROR Sample error for filtering') | + Set-Content -Path $sample + Start-Process -FilePath $exe -ArgumentList ('"{0}"' -f $sample) + Write-Host 'Check that LogExpert opens the sample log, displays both lines, and can filter for ERROR.' + Write-Host 'Also check the columnizer menu and Help/About. An installer success alone is not an application pass.' + # Sandbox logon may not provide usable console input. A dialog waits for an + # explicit result independently of PowerShell's console visibility/stdin. + $dialogs = New-Object -ComObject WScript.Shell + do { + $message = "Check the sample log, filtering for ERROR, the columnizer menu, and Help/About.`r`n`r`nClose LogExpert when finished, then click Yes if all checks passed, or No if something failed." + # Yes/No, question icon, default No, bring to foreground; no timeout. + $answer = $dialogs.Popup($message, 0, 'LogExpert application check', 4 + 32 + 256 + 65536) + Write-Host "Manual application check dialog result: $answer (6 = Yes)" + if ($answer -ne 6) { throw 'Manual application smoke test was not confirmed as PASS.' } + $stillRunning = @(Get-Process LogExpert -ErrorAction SilentlyContinue).Count -gt 0 + if ($stillRunning) { + [void]$dialogs.Popup('LogExpert is still running. Close it before confirming the check.', 0, 'Close LogExpert', 48 + 65536) + } + } while ($stillRunning) + Invoke-WinGet uninstall --product-code '{9C6E17B8-912C-45F9-9E7F-49CEAD8D6D7A}_is1' --silent --disable-interactivity + if (@(Get-LogExpertRegistration).Count -ne 0 -or (Test-Path $exe)) { + throw 'Uninstall left the application executable or uninstall registration behind.' + } + if ($Resume) { + Write-Host 'PASS: resumed registration, runtime, manual application check, and uninstall. See the original run for installation results.' -ForegroundColor Green + } + else { + Write-Host 'PASS: install, dependency, registration, manual application check, and uninstall.' -ForegroundColor Green + } +} +catch { + Write-Host "FAIL: $($_.Exception.Message)" -ForegroundColor Red + $_ | Out-String | Set-Content (Join-Path $results 'failure.txt') +} +finally { + $logLocations = @{ + 'WinGet-logs' = Join-Path $env:LOCALAPPDATA 'Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\DiagOutputDir' + 'WinGet-temp-logs' = Join-Path $env:TEMP 'WinGet' + } + foreach ($entry in $logLocations.GetEnumerator()) { + if (Test-Path $entry.Value) { + Copy-Item -Path $entry.Value -Destination (Join-Path $results $entry.Key) -Recurse -Force + } + } + Write-Host "Results: $results. Copy them to the host before closing Windows Sandbox." + Stop-Transcript +} diff --git a/winget/LogExperts.LogExpert.installer.yaml b/winget/LogExperts.LogExpert.installer.yaml index a8fc64ea..2adcd926 100644 --- a/winget/LogExperts.LogExpert.installer.yaml +++ b/winget/LogExperts.LogExpert.installer.yaml @@ -4,6 +4,8 @@ PackageIdentifier: LogExperts.LogExpert PackageVersion: 1.40.3 InstallerType: inno +Scope: machine +ProductCode: '{9C6E17B8-912C-45F9-9E7F-49CEAD8D6D7A}_is1' Dependencies: PackageDependencies: - PackageIdentifier: Microsoft.DotNet.DesktopRuntime.10 @@ -13,4 +15,4 @@ Installers: InstallerSha256: f32bde3703da1db0c1a963d37ee0eeab029bb18cbe3b8730ea12eda0eaafe2fa ManifestType: installer ManifestVersion: 1.10.0 -ReleaseDate: 2026-06-12 \ No newline at end of file +ReleaseDate: 2026-06-15