Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
All notable changes to xpp-lens (called xpp-graft up to 1.2.0). Versions follow `<Version>` in
`src/XppLens/XppLens.csproj`; release tags are `vX.Y.Z`.

## 1.3.3

### Added
- An icon (a lens over X++ on a violet tile) for `xpplens.exe`, the setup program, the Start menu and Windows'
installed apps; drawn by `packaging/icon/make-icon.ps1`.
- Start menu entry `xpp-lens`: a console that shows the command list and takes any `xpplens` command, also right
after installing, before Explorer picks up the new PATH.

### Fixed
- `xpplens` failed to start with "Failed to determine default extraction location" when TEMP pointed to a missing
folder (a remote desktop session's `Temp\<n>` after a cleanup), e.g. from the Start menu entries. The SQLite library
now ships next to `xpplens.exe` (`bin\e_sqlite3.dll`) instead of being extracted to TEMP at every start; the setup
program renames it like the running EXE when an open Claude session holds it.

## 1.3.2 — 2026-10-26

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The install package is not part of the repository (`dist\` is excluded by `.giti
**A. Setup program** (from 1.3.0) — download `xpp-lens-setup-X.Y.Z.exe` from the **Releases** tab and run it. A
wizard in Polish or English, for the current user, no administrator rights: it finds `PackagesLocalDirectory`, asks for
the label languages and whether to index the Microsoft standard, registers xpp-lens in Claude and offers to build the
index. It puts `xpplens` on your PATH, adds Start menu entries (status, update the index, check for updates) and an
index. It puts `xpplens` on your PATH, adds Start menu entries (an `xpp-lens` console with the command list, status, update the index, check for updates) and an
entry in Windows' installed apps. An installation made by `install.ps1` in `C:\Tools\xpp-lens` is taken over in place,
with its settings and index. The program is not signed yet: when Windows shows "Windows protected your PC", choose
"More info" → "Run anyway".
Expand Down
2 changes: 1 addition & 1 deletion README.pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Paczki instalacyjnej nie ma w repozytorium (`dist\` jest pomijany przez `.gitign
**A. Instalator** (od 1.3.0) — pobierz `xpp-lens-setup-X.Y.Z.exe` z zakładki **Releases** i uruchom. Kreator po polsku
albo po angielsku, dla bieżącego użytkownika, bez uprawnień administratora: wykrywa `PackagesLocalDirectory`, pyta
o języki etykiet i o indeksowanie standardu Microsoftu, rejestruje xpp-lens w Claude i proponuje zbudowanie indeksu.
Dopisuje `xpplens` do PATH, dodaje skróty w menu Start (status, aktualizacja indeksu, sprawdzenie aktualizacji) i wpis
Dopisuje `xpplens` do PATH, dodaje skróty w menu Start (konsola `xpp-lens` z listą poleceń, status, aktualizacja indeksu, sprawdzenie aktualizacji) i wpis
w „Aplikacjach” Windows. Instalację zrobioną przez `install.ps1` w `C:\Tools\xpp-lens` przejmuje w miejscu, razem
z ustawieniami i indeksem. Program nie jest jeszcze podpisany: gdy Windows pokaże „System Windows ochronił komputer”,
wybierz „Więcej informacji” → „Uruchom mimo to”.
Expand Down
4 changes: 3 additions & 1 deletion pack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ if ($FrameworkDependent) {
$publishArgs += @('--self-contained', 'false')
}
else {
$publishArgs += @('--self-contained', 'true', '-p:PublishSingleFile=true', '-p:IncludeNativeLibrariesForSelfExtract=true', '-p:EnableCompressionInSingleFile=true')
# Native libraries (SQLite) stay next to the EXE: bundled ones would be extracted to %TEMP% at every start, which
# fails when TEMP points to a missing folder (a remote desktop session's Temp\<n> after a cleanup).
$publishArgs += @('--self-contained', 'true', '-p:PublishSingleFile=true', '-p:IncludeNativeLibrariesForSelfExtract=false', '-p:EnableCompressionInSingleFile=true')
}

Write-Host "publishing ($(if ($FrameworkDependent) { 'requires .NET 9' } else { 'self-contained' }))..." -ForegroundColor Cyan
Expand Down
Binary file added packaging/icon/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 100 additions & 0 deletions packaging/icon/make-icon.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<#
.SYNOPSIS
Draws the xpp-lens icon: a lens over an X on a violet tile (X++ code under the lens).
Writes xpplens.ico (16-256 px, PNG frames) and icon.png (512 px) next to this script.
#>
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing

function New-RoundedRect([float] $x, [float] $y, [float] $w, [float] $h, [float] $r) {
$p = New-Object System.Drawing.Drawing2D.GraphicsPath
$d = 2 * $r
$p.AddArc($x, $y, $d, $d, 180, 90)
$p.AddArc($x + $w - $d, $y, $d, $d, 270, 90)
$p.AddArc($x + $w - $d, $y + $h - $d, $d, $d, 0, 90)
$p.AddArc($x, $y + $h - $d, $d, $d, 90, 90)
$p.CloseFigure()
return $p
}

function New-IconBitmap([int] $size) {
$bmp = New-Object System.Drawing.Bitmap $size, $size, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$g.Clear([System.Drawing.Color]::Transparent)
$s = [float] $size

# Tile: violet, lighter at the top.
$margin = [Math]::Max(0.5, $s * 0.03)
$tile = New-RoundedRect $margin $margin ($s - 2 * $margin) ($s - 2 * $margin) ($s * 0.2)
$grad = New-Object System.Drawing.Drawing2D.LinearGradientBrush (New-Object System.Drawing.PointF 0, 0), (New-Object System.Drawing.PointF 0, $s),
([System.Drawing.Color]::FromArgb(255, 134, 76, 204)), ([System.Drawing.Color]::FromArgb(255, 78, 36, 138))
$g.FillPath($grad, $tile)

$white = [System.Drawing.Color]::White
# Lens: a ring, centred a little up and left, with a handle to the bottom right.
$cx = $s * 0.43; $cy = $s * 0.43; $r = $s * 0.24
$ring = [Math]::Max(1.5, $s * 0.075)
if ($size -ge 24) {
# An X inside the lens (left out at 16-20 px, where it would blur into the ring).
$x = New-Object System.Drawing.Pen $white, ([Math]::Max(1.5, $s * 0.06))
$x.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
$x.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
$a = $r * 0.4
$g.DrawLine($x, $cx - $a, $cy - $a, $cx + $a, $cy + $a)
$g.DrawLine($x, $cx - $a, $cy + $a, $cx + $a, $cy - $a)
}
if ($size -ge 32) {
# "++" below the lens, left of the handle: together X++.
$plus = New-Object System.Drawing.Pen $white, ([Math]::Max(1.5, $s * 0.05))
$plus.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
$plus.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
$p = $s * 0.055; $py = $s * 0.83
foreach ($px in ($s * 0.24), ($s * 0.42)) {
$g.DrawLine($plus, $px - $p, $py, $px + $p, $py)
$g.DrawLine($plus, $px, $py - $p, $px, $py + $p)
}
}
$pen = New-Object System.Drawing.Pen $white, $ring
$g.DrawEllipse($pen, $cx - $r, $cy - $r, 2 * $r, 2 * $r)
$handle = New-Object System.Drawing.Pen $white, ([Math]::Max(2, $s * 0.11))
$handle.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
$handle.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
$a = 0.7071 * ($r + $ring / 2)
$g.DrawLine($handle, $cx + $a, $cy + $a, $s * 0.76, $s * 0.76)
$g.Dispose()
return $bmp
}

$dir = $PSScriptRoot
$sizes = 16, 20, 24, 32, 40, 48, 64, 128, 256
$frames = foreach ($n in $sizes) {
$bmp = New-IconBitmap $n
$ms = New-Object System.IO.MemoryStream
$bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
, $ms.ToArray()
}

# ICO: header, one directory entry per size, then the PNG frames.
$out = New-Object System.IO.MemoryStream
$w = New-Object System.IO.BinaryWriter $out
$w.Write([UInt16]0); $w.Write([UInt16]1); $w.Write([UInt16]$sizes.Count)
$offset = 6 + 16 * $sizes.Count
for ($i = 0; $i -lt $sizes.Count; $i++) {
$n = $sizes[$i]
$dim = if ($n -ge 256) { 0 } else { $n }
$w.Write([byte]$dim); $w.Write([byte]$dim); $w.Write([byte]0); $w.Write([byte]0)
$w.Write([UInt16]1); $w.Write([UInt16]32)
$w.Write([UInt32]$frames[$i].Length); $w.Write([UInt32]$offset)
$offset += $frames[$i].Length
}
foreach ($f in $frames) { $w.Write($f) }
$w.Flush()
[IO.File]::WriteAllBytes((Join-Path $dir 'xpplens.ico'), $out.ToArray())

$big = New-IconBitmap 512
$big.Save((Join-Path $dir 'icon.png'), [System.Drawing.Imaging.ImageFormat]::Png)
$big.Dispose()
Write-Host "written xpplens.ico and icon.png in $dir"
Binary file added packaging/icon/xpplens.ico
Binary file not shown.
44 changes: 37 additions & 7 deletions packaging/inno/xpp-lens.iss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ DisableDirPage=yes
DisableProgramGroupPage=yes
DefaultGroupName=xpp-lens
DisableReadyPage=yes
SetupIconFile={#SourcePath}..\icon\xpplens.ico
; The program folder goes on the user's PATH, so "xpplens" works in any new terminal.
ChangesEnvironment=yes
PrivilegesRequired=lowest
Expand All @@ -56,6 +57,9 @@ Source: "{#Source}\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion

[Icons]
; Start menu: commands open in a console that stays open (xpplens commands work there too).
; "xpp-lens" is the general one: the command list, then any xpplens command (PATH set here too,
; in case Explorer has not picked up the new PATH yet).
Name: "{group}\xpp-lens"; Filename: "{cmd}"; Parameters: "/k ""title xpp-lens & set ""PATH={app}\bin;%PATH%"" & xpplens help"""; WorkingDir: "{%USERPROFILE}"; IconFilename: "{app}\bin\xpplens.exe"; Comment: "{cm:IconConsole}"
Name: "{group}\{cm:IconStatus}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\xpplens.exe"" status"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\xpplens.exe"
Name: "{group}\{cm:IconBuild}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\xpplens.exe"" build"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\xpplens.exe"
Name: "{group}\{cm:IconUpdate}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\xpplens.exe"" update"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\xpplens.exe"
Expand Down Expand Up @@ -98,6 +102,7 @@ en.BuildNow=Build the index now (a console window shows the progress)
en.RestartClaude=Restart Claude Desktop so all its sessions use the new version (interrupts work in progress there)
en.OpenReadme=Read how to use xpp-lens
en.ReadmeUrl=https://github.com/willowdev-code/xpp-lens#readme
en.IconConsole=Console with the xpplens commands
en.IconStatus=xpp-lens - status
en.IconBuild=xpp-lens - update the index
en.IconUpdate=xpp-lens - check for updates
Expand All @@ -124,6 +129,7 @@ pl.BuildNow=Zbuduj indeks teraz (okno konsoli pokaże postęp)
pl.RestartClaude=Uruchom ponownie Claude Desktop, żeby wszystkie jego sesje używały nowej wersji (przerywa trwającą tam pracę)
pl.OpenReadme=Przeczytaj, jak używać xpp-lens
pl.ReadmeUrl=https://github.com/willowdev-code/xpp-lens/blob/main/README.pl.md
pl.IconConsole=Konsola z poleceniami xpplens
pl.IconStatus=xpp-lens - status
pl.IconBuild=xpp-lens - aktualizacja indeksu
pl.IconUpdate=xpp-lens - sprawdź aktualizacje
Expand Down Expand Up @@ -319,17 +325,41 @@ begin
end;
end;

{ A running xpplens.exe (an open Claude session) cannot be overwritten but can be renamed; xpplens removes *.old
files when it starts. Whether this is an update is decided here, before the files are copied. }
function PrepareToInstall(var NeedsRestart: Boolean): String;
{ A running xpplens.exe and the e_sqlite3.dll it loaded (an open Claude session) cannot be overwritten but can be
renamed; xpplens removes *.old files when it starts. Whether this is an update is decided here, before the files
are copied. }
procedure RenameInUse(Pattern: String);
var
Exe: String;
Bin, Stamp: String;
Found: TFindRec;
Names: TArrayOfString;
I: Integer;
begin
Bin := ExpandConstant('{app}\bin\');
Stamp := GetDateTimeString('yyyymmddhhnnss', #0, #0);
{ Names first, renames after: renaming while the folder is being listed could list a file twice. }
if FindFirst(Bin + Pattern, Found) then
try
repeat
if (Found.Attributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
begin
SetArrayLength(Names, GetArrayLength(Names) + 1);
Names[GetArrayLength(Names) - 1] := Found.Name;
end;
until not FindNext(Found);
finally
FindClose(Found);
end;
for I := 0 to GetArrayLength(Names) - 1 do
RenameFile(Bin + Names[I], Bin + Names[I] + '.' + Stamp + '.old');
end;

function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
Result := '';
WasUpdate := FileExists(ExpandConstant('{app}\xpplens.json'));
Exe := ExpandConstant('{app}\bin\xpplens.exe');
if FileExists(Exe) then
RenameFile(Exe, Exe + '.' + GetDateTimeString('yyyymmddhhnnss', #0, #0) + '.old');
RenameInUse('*.exe');
RenameInUse('*.dll');
end;

procedure CurStepChanged(CurStep: TSetupStep);
Expand Down
4 changes: 3 additions & 1 deletion src/XppLens/XppLens.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<!-- Drawn by packaging/icon/make-icon.ps1; shown in Explorer, Settings -> Apps and the Start menu. -->
<ApplicationIcon>..\..\packaging\icon\xpplens.ico</ApplicationIcon>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -20,7 +22,7 @@
<AssemblyTitle>xpp-lens — X++ code index for MCP</AssemblyTitle>
<Description>X++ (Dynamics 365 F&amp;O) code index served to AI assistants over MCP</Description>
<Copyright>Copyright © 2026 WillowDev</Copyright>
<Version>1.3.2</Version>
<Version>1.3.3</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading