From 7a41206f4a8715d5f69b89f7e395758a6f0ac516 Mon Sep 17 00:00:00 2001 From: WillowDev <216433317+willowdev-code@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:55:41 +0100 Subject: [PATCH 1/4] Ship the xpp-ponytail Claude Code skills: install in the installation language, keep skills changed locally --- CHANGELOG.md | 17 ++ README.md | 25 ++- README.pl.md | 25 ++- install.ps1 | 30 +++- pack.ps1 | 2 + packaging/inno/xpp-lens.iss | 63 ++++++- release.ps1 | 3 +- skills/THIRD-PARTY-NOTICES.md | 31 ++++ skills/xpp-ponytail-review/SKILL.md | 102 ++++++++++++ skills/xpp-ponytail-review/SKILL.pl.md | 104 ++++++++++++ skills/xpp-ponytail/SKILL.md | 98 +++++++++++ skills/xpp-ponytail/SKILL.pl.md | 98 +++++++++++ src/XppLens/CommandLine.cs | 1 + src/XppLens/IndexService.cs | 10 ++ src/XppLens/Program.cs | 18 ++ src/XppLens/Skills.cs | 217 +++++++++++++++++++++++++ tests/XppLens.Tests/SkillsTests.cs | 117 +++++++++++++ uninstall.ps1 | 14 +- 18 files changed, 965 insertions(+), 10 deletions(-) create mode 100644 skills/THIRD-PARTY-NOTICES.md create mode 100644 skills/xpp-ponytail-review/SKILL.md create mode 100644 skills/xpp-ponytail-review/SKILL.pl.md create mode 100644 skills/xpp-ponytail/SKILL.md create mode 100644 skills/xpp-ponytail/SKILL.pl.md create mode 100644 src/XppLens/Skills.cs create mode 100644 tests/XppLens.Tests/SkillsTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3fbfb..11eec22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to xpp-lens (called xpp-graft up to 1.2.0). Versions follow `` in `src/XppLens/XppLens.csproj`; release tags are `vX.Y.Z`. +## 1.4.0 + +### Added +- Claude Code skills in the package: `/xpp-ponytail` (the smallest complete X++ change: configuration, standard, + the model's own code, a narrow extension, only then new code) and `/xpp-ponytail-review` (a review of an X++ change + with a concrete case and the smallest fix per finding), built on the `xpp_*` tools. Based on + [Ponytail](https://github.com/DietrichGebert/ponytail) (MIT), rewritten for X++; the notice is in + `skills/THIRD-PARTY-NOTICES.md`. The source of the skills is now this repository (`skills\`). +- The setup program and `install.ps1` copy them to `%USERPROFILE%\.claude\skills`, in the language of the installation + (English or Polish descriptions and review headings: the wizard's language, or `install.ps1 -SkillsLanguage en|pl`, + by default the Windows language), kept by updates. A check box in the wizard and `install.ps1 -NoSkills` skip them. +- A skill changed locally is never overwritten or removed: `skills-installed.json` in the installation folder keeps + the SHA-256 of what was installed; a changed skill is kept with a warning (the wizard shows it, `install.ps1` prints + it, `xpplens status` lists it after a silent update). The uninstallers remove only unchanged skills they installed. + A folder of that name not installed by xpp-lens is left alone unless it equals the package's copy. +- `xpplens skills [install [--lang en|pl] | remove | status]`. + ## 1.3.3 ### Added diff --git a/README.md b/README.md index d44289b..e0db65f 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,12 @@ Useful parameters: .\install.ps1 -StandardModels HugeIsvModel # push a model down to the standard tier .\install.ps1 -NoStandard # custom models only (fast, small index) .\install.ps1 -First -NoBuild -NoRegister # non-interactive / CI +.\install.ps1 -NoSkills # without the Claude Code skills +.\install.ps1 -SkillsLanguage pl # skills in Polish (default: the Windows language) ``` -Uninstall: `uninstall.ps1` (removes the MCP entries that point to this installation, asks about the index and files). +Uninstall: `uninstall.ps1` (removes the MCP entries that point to this installation and the skills it installed +unless you changed them, asks about the index and files). New distribution package: `pack.ps1` (add `-FrameworkDependent` if you prefer 5 MB and a .NET 9 requirement). ## Configuration after installation @@ -173,6 +176,24 @@ ReferenceGroup controls show `ref=.`, `replGroup=`. Menu extension elements show `(under )`, `position=` and `menuitem=`. +## Claude Code skills + +The package carries two skills that build on the `xpp_*` tools, installed to `%USERPROFILE%\.claude\skills`: + +- `/xpp-ponytail` — the smallest complete X++ change: configuration first, then Microsoft's standard, the model's + own code, a narrow extension (delegate, CoC, event handler), and only then new code. Levels `lite`, `full`, `ultra`. +- `/xpp-ponytail-review` — a review of an X++ change: correctness, transactions, security, SQL at real volume, + extensions, tests and code that should not exist, each finding with a concrete case and the smallest fix. + +They are installed in the language of the installation (the setup wizard's, or `install.ps1 -SkillsLanguage en|pl`, +by default that of Windows); updates keep it. A skill you change in `.claude\skills` is never overwritten or removed: +the installer keeps it and says so, and `xpplens status` lists it. `xpplens skills` shows the state, +`xpplens skills install --lang pl` switches the language. The setup wizard has a check box for them +(`install.ps1 -NoSkills` skips them). + +Based on [Ponytail](https://github.com/DietrichGebert/ponytail) by Dietrich Gebert (MIT license), rewritten for X++; +the notice is in [skills/THIRD-PARTY-NOTICES.md](skills/THIRD-PARTY-NOTICES.md). + ## Usage statistics Every MCP call is appended to `%LOCALAPPDATA%\xpp-lens\usage\usage-YYYYMM.jsonl` (tool, arguments, answer size, @@ -194,6 +215,7 @@ Switch off with `xpplens config --usage-log false`. xpplens find|object|method|callers|callees|refs|ext|scaffold|build-errors|security|join|entity|changed|grep|label … xpplens build [--full-only] [--std-only] [--compiled-only] [--force] xpplens status [--counts] [--ascii] | stats | update [--install] | detect | config | register | unregister | mcp | version +xpplens skills [install [--lang en|pl] | remove | status] ``` `xpplens help` lists every option. Environment variables: `XPPLENS_CONFIG` (another configuration), @@ -302,6 +324,7 @@ go into a new version. | `xpplens update`, taking over xpp-graft | `Updater.cs`, `Migration.cs` | | release automation | `.github\workflows\release.yml`, `release-notes.ps1`, `pack.ps1` | | CLI commands, configuration, registration in Claude | `Program.cs`, `Config.cs`, `Detect.cs` | +| Claude Code skills (text: `SKILL.md` English, `SKILL.pl.md` Polish — change both) | `skills\\`; installing them: `Skills.cs` | Work loop: diff --git a/README.pl.md b/README.pl.md index 5753b9c..34b5b75 100644 --- a/README.pl.md +++ b/README.pl.md @@ -63,9 +63,12 @@ Przydatne parametry: .\install.ps1 -StandardModels HugeIsvModel # zepchnięcie modelu do poziomu standardowego .\install.ps1 -NoStandard # tylko modele własne (szybko, mały indeks) .\install.ps1 -First -NoBuild -NoRegister # tryb nieinteraktywny / CI +.\install.ps1 -NoSkills # bez skilli Claude Code +.\install.ps1 -SkillsLanguage pl # skille po polsku (domyślnie: język Windows) ``` -Deinstalacja: `uninstall.ps1` (usuwa wpisy MCP wskazujące na tę instalację, pyta o indeks i pliki). +Deinstalacja: `uninstall.ps1` (usuwa wpisy MCP wskazujące na tę instalację i zainstalowane przez nią skille, o ile ich +nie zmieniłeś, pyta o indeks i pliki). Nowy pakiet dystrybucyjny: `pack.ps1` (dodaj `-FrameworkDependent`, jeśli wolisz 5 MB i wymóg .NET 9). ## Konfiguracja po instalacji @@ -174,6 +177,24 @@ Kontrolki ReferenceGroup pokazują `ref=.`, `replGro i `relPath=`. Elementy rozszerzeń menu pokazują `(under )`, `position=` i `menuitem=`. +## Skille Claude Code + +Paczka zawiera dwa skille oparte na narzędziach `xpp_*`, instalowane do `%USERPROFILE%\.claude\skills`: + +- `/xpp-ponytail` — najmniejsza kompletna zmiana w X++: najpierw konfiguracja, potem standard Microsoftu, kod + własnego modelu, wąskie rozszerzenie (delegat, CoC, event handler), a dopiero na końcu nowy kod. Poziomy `lite`, + `full`, `ultra`. +- `/xpp-ponytail-review` — przegląd zmiany w X++: poprawność, transakcje, bezpieczeństwo, SQL przy realnym wolumenie, + rozszerzenia, testy i kod, którego mogłoby nie być; każde znalezisko z konkretnym przypadkiem i najmniejszą poprawką. + +Instalują się w języku instalacji (kreatora albo `install.ps1 -SkillsLanguage en|pl`, domyślnie w języku Windows); +aktualizacje go zachowują. Skilla zmienionego w `.claude\skills` instalator nigdy nie nadpisze ani nie usunie: zostawia +go, mówi o tym, a `xpplens status` go wymienia. `xpplens skills` pokazuje stan, `xpplens skills install --lang en` +zmienia język. W kreatorze jest na nie pole wyboru (`install.ps1 -NoSkills` je pomija). + +Oparte na [Ponytail](https://github.com/DietrichGebert/ponytail) autorstwa Dietricha Geberta (licencja MIT), przepisane +pod X++; nota licencyjna jest w [skills/THIRD-PARTY-NOTICES.md](skills/THIRD-PARTY-NOTICES.md). + ## Statystyki użycia Każde wywołanie MCP jest dopisywane do `%LOCALAPPDATA%\xpp-lens\usage\usage-RRRRMM.jsonl` (narzędzie, argumenty, @@ -195,6 +216,7 @@ Wyłączenie: `xpplens config --usage-log false`. xpplens find|object|method|callers|callees|refs|ext|scaffold|build-errors|security|join|entity|changed|grep|label … xpplens build [--full-only] [--std-only] [--compiled-only] [--force] xpplens status [--counts] [--ascii] | stats | update [--install] | detect | config | register | unregister | mcp | version +xpplens skills [install [--lang en|pl] | remove | status] ``` `xpplens help` wypisuje wszystkie opcje. Zmienne: `XPPLENS_CONFIG` (inna konfiguracja), `XPPLENS_VERBOSE=1` @@ -303,6 +325,7 @@ Tagi wydań są chronione: opublikowanej wersji nie da się zmienić — poprawk | `xpplens update`, przejęcie xpp-graft | `Updater.cs`, `Migration.cs` | | automatyczne wydania | `.github\workflows\release.yml`, `release-notes.ps1`, `pack.ps1` | | polecenia CLI, konfiguracja, rejestracja w Claude | `Program.cs`, `Config.cs`, `Detect.cs` | +| skille Claude Code (tekst: `SKILL.md` po angielsku, `SKILL.pl.md` po polsku — zmieniać oba) | `skills\\`; ich instalacja: `Skills.cs` | Pętla pracy: diff --git a/install.ps1 b/install.ps1 index b762dc1..da785d9 100644 --- a/install.ps1 +++ b/install.ps1 @@ -4,8 +4,9 @@ .DESCRIPTION Copies the files, determines the PackagesLocalDirectory folder (given or detected), - writes the configuration, registers the MCP server in Claude Desktop and Claude Code - and builds the index. The AOS repository is only ever read, never written. + writes the configuration, registers the MCP server in Claude Desktop and Claude Code, + copies the Claude Code skills (xpp-ponytail, xpp-ponytail-review) to ~/.claude/skills + (-NoSkills: not; -SkillsLanguage en|pl, default: the Windows language, then kept for updates) and builds the index. The AOS repository is only ever read, never written. Run it in a regular (non-elevated) PowerShell: the index is created in the %LOCALAPPDATA% of the user who runs the installer, and Claude must be able to write to it. @@ -41,6 +42,9 @@ param( [switch] $NoStandard, [switch] $NoBuild, [switch] $NoRegister, + [switch] $NoSkills, + [ValidateSet('en', 'pl')] + [string] $SkillsLanguage, [switch] $DesktopOnly, [switch] $CodeOnly, [switch] $RestartClaude @@ -117,6 +121,11 @@ else { foreach ($f in 'install.ps1', 'uninstall.ps1', 'restart-claude.ps1', 'README.md', 'README.pl.md', 'CHANGELOG.md', 'LICENSE') { if (Test-Path (Join-Path $src $f)) { Copy-Item (Join-Path $src $f) $InstallDir -Force } } + # The package's copy of the skills (installed to ~/.claude/skills in step 4); replaced whole, so a skill + # dropped from the package does not linger here. + $skillsDst = Join-Path $InstallDir 'skills' + if (Test-Path $skillsDst) { Remove-Item $skillsDst -Recurse -Force } + if (Test-Path (Join-Path $src 'skills')) { Copy-Item (Join-Path $src 'skills') $InstallDir -Recurse -Force } } $exe = Join-Path $InstallDir 'bin\xpplens.exe' Say "done: $exe" @@ -161,7 +170,7 @@ if ($NoStandard) { $cfgArgs += @('--index-standard', 'false') } & $exe @cfgArgs if ($LASTEXITCODE -ne 0) { throw "Configuration failed." } -Head "4/5 Registering in Claude" +Head "4/5 Registering in Claude (MCP server, skills)" if ($NoRegister) { Say "skipped (-NoRegister). Manually: `"$exe`" register" } @@ -171,6 +180,21 @@ else { if ($CodeOnly) { $regArgs += '--code' } & $exe @regArgs } +# Claude Code skills (xpp-ponytail): a skill changed locally is kept and reported with a warning. +if ($NoSkills) { + Say "skills skipped (-NoSkills). Manually: `"$exe`" skills install" +} +else { + # Language of the skill texts: -SkillsLanguage, else kept from the last installation, else that of Windows. + $skillArgs = @('skills', 'install') + if ($SkillsLanguage) { $skillArgs += @('--lang', $SkillsLanguage) } + elseif (-not (Test-Path (Join-Path $InstallDir 'skills-installed.json'))) { + $skillArgs += @('--lang', $(if ((Get-UICulture).TwoLetterISOLanguageName -eq 'pl') { 'pl' } else { 'en' })) + } + & $exe @skillArgs | ForEach-Object { + if ($_ -like 'warning:*') { Write-Host " $_" -ForegroundColor Yellow } else { Say $_ } + } +} Head "5/5 Building the index" if ($NoBuild) { diff --git a/pack.ps1 b/pack.ps1 index 8018142..88e2554 100644 --- a/pack.ps1 +++ b/pack.ps1 @@ -61,6 +61,8 @@ Remove-Item (Join-Path $stage 'bin\*.pdb') -Force -ErrorAction SilentlyContinue foreach ($f in 'install.ps1', 'uninstall.ps1', 'restart-claude.ps1', 'README.md', 'README.pl.md', 'CHANGELOG.md', 'LICENSE') { Copy-Item (Join-Path $PSScriptRoot $f) $stage -Force } +# Claude Code skills (one folder each): install.ps1 and the setup program copy them to ~/.claude/skills. +Copy-Item (Join-Path $PSScriptRoot 'skills') $stage -Recurse -Force $size = [math]::Round((Get-ChildItem $stage -Recurse -File | Measure-Object Length -Sum).Sum / 1MB, 1) Write-Host "package: $stage ($size MB)" -ForegroundColor Cyan diff --git a/packaging/inno/xpp-lens.iss b/packaging/inno/xpp-lens.iss index 0189e90..0ed1af5 100644 --- a/packaging/inno/xpp-lens.iss +++ b/packaging/inno/xpp-lens.iss @@ -52,6 +52,10 @@ VersionInfoVersion={#FileVersion} Name: "en"; MessagesFile: "compiler:Default.isl" Name: "pl"; MessagesFile: "compiler:Languages\Polish.isl" +[InstallDelete] +; The package's copy of the skills is replaced whole, so a skill dropped from the package does not linger. +Type: filesandordirs; Name: "{app}\skills" + [Files] Source: "{#Source}\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion @@ -96,6 +100,11 @@ en.StdTitle=Microsoft standard code en.StdSub=Objects, signatures and calls of the standard models en.StdText=Recommended: Claude then also finds callers and extensions inside Microsoft code. The first indexing takes 20-90 minutes (in the background), later only what changed. en.StdOption=Index the Microsoft standard models (recommended) +en.SkillsTitle=Claude Code skills +en.SkillsSub=xpp-ponytail and xpp-ponytail-review +en.SkillsText=Two skills for X++ work in Claude Code: /xpp-ponytail (the smallest complete change: configuration, standard, extension, only then new code) and /xpp-ponytail-review (a review of an X++ change). They go to .claude\skills in your user profile; a skill you change there is never overwritten. +en.SkillsOption=Install the Claude Code skills (xpp-ponytail) +en.SkillsKept=Skills you changed were kept and not updated:%n%n%1 en.ConfigFailed=Saving the settings did not work:%n%n%1 en.Registered=xpp-lens is ready. Open Claude and ask, for example: "find the method SalesTable.validateWrite". en.BuildNow=Build the index now (a console window shows the progress) @@ -123,6 +132,11 @@ pl.StdTitle=Kod standardowy Microsoftu pl.StdSub=Obiekty, sygnatury i wywołania modeli standardowych pl.StdText=Zalecane: Claude znajdzie wtedy także wywołania i rozszerzenia w kodzie Microsoftu. Pierwsze indeksowanie trwa 20-90 minut (w tle), później tylko zmiany. pl.StdOption=Indeksuj modele standardowe Microsoftu (zalecane) +pl.SkillsTitle=Skille Claude Code +pl.SkillsSub=xpp-ponytail i xpp-ponytail-review +pl.SkillsText=Dwa skille do pracy z X++ w Claude Code: /xpp-ponytail (najmniejsza kompletna zmiana: konfiguracja, standard, rozszerzenie, dopiero potem nowy kod) i /xpp-ponytail-review (przegląd zmiany X++). Trafiają do .claude\skills w profilu użytkownika; skilla, którego tam zmienisz, instalator nigdy nie nadpisze. +pl.SkillsOption=Zainstaluj skille Claude Code (xpp-ponytail) +pl.SkillsKept=Zmienione przez ciebie skille zostały zachowane i nie zostały zaktualizowane:%n%n%1 pl.ConfigFailed=Nie udało się zapisać ustawień:%n%n%1 pl.Registered=xpp-lens jest gotowy. Otwórz Claude'a i zapytaj np.: "znajdź metodę SalesTable.validateWrite". pl.BuildNow=Zbuduj indeks teraz (okno konsoli pokaże postęp) @@ -141,6 +155,7 @@ var PkgInfo: TNewStaticText; LangPage: TInputQueryWizardPage; StdPage: TInputOptionWizardPage; + SkillsPage: TInputOptionWizardPage; Detected: Boolean; WasUpdate: Boolean; RemoveUserData: Boolean; @@ -259,6 +274,36 @@ begin StdPage := CreateInputOptionPage(LangPage.ID, CustomMessage('StdTitle'), CustomMessage('StdSub'), CustomMessage('StdText'), False, False); StdPage.Add(CustomMessage('StdOption')); StdPage.Values[0] := True; + + { Shown on updates too; a silent update (xpplens update --install) keeps the choice of the last installation. } + SkillsPage := CreateInputOptionPage(StdPage.ID, CustomMessage('SkillsTitle'), CustomMessage('SkillsSub'), CustomMessage('SkillsText'), False, False); + SkillsPage.Add(CustomMessage('SkillsOption')); + SkillsPage.Values[0] := GetPreviousData('Skills', '1') = '1'; +end; + +procedure RegisterPreviousData(PreviousDataKey: Integer); +begin + if SkillsPage.Values[0] then + SetPreviousData(PreviousDataKey, 'Skills', '1') + else + SetPreviousData(PreviousDataKey, 'Skills', '0'); +end; + +{ The "warning:" lines of "xpplens skills install / remove" (skills the user changed, kept as they are). } +function SkillWarnings(Output: String): String; +var + Line: String; + P: Integer; +begin + Result := ''; + Output := Output + #10; + while Output <> '' do + begin + P := Pos(#10, Output); + Line := Trim(Copy(Output, 1, P - 1)); + Delete(Output, 1, P); + if Pos('warning:', Line) = 1 then Result := Result + Trim(Copy(Line, 9, Length(Line))) + #13#10; + end; end; { Fills the folder page from "xpplens detect" (lines: path TAB packages TAB source). } @@ -383,6 +428,14 @@ begin end; { Registration in Claude Desktop and Claude Code (an update re-registers the same path). } RunXpp(Exe, 'register', Output); + { Claude Code skills; one the user changed is kept, and said so (no message box in a silent update). } + if SkillsPage.Values[0] then + begin + { In the wizard's language (a silent update runs in the language of the last installation). } + RunXpp(Exe, 'skills install --lang ' + ActiveLanguage, Output); + Output := SkillWarnings(Output); + if (Output <> '') and not WizardSilent then MsgBox(FmtMessage(CustomMessage('SkillsKept'), [Output]), mbInformation, MB_OK); + end; { Silent update with /RESTARTCLAUDE=1 (xpplens update --install --restart-claude). } if ExpandConstant('{param:RESTARTCLAUDE|0}') = '1' then Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + ExpandConstant('{app}\restart-claude.ps1') + '" -Version {#Version}', @@ -392,10 +445,18 @@ end; { Before the files go: ask whether to remove the index and settings (default: keep them). After: take the program folder off the PATH and delete the data when asked to. } procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + Output: String; begin if CurUninstallStep = usUninstall then + begin RemoveUserData := not UninstallSilent and - (MsgBox(CustomMessage('RemoveData'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES) + (MsgBox(CustomMessage('RemoveData'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES); + { Only the skills xpp-lens installed and nobody changed; a changed one stays where it is. } + RunXpp(ExpandConstant('{app}\bin\xpplens.exe'), 'skills remove', Output); + Output := SkillWarnings(Output); + if (Output <> '') and not UninstallSilent then MsgBox(Output, mbInformation, MB_OK); + end else if CurUninstallStep = usPostUninstall then begin RemoveFromPath(ExpandConstant('{app}\bin')); diff --git a/release.ps1 b/release.ps1 index 27b5464..294f9fb 100644 --- a/release.ps1 +++ b/release.ps1 @@ -238,7 +238,8 @@ $archive = [IO.Compression.ZipFile]::OpenRead($zip) $entries = @($archive.Entries | ForEach-Object { $_.FullName }) $archive.Dispose() Remove-Item $tmp -Recurse -Force -foreach ($e in 'xpp-lens/install.ps1', 'xpp-lens/bin/xpplens.exe', 'xpp-lens/CHANGELOG.md') { +foreach ($e in 'xpp-lens/install.ps1', 'xpp-lens/bin/xpplens.exe', 'xpp-lens/bin/e_sqlite3.dll', 'xpp-lens/CHANGELOG.md', + 'xpp-lens/skills/xpp-ponytail/SKILL.md', 'xpp-lens/skills/xpp-ponytail-review/SKILL.md') { if ($entries -notcontains $e) { Fail "$asset has no $e (entries: $($entries -join ', '))" } } Ok "${asset}: SHA-256 matches, layout ok, $(if ($Pre) { 'test release' } else { 'regular release' })" diff --git a/skills/THIRD-PARTY-NOTICES.md b/skills/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..f4456d7 --- /dev/null +++ b/skills/THIRD-PARTY-NOTICES.md @@ -0,0 +1,31 @@ +# Third-party notices + +The skills in this folder (`xpp-ponytail`, `xpp-ponytail-review`) are WillowDev's work for X++, built on the ladder +and the review structure of [Ponytail](https://github.com/DietrichGebert/ponytail) and reusing some of its wording. +Those parts are used under Ponytail's license: + +``` +MIT License + +Copyright (c) 2026 DietrichGebert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +Everything else in xpp-lens is under its own [LICENSE](../LICENSE). diff --git a/skills/xpp-ponytail-review/SKILL.md b/skills/xpp-ponytail-review/SKILL.md new file mode 100644 index 0000000..cec599a --- /dev/null +++ b/skills/xpp-ponytail-review/SKILL.md @@ -0,0 +1,102 @@ +--- +name: xpp-ponytail-review +description: Review of an X++ (Dynamics 365 F&O) change like the senior who gets paged when production breaks - correctness, security, transactions, SQL performance, extensibility, tests and unneeded code. Reads the related code through xpp-lens, not just the diff; explains every finding in plain words. Use when someone asks to "review this", "review my change", "check my X++ code", "is this overengineered", "review the last changeset", /xpp-ponytail-review. To review a PR against its Azure DevOps work item, use dev-pr. +--- + +Review an X++ change like the senior developer who will be paged when it breaks in production. +Order of importance: correct, safe, transactions and data, holds under real volume, upgrade-safe, tested, +lean. Every extra object must be built, synced, secured and upgraded with each Microsoft update. +This is a report the user asked for, so give it in full. It lists findings and changes no code. + +Based on `ponytail-review` of [Ponytail](https://github.com/DietrichGebert/ponytail) (MIT), rewritten for X++. + +## 1. Understand first + +- Review what the user names: files, objects, a model, a changeset, a PR. Nothing named: `xpp_changed + since=build` (changed and not compiled yet), or the files changed on disk today (`xpp_changed since=`). +- Read the changed methods with `xpp_method`, then the code they touch: + - `xpp_callers` / `xpp_refs` for every changed method, field or signature - custom and Microsoft callers, + - `xpp_callees` for what the changed code uses, + - `xpp_extensions` for other CoC / handlers on the same standard methods (order and double effects), + - `xpp_entity`, `xpp_security` when tables, fields or menu items changed. +- Trace the real flow: where data comes in (form, entity/integration, batch), what is written, what is posted. +- Find the expected volume: an interactive form for one user, or a batch over all lines of all companies. + Judge scale against that, and say which volume you assumed. +- `xpp_build_errors` - errors of the last build in the touched model count as findings. + +## 2. Look for + +1. **Bug:** wrong result or crash - empty buffer not checked, `dateNull()`, zero or negative quantity, + rounding of amounts, wrong company (`DataAreaId`, missing `changeCompany`), wrong language, `InventDim` + compared by fields instead of `InventDimId`, an enum value added by someone else not handled in a `switch`, + a caller broken by a signature change, a fix in one caller while the shared method stays broken. +2. **CoC and extensions:** CoC without `next`, or `next` called conditionally or twice; logic before `next` that + belongs after it (or the reverse); an event handler changing args it does not own; a copied standard method + body instead of an extension; a wrapped method that is `[Hookable(false)]` / `internal` / likely to change. +3. **Transactions and data:** `ttsbegin` without its `ttscommit` on every path; a transaction split so a document + can be half-posted; `throw` caught and swallowed inside a transaction; record written without `forUpdate` / + `selectForUpdate`; `skipDataMethods` / `skipDatabaseLog` / `skipEvents` / `skipBusinessEvents` that skips + logic the data needs; `doInsert` / `doUpdate` bypassing validation; missing `validateWrite` / `validateField` + on data from users or integrations. +4. **Security:** a menu item without a privilege, a privilege not in any duty/role (`xpp_security`); data shown + or changed across companies the user cannot see; `crossCompany` without need; direct SQL + (`Statement`, `executeQuery`) built from strings; secrets or URLs in code instead of parameters/Key Vault. +5. **Volume and SQL:** `select` inside `while select` (N+1) where a join, `exists join` or set-based statement + works; record-by-record `insert`/`update`/`delete` where `insert_recordset` / `update_recordset` / + `delete_from` / `RecordInsertList` fit (and would not be downgraded by overridden methods); missing + `firstonly`; no field list on wide tables; a where clause without a supporting index (`xpp_object` shows + indexes); work repeated per line in a batch that could run once; a batch that is not split into tasks for + large volume; display methods doing queries on grids. +6. **Upgrade and structure:** hard-coded texts instead of labels; hard-coded company, account or ID values; a + new model reference; a new parameter table or option nobody asked for; naming outside the team prefix. +7. **Missing test:** risky new logic (posting, money, a branch, a data write, a bug fix) with no SysTest or no + named manual check. One good test, not coverage. +8. **Lean:** code that should not exist or should be smaller. + - configure: a setting already does it (name it), + - standard: Microsoft's framework already does it (name the class/method), + - reuse: the model already has it (name the object), + - delete: dead code, unused parameters, speculative options, + - yagni: interface or factory with one implementation, parameter nobody sets, + - narrow: CoC on a big method where a delegate or smaller method exists. + +## 3. Check before you report + +- Every finding needs a concrete case: "this data or this situation leads to this wrong result". No case, no + finding. +- Re-check with xpp-lens: the caller exists, the buffer can really be empty, the index really is missing, no + other extension already handles it. +- A shortcut marked `// shortcut:` that names its limit is a decision, not a finding, unless the expected volume + already crosses it. +- Propose the smallest fix that works. Prefer fixes that delete code or move it to configuration or standard. + Never add layers, frameworks or parameters the problem does not need. +- No style taste, no "consider", no vague worries. Best-practice warnings only when they hide a real problem. + +## 4. Output + +In the user's language (English by default), short sentences, everyday words. Explain a technical term the first +time you use it. The reader may never have seen this code. + +Start with `What this change does:` in two or three sentences. + +Then the findings in three groups, skip empty groups: +- **Fix before deploying:** bug, security, data loss or half-posted documents, breaks at the expected volume. +- **Worth fixing:** risky code without a test, real slowness, a copied standard body, code that should not exist. +- **Minor:** small speed-ups, shorter forms. + +Number findings across all groups, so the user can say "fix 2 and 5". Every finding has all four parts, each +one or two short sentences: + +2. **Order half-posted** (`MyModel/AxClass/SalesFormLetter_Extension.xml`, `postInvoice`) + - **What it is:** The extension posts the invoice and right away creates a document in an external system. + - **Problem:** The service call is inside `ttsbegin`. When the service does not answer, the transaction rolls + back, but the document on the other side already exists. + - **Fix:** Move the call after `ttscommit` or into a business event. A few lines, same logic. + - **If we skip it:** External documents without an invoice in D365, reconciled by hand. + +End with: +- `Verdict: OK to deploy.` or `Verdict: fix 1 and 3 first.` +- `Lean: - lines possible.` when lean findings exist. +- `DB sync: needed / not needed.` +- `Not checked:` one line, if something mattered and you could not check it. + +Nothing found: `What this change does:`, then `Looks good. OK to deploy.` and one line on what you checked. diff --git a/skills/xpp-ponytail-review/SKILL.pl.md b/skills/xpp-ponytail-review/SKILL.pl.md new file mode 100644 index 0000000..58e5214 --- /dev/null +++ b/skills/xpp-ponytail-review/SKILL.pl.md @@ -0,0 +1,104 @@ +--- +name: xpp-ponytail-review +description: Przegląd zmiany w X++ (Dynamics 365 F&O) jak senior, którego budzą, gdy coś padnie na produkcji - poprawność, bezpieczeństwo, transakcje, wydajność SQL, rozszerzalność, testy i zbędny kod. Czyta powiązany kod przez xpp-lens, nie tylko diff; każde znalezisko wyjaśnia prostym językiem. Użyj, gdy ktoś prosi „zrób review”, „przejrzyj zmianę”, „sprawdź mój kod X++”, „czy to jest przekombinowane”, „review ostatniego changesetu”, /xpp-ponytail-review. Do przeglądu PR względem work itemu w Azure DevOps użyj dev-pr. +--- + +Review an X++ change like the senior developer who will be paged when it breaks in production. +Order of importance: correct, safe, transactions and data, holds under real volume, upgrade-safe, tested, +lean. Every extra object must be built, synced, secured and upgraded with each Microsoft update. +This is a report the user asked for, so give it in full. It lists findings and changes no code. + +Based on `ponytail-review` of [Ponytail](https://github.com/DietrichGebert/ponytail) (MIT), rewritten for X++. + +## 1. Understand first + +- Review what the user names: files, objects, a model, a changeset, a PR. Nothing named: `xpp_changed + since=build` (changed and not compiled yet), or the files changed on disk today (`xpp_changed since=`). +- Read the changed methods with `xpp_method`, then the code they touch: + - `xpp_callers` / `xpp_refs` for every changed method, field or signature - custom and Microsoft callers, + - `xpp_callees` for what the changed code uses, + - `xpp_extensions` for other CoC / handlers on the same standard methods (order and double effects), + - `xpp_entity`, `xpp_security` when tables, fields or menu items changed. +- Trace the real flow: where data comes in (form, entity/integration, batch), what is written, what is posted. +- Find the expected volume: an interactive form for one user, or a batch over all lines of all companies. + Judge scale against that, and say which volume you assumed. +- `xpp_build_errors` - errors of the last build in the touched model count as findings. + +## 2. Look for + +1. **Bug:** wrong result or crash - empty buffer not checked, `dateNull()`, zero or negative quantity, + rounding of amounts, wrong company (`DataAreaId`, missing `changeCompany`), wrong language, `InventDim` + compared by fields instead of `InventDimId`, an enum value added by someone else not handled in a `switch`, + a caller broken by a signature change, a fix in one caller while the shared method stays broken. +2. **CoC and extensions:** CoC without `next`, or `next` called conditionally or twice; logic before `next` that + belongs after it (or the reverse); an event handler changing args it does not own; a copied standard method + body instead of an extension; a wrapped method that is `[Hookable(false)]` / `internal` / likely to change. +3. **Transactions and data:** `ttsbegin` without its `ttscommit` on every path; a transaction split so a document + can be half-posted; `throw` caught and swallowed inside a transaction; record written without `forUpdate` / + `selectForUpdate`; `skipDataMethods` / `skipDatabaseLog` / `skipEvents` / `skipBusinessEvents` that skips + logic the data needs; `doInsert` / `doUpdate` bypassing validation; missing `validateWrite` / `validateField` + on data from users or integrations. +4. **Security:** a menu item without a privilege, a privilege not in any duty/role (`xpp_security`); data shown + or changed across companies the user cannot see; `crossCompany` without need; direct SQL + (`Statement`, `executeQuery`) built from strings; secrets or URLs in code instead of parameters/Key Vault. +5. **Volume and SQL:** `select` inside `while select` (N+1) where a join, `exists join` or set-based statement + works; record-by-record `insert`/`update`/`delete` where `insert_recordset` / `update_recordset` / + `delete_from` / `RecordInsertList` fit (and would not be downgraded by overridden methods); missing + `firstonly`; no field list on wide tables; a where clause without a supporting index (`xpp_object` shows + indexes); work repeated per line in a batch that could run once; a batch that is not split into tasks for + large volume; display methods doing queries on grids. +6. **Upgrade and structure:** hard-coded texts instead of labels; hard-coded company, account or ID values; a + new model reference; a new parameter table or option nobody asked for; naming outside the team prefix. +7. **Missing test:** risky new logic (posting, money, a branch, a data write, a bug fix) with no SysTest or no + named manual check. One good test, not coverage. +8. **Lean:** code that should not exist or should be smaller. + - configure: a setting already does it (name it), + - standard: Microsoft's framework already does it (name the class/method), + - reuse: the model already has it (name the object), + - delete: dead code, unused parameters, speculative options, + - yagni: interface or factory with one implementation, parameter nobody sets, + - narrow: CoC on a big method where a delegate or smaller method exists. + +## 3. Check before you report + +- Every finding needs a concrete case: "this data or this situation leads to this wrong result". No case, no + finding. +- Re-check with xpp-lens: the caller exists, the buffer can really be empty, the index really is missing, no + other extension already handles it. +- A shortcut marked `// shortcut:` that names its limit is a decision, not a finding, unless the expected volume + already crosses it. +- Propose the smallest fix that works. Prefer fixes that delete code or move it to configuration or standard. + Never add layers, frameworks or parameters the problem does not need. +- No style taste, no "consider", no vague worries. Best-practice warnings only when they hide a real problem. + +## 4. Output + +In the user's language (Polish by default), short sentences, everyday words. Explain a technical term the first +time you use it. The reader may never have seen this code. + +Start with `Co robi ta zmiana:` in two or three sentences. + +Then the findings in three groups, skip empty groups: +- **Do poprawy przed wdrożeniem:** bug, security, data loss or half-posted documents, breaks at the expected + volume. +- **Warto poprawić:** risky code without a test, real slowness, a copied standard body, code that should not + exist. +- **Drobne:** small speed-ups, shorter forms. + +Number findings across all groups, so the user can say "popraw 2 i 5". Every finding has all four parts, each +one or two short sentences: + +2. **Zamówienie częściowo zaksięgowane** (`MyModel/AxClass/SalesFormLetter_Extension.xml`, `postInvoice`) + - **Co to jest:** Rozszerzenie księguje fakturę i od razu tworzy dokument w systemie zewnętrznym. + - **Problem:** Wywołanie usługi jest wewnątrz `ttsbegin`. Gdy usługa nie odpowie, transakcja się wycofa, ale + dokument po drugiej stronie już istnieje. + - **Poprawka:** Przenieść wywołanie po `ttscommit` albo do business eventu. Kilka linii, logika bez zmian. + - **Jeśli pominiemy:** Dokumenty zewnętrzne bez faktury w D365, uzgadniane ręcznie. + +End with: +- `Werdykt: Można wdrażać.` or `Werdykt: najpierw popraw 1 i 3.` +- `Lean: - linii możliwe.` when lean findings exist. +- `Synchronizacja DB: potrzebna / niepotrzebna.` +- `Nie sprawdzone:` one line, if something mattered and you could not check it. + +Nothing found: `Co robi ta zmiana:`, then `Wygląda dobrze. Można wdrażać.` and one line on what you checked. diff --git a/skills/xpp-ponytail/SKILL.md b/skills/xpp-ponytail/SKILL.md new file mode 100644 index 0000000..ce6cdee --- /dev/null +++ b/skills/xpp-ponytail/SKILL.md @@ -0,0 +1,98 @@ +--- +name: xpp-ponytail +description: Lazy senior X++ mode for Dynamics 365 F&O - the smallest complete change: configuration first, then Microsoft's standard, existing code in the model, an extension (CoC, event handler), and only then new code. Use for every X++ programming task (a new feature, a bug fix, refactoring, extending the standard) and when someone says "ponytail", "xpp-ponytail", "simplest", "keep it simple", "don't overengineer", "yagni", or complains about too much code. Levels - lite, full (default), ultra. Uses the xpp-lens MCP server. +argument-hint: "[lite|full|ultra|off]" +--- + +# X++ Ponytail + +You are a lazy senior Dynamics 365 F&O developer. The best X++ is the X++ never written: every new object +must be built, synced, deployed, secured, upgraded with each Microsoft update and supported for years. You solve +the whole problem with the least new code. End your reply with one or two lines: what you skipped or did not +check, and any risk the user must know. + +Active for the whole session until the user says "stop ponytail" or "normal mode". Switch level: +`/xpp-ponytail lite|full|ultra`. + +Based on the ladder of [Ponytail](https://github.com/DietrichGebert/ponytail) (MIT), rewritten for X++. + +## Before you write + +Read the task and the code it touches - with xpp-lens, not Grep/Read on AOT XML: +- `xpp_find` / `xpp_object` - where the object is and what it has (fields, indexes, relations, methods). +- `xpp_method` - the source of the methods you change. +- `xpp_callers` / `xpp_refs` - every caller and use, custom and Microsoft. +- `xpp_extensions` - CoC classes, event handlers, table/form extensions already on the object. Someone may + already wrap the method you are about to wrap. + +List every place the change must reach: callers, extensions, data entities (`xpp_entity`), security +(`xpp_security`), labels, forms, batch jobs, tests. Check what it could break: data it would change or expose, +companies it would cross, callers and integrations that stop working. That is scope. Extra features are not. + +## The smallest complete change + +Take the first rung that fully works: + +1. **Does it need to exist?** Skip options, parameters and flexibility nobody asked for; name them in one line. + A vague request gets the smallest version that does the core job. +2. **Can it be configured?** Parameters, number sequences, workflows, financial dimensions, posting profiles, + batch recurrence, Electronic Reporting, personalization, feature management. Say which setting and stop - + configuration beats any code. +3. **Does the standard already do it?** Microsoft's framework and existing standard logic: SysOperation, + `NumberSeq`, `DateTimeUtil`, `SysQuery`, `Global` helpers, `SysDictTable`, `InventDim`/`InventDimId` + handling, `LedgerDimensionFacade`, existing services and classes. Find it with `xpp_find` / `xpp_grep`. +4. **Is it already in our model?** A helper, base class, extension or pattern the team uses. Reuse it the way + the surrounding code does. +5. **Can the standard be extended at the narrowest point?** In this order: + - a **delegate** Microsoft provides for exactly this, + - **CoC** on the smallest method that owns the behavior (always call `next`; `xpp_scaffold` gives the exact + signature), + - a **pre/post event handler** when CoC cannot reach (`xpp_scaffold`), + - a **table/form extension** for new fields or controls. + Never copy a standard method's body to change one line. +6. **One line a reader gets at a glance?** One line. +7. **Otherwise:** the minimum new code, in the model the team already uses. + +### Rules + +- Lazy about the solution, never about the change: finish every part the task needs - labels for every + user-visible text, the privilege for a new menu item, the extension on the data entity when a table gets a + field, callers and tests the change breaks. +- No abstraction, interface, factory, parameter table, new model reference or "for later" code nobody asked for. + A new model reference is a new dependency: avoid it unless the task needs it. +- Keep values in the form the platform gives you: `RecId`, `utcdatetime`, `DimensionDynamicAccount`, + `InventDimId`. No conversions for convenience. +- Keep the structure the codebase has: its models, naming prefix, class layout, label file. +- Database work in sets, not loops: `update_recordset` / `insert_recordset` / `delete_from` or a join instead of + a `select` inside a `while select`. Keep `ttsbegin`/`ttscommit` paired and around the whole unit of work. + Use `skipDataMethods` / `skipDatabaseLog` / `skipEvents` only when you know what you skip and say it. +- `select firstonly` when you need one record; a field list when you need a few fields; an existing index for + the where clause. +- Cross-company access only when the task needs it, with `crossCompany` and a company list. +- Comment only the why the code cannot show, in one line. +- Bug fix: before editing, `xpp_callers` on the method you touch; fix the root cause once in shared code, not in + one caller. +- Code you move or merge keeps its validation and error handling (`validateWrite`, `checkFailed`, `throw error`). +- Between options of equal size, take the one correct on edge cases: empty buffer, zero quantity, rounding with + `CurrencyExchangeHelper`/`roundAmount`, other companies, other languages, `dateNull()`. +- New non-trivial logic (a branch, a loop, money, posting, security, data writes) leaves one small SysTest + (`SysTestCase`) when the model has a test project; otherwise name in the reply the exact manual check. + Trivial changes need none. +- A shortcut with a known limit gets a comment: `// shortcut: , `. + +Never cut: validation of user and integration input, transaction and error handling that prevents data loss or +half-posted documents, security (privileges, duties, `validateField`/`validateWrite`, data access), labels, +anything the user asked for. + +## After you write + +- `xpp_build_errors` after the user builds; fix what the change broke. +- Say whether the change needs a DB sync (table, field, index, enum value, view, entity) or only a build. + +## Levels + +| Level | Behavior | +|-------|----------| +| **lite** | Build what was asked. Name the smaller option (configuration, standard, narrower extension) in one line and let the user pick. | +| **full** | The rules above. Default. | +| **ultra** | Also question the request: before building, push back on any part the need does not justify - often the answer is a setting, not code. | diff --git a/skills/xpp-ponytail/SKILL.pl.md b/skills/xpp-ponytail/SKILL.pl.md new file mode 100644 index 0000000..f3c3320 --- /dev/null +++ b/skills/xpp-ponytail/SKILL.pl.md @@ -0,0 +1,98 @@ +--- +name: xpp-ponytail +description: Tryb „leniwego seniora X++” dla Dynamics 365 F&O - najmniejsza kompletna zmiana: najpierw konfiguracja, potem standard Microsoftu, istniejący kod modelu, rozszerzenie (CoC, event handler), a dopiero na końcu nowy kod. Użyj przy każdym zadaniu programistycznym w X++ (nowa funkcja, poprawka błędu, refaktoryzacja, rozszerzenie standardu) oraz gdy ktoś mówi „ponytail”, „xpp-ponytail”, „najprościej”, „bez przekombinowania”, „yagni”, albo narzeka na nadmiar kodu. Poziomy - lite, full (domyślny), ultra. Korzysta z serwera MCP xpp-lens. +argument-hint: "[lite|full|ultra|off]" +--- + +# X++ Ponytail + +You are a lazy senior Dynamics 365 F&O developer. The best X++ is the X++ never written: every new object +must be built, synced, deployed, secured, upgraded with each Microsoft update and supported for years. You solve +the whole problem with the least new code. End your reply with one or two lines: what you skipped or did not +check, and any risk the user must know. + +Active for the whole session until the user says "stop ponytail" or "normal mode". Switch level: +`/xpp-ponytail lite|full|ultra`. + +Based on the ladder of [Ponytail](https://github.com/DietrichGebert/ponytail) (MIT), rewritten for X++. + +## Before you write + +Read the task and the code it touches - with xpp-lens, not Grep/Read on AOT XML: +- `xpp_find` / `xpp_object` - where the object is and what it has (fields, indexes, relations, methods). +- `xpp_method` - the source of the methods you change. +- `xpp_callers` / `xpp_refs` - every caller and use, custom and Microsoft. +- `xpp_extensions` - CoC classes, event handlers, table/form extensions already on the object. Someone may + already wrap the method you are about to wrap. + +List every place the change must reach: callers, extensions, data entities (`xpp_entity`), security +(`xpp_security`), labels, forms, batch jobs, tests. Check what it could break: data it would change or expose, +companies it would cross, callers and integrations that stop working. That is scope. Extra features are not. + +## The smallest complete change + +Take the first rung that fully works: + +1. **Does it need to exist?** Skip options, parameters and flexibility nobody asked for; name them in one line. + A vague request gets the smallest version that does the core job. +2. **Can it be configured?** Parameters, number sequences, workflows, financial dimensions, posting profiles, + batch recurrence, Electronic Reporting, personalization, feature management. Say which setting and stop - + configuration beats any code. +3. **Does the standard already do it?** Microsoft's framework and existing standard logic: SysOperation, + `NumberSeq`, `DateTimeUtil`, `SysQuery`, `Global` helpers, `SysDictTable`, `InventDim`/`InventDimId` + handling, `LedgerDimensionFacade`, existing services and classes. Find it with `xpp_find` / `xpp_grep`. +4. **Is it already in our model?** A helper, base class, extension or pattern the team uses. Reuse it the way + the surrounding code does. +5. **Can the standard be extended at the narrowest point?** In this order: + - a **delegate** Microsoft provides for exactly this, + - **CoC** on the smallest method that owns the behavior (always call `next`; `xpp_scaffold` gives the exact + signature), + - a **pre/post event handler** when CoC cannot reach (`xpp_scaffold`), + - a **table/form extension** for new fields or controls. + Never copy a standard method's body to change one line. +6. **One line a reader gets at a glance?** One line. +7. **Otherwise:** the minimum new code, in the model the team already uses. + +### Rules + +- Lazy about the solution, never about the change: finish every part the task needs - labels for every + user-visible text, the privilege for a new menu item, the extension on the data entity when a table gets a + field, callers and tests the change breaks. +- No abstraction, interface, factory, parameter table, new model reference or "for later" code nobody asked for. + A new model reference is a new dependency: avoid it unless the task needs it. +- Keep values in the form the platform gives you: `RecId`, `utcdatetime`, `DimensionDynamicAccount`, + `InventDimId`. No conversions for convenience. +- Keep the structure the codebase has: its models, naming prefix, class layout, label file. +- Database work in sets, not loops: `update_recordset` / `insert_recordset` / `delete_from` or a join instead of + a `select` inside a `while select`. Keep `ttsbegin`/`ttscommit` paired and around the whole unit of work. + Use `skipDataMethods` / `skipDatabaseLog` / `skipEvents` only when you know what you skip and say it. +- `select firstonly` when you need one record; a field list when you need a few fields; an existing index for + the where clause. +- Cross-company access only when the task needs it, with `crossCompany` and a company list. +- Comment only the why the code cannot show, in one line. +- Bug fix: before editing, `xpp_callers` on the method you touch; fix the root cause once in shared code, not in + one caller. +- Code you move or merge keeps its validation and error handling (`validateWrite`, `checkFailed`, `throw error`). +- Between options of equal size, take the one correct on edge cases: empty buffer, zero quantity, rounding with + `CurrencyExchangeHelper`/`roundAmount`, other companies, other languages, `dateNull()`. +- New non-trivial logic (a branch, a loop, money, posting, security, data writes) leaves one small SysTest + (`SysTestCase`) when the model has a test project; otherwise name in the reply the exact manual check. + Trivial changes need none. +- A shortcut with a known limit gets a comment: `// shortcut: , `. + +Never cut: validation of user and integration input, transaction and error handling that prevents data loss or +half-posted documents, security (privileges, duties, `validateField`/`validateWrite`, data access), labels, +anything the user asked for. + +## After you write + +- `xpp_build_errors` after the user builds; fix what the change broke. +- Say whether the change needs a DB sync (table, field, index, enum value, view, entity) or only a build. + +## Levels + +| Level | Behavior | +|-------|----------| +| **lite** | Build what was asked. Name the smaller option (configuration, standard, narrower extension) in one line and let the user pick. | +| **full** | The rules above. Default. | +| **ultra** | Also question the request: before building, push back on any part the need does not justify - often the answer is a setting, not code. | diff --git a/src/XppLens/CommandLine.cs b/src/XppLens/CommandLine.cs index 1b805ad..273dc36 100644 --- a/src/XppLens/CommandLine.cs +++ b/src/XppLens/CommandLine.cs @@ -25,6 +25,7 @@ public sealed partial class CommandLine ], ["register"] = ["name=", "exe=", "desktop", "code"], ["unregister"] = ["name=", "exe=", "desktop", "code", "any"], + ["skills"] = ["lang="], ["build"] = ["full-only", "std-only", "compiled-only", "force"], ["status"] = ["counts", "ascii"], ["find"] = ["kind=", "type=", "model=", "limit=", "property="], diff --git a/src/XppLens/IndexService.cs b/src/XppLens/IndexService.cs index 33b8b1f..75f50c9 100644 --- a/src/XppLens/IndexService.cs +++ b/src/XppLens/IndexService.cs @@ -548,6 +548,16 @@ SELECT COUNT(DISTINCT md.id), COUNT(f.id), MAX(md.indexed_utc) sb.AppendLine(Names.Ready ? $"names in memory: {TextTable.Num(Names.Count)} (wildcard searches without scanning the index)" : "names in memory: being read"); + // A silent update cannot show the setup program's warning: changed skills are reported here. + try + { + var kept = Skills.Default().ChangedLocally(); + if (kept.Count > 0) + sb.AppendLine($"skills changed locally (kept, updates skip them): {string.Join(", ", kept)} — 'xpplens skills'"); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Text.Json.JsonException) + { + } sb.AppendLine(); var full = Models.Where(m => m.Full).Select(m => m.Package == m.Name ? m.Name : $"{m.Package}/{m.Name}").ToList(); diff --git a/src/XppLens/Program.cs b/src/XppLens/Program.cs index 7d4fbb4..6ba49f1 100644 --- a/src/XppLens/Program.cs +++ b/src/XppLens/Program.cs @@ -72,6 +72,21 @@ static void PrintTables(Func render, bool ascii) return 0; } +if (cmd == "skills") +{ + // Used by install.ps1, uninstall.ps1 and the setup program: Claude Code skills from the package's skills folder. + var skills = Skills.Default(); + var report = (rest.Count > 0 ? rest[0].ToLowerInvariant() : "status") switch + { + "install" => skills.Install(Opt("lang")), + "remove" => skills.Remove(), + "status" => skills.Status(), + var other => throw new ArgumentException($"unknown action '{other}' for 'skills' (install, remove, status)"), + }; + foreach (var l in report) Console.WriteLine(l); + return 0; +} + var cfg = Config.Load(); Usage.Enabled = Usage.Enabled && cfg.UsageLog; @@ -303,6 +318,9 @@ xpplens label <@id|text> [--lang l] [--limit n] xpplens mcp MCP server over stdio xpplens register [--desktop] [--code] [--name xpp-lens] add to Claude config(s) xpplens unregister [--desktop] [--code] [--any] (--any: also other installations) + xpplens skills [install [--lang en|pl]|remove|status] Claude Code skills of the package in ~/.claude/skills + (a skill changed locally is kept, never overwritten; + --lang: the language of their texts, kept for updates) xpplens version options: --name value, --name=value or -name value; '--' ends the options (for an argument starting with '-') diff --git a/src/XppLens/Skills.cs b/src/XppLens/Skills.cs new file mode 100644 index 0000000..3beb77a --- /dev/null +++ b/src/XppLens/Skills.cs @@ -0,0 +1,217 @@ +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace XppLens; + +/// +/// Claude Code skills shipped in the package's skills folder (one folder per skill), copied to ~/.claude/skills. +/// A file "NAME.pl.md" next to "NAME.md" is the Polish variant: installed as NAME.md when the language is pl, never +/// under its own name. skills-installed.json in the installation folder keeps the language and the SHA-256 of every +/// file xpp-lens wrote: a skill whose files differ from it was changed by the user and is neither overwritten nor +/// removed, and a folder of that name that xpp-lens did not write is left alone unless it equals the package's copy. +/// +internal sealed partial class Skills(string source, string target, string manifest) +{ + public const string ManifestName = "skills-installed.json"; + public const string DefaultLanguage = "en"; + + sealed class Manifest + { + public string Language { get; set; } = DefaultLanguage; + public Dictionary> Skills { get; set; } = new(StringComparer.OrdinalIgnoreCase); + } + + static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + [GeneratedRegex(@"^(?.+)\.(?[a-z]{2})(?\.[^.]+)$", RegexOptions.IgnoreCase)] + private static partial Regex VariantPattern(); + + public static Skills Default() => new( + Path.Combine(Updater.InstallDir(), "skills"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".claude", "skills"), + Path.Combine(Updater.InstallDir(), ManifestName)); + + /// + /// Copies the package's skills in (null: the language of the last installation, else + /// English); returns the report lines (a kept skill starts with "warning:"). + /// + public List Install(string? language = null) + { + var lines = new List(); + if (!Directory.Exists(source)) + { + lines.Add($"no skills in this package ({source})"); + return lines; + } + var m = Load(); + var lang = (language ?? m.Language).Trim().ToLowerInvariant(); + foreach (var dir in Directory.GetDirectories(source).Order(StringComparer.OrdinalIgnoreCase)) + { + var name = Path.GetFileName(dir); + var dest = Path.Combine(target, name); + var files = Files(dir, lang); + var fresh = files.ToDictionary(p => p.Key, p => Updater.Sha256Of(p.Value), StringComparer.OrdinalIgnoreCase); + var known = m.Skills.GetValueOrDefault(name); + var existed = known != null && Directory.Exists(dest); + if (existed) + { + if (Changed(dest, known!) is { } file) + { + lines.Add($"warning: {name} was changed locally ({file}) - kept, not updated: {dest}"); + continue; + } + if (Same(known!, fresh)) + { + lines.Add($"{name}: up to date"); + continue; + } + } + else if (Directory.Exists(dest) && Directory.EnumerateFileSystemEntries(dest).Any()) + { + // Not written by xpp-lens (a manual copy, or a change kept through an uninstall): taken over only + // when it is the same as the package's copy. + if (Changed(dest, fresh) is { } file) + { + lines.Add($"warning: {name} already exists and differs from this package ({file}) - kept, not installed: {dest}"); + continue; + } + } + + // Files of the previous version that the new one no longer has (unchanged, as checked above). + if (existed) + foreach (var old in known!.Keys.Where(k => !fresh.ContainsKey(k))) + DeleteFile(dest, old); + foreach (var (rel, from) in files) + { + var to = Path.Combine(dest, rel); + Directory.CreateDirectory(Path.GetDirectoryName(to)!); + File.Copy(from, to, overwrite: true); + } + lines.Add($"{name}: {(existed ? "updated" : "installed")} ({lang}) -> {dest}"); + m.Skills[name] = fresh; + } + m.Language = lang; + Save(m); + return lines; + } + + /// Removes the skills xpp-lens installed and the user did not change. + public List Remove() + { + var lines = new List(); + foreach (var (name, known) in Load().Skills.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase)) + { + var dest = Path.Combine(target, name); + if (!Directory.Exists(dest)) continue; + if (Changed(dest, known) is { } file) + { + lines.Add($"warning: {name} was changed locally ({file}) - left in place: {dest}"); + continue; + } + foreach (var f in known.Keys) DeleteFile(dest, f); + if (Directory.Exists(dest) && !Directory.EnumerateFileSystemEntries(dest).Any()) Directory.Delete(dest); + lines.Add($"{name}: removed"); + } + if (File.Exists(manifest)) File.Delete(manifest); + return lines; + } + + /// One line per skill of the package or of the last installation. + public List Status() + { + var m = Load(); + var names = m.Skills.Keys + .Concat(Directory.Exists(source) ? Directory.GetDirectories(source).Select(d => Path.GetFileName(d)) : []) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Order(StringComparer.OrdinalIgnoreCase); + var lines = new List(); + foreach (var name in names) + { + var dest = Path.Combine(target, name); + if (!m.Skills.TryGetValue(name, out var known)) + lines.Add($"{name}: not installed by xpp-lens{(Directory.Exists(dest) ? $" (a folder of that name exists: {dest})" : "")}"); + else if (!Directory.Exists(dest)) + lines.Add($"{name}: removed by the user (the next installation copies it again)"); + else if (Changed(dest, known) is { } file) + lines.Add($"{name}: changed locally ({file}) - kept, updates skip it"); + else + lines.Add($"{name}: installed ({m.Language}), unchanged -> {dest}"); + } + return lines; + } + + /// Installed skills the user changed since (kept by updates). + public List ChangedLocally() => + Load().Skills + .Where(p => Directory.Exists(Path.Combine(target, p.Key)) && Changed(Path.Combine(target, p.Key), p.Value) != null) + .Select(p => p.Key) + .Order(StringComparer.OrdinalIgnoreCase) + .ToList(); + + /// + /// Files of a skill as installed: relative path → source file, with the variant of in + /// place of its base file, and no variant under its own name. + /// + internal static Dictionary Files(string dir, string lang) + { + var all = Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories) + .Select(f => Path.GetRelativePath(dir, f)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var rel in all.Order(StringComparer.OrdinalIgnoreCase)) + { + var v = VariantPattern().Match(rel); + var baseFile = v.Success ? v.Groups["base"].Value + v.Groups["ext"].Value : null; + if (baseFile != null && all.Contains(baseFile)) + { + if (v.Groups["lang"].Value.Equals(lang, StringComparison.OrdinalIgnoreCase)) + result[baseFile] = Path.Combine(dir, rel); + continue; + } + result.TryAdd(rel, Path.Combine(dir, rel)); + } + return result; + } + + /// The first file that is missing or differs from its recorded hash, or null. + static string? Changed(string dir, Dictionary known) + { + foreach (var (file, hash) in known.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase)) + { + var path = Path.Combine(dir, file); + if (!File.Exists(path) || !Updater.Sha256Of(path).Equals(hash, StringComparison.OrdinalIgnoreCase)) return file; + } + return null; + } + + static bool Same(Dictionary a, Dictionary b) => + a.Count == b.Count && a.All(p => b.TryGetValue(p.Key, out var v) && v.Equals(p.Value, StringComparison.OrdinalIgnoreCase)); + + static void DeleteFile(string dir, string file) + { + var path = Path.Combine(dir, file); + if (File.Exists(path)) File.Delete(path); + // Empty subfolders left behind (never the skill folder itself here). + var root = dir.TrimEnd('\\', '/').Length; + for (var d = Path.GetDirectoryName(path); d != null && d.Length > root; d = Path.GetDirectoryName(d)) + { + if (!Directory.Exists(d) || Directory.EnumerateFileSystemEntries(d).Any()) break; + Directory.Delete(d); + } + } + + Manifest Load() + { + if (!File.Exists(manifest)) return new Manifest(); + var m = JsonSerializer.Deserialize(File.ReadAllText(manifest), JsonOpts) ?? new Manifest(); + m.Skills = new(m.Skills.ToDictionary(p => p.Key, p => new Dictionary(p.Value, StringComparer.OrdinalIgnoreCase)), + StringComparer.OrdinalIgnoreCase); + return m; + } + + void Save(Manifest m) + { + Directory.CreateDirectory(Path.GetDirectoryName(manifest)!); + File.WriteAllText(manifest, JsonSerializer.Serialize(m, JsonOpts)); + } +} diff --git a/tests/XppLens.Tests/SkillsTests.cs b/tests/XppLens.Tests/SkillsTests.cs new file mode 100644 index 0000000..0de6733 --- /dev/null +++ b/tests/XppLens.Tests/SkillsTests.cs @@ -0,0 +1,117 @@ +namespace XppLens.Tests; + +public sealed class SkillsTests : IDisposable +{ + readonly string _root = Path.Combine(Path.GetTempPath(), "xpplens-skills-" + Guid.NewGuid().ToString("N")); + readonly string _package, _target, _manifest; + + public SkillsTests() + { + _package = Path.Combine(_root, "package", "skills"); + _target = Path.Combine(_root, "home", ".claude", "skills"); + _manifest = Path.Combine(_root, "package", Skills.ManifestName); + Write(_package, "demo/SKILL.md", "---\ndescription: English\n---\nbody"); + Write(_package, "demo/SKILL.pl.md", "---\ndescription: Polski\n---\nbody"); + Write(_package, "demo/reference/notes.md", "notes"); + Write(_package, "other/SKILL.md", "other"); + } + + public void Dispose() + { + try { Directory.Delete(_root, recursive: true); } catch (IOException) { } + } + + static void Write(string dir, string rel, string text) + { + var path = Path.Combine(dir, rel); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, text); + } + + Skills Make() => new(_package, _target, _manifest); + string Installed(string rel) => File.ReadAllText(Path.Combine(_target, rel)); + + [Fact] + public void Installs_the_language_variant_as_the_base_file() + { + Make().Install("pl"); + Assert.Contains("Polski", Installed("demo/SKILL.md")); + Assert.False(File.Exists(Path.Combine(_target, "demo", "SKILL.pl.md"))); + Assert.Equal("notes", Installed("demo/reference/notes.md")); + Assert.Equal("other", Installed("other/SKILL.md")); + + // An update without a language keeps the one of the last installation. + var again = Make().Install(); + Assert.Contains("demo: up to date", again); + Assert.Contains("Polski", Installed("demo/SKILL.md")); + + Make().Install("en"); + Assert.Contains("English", Installed("demo/SKILL.md")); + } + + [Fact] + public void Unchanged_skill_is_updated_and_removed() + { + Make().Install("en"); + Write(_package, "demo/SKILL.md", "---\ndescription: English v2\n---\nbody"); + File.Delete(Path.Combine(_package, "demo", "reference", "notes.md")); + + var lines = Make().Install(); + Assert.Contains(lines, l => l.StartsWith("demo: updated", StringComparison.Ordinal)); + Assert.Contains("English v2", Installed("demo/SKILL.md")); + Assert.False(Directory.Exists(Path.Combine(_target, "demo", "reference"))); + + Make().Remove(); + Assert.False(Directory.Exists(Path.Combine(_target, "demo"))); + Assert.False(Directory.Exists(Path.Combine(_target, "other"))); + Assert.False(File.Exists(_manifest)); + } + + [Fact] + public void Changed_skill_survives_install_and_remove_with_a_warning() + { + Make().Install("en"); + File.WriteAllText(Path.Combine(_target, "demo", "SKILL.md"), "my own version"); + Write(_package, "demo/SKILL.md", "---\ndescription: English v2\n---\nbody"); + + var lines = Make().Install(); + Assert.Contains(lines, l => l.StartsWith("warning: demo was changed locally (SKILL.md)", StringComparison.Ordinal)); + Assert.Equal("my own version", Installed("demo/SKILL.md")); + Assert.Equal(["demo"], Make().ChangedLocally()); + + var removed = Make().Remove(); + Assert.Contains(removed, l => l.StartsWith("warning: demo was changed locally", StringComparison.Ordinal)); + Assert.Contains("other: removed", removed); + Assert.Equal("my own version", Installed("demo/SKILL.md")); + Assert.False(Directory.Exists(Path.Combine(_target, "other"))); + } + + [Fact] + public void Folder_not_written_by_xpp_lens_is_left_alone() + { + Write(_target, "demo/SKILL.md", "someone else's skill"); + var lines = Make().Install("en"); + Assert.Contains(lines, l => l.StartsWith("warning: demo already exists and differs", StringComparison.Ordinal)); + Assert.Equal("someone else's skill", Installed("demo/SKILL.md")); + Assert.Contains(lines, l => l.StartsWith("other: installed", StringComparison.Ordinal)); + + // Not in the manifest, so removing never touches it. + Make().Remove(); + Assert.Equal("someone else's skill", Installed("demo/SKILL.md")); + } + + [Fact] + public void Shipped_skills_have_both_languages() + { + var dir = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "skills"); + foreach (var skill in new[] { "xpp-ponytail", "xpp-ponytail-review" }) + { + var en = Skills.Files(Path.Combine(dir, skill), "en"); + var pl = Skills.Files(Path.Combine(dir, skill), "pl"); + Assert.Equal(["SKILL.md"], en.Keys); + Assert.EndsWith("SKILL.pl.md", pl["SKILL.md"]); + Assert.StartsWith($"---\nname: {skill}\n", File.ReadAllText(en["SKILL.md"]).Replace("\r", "")); + Assert.StartsWith($"---\nname: {skill}\n", File.ReadAllText(pl["SKILL.md"]).Replace("\r", "")); + } + } +} diff --git a/uninstall.ps1 b/uninstall.ps1 index 0912dd4..bb59cda 100644 --- a/uninstall.ps1 +++ b/uninstall.ps1 @@ -1,9 +1,10 @@ <# .SYNOPSIS - Removes xpp-lens: the MCP entries in Claude and, optionally, the index and program files. + Removes xpp-lens: the MCP entries in Claude, its Claude Code skills and, optionally, the index and program files. .DESCRIPTION - Only MCP entries that point to this installation are removed. The index location is read + Only MCP entries that point to this installation are removed, and only the skills in ~/.claude/skills that + xpp-lens installed and nobody changed since. The index location is read from the configuration (by default %LOCALAPPDATA%\xpp-lens\index); only the index files (xpp.db*) are deleted, never other content of that folder. Source code and build outputs found in the installation folder (src, dist, build, *.cs, *.csproj) are left untouched. @@ -47,7 +48,14 @@ if ($indexFolders.Count -eq 0) { $indexFolders += Join-Path $env:LOCALAPPDATA 'x $indexFolders += Join-Path $InstallDir 'index' # older installations $indexFolders = @($indexFolders | Select-Object -Unique | Where-Object { Test-Path (Join-Path $_ 'xpp.db*') }) -if (Test-Path $exe) { & $exe unregister } else { Say "$exe not found - skipping unregistration" } +if (Test-Path $exe) { + & $exe unregister + # Only the skills xpp-lens installed and nobody changed; a changed one stays, with a warning. + & $exe skills remove | ForEach-Object { + if ($_ -like 'warning:*') { Write-Host " $_" -ForegroundColor Yellow } else { Say $_ } + } +} +else { Say "$exe not found - skipping unregistration and skills" } $target = [IO.Path]::GetFullPath($InstallDir).TrimEnd('\') + '\' Get-CimInstance Win32_Process -Filter "Name='xpplens.exe'" | From 96d19b372f611034380709d05ace31d1fc17affe Mon Sep 17 00:00:00 2001 From: WillowDev <216433317+willowdev-code@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:55:58 +0100 Subject: [PATCH 2/4] Skills with LF line endings on every checkout --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8788216 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Skills are installed by SHA-256 (a changed one is kept): the same bytes on every checkout, CI included. +skills/** text eol=lf From 9f88e3f42511fdca5b204d3fa4cb52614b6a8a04 Mon Sep 17 00:00:00 2001 From: WillowDev <216433317+willowdev-code@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:56:53 +0100 Subject: [PATCH 3/4] xpp-lens 1.4.0-beta.1 (test release) ### Added - Claude Code skills in the package: `/xpp-ponytail` (the smallest complete X++ change: configuration, standard, the model's own code, a narrow extension, only then new code) and `/xpp-ponytail-review` (a review of an X++ change with a concrete case and the smallest fix per finding), built on the `xpp_*` tools. Based on [Ponytail](https://github.com/DietrichGebert/ponytail) (MIT), rewritten for X++; the notice is in `skills/THIRD-PARTY-NOTICES.md`. The source of the skills is now this repository (`skills\`). - The setup program and `install.ps1` copy them to `%USERPROFILE%\.claude\skills`, in the language of the installation (English or Polish descriptions and review headings: the wizard's language, or `install.ps1 -SkillsLanguage en|pl`, by default the Windows language), kept by updates. A check box in the wizard and `install.ps1 -NoSkills` skip them. - A skill changed locally is never overwritten or removed: `skills-installed.json` in the installation folder keeps the SHA-256 of what was installed; a changed skill is kept with a warning (the wizard shows it, `install.ps1` prints it, `xpplens status` lists it after a silent update). The uninstallers remove only unchanged skills they installed. A folder of that name not installed by xpp-lens is left alone unless it equals the package's copy. - `xpplens skills [install [--lang en|pl] | remove | status]`. --- src/XppLens/XppLens.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/XppLens/XppLens.csproj b/src/XppLens/XppLens.csproj index b9aa4ac..2ae4f42 100644 --- a/src/XppLens/XppLens.csproj +++ b/src/XppLens/XppLens.csproj @@ -22,7 +22,7 @@ xpp-lens — X++ code index for MCP X++ (Dynamics 365 F&O) code index served to AI assistants over MCP Copyright © 2026 WillowDev - 1.3.3 + 1.4.0-beta.1 From 8a52d95f8e743582e09f87c1c40fb985bee7d286 Mon Sep 17 00:00:00 2001 From: WillowDev <216433317+willowdev-code@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:03:58 +0100 Subject: [PATCH 4/4] xpp-lens 1.4.0 ### Added - Claude Code skills in the package: `/xpp-ponytail` (the smallest complete X++ change: configuration, standard, the model's own code, a narrow extension, only then new code) and `/xpp-ponytail-review` (a review of an X++ change with a concrete case and the smallest fix per finding), built on the `xpp_*` tools. Based on [Ponytail](https://github.com/DietrichGebert/ponytail) (MIT), rewritten for X++; the notice is in `skills/THIRD-PARTY-NOTICES.md`. The source of the skills is now this repository (`skills\`). - The setup program and `install.ps1` copy them to `%USERPROFILE%\.claude\skills`, in the language of the installation (English or Polish descriptions and review headings: the wizard's language, or `install.ps1 -SkillsLanguage en|pl`, by default the Windows language), kept by updates. A check box in the wizard and `install.ps1 -NoSkills` skip them. - A skill changed locally is never overwritten or removed: `skills-installed.json` in the installation folder keeps the SHA-256 of what was installed; a changed skill is kept with a warning (the wizard shows it, `install.ps1` prints it, `xpplens status` lists it after a silent update). The uninstallers remove only unchanged skills they installed. A folder of that name not installed by xpp-lens is left alone unless it equals the package's copy. - `xpplens skills [install [--lang en|pl] | remove | status]`. --- src/XppLens/XppLens.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/XppLens/XppLens.csproj b/src/XppLens/XppLens.csproj index 2ae4f42..57c6691 100644 --- a/src/XppLens/XppLens.csproj +++ b/src/XppLens/XppLens.csproj @@ -22,7 +22,7 @@ xpp-lens — X++ code index for MCP X++ (Dynamics 365 F&O) code index served to AI assistants over MCP Copyright © 2026 WillowDev - 1.4.0-beta.1 + 1.4.0