diff --git a/CHANGELOG.md b/CHANGELOG.md index 2910f4d..72b2b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.5.1 — 2026-10-15 + +- An icon: the program, the installer, Settings → Apps, the Start menu and the Claude Desktop extension. +- Start menu folder "ADO Lens": settings (`adolens setup`), status, skills for Claude Desktop, uninstall. +- The installer puts the program folder on the user's PATH: `adolens` works in any new terminal. +- Uninstall asks whether to remove the stored PATs and the settings and cache too (default: keep them); it also takes + the program off the PATH. `adolens logout --all` removes every stored PAT. + ## 0.5.0 — 2026-10-15 Tests, templates, and two more ways to install. diff --git a/README.md b/README.md index c6bb1cd..9a8fb44 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +ADO Lens + # ADO Lens for Azure DevOps — an MCP server for Claude [![CI](https://github.com/willowdev-code/ado-lens/actions/workflows/ci.yml/badge.svg)](https://github.com/willowdev-code/ado-lens/actions/workflows/ci.yml) @@ -47,8 +49,12 @@ The install package is not part of the repository (`dist\` is excluded by `.giti administrator rights. It asks for the Azure DevOps link and the PAT (checked before anything is installed), who uses it (Product Owner / developer / everything), installs the skills and registers ADO Lens in Claude; an update skips the questions. Until the program is signed, Windows SmartScreen may show "Windows protected your PC" for a file downloaded -in a browser: *More info → Run anyway* (or use the one-line install above, which avoids it). Uninstall it from -*Settings → Apps*. +in a browser: *More info → Run anyway* (or use the one-line install above, which avoids it). + +After installing, the Start menu has an **ADO Lens** folder: *settings* (mode, environments, another connection — +`adolens setup` in a console), *status* (is everything working) and the skills for Claude Desktop; `adolens` works in +any new terminal. Uninstall from *Settings → Apps → ADO Lens* (or the Start menu): it removes the program, its Claude +registrations and skills, and asks whether to remove the stored PATs and settings too (by default they stay). **Claude Desktop only (extension).** `ado-lens-X.Y.Z.mcpb` from **Releases**: open it with Claude Desktop (or drag it into *Settings → Extensions*) and fill in the project link and the PAT, which Claude Desktop keeps in the system's secure @@ -103,7 +109,7 @@ mentions it once per session; say "update ADO Lens" and `ado_upgrade` downloads installer in its own window. From a terminal: `adolens upgrade` shows what is new, `adolens upgrade --install` installs it (`--pre` includes test releases). Settings, PATs and metadata are kept; Claude may stay open. -Uninstall: `uninstall.ps1` in the installation folder (removes the Claude registrations and skills of this installation, +Uninstall (ZIP and one-line installs): `uninstall.ps1` in the installation folder (removes the Claude registrations and skills of this installation, asks about the stored PATs and the local data). ### The PAT diff --git a/README.pl.md b/README.pl.md index 2f7137a..e00ef9e 100644 --- a/README.pl.md +++ b/README.pl.md @@ -1,3 +1,5 @@ +ADO Lens + # ADO Lens for Azure DevOps — serwer MCP dla Claude'a [![CI](https://github.com/willowdev-code/ado-lens/actions/workflows/ci.yml/badge.svg)](https://github.com/willowdev-code/ado-lens/actions/workflows/ci.yml) @@ -48,7 +50,11 @@ administratora. Pyta o link do Azure DevOps i PAT (sprawdzane, zanim cokolwiek s Owner / programista / wszystko), instaluje skille i rejestruje ADO Lens w Claudzie; aktualizacja pomija pytania. Dopóki program nie jest podpisany, Windows SmartScreen może przy pliku pobranym przeglądarką pokazać „System Windows ochronił ten komputer”: *Więcej informacji → Uruchom mimo to* (albo użyj instalacji jedną linijką powyżej, która tego unika). -Odinstalowanie: *Ustawienia → Aplikacje*. + +Po instalacji w menu Start jest folder **ADO Lens**: *ustawienia* (tryb, środowiska, kolejne połączenie — `adolens setup` +w konsoli), *status* (czy wszystko działa) i skille dla Claude Desktop; `adolens` działa w każdym nowym terminalu. +Odinstalowanie: *Ustawienia → Aplikacje → ADO Lens* (albo z menu Start) — usuwa program, jego rejestracje w Claudzie +i skille, i pyta, czy usunąć też zapisane PAT-y i ustawienia (domyślnie zostają). **Tylko Claude Desktop (rozszerzenie).** `ado-lens-X.Y.Z.mcpb` z **Releases**: otwórz go w Claude Desktop (albo przeciągnij do *Settings → Extensions*) i wpisz link do projektu oraz PAT, który Claude Desktop trzyma w bezpiecznym @@ -105,7 +111,7 @@ w sesji; wystarczy powiedzieć „zaktualizuj ADO Lens”, a `ado_upgrade` pobie instalator w osobnym oknie. Z terminala: `adolens upgrade` pokazuje, co nowego, `adolens upgrade --install` instaluje (`--pre` uwzględnia wydania testowe). Ustawienia, PAT-y i metadane zostają; Claude może być otwarty. -Deinstalacja: `uninstall.ps1` w folderze instalacji (usuwa rejestracje i skille tej instalacji w Claude, pyta o zapisane +Deinstalacja (instalacja z ZIP-a i jedną linijką): `uninstall.ps1` w folderze instalacji (usuwa rejestracje i skille tej instalacji w Claude, pyta o zapisane PAT-y i dane lokalne). ### PAT diff --git a/pack.ps1 b/pack.ps1 index 9a5471f..7335e7b 100644 --- a/pack.ps1 +++ b/pack.ps1 @@ -92,6 +92,7 @@ if (-not $NoZip) { [IO.File]::WriteAllText((Join-Path $mcpbStage 'manifest.json'), $manifest, (New-Object Text.UTF8Encoding $false)) Copy-Item (Join-Path $stage 'bin') $mcpbStage -Recurse Copy-Item (Join-Path $stage 'LICENSE') $mcpbStage + Copy-Item (Join-Path $PSScriptRoot 'packaging\icon\icon.png') $mcpbStage $mcpb = Join-Path $OutDir "ado-lens-$version.mcpb" New-Zip $mcpbStage $mcpb $false Write-Checksum $mcpb 'mcpb' diff --git a/packaging/icon/adolens.ico b/packaging/icon/adolens.ico new file mode 100644 index 0000000..37bf0eb Binary files /dev/null and b/packaging/icon/adolens.ico differ diff --git a/packaging/icon/icon.png b/packaging/icon/icon.png new file mode 100644 index 0000000..c3ba3d6 Binary files /dev/null and b/packaging/icon/icon.png differ diff --git a/packaging/icon/make-icon.ps1 b/packaging/icon/make-icon.ps1 new file mode 100644 index 0000000..610ae91 --- /dev/null +++ b/packaging/icon/make-icon.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS + Draws the ADO Lens icon: a lens over three work item bars on an Azure blue tile. + Writes adolens.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: Azure blue, 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, 0, 137, 230)), ([System.Drawing.Color]::FromArgb(255, 0, 90, 170)) + $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 32) { + # Three work item bars inside the lens (left out at small sizes, where they would blur). + $bar = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(235, 255, 255, 255)) + $bh = $s * 0.045 + foreach ($i in 0..2) { + $w = $r * (1.05 - 0.25 * $i) + $y = $cy - $r * 0.42 + $i * $r * 0.42 - $bh / 2 + $path = New-RoundedRect ($cx - $r * 0.55) $y $w $bh ($bh / 2) + $g.FillPath($bar, $path) + } + } + $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 'adolens.ico'), $out.ToArray()) + +$big = New-IconBitmap 512 +$big.Save((Join-Path $dir 'icon.png'), [System.Drawing.Imaging.ImageFormat]::Png) +$big.Dispose() +Write-Host "written adolens.ico and icon.png in $dir" diff --git a/packaging/inno/ado-lens.iss b/packaging/inno/ado-lens.iss index 3b0cb7a..e7039f8 100644 --- a/packaging/inno/ado-lens.iss +++ b/packaging/inno/ado-lens.iss @@ -26,7 +26,11 @@ AppSupportURL=https://github.com/willowdev-code/ado-lens/issues DefaultDirName={localappdata}\Programs\ADO Lens DisableDirPage=yes DisableProgramGroupPage=yes +DefaultGroupName=ADO Lens DisableReadyPage=yes +SetupIconFile={#SourcePath}..\icon\adolens.ico +; The program folder goes on the user's PATH, so "adolens" works in any new terminal. +ChangesEnvironment=yes PrivilegesRequired=lowest ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible @@ -52,6 +56,19 @@ Source: "{#DesktopSkills}\*.zip"; DestDir: "{app}\skills-for-claude-desktop"; Fl ; Claude Code reads skills from %USERPROFILE%\.claude\skills (only when Claude Code is there); removed on uninstall. Source: "{#Source}\skills\*"; DestDir: "{%USERPROFILE}\.claude\skills"; Flags: recursesubdirs ignoreversion; Check: HasClaudeCode +[Icons] +; Start menu: settings and status open in a console that stays open (adolens commands work there too). +Name: "{group}\{cm:IconSettings}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\adolens.exe"" setup"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\adolens.exe" +Name: "{group}\{cm:IconStatus}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\adolens.exe"" status"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\adolens.exe" +Name: "{group}\{cm:IconSkills}"; Filename: "{app}\skills-for-claude-desktop" +Name: "{group}\{cm:UninstallProgram,ADO Lens}"; Filename: "{uninstallexe}"; IconFilename: "{app}\bin\adolens.exe" + +[Registry] +Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}\bin"; Check: NeedsAddPath(ExpandConstant('{app}\bin')) + +[UninstallDelete] +Type: files; Name: "{app}\bin\*.old" + [UninstallRun] Filename: "{app}\bin\adolens.exe"; Parameters: "unregister"; Flags: runhidden waituntilterminated; RunOnceId: "unregister" @@ -60,6 +77,14 @@ Filename: "{app}\skills-for-claude-desktop"; Description: "{cm:OpenSkills}"; Fla Filename: "https://github.com/willowdev-code/ado-lens#readme"; Description: "{cm:OpenReadme}"; Flags: postinstall shellexec skipifsilent unchecked [CustomMessages] +en.IconSettings=ADO Lens - settings +en.IconStatus=ADO Lens - status +en.IconSkills=ADO Lens - skills for Claude Desktop +en.RemoveData=Also remove the stored personal access tokens (Windows Credential Manager) and the settings and cache of ADO Lens?%n%nChoose No to keep them for a later installation. +pl.IconSettings=ADO Lens - ustawienia +pl.IconStatus=ADO Lens - status +pl.IconSkills=ADO Lens - skille dla Claude Desktop +pl.RemoveData=Usunąć też zapisane tokeny dostępu (Menedżer poświadczeń Windows) oraz ustawienia i pamięć podręczną ADO Lens?%n%nWybierz Nie, aby zachować je na potrzeby ponownej instalacji. en.ConnTitle=Azure DevOps en.ConnSub=Where your work items are en.ConnText=Paste a link to your Azure DevOps project from the browser (for example https://dev.azure.com/contoso/Sales), or the organization name. @@ -119,6 +144,64 @@ var function SetEnvironmentVariable(lpName: String; lpValue: String): Boolean; external 'SetEnvironmentVariableW@kernel32.dll stdcall'; +{ Whether a folder is missing from the user's PATH (compared without case and trailing backslash). } +function NeedsAddPath(Dir: String): Boolean; +var + Path: String; +begin + if not RegQueryStringValue(HKCU, 'Environment', 'Path', Path) then + begin + Result := True; + Exit; + end; + Result := Pos(';' + Lowercase(RemoveBackslashUnlessRoot(Dir)) + ';', ';' + Lowercase(Path) + ';') = 0; +end; + +procedure RemoveFromPath(Dir: String); +var + Path, Item, Kept: String; + P: Integer; +begin + if not RegQueryStringValue(HKCU, 'Environment', 'Path', Path) then Exit; + Kept := ''; + Path := Path + ';'; + while Path <> '' do + begin + P := Pos(';', Path); + Item := Copy(Path, 1, P - 1); + Delete(Path, 1, P); + if (Item <> '') and (Lowercase(RemoveBackslashUnlessRoot(Item)) <> Lowercase(RemoveBackslashUnlessRoot(Dir))) then + begin + if Kept <> '' then Kept := Kept + ';'; + Kept := Kept + Item; + end; + end; + RegWriteExpandStringValue(HKCU, 'Environment', 'Path', Kept); +end; + +var + RemoveUserData: Boolean; + +{ Before the files go: ask whether to remove the stored PATs and the local data (default: keep them), and sign out + with the program while it is still there. After: take the program folder off the PATH and delete the data. } +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + Code: Integer; +begin + if CurUninstallStep = usUninstall then + begin + RemoveUserData := not UninstallSilent and + (MsgBox(CustomMessage('RemoveData'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES); + if RemoveUserData then + Exec(ExpandConstant('{app}\bin\adolens.exe'), 'logout --all', '', SW_HIDE, ewWaitUntilTerminated, Code); + end + else if CurUninstallStep = usPostUninstall then + begin + RemoveFromPath(ExpandConstant('{app}\bin')); + if RemoveUserData then DelTree(ExpandConstant('{localappdata}\ado-lens'), True, True, True); + end; +end; + function HasClaudeCode: Boolean; begin Result := DirExists(ExpandConstant('{%USERPROFILE}\.claude')); diff --git a/packaging/mcpb/manifest.json b/packaging/mcpb/manifest.json index 5c53897..64930b9 100644 --- a/packaging/mcpb/manifest.json +++ b/packaging/mcpb/manifest.json @@ -17,6 +17,7 @@ "documentation": "https://github.com/willowdev-code/ado-lens#readme", "support": "https://github.com/willowdev-code/ado-lens/issues", "license": "MIT", + "icon": "icon.png", "keywords": ["azure devops", "work items", "sprint", "wiki", "tfvc", "git", "pull requests"], "server": { "type": "binary", diff --git a/src/AdoLens/AdoLens.csproj b/src/AdoLens/AdoLens.csproj index f579b8d..d08b4fa 100644 --- a/src/AdoLens/AdoLens.csproj +++ b/src/AdoLens/AdoLens.csproj @@ -12,6 +12,8 @@ true true en + + ..\..\packaging\icon\adolens.ico @@ -21,7 +23,7 @@ ADO Lens for Azure DevOps Azure DevOps (work items, boards, wiki, Git and TFVC, pipelines) served to AI assistants over MCP Copyright © 2026 WillowDev - 0.5.0 + 0.5.1 diff --git a/src/AdoLens/Cli.cs b/src/AdoLens/Cli.cs index e371266..8a819ab 100644 --- a/src/AdoLens/Cli.cs +++ b/src/AdoLens/Cli.cs @@ -22,7 +22,7 @@ public static void PrintUsage() => Console.WriteLine(""" login add or update a connection: organization, default project and PAT --org --project --team --connection --pat-stdin (read the PAT from standard input) --default (make it the default connection) - logout remove the stored PAT of a connection --connection + logout remove the stored PAT of a connection --connection | --all connections list the connections config show or change settings --connection --project --team (defaults of a connection) @@ -184,6 +184,13 @@ public async Task LoginAsync() public int Logout() { var settings = Settings.Load(SettingsPath); + if (cl.Flag("all")) + { + // Uninstall: every stored PAT of the connections in the settings. + foreach (var conn in settings.Connections.Keys) + Console.WriteLine(CredentialStore.Delete(conn) ? $"removed {CredentialStore.TargetFor(conn)}" : $"no stored PAT for '{conn}'"); + return 0; + } var name = cl.Opt("connection") ?? settings.DefaultConnection; if (string.IsNullOrWhiteSpace(name)) { Console.WriteLine("no connection configured - nothing to remove"); return 0; } Console.WriteLine(CredentialStore.Delete(name) diff --git a/src/AdoLens/CommandLine.cs b/src/AdoLens/CommandLine.cs index 1212b13..1c015f7 100644 --- a/src/AdoLens/CommandLine.cs +++ b/src/AdoLens/CommandLine.cs @@ -17,7 +17,7 @@ public sealed partial class CommandLine ["login"] = ["connection=", "org=", "project=", "team=", "pat-stdin", "pat-env=", "default"], ["tests"] = ["connection=", "team=", "detail="], ["test-create"] = ["connection=", "file=", "plan=", "suite="], - ["logout"] = ["connection="], + ["logout"] = ["connection=", "all"], ["connections"] = [], ["trace"] = ["connection=", "detail="], ["pr"] = ["connection=", "project=", "repo=", "list=", "detail="],