From 0929211f114b6c288f2d117c8468a8e8739936f7 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 14:52:11 +0200 Subject: [PATCH 01/23] chore: update AXSharp package versions to 0.47.0-alpha.484 and reconcile transitive dependencies --- .config/dotnet-tools.json | 6 +- Directory.Packages.props | 12 +- scripts/update_axsharp_versions.ps1 | 170 +++++++++++++++++++++++++++- 3 files changed, 176 insertions(+), 12 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d91096d5a..d48523e89 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "AXSharp.ixc": { - "version": "0.47.0-alpha.479", + "version": "0.47.0-alpha.484", "commands": [ "ixc" ], @@ -17,14 +17,14 @@ "rollForward": false }, "AXSharp.ixd": { - "version": "0.47.0-alpha.479", + "version": "0.47.0-alpha.484", "commands": [ "ixd" ], "rollForward": false }, "AXSharp.ixr": { - "version": "0.47.0-alpha.479", + "version": "0.47.0-alpha.484", "commands": [ "ixr" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index b0b218dc6..f99f35460 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,12 +11,12 @@ - - - - - - + + + + + + diff --git a/scripts/update_axsharp_versions.ps1 b/scripts/update_axsharp_versions.ps1 index 839983692..7082969da 100644 --- a/scripts/update_axsharp_versions.ps1 +++ b/scripts/update_axsharp_versions.ps1 @@ -8,7 +8,11 @@ 3. Updates versions for: - .config/dotnet-tools.json (all tools whose name starts with AXSharp. or Inxton.Operon.) - src/Directory.Packages.props (all and Include="Inxton.Operon.*" ... /> entries) - 4. Supports -DryRun to preview changes and -Verbose for detailed logging. + 4. Reconciles AXSharp's transitive (third-party) dependencies: reads each AXSharp.* package's + .nuspec at the resolved version, and bumps any already-pinned entry up to the + version AXSharp requires (bump-up only; never downgrades, never adds new entries). Useful with + CentralPackageTransitivePinningEnabled. Disable with -SkipTransitive. + 5. Supports -DryRun to preview changes and -Verbose for detailed logging. .PARAMETER AxSharpVersion Explicit version to set for AXSharp.* packages instead of auto-detecting the latest from NuGet. @@ -39,7 +43,8 @@ param( [string]$Username, # Optional for private feed auth (GitHub Packages). If omitted and Token supplied, 'USERNAME' placeholder is used. [string]$Token, # Personal Access Token or NuGet API key for private feed (PAT needs packaging:read scope) [switch]$NormalizeJson, # When set, rewrites dotnet-tools.json with standard compact formatting instead of preserving existing indentation - [switch]$ListAvailable # If set, lists available versions (after auth) and exits (unless versions also supplied) + [switch]$ListAvailable, # If set, lists available versions (after auth) and exits (unless versions also supplied) + [switch]$SkipTransitive # If set, does not reconcile AXSharp's transitive (third-party) dependencies in Directory.Packages.props ) Set-StrictMode -Version Latest @@ -181,6 +186,107 @@ function Get-LatestVersion { } } +function Get-FeedContext { + # Resolves the service index once and returns the PackageBaseAddress + auth headers + # for reuse across multiple package requests (versions, nuspec, ...). + param([string]$Feed,[string]$User,[string]$Tok) + $serviceIndexUrl = if($Feed.ToLower().EndsWith('index.json')) { $Feed } else { ($Feed.TrimEnd('/')) + '/index.json' } + $headers = @{} + if($Tok){ + $u = if($User){$User}else{'USERNAME'} + $basic = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $u,$Tok))) + $headers['Authorization'] = "Basic $basic" + } + $si = Invoke-RestMethod -Uri $serviceIndexUrl -Headers $headers -TimeoutSec 30 + if(-not $si.resources){ throw "Service index missing resources at $serviceIndexUrl" } + $pkgBase = ($si.resources | Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | Select-Object -First 1).'@id' + if(-not $pkgBase){ throw 'PackageBaseAddress/3.0.0 resource not found in service index.' } + if($pkgBase[-1] -ne '/') { $pkgBase += '/' } + [PSCustomObject]@{ PkgBase=$pkgBase; Headers=$headers } +} + +function Get-NuspecXml { + # Returns the .nuspec of a package@version as an [xml]. Tries the flat-container .nuspec + # endpoint first (served by nuget.org); on 404 (GitHub Packages does not expose it) falls + # back to downloading the .nupkg and extracting the .nuspec entry from the zip. + param([string]$PkgBase,[hashtable]$Headers,[string]$PackageId,[string]$Version) + $lowerId = $PackageId.ToLower() + + $nuspecUrl = "$PkgBase$lowerId/$Version/$lowerId.nuspec" + try { + $resp = Invoke-WebRequest -Uri $nuspecUrl -Headers $Headers -TimeoutSec 30 -UseBasicParsing + return [xml]$resp.Content + } catch { + $code = $null + if($_.Exception.Response){ $code = [int]$_.Exception.Response.StatusCode } + if($code -and $code -ne 404){ throw "Failed to fetch nuspec ($nuspecUrl): $($_.Exception.Message)" } + # 404 -> feed does not serve standalone .nuspec; fall back to the .nupkg below. + } + + $nupkgUrl = "$PkgBase$lowerId/$Version/$lowerId.$Version.nupkg" + $tmpPkg = [System.IO.Path]::GetTempFileName() + '.nupkg' + try { + Invoke-WebRequest -Uri $nupkgUrl -Headers $Headers -TimeoutSec 60 -UseBasicParsing -OutFile $tmpPkg + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue + $zip = [System.IO.Compression.ZipFile]::OpenRead($tmpPkg) + try { + $entry = $zip.Entries | Where-Object { $_.FullName.ToLower().EndsWith('.nuspec') } | Select-Object -First 1 + if(-not $entry){ throw "No .nuspec entry inside $nupkgUrl" } + $sr = New-Object System.IO.StreamReader($entry.Open()) + try { $content = $sr.ReadToEnd() } finally { $sr.Dispose() } + } finally { + $zip.Dispose() + } + return [xml]$content + } catch { + throw "Failed to read package metadata for ${PackageId}@${Version} (tried nuspec + nupkg): $($_.Exception.Message)" + } finally { + Remove-Item -LiteralPath $tmpPkg -ErrorAction SilentlyContinue + } +} + +function Get-PackageDependencies { + # Reads a package's .nuspec at a specific version and returns its declared dependencies + # as a flat list of [PSCustomObject]@{ Id; Version }, across all target frameworks. + param([string]$PkgBase,[hashtable]$Headers,[string]$PackageId,[string]$Version) + $doc = Get-NuspecXml -PkgBase $PkgBase -Headers $Headers -PackageId $PackageId -Version $Version + $nsUri = $doc.DocumentElement.NamespaceURI + if($nsUri){ + $nsm = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $nsm.AddNamespace('n',$nsUri) + $nodes = $doc.SelectNodes('//n:dependency',$nsm) + } else { + $nodes = $doc.SelectNodes('//dependency') + } + $deps = @() + foreach($n in $nodes){ + $id = $n.GetAttribute('id') + $ver = $n.GetAttribute('version') + if($id){ $deps += [PSCustomObject]@{ Id=$id; Version=$ver } } + } + return $deps +} + +function Get-VersionLowerBound { + # Extracts the lower-bound version from a NuGet dependency version string. + # Handles plain ("1.2.3"), exact ("[1.2.3]") and range ("[1.2.3, )", "(1.0,2.0)") forms. + # Returns $null when no usable lower bound exists (e.g. "(,2.0)"). + param([string]$Range) + if([string]::IsNullOrWhiteSpace($Range)){ return $null } + $r = $Range.Trim().Trim('[',']','(',')',' ') + $r = $r.Split(',')[0].Trim() + if([string]::IsNullOrWhiteSpace($r)){ return $null } + return $r +} + +function Test-VersionGreater { + # Returns $true when semver-ish version $A is strictly greater than $B. + param([string]$A,[string]$B) + if([string]::IsNullOrWhiteSpace($B)){ return $true } + if([string]::IsNullOrWhiteSpace($A)){ return $false } + return ( (Compare-VersionRecord (ConvertTo-VersionRecord $A) (ConvertTo-VersionRecord $B)) -gt 0 ) +} + # Query for AXSharp version if(-not $AxSharpVersion){ Write-Info "Discovering latest AXSharp.ixc version from source: $Source ..." @@ -340,12 +446,70 @@ $propsUpdated = [System.Text.RegularExpressions.Regex]::Replace($propsUpdated, $ } }) +### Reconcile AXSharp transitive (third-party) dependencies +# Read each AXSharp.* package's .nuspec at the resolved version, then bump any already-pinned +# entry up to the version AXSharp requires. Bump-up only: never downgrades a +# deliberately-pinned newer version, and never adds entries that aren't already listed. +if(-not $SkipTransitive){ + Write-Info 'Reconciling AXSharp transitive dependencies...' + $axPkgIds = [regex]::Matches($propsRaw,')' + $propsUpdated = [System.Text.RegularExpressions.Regex]::Replace($propsUpdated, $pattern, { + param($m) + $old = $m.Groups[2].Value + if(Test-VersionGreater $newVer $old){ + $script:changes += "Directory.Packages.props (transitive): $depId $old -> $newVer" + return $m.Groups[1].Value + $newVer + $m.Groups[3].Value + } else { + if($Detailed -and $old -ne $newVer){ Write-Info "Transitive $depId left at $old (AXSharp requires >= $newVer; pinned version is newer)" } + return $m.Value + } + }) + } + } +} + if(-not $DryRun){ if($propsUpdated -ne $propsRaw){ #Set-Content -Path $propsPath -Value $propsUpdated -Encoding UTF8 Write-Utf8NoBom-LF -Path $propsPath -Content $propsUpdated } elseif($Detailed){ - Write-Info 'No AXSharp.* or Inxton.Operon.* entries needed updating in Directory.Packages.props.' + Write-Info 'No AXSharp.*, Inxton.Operon.* or transitive entries needed updating in Directory.Packages.props.' } } From 32e04d4a00632d5b806b008b2c5ff81c5072ba3f Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 16:17:31 +0200 Subject: [PATCH 02/23] chore: remove package-lock.json from AXOpen.Data.Blazor --- src/data/src/AXOpen.Data.Blazor/package-lock.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 src/data/src/AXOpen.Data.Blazor/package-lock.json diff --git a/src/data/src/AXOpen.Data.Blazor/package-lock.json b/src/data/src/AXOpen.Data.Blazor/package-lock.json deleted file mode 100644 index a1067c7ca..000000000 --- a/src/data/src/AXOpen.Data.Blazor/package-lock.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "bootstrap-icons": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz", - "integrity": "sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw==" - } - } -} From f0bd4da8760e343e830a15160b6789aafc2a5a95 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 16:21:38 +0200 Subject: [PATCH 03/23] chore: add skill for updating AXSharp and Inxton.Operon package versions --- .../skills/update-axsharp-version/SKILL.md | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 .claude/skills/update-axsharp-version/SKILL.md diff --git a/.claude/skills/update-axsharp-version/SKILL.md b/.claude/skills/update-axsharp-version/SKILL.md new file mode 100644 index 000000000..3cfc27e2d --- /dev/null +++ b/.claude/skills/update-axsharp-version/SKILL.md @@ -0,0 +1,167 @@ +--- +name: update-axsharp-version +description: Update AXSharp.* and Inxton.Operon.* package versions in this repo (via scripts/update_axsharp_versions.ps1), verify the build, update the central CHANGELOG.md, and open a PR with a generated description. Use when the user asks to bump/update AXSharp versions or open a dependency-update PR. Defaults to the latest versions from the feed; accepts optional explicit versions. +--- + +# Update AXSharp version + +Drives the AXSharp / Inxton.Operon dependency bump end to end: run the existing update script → +verify the build → record the change in the central `CHANGELOG.md` → open a PR. + +The actual file edits live in [`scripts/update_axsharp_versions.ps1`](../../../scripts/update_axsharp_versions.ps1). +**Do not reimplement version rewriting** — this skill only orchestrates around that script. + +By default the script auto-detects the latest `AXSharp.ixc` and `Inxton.Operon` versions from the +GitHub Packages feed. The user may pass explicit versions; thread them through as +`-AxSharpVersion ` / `-OperonVersion ` on every script invocation below. + +## Procedure + +### 1. Preconditions + +- Run all commands from the **repo root**. The script resolves its own `repoRoot`, but `git`/`gh` + operations assume root. +- **Token (auto-detect mode only):** the script needs a feed token with `read:packages` scope. It + reads, in order, `AXSHARP_FEED_TOKEN`, `GITHUB_PACKAGES_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`, + `NUGET_TOKEN`. If none is set **and** no explicit versions were given, tell the user to set one + (or pass `-Token`) and stop. Skip this check when explicit versions are supplied. +- **Clean tree:** run `git status`. If the working tree is dirty, surface it and stop — do not bundle + unrelated changes into a dependency PR. + +### 2. Branch + +Check the current branch (`git branch --show-current`). If on the default branch `dev` (or any +protected branch), create a working branch first. Prefer a version-tagged name once the version is +known, otherwise a generic one: + +```powershell +git switch -c chore/update-axsharp-versions +``` + +If already on a non-default working branch, stay on it. + +### 3. Preview (recommended) + +Show the user what will change before touching files: + +```powershell +pwsh -File scripts/update_axsharp_versions.ps1 -DryRun -Detailed +``` + +Append `-AxSharpVersion -OperonVersion ` to override. Use `-ListAvailable` to inspect feed +versions. The dry run must not modify files — `git status` stays clean. + +### 4. Apply + +Run for real and **capture stdout** — the `Summary of changes:` and `Target versions:` blocks feed +the CHANGELOG entry, commit message, and PR body: + +```powershell +pwsh -File scripts/update_axsharp_versions.ps1 +# or, with overrides: +pwsh -File scripts/update_axsharp_versions.ps1 -AxSharpVersion -OperonVersion +``` + +Handle outcomes: +- **Non-zero exit** — exit `2` = feed/auth failure, exit `3` = JSON parse failure. Report the error + output and stop. +- **No changes** — if `git status` shows nothing changed, report "already up to date at + ``" and stop. Do **not** open a PR. + +Record the resolved `` and `` from the script's `Target versions:` +block for the steps below. + +### 5. Verify build + +Confirm the new package set resolves before opening the PR (build only, no tests): + +```powershell +dotnet run --project cake/Build.csproj -- --do-apax-update +``` + +`--do-apax-update` pulls the apax-side packages; the .NET restore covers the central-managed NuGet +bumps. For a heavier check, [`scripts/build_with_update.ps1`](../../../scripts/build_with_update.ps1) +runs build + tests at level 10. + +If the build **fails**: report the failure with output and **do not open the PR**. Leave the changes +on the branch for the user to inspect. + +### 6. Update central CHANGELOG.md + +Prepend a new entry at the **top** of [`CHANGELOG.md`](../../../CHANGELOG.md) (newest-on-top, above +the current first `###` heading), matching the existing structured format. Use category tag `[DEPS]`. +Fill bullets from the script's `Summary of changes:` block. + +```markdown +### [DEPS] Update AXSharp / Inxton.Operon package versions to + +**Note:** Dependency version bump via `scripts/update_axsharp_versions.ps1`. No source change. +Branch: ``. + +- chore: AXSharp.* packages updated to `` in `.config/dotnet-tools.json` and `Directory.Packages.props`. +- chore: Inxton.Operon.* packages updated to ``. +- chore: reconciled AXSharp transitive (third-party) dependencies in `Directory.Packages.props` (bump-up only). + + +**Impact:** +- Repo now builds and tests against AXSharp `` / Inxton.Operon ``. + +**Risks/Review:** +- Transitive pinned versions were bumped up to satisfy AXSharp's nuspec requirements; review `Directory.Packages.props` diff. + +**Testing:** +- `dotnet run --project cake/Build.csproj -- --do-apax-update` — build succeeds with the new package set. +``` + +### 7. Commit + +Match the repo convention (see commit `0929211f1`): + +```powershell +git add .config/dotnet-tools.json Directory.Packages.props CHANGELOG.md +git commit -m "chore: update AXSharp package versions to and reconcile transitive dependencies" +git push -u origin HEAD +``` + +Scope the `git add` to the touched files. (The script edits only these in a normal run.) + +### 8. Create PR + +```powershell +gh pr create --base dev --title "chore: update AXSharp package versions to " --body "" +``` + +Use the template below for ``, filling placeholders and pasting the script's +`Summary of changes:` lines as a bullet list. Report the resulting PR URL to the user. + +#### PR body template + +```markdown +## Update AXSharp / Inxton.Operon package versions + +Bumps AXSharp.* and Inxton.Operon.* packages and reconciles transitive (third-party) dependencies +that AXSharp requires, via `scripts/update_axsharp_versions.ps1`. + +### Target versions +- **AXSharp.\***: `` +- **Inxton.Operon.\***: `` + +### Changes +- `.config/dotnet-tools.json` — tool versions bumped +- `Directory.Packages.props` — package versions + bump-up of pinned transitive deps +- `CHANGELOG.md` — `[DEPS]` entry prepended + + + +### Verification +- [x] `dotnet run --project cake/Build.csproj -- --do-apax-update` succeeded + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +``` + +## Notes + +- Script switches: `-DryRun`, `-Detailed`, `-ListAvailable`, `-AxSharpVersion`, `-OperonVersion`, + `-SkipTransitive`, `-Source`, `-Token`, `-Username`, `-NormalizeJson`. +- Default PR base branch: `dev`. +- Build options live in [`cake/BuildParameters.cs`](../../../cake/BuildParameters.cs). From fd90b840c7f8545239d82e43846ce61dd8e40dd3 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 16:21:50 +0200 Subject: [PATCH 04/23] chore: change develop branch mode to ContinuousDeployment in GitVersion.yml --- GitVersion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GitVersion.yml b/GitVersion.yml index ef800d38e..c4aeb37fd 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -15,7 +15,7 @@ branches: pre-release-weight: 55000 develop: regex: ^dev(elop)?(ment)?$ - mode: ManualDeployment + mode: ContinuousDeployment label: alpha increment: Minor prevent-increment: From 029813e444b10e1629a6bbd93e618eff969d71ec Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 17:12:47 +0200 Subject: [PATCH 05/23] feat: add vulnerability scanning and reporting for npm and NuGet dependencies - Implemented `update-vulnerable-deps.ps1` script to scan for vulnerable dependencies and apply safe fixes. - Introduced `_deps-common.ps1` for shared helper functions used in dependency management. - Updated `Directory.Packages.props` to include security pins for vulnerable packages. - Added `.gitignore` entries to exclude vulnerability scan reports. - Created `apax.yml` for managing dependencies in the apax traversal project. --- .gitignore | 3 + Directory.Packages.props | 18 +- scripts/_deps-common.ps1 | 153 ++++++++ scripts/update-vulnerable-deps.ps1 | 594 +++++++++++++++++++++++++++++ src/traversals/apax/apax.yml | 51 +++ 5 files changed, 807 insertions(+), 12 deletions(-) create mode 100644 scripts/_deps-common.ps1 create mode 100644 scripts/update-vulnerable-deps.ps1 create mode 100644 src/traversals/apax/apax.yml diff --git a/.gitignore b/.gitignore index 6188d277e..cb9410427 100644 --- a/.gitignore +++ b/.gitignore @@ -439,3 +439,6 @@ operon-variables.css **/app/gsd/source/** *.lscache + +# Vulnerability scan reports (scripts/update-vulnerable-deps.ps1) +scripts/reports/ diff --git a/Directory.Packages.props b/Directory.Packages.props index f99f35460..6ccee4040 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -88,15 +88,9 @@ - - - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/scripts/_deps-common.ps1 b/scripts/_deps-common.ps1 new file mode 100644 index 000000000..6db927c92 --- /dev/null +++ b/scripts/_deps-common.ps1 @@ -0,0 +1,153 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS +Shared helpers for dependency-management scripts (dot-source this file). + +.DESCRIPTION +Common logging, file IO, semver comparison, NuGet v3 feed access, token discovery and +severity utilities used by scripts/update-vulnerable-deps.ps1 (and reusable by +scripts/update_axsharp_versions.ps1). Several functions here originated in +update_axsharp_versions.ps1 and were factored out so the two scripts share one source of truth. + +Dot-source it from a script: + . "$PSScriptRoot/_deps-common.ps1" +#> + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- +function Write-Info($msg){ Write-Host "[INFO ] $msg" -ForegroundColor Cyan } +function Write-Warn($msg){ Write-Host "[WARN ] $msg" -ForegroundColor Yellow } +function Write-Err ($msg){ Write-Host "[ERROR] $msg" -ForegroundColor Red } + +# --------------------------------------------------------------------------- +# File IO +# --------------------------------------------------------------------------- +function Write-Utf8NoBom-LF { + # Writes text as UTF-8 (no BOM) with LF line endings to preserve repo conventions. + param([string]$Path, [string]$Content) + $lf = ($Content -replace "`r`n", "`n") -replace "`r", "`n" + [System.IO.File]::WriteAllText($Path, $lf, [System.Text.UTF8Encoding]::new($false)) +} + +# --------------------------------------------------------------------------- +# Token discovery +# --------------------------------------------------------------------------- +function Resolve-FeedToken { + # Returns the first non-empty token from the supplied value or the candidate env vars. + param( + [string]$Token, + [string[]]$EnvCandidates = @('NUGET_TOKEN','GITHUB_PACKAGES_TOKEN','GITHUB_TOKEN','GH_TOKEN'), + [switch]$Detailed + ) + if($Token){ return $Token } + foreach($c in $EnvCandidates){ + $candidate = [Environment]::GetEnvironmentVariable($c) + if($candidate){ + if($Detailed){ Write-Info "Using token from environment variable $c" } + return $candidate + } + } + return $null +} + +# --------------------------------------------------------------------------- +# Semver parsing / comparison (build metadata ignored for ordering) +# --------------------------------------------------------------------------- +function ConvertTo-VersionRecord { + param([string]$v) + $core = $v + $pre = '' + $buildSplit = $core.Split('+',2) + if($buildSplit.Count -gt 1){ $core = $buildSplit[0] } + $dashIdx = $core.IndexOf('-') + if($dashIdx -ge 0){ + $pre = $core.Substring($dashIdx + 1) + $core = $core.Substring(0,$dashIdx) + } + $parts = $core.Split('.') + [int]$maj = if($parts.Count -gt 0){ $parts[0] } else { 0 } + [int]$min = if($parts.Count -gt 1){ $parts[1] } else { 0 } + [int]$pat = if($parts.Count -gt 2){ $parts[2] } else { 0 } + $preSegs = @() + if($pre){ $preSegs = $pre.Split('.') } + [PSCustomObject]@{ Original=$v; Major=$maj; Minor=$min; Patch=$pat; Pre=$pre; PreSegs=$preSegs } +} + +function Compare-VersionRecord { + param($a,$b) + if($a.Major -ne $b.Major){ return [Math]::Sign($a.Major - $b.Major) } + if($a.Minor -ne $b.Minor){ return [Math]::Sign($a.Minor - $b.Minor) } + if($a.Patch -ne $b.Patch){ return [Math]::Sign($a.Patch - $b.Patch) } + $aHasPre = [string]::IsNullOrEmpty($a.Pre) -ne $true + $bHasPre = [string]::IsNullOrEmpty($b.Pre) -ne $true + if($aHasPre -and -not $bHasPre){ return -1 } + if($bHasPre -and -not $aHasPre){ return 1 } + if(-not $aHasPre -and -not $bHasPre){ return 0 } + $len = [Math]::Max($a.PreSegs.Count,$b.PreSegs.Count) + for($i=0;$i -lt $len;$i++){ + if($i -ge $a.PreSegs.Count){ return -1 } + if($i -ge $b.PreSegs.Count){ return 1 } + $as = $a.PreSegs[$i]; $bs = $b.PreSegs[$i] + $aNum = $as -as [int]; $bNum = $bs -as [int] + $aIsNum = $aNum -ne $null; $bIsNum = $bNum -ne $null + if($aIsNum -and $bIsNum){ if($aNum -ne $bNum){ return [Math]::Sign($aNum - $bNum) } } + elseif($aIsNum -and -not $bIsNum){ return -1 } + elseif($bIsNum -and -not $aIsNum){ return 1 } + else { $cmp = [string]::Compare($as,$bs,$true); if($cmp -ne 0){ return [Math]::Sign($cmp) } } + } + return 0 +} + +function Test-VersionGreater { + # Returns $true when semver-ish version $A is strictly greater than $B. + param([string]$A,[string]$B) + if([string]::IsNullOrWhiteSpace($B)){ return $true } + if([string]::IsNullOrWhiteSpace($A)){ return $false } + return ( (Compare-VersionRecord (ConvertTo-VersionRecord $A) (ConvertTo-VersionRecord $B)) -gt 0 ) +} + +function Test-IsPrerelease { + param([string]$v) + if([string]::IsNullOrWhiteSpace($v)){ return $false } + return -not [string]::IsNullOrEmpty((ConvertTo-VersionRecord $v).Pre) +} + +# --------------------------------------------------------------------------- +# NuGet v3 feed access +# --------------------------------------------------------------------------- +function Get-FeedContext { + # Resolves the service index once and returns the PackageBaseAddress + auth headers + # for reuse across multiple package requests. + param([string]$Feed,[string]$User,[string]$Tok) + $serviceIndexUrl = if($Feed.ToLower().EndsWith('index.json')) { $Feed } else { ($Feed.TrimEnd('/')) + '/index.json' } + $headers = @{} + if($Tok){ + $u = if($User){$User}else{'USERNAME'} + $basic = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $u,$Tok))) + $headers['Authorization'] = "Basic $basic" + } + $si = Invoke-RestMethod -Uri $serviceIndexUrl -Headers $headers -TimeoutSec 30 + if(-not $si.resources){ throw "Service index missing resources at $serviceIndexUrl" } + $pkgBase = ($si.resources | Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | Select-Object -First 1).'@id' + if(-not $pkgBase){ throw 'PackageBaseAddress/3.0.0 resource not found in service index.' } + if($pkgBase[-1] -ne '/') { $pkgBase += '/' } + [PSCustomObject]@{ PkgBase=$pkgBase; Headers=$headers } +} + +function Get-PackageVersionsFromFeed { + # Returns the raw list of available version strings for a package id on a v3 feed. + param([string]$PkgBase,[hashtable]$Headers,[string]$PackageId) + $lowerId = $PackageId.ToLower() + $indexUrl = "$PkgBase$lowerId/index.json" + try { + $idx = Invoke-RestMethod -Uri $indexUrl -Headers $Headers -TimeoutSec 30 + } catch { + $code = $null + if($_.Exception.Response){ $code = [int]$_.Exception.Response.StatusCode } + if($code -eq 404){ return @() } # package id unknown on this feed + throw "Failed to fetch version index ($indexUrl): $($_.Exception.Message)" + } + if(-not $idx.versions){ return @() } + return $idx.versions +} diff --git a/scripts/update-vulnerable-deps.ps1 b/scripts/update-vulnerable-deps.ps1 new file mode 100644 index 000000000..c2d68c6a3 --- /dev/null +++ b/scripts/update-vulnerable-deps.ps1 @@ -0,0 +1,594 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS +Scans the whole repository for vulnerable npm and NuGet dependencies and applies safe fixes. + +.DESCRIPTION +One command to remediate moderate-and-above security advisories across both ecosystems: + + NuGet - iterates every .csproj, restores it, runs `dotnet list package --vulnerable + --include-transitive`, dedupes advisories, SKIPS AXSharp.*/Inxton.Operon.* (those are + owned by scripts/update_axsharp_versions.ps1), resolves the minimum safe version from + the GitHub Advisory Database, and writes the bump to Directory.Packages.props + (direct = bump in place, transitive = add a new pinned ). + + npm - audits the 6 source package.json projects (installing node_modules when missing) and + runs `npm audit fix` (safe, no --force). Advisories that would need a breaking major + bump are reported as unfixed. + +A timestamped Markdown + JSON report is written to scripts/reports/. With -CreatePR the changes +are committed to branch 'fix/vulnerable-dependencies' off dev and a PR is opened against dev. + +Exit code is non-zero when vulnerabilities at/above -MinSeverity remain unfixed, so CI can gate on +it. + +.PARAMETER DryRun +Preview everything; make no file changes, no commits, no PR. (Still exits non-zero if vulns found.) + +.PARAMETER NpmOnly +Scan/fix npm only. + +.PARAMETER NuGetOnly +Scan/fix NuGet only. + +.PARAMETER CreatePR +Commit fixes to branch 'fix/vulnerable-dependencies' off origin/dev and open a PR against dev. +Cannot be combined with -DryRun. + +.PARAMETER MinSeverity +Lowest severity to act on: low | moderate | high | critical. Default: moderate. + +.PARAMETER Source +NuGet v3 feed used to look up available versions (public packages). Default nuget.org. + +.PARAMETER Token +Token for the GitHub Advisory API (avoids rate limits). Falls back to NUGET_TOKEN / +GITHUB_PACKAGES_TOKEN / GITHUB_TOKEN / GH_TOKEN. + +.PARAMETER Detailed +Verbose logging. + +.EXAMPLE +./update-vulnerable-deps.ps1 -DryRun -Detailed + +.EXAMPLE +./update-vulnerable-deps.ps1 -NuGetOnly + +.EXAMPLE +./update-vulnerable-deps.ps1 -CreatePR +#> + +[CmdletBinding()] +param( + [switch]$DryRun, + [switch]$NpmOnly, + [switch]$NuGetOnly, + [switch]$CreatePR, + [ValidateSet('low','moderate','high','critical')] + [string]$MinSeverity = 'moderate', + [string]$Source = 'https://api.nuget.org/v3/index.json', + [string]$Token, + [switch]$Detailed +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptRoot/_deps-common.ps1" + +$repoRoot = Split-Path -Parent $scriptRoot +$propsPath = Join-Path $repoRoot 'Directory.Packages.props' + +# --------------------------------------------------------------------------- +# Validation & constants +# --------------------------------------------------------------------------- +if($DryRun -and $CreatePR){ Write-Err '-DryRun and -CreatePR cannot be combined.'; exit 1 } +if($NpmOnly -and $NuGetOnly){ Write-Err '-NpmOnly and -NuGetOnly cannot be combined.'; exit 1 } + +$doNuget = -not $NpmOnly +$doNpm = -not $NuGetOnly + +$SeverityRank = @{ low = 1; moderate = 2; high = 3; critical = 4 } +$minRank = $SeverityRank[$MinSeverity] + +# Packages owned by update_axsharp_versions.ps1 - never touched here. +$SkipPattern = '^(AXSharp|Inxton\.Operon|AXOpen)\b' + +$Token = Resolve-FeedToken -Token $Token -Detailed:$Detailed + +# Source npm projects (explicit list - avoids bin/obj/ctrl generated copies). +$NpmProjects = @( + 'src/components.abb.robotics/package.json' + 'src/components.abstractions/package.json' + 'src/data/package.json' + 'src/inspectors/package.json' + 'src/showcase/app/ix-blazor/showcase.blazor/package.json' + 'src/styling/src/package.json' +) | ForEach-Object { Join-Path $repoRoot $_ } + +# Accumulators for the report. +$NuGetFixed = New-Object System.Collections.ArrayList +$NuGetSkipped = New-Object System.Collections.ArrayList +$NuGetUnfixed = New-Object System.Collections.ArrayList +$NpmResults = New-Object System.Collections.ArrayList +$ScanErrors = New-Object System.Collections.ArrayList + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +function Test-CommandAvailable { + param([string]$Name) + $null -ne (Get-Command $Name -ErrorAction SilentlyContinue) +} + +function Get-GhsaIdFromUrl { + param([string]$Url) + if($Url -and ($Url -match '(GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4})')){ return $Matches[1] } + return $null +} + +$script:AdvisoryCache = @{} +function Get-AdvisoryPatchedVersion { + # Queries the GitHub Advisory Database for the first patched version of $PackageId. + # Returns the patched version string, or $null when it cannot be determined. + param([string]$GhsaId,[string]$PackageId) + if(-not $GhsaId){ return $null } + if(-not $script:AdvisoryCache.ContainsKey($GhsaId)){ + $headers = @{ 'User-Agent' = 'axopen-update-vulnerable-deps'; 'Accept' = 'application/vnd.github+json' } + if($Token){ $headers['Authorization'] = "Bearer $Token" } + try { + $script:AdvisoryCache[$GhsaId] = Invoke-RestMethod -Uri "https://api.github.com/advisories/$GhsaId" -Headers $headers -TimeoutSec 30 + } catch { + if($Detailed){ Write-Warn "Advisory lookup failed for ${GhsaId}: $($_.Exception.Message)" } + $script:AdvisoryCache[$GhsaId] = $null + } + } + $adv = $script:AdvisoryCache[$GhsaId] + if(-not $adv -or -not $adv.vulnerabilities){ return $null } + foreach($v in $adv.vulnerabilities){ + if($v.package -and $v.package.ecosystem -eq 'nuget' -and $v.package.name -and ($v.package.name.ToLower() -eq $PackageId.ToLower())){ + $fpv = $v.first_patched_version + if($null -eq $fpv){ return $null } + if($fpv -is [string]){ return $fpv } + if($fpv.PSObject.Properties.Name -contains 'identifier'){ return $fpv.identifier } + } + } + return $null +} + +function Resolve-SafeNuGetVersion { + # Determines the minimum safe version for a vulnerable package: + # 1. the highest 'first patched version' across the package's advisories, then + # 2. snapped up to the lowest version actually available on the feed that is >= that patch + # and strictly greater than the current resolved version. + # Returns $null (=> report as unfixed) when no safe version can be determined. + param([string]$PackageId,[string]$CurrentVersion,[string[]]$GhsaIds,$FeedCtx) + $patched = $null + foreach($g in $GhsaIds){ + $p = Get-AdvisoryPatchedVersion -GhsaId $g -PackageId $PackageId + if($p -and (Test-VersionGreater $p $patched)){ $patched = $p } + } + if(-not $patched){ return $null } + + $available = @() + try { $available = Get-PackageVersionsFromFeed -PkgBase $FeedCtx.PkgBase -Headers $FeedCtx.Headers -PackageId $PackageId } catch { $available = @() } + if(-not $available -or $available.Count -eq 0){ + # Feed gave us nothing; trust the advisory's patched version if it improves on current. + if(Test-VersionGreater $patched $CurrentVersion){ return $patched } else { return $null } + } + + $currentIsPre = Test-IsPrerelease $CurrentVersion + $candidates = $available | + Where-Object { ($currentIsPre) -or (-not (Test-IsPrerelease $_)) } | # stable only unless current is pre + Where-Object { (Test-VersionGreater $_ $CurrentVersion) -and -not (Test-VersionGreater $patched $_) } # > current AND >= patched + + if(-not $candidates -or @($candidates).Count -eq 0){ return $null } + + # lowest such candidate = minimum safe bump + $best = @($candidates)[0] + foreach($c in $candidates){ if(Test-VersionGreater $best $c){ $best = $c } } + return $best +} + +# --------------------------------------------------------------------------- +# Directory.Packages.props editing +# --------------------------------------------------------------------------- +function Update-PropsDirect { + # Bumps an existing in place. + # Returns updated content, or $null if the package id was not found. + param([string]$Content,[string]$PackageId,[string]$NewVersion) + $idEsc = [regex]::Escape($PackageId) + # Include before Version (repo convention) + $rx1 = "(]*\bInclude=`"$idEsc`"[^>]*\bVersion=`")[^`"]*(`")" + if([regex]::IsMatch($Content,$rx1)){ + return [regex]::Replace($Content,$rx1,"`${1}$NewVersion`${2}",1) + } + # Version before Include (defensive) + $rx2 = "(]*\bVersion=`")[^`"]*(`"[^>]*\bInclude=`"$idEsc`")" + if([regex]::IsMatch($Content,$rx2)){ + return [regex]::Replace($Content,$rx2,"`${1}$NewVersion`${2}",1) + } + return $null +} + +function Test-PropsHasPackage { + param([string]$Content,[string]$PackageId) + $idEsc = [regex]::Escape($PackageId) + return [regex]::IsMatch($Content,"]*\bInclude=`"$idEsc`"") +} + +function Add-PropsTransitivePin { + # Adds a into a managed "security pins" ItemGroup, creating it before + # if absent. Returns updated content. + param([string]$Content,[string]$PackageId,[string]$NewVersion) + $marker = '' + $entry = " " + if($Content.Contains($marker)){ + # Insert as the first entry inside the existing managed ItemGroup. + $rx = [regex]::Escape($marker) + "(\r?\n\s*)" + return [regex]::Replace($Content,$rx,"`$0`r`n$entry",1) + } + $block = " $marker`r`n `r`n$entry`r`n `r`n" + return ($Content -replace '\s*$',$block) +} + +# --------------------------------------------------------------------------- +# NuGet scan & fix +# --------------------------------------------------------------------------- +function Invoke-NuGetScan { + Write-Info 'Scanning NuGet dependencies...' + if(-not (Test-CommandAvailable 'dotnet')){ Write-Err 'dotnet CLI not found on PATH.'; [void]$ScanErrors.Add('dotnet CLI not found'); return } + + $csprojFiles = Get-ChildItem -Path (Join-Path $repoRoot 'src') -Recurse -Filter '*.csproj' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' } + # Include the cake build project (ships in the repo). + $cakeProj = Get-ChildItem -Path (Join-Path $repoRoot 'cake') -Recurse -Filter '*.csproj' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' } + $csprojFiles = @($csprojFiles) + @($cakeProj) + + Write-Info "Found $($csprojFiles.Count) project(s) to scan." + + # packageId -> aggregated advisory record + $vuln = @{} + $total = $csprojFiles.Count + $i = 0 + foreach($proj in $csprojFiles){ + $i++ + $pct = if($total -gt 0){ [int](($i-1) / $total * 100) } else { 0 } + Write-Progress -Activity 'Scanning NuGet projects' ` + -Status ("[$i/$total] $($proj.Name) - $($vuln.Count) vulnerable package(s) so far") ` + -CurrentOperation 'restoring...' -PercentComplete $pct + if($Detailed){ Write-Info "[$i/$total] $($proj.FullName)" } + try { + & dotnet restore $proj.FullName --nologo *> $null + } catch { + [void]$ScanErrors.Add("restore failed: $($proj.FullName)") + if($Detailed){ Write-Warn "restore failed: $($proj.Name)" } + continue + } + Write-Progress -Activity 'Scanning NuGet projects' ` + -Status ("[$i/$total] $($proj.Name) - $($vuln.Count) vulnerable package(s) so far") ` + -CurrentOperation 'listing vulnerabilities...' -PercentComplete $pct + $raw = & dotnet list $proj.FullName package --vulnerable --include-transitive --format json 2>$null | Out-String + if(-not $raw.Trim()){ continue } + try { $parsed = $raw | ConvertFrom-Json } catch { [void]$ScanErrors.Add("unparseable output: $($proj.FullName)"); continue } + if(-not ($parsed.PSObject.Properties.Name -contains 'projects')){ continue } + foreach($p in $parsed.projects){ + if(-not ($p.PSObject.Properties.Name -contains 'frameworks') -or -not $p.frameworks){ continue } + foreach($fw in $p.frameworks){ + foreach($kind in @('topLevelPackages','transitivePackages')){ + if(-not ($fw.PSObject.Properties.Name -contains $kind) -or -not $fw.$kind){ continue } + $isDirect = ($kind -eq 'topLevelPackages') + foreach($pkg in $fw.$kind){ + if(-not ($pkg.PSObject.Properties.Name -contains 'vulnerabilities') -or -not $pkg.vulnerabilities){ continue } + $id = $pkg.id + $resolved = $pkg.resolvedVersion + foreach($adv in $pkg.vulnerabilities){ + $sev = ("$($adv.severity)").ToLower() + $rank = if($SeverityRank.ContainsKey($sev)){ $SeverityRank[$sev] } else { 0 } + if($rank -lt $minRank){ continue } + $url = if($adv.PSObject.Properties.Name -contains 'advisoryurl'){ $adv.advisoryurl } else { '' } + if(-not $vuln.ContainsKey($id)){ + $vuln[$id] = [PSCustomObject]@{ + Id=$id; Resolved=$resolved; IsDirect=$isDirect; MaxRank=$rank; + Severity=$sev; Urls=(New-Object System.Collections.ArrayList); Projects=(New-Object System.Collections.ArrayList) + } + } + $rec = $vuln[$id] + if($isDirect){ $rec.IsDirect = $true } + if($rank -gt $rec.MaxRank){ $rec.MaxRank = $rank; $rec.Severity = $sev } + if($url -and -not $rec.Urls.Contains($url)){ [void]$rec.Urls.Add($url) } + if(-not $rec.Projects.Contains($proj.Name)){ [void]$rec.Projects.Add($proj.Name) } + } + } + } + } + } + } + + Write-Progress -Activity 'Scanning NuGet projects' -Completed + + if($vuln.Count -eq 0){ Write-Info 'No NuGet vulnerabilities at/above the threshold.'; return } + Write-Info "Found $($vuln.Count) vulnerable NuGet package(s) at/above '$MinSeverity'." + + # Resolve safe versions & apply. + $feedCtx = $null + try { $feedCtx = Get-FeedContext -Feed $Source -Tok $null } catch { Write-Warn "Could not initialise NuGet feed context: $($_.Exception.Message)" } + + $content = Get-Content -LiteralPath $propsPath -Raw + $changed = $false + + foreach($id in ($vuln.Keys | Sort-Object)){ + $rec = $vuln[$id] + if($id -match $SkipPattern){ + [void]$NuGetSkipped.Add([PSCustomObject]@{ Id=$id; Current=$rec.Resolved; Severity=$rec.Severity; Reason='skipped-AXSharp (owned by update_axsharp_versions.ps1)' }) + continue + } + $ghsaIds = @($rec.Urls | ForEach-Object { Get-GhsaIdFromUrl $_ } | Where-Object { $_ }) + $safe = $null + if($feedCtx){ $safe = Resolve-SafeNuGetVersion -PackageId $id -CurrentVersion $rec.Resolved -GhsaIds $ghsaIds -FeedCtx $feedCtx } + + if(-not $safe){ + [void]$NuGetUnfixed.Add([PSCustomObject]@{ Id=$id; Current=$rec.Resolved; Severity=$rec.Severity; Direct=$rec.IsDirect; Advisories=($rec.Urls -join ' '); Reason='no safe version determined (manual review)' }) + continue + } + + $action = $null + if(Test-PropsHasPackage -Content $content -PackageId $id){ + $updated = Update-PropsDirect -Content $content -PackageId $id -NewVersion $safe + if($updated){ $content = $updated; $action = 'bumped (direct)' } + } else { + $content = Add-PropsTransitivePin -Content $content -PackageId $id -NewVersion $safe + $action = 'pinned (transitive)' + } + if($action){ + $changed = $true + [void]$NuGetFixed.Add([PSCustomObject]@{ Id=$id; From=$rec.Resolved; To=$safe; Severity=$rec.Severity; Direct=$rec.IsDirect; Action=$action; Advisories=($rec.Urls -join ' ') }) + Write-Info (" {0}: {1} -> {2} [{3}]" -f $id,$rec.Resolved,$safe,$action) + } else { + [void]$NuGetUnfixed.Add([PSCustomObject]@{ Id=$id; Current=$rec.Resolved; Severity=$rec.Severity; Direct=$rec.IsDirect; Advisories=($rec.Urls -join ' '); Reason='could not edit Directory.Packages.props' }) + } + } + + if($changed){ + if($DryRun){ + Write-Warn '[DRY RUN] Directory.Packages.props would be updated (not written).' + } else { + Write-Utf8NoBom-LF -Path $propsPath -Content $content + Write-Info 'Directory.Packages.props updated.' + } + } +} + +# --------------------------------------------------------------------------- +# npm scan & fix +# --------------------------------------------------------------------------- +function Get-NpmCountsAtOrAbove { + param($AuditObj) + $total = 0 + if($AuditObj -and $AuditObj.PSObject.Properties.Name -contains 'metadata' -and $AuditObj.metadata.PSObject.Properties.Name -contains 'vulnerabilities'){ + $v = $AuditObj.metadata.vulnerabilities + foreach($sev in @('moderate','high','critical')){ + if($SeverityRank[$sev] -ge $minRank -and ($v.PSObject.Properties.Name -contains $sev)){ $total += [int]$v.$sev } + } + if($minRank -le 1 -and ($v.PSObject.Properties.Name -contains 'low')){ $total += [int]$v.low } + } + return $total +} + +function Invoke-NpmScan { + Write-Info 'Scanning npm dependencies...' + if(-not (Test-CommandAvailable 'npm')){ Write-Err 'npm not found on PATH.'; [void]$ScanErrors.Add('npm not found'); return } + + $npmTotal = $NpmProjects.Count + $npmIdx = 0 + foreach($pkgJson in $NpmProjects){ + $npmIdx++ + if(-not (Test-Path -LiteralPath $pkgJson)){ if($Detailed){ Write-Warn "missing: $pkgJson" }; continue } + $dir = Split-Path -Parent $pkgJson + $name = (Resolve-Path -LiteralPath $dir).Path.Replace((Resolve-Path $repoRoot).Path,'').TrimStart('\','/') + Write-Progress -Activity 'Scanning npm projects' -Status "[$npmIdx/$npmTotal] $name" ` + -PercentComplete ([int](($npmIdx-1) / $npmTotal * 100)) + Write-Info " $name" + Push-Location $dir + try { + if(-not (Test-Path 'node_modules')){ + if($DryRun){ Write-Warn " [DRY RUN] would run: npm install" } + else { & npm install --no-audit --no-fund *> $null } + } + + $beforeRaw = & npm audit --json 2>$null | Out-String + $before = $null; try { $before = $beforeRaw | ConvertFrom-Json } catch {} + $beforeCount = Get-NpmCountsAtOrAbove $before + + if($beforeCount -le 0){ + [void]$NpmResults.Add([PSCustomObject]@{ Project=$name; Before=0; After=0; Fixed=0; Note='clean' }) + Pop-Location; continue + } + + if($DryRun){ + Write-Warn " [DRY RUN] would run: npm audit fix ($beforeCount vuln >= $MinSeverity)" + [void]$NpmResults.Add([PSCustomObject]@{ Project=$name; Before=$beforeCount; After=$beforeCount; Fixed=0; Note='dry-run (not fixed)' }) + Pop-Location; continue + } + + & npm audit fix *> $null + $afterRaw = & npm audit --json 2>$null | Out-String + $after = $null; try { $after = $afterRaw | ConvertFrom-Json } catch {} + $afterCount = Get-NpmCountsAtOrAbove $after + $note = if($afterCount -gt 0){ "$afterCount remain (need --force / breaking major)" } else { 'all fixed' } + [void]$NpmResults.Add([PSCustomObject]@{ Project=$name; Before=$beforeCount; After=$afterCount; Fixed=($beforeCount-$afterCount); Note=$note }) + Write-Info (" before={0} after={1} ({2})" -f $beforeCount,$afterCount,$note) + } catch { + [void]$ScanErrors.Add("npm error in ${name}: $($_.Exception.Message)") + Write-Warn " error: $($_.Exception.Message)" + } finally { + Pop-Location + } + } + Write-Progress -Activity 'Scanning npm projects' -Completed +} + +# --------------------------------------------------------------------------- +# Report +# --------------------------------------------------------------------------- +function Write-Report { + param([string]$Stamp) + $reportsDir = Join-Path $scriptRoot 'reports' + if(-not (Test-Path $reportsDir)){ New-Item -ItemType Directory -Path $reportsDir -Force | Out-Null } + $mdPath = Join-Path $reportsDir "vuln-report-$Stamp.md" + $jsonPath = Join-Path $reportsDir "vuln-report-$Stamp.json" + + $payload = [PSCustomObject]@{ + timestamp = $Stamp + minSeverity = $MinSeverity + dryRun = [bool]$DryRun + ecosystems = @{ nuget = [bool]$doNuget; npm = [bool]$doNpm } + nuget = @{ fixed = $NuGetFixed; skipped = $NuGetSkipped; unfixed = $NuGetUnfixed } + npm = $NpmResults + scanErrors = $ScanErrors + } + Write-Utf8NoBom-LF -Path $jsonPath -Content ($payload | ConvertTo-Json -Depth 8) + + $sb = New-Object System.Text.StringBuilder + [void]$sb.AppendLine("# Vulnerable dependency report") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("- Generated: $Stamp") + [void]$sb.AppendLine("- Min severity: **$MinSeverity**") + [void]$sb.AppendLine("- Dry run: $([bool]$DryRun)") + [void]$sb.AppendLine("- Ecosystems: NuGet=$doNuget, npm=$doNpm") + [void]$sb.AppendLine("") + if($doNuget){ + [void]$sb.AppendLine("## NuGet") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("### Fixed ($($NuGetFixed.Count))") + [void]$sb.AppendLine("| Package | From | To | Severity | Kind | Action |") + [void]$sb.AppendLine("|---|---|---|---|---|---|") + foreach($f in $NuGetFixed){ [void]$sb.AppendLine("| $($f.Id) | $($f.From) | $($f.To) | $($f.Severity) | $(if($f.Direct){'direct'}else{'transitive'}) | $($f.Action) |") } + [void]$sb.AppendLine("") + [void]$sb.AppendLine("### Unfixed - manual review ($($NuGetUnfixed.Count))") + [void]$sb.AppendLine("| Package | Current | Severity | Reason | Advisories |") + [void]$sb.AppendLine("|---|---|---|---|---|") + foreach($u in $NuGetUnfixed){ [void]$sb.AppendLine("| $($u.Id) | $($u.Current) | $($u.Severity) | $($u.Reason) | $($u.Advisories) |") } + [void]$sb.AppendLine("") + [void]$sb.AppendLine("### Skipped ($($NuGetSkipped.Count))") + foreach($s in $NuGetSkipped){ [void]$sb.AppendLine("- $($s.Id) ($($s.Current), $($s.Severity)) - $($s.Reason)") } + [void]$sb.AppendLine("") + } + if($doNpm){ + [void]$sb.AppendLine("## npm") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("| Project | Before | After | Fixed | Note |") + [void]$sb.AppendLine("|---|---|---|---|---|") + foreach($n in $NpmResults){ [void]$sb.AppendLine("| $($n.Project) | $($n.Before) | $($n.After) | $($n.Fixed) | $($n.Note) |") } + [void]$sb.AppendLine("") + } + if($ScanErrors.Count -gt 0){ + [void]$sb.AppendLine("## Scan errors") + foreach($e in $ScanErrors){ [void]$sb.AppendLine("- $e") } + } + Write-Utf8NoBom-LF -Path $mdPath -Content $sb.ToString() + + Write-Info "Report: $mdPath" + return $mdPath +} + +# --------------------------------------------------------------------------- +# Commit + PR +# --------------------------------------------------------------------------- +function Invoke-CreatePR { + param([string]$ReportPath) + if(-not (Test-CommandAvailable 'git')){ Write-Err 'git not found; cannot create PR.'; return } + if(-not (Test-CommandAvailable 'gh')){ Write-Err 'gh CLI not found; cannot open PR.'; return } + + $branch = 'fix/vulnerable-dependencies' + Write-Info "Creating branch '$branch' off origin/dev and opening PR..." + + & git -C $repoRoot fetch origin dev --quiet + # Carry working-tree fixes onto a fresh branch from origin/dev. + & git -C $repoRoot stash push -u -m 'vuln-fix-wip' *> $null + $stashed = ($LASTEXITCODE -eq 0) + & git -C $repoRoot switch -C $branch origin/dev + if($LASTEXITCODE -ne 0){ Write-Err "Failed to create branch $branch."; if($stashed){ & git -C $repoRoot stash pop *> $null }; return } + if($stashed){ + & git -C $repoRoot stash pop + if($LASTEXITCODE -ne 0){ Write-Err 'Stash pop conflicted; resolve manually. Aborting PR.'; return } + } + + & git -C $repoRoot add -- 'Directory.Packages.props' '**/package.json' '**/package-lock.json' + & git -C $repoRoot commit -m @' +fix(deps): remediate moderate+ npm & NuGet vulnerabilities + +Automated by scripts/update-vulnerable-deps.ps1. + +Co-Authored-By: Claude Opus 4.8 (1M context) +'@ + if($LASTEXITCODE -ne 0){ Write-Warn 'Nothing to commit (no changes staged). Skipping PR.'; return } + + & git -C $repoRoot push -u origin $branch + if($LASTEXITCODE -ne 0){ Write-Err 'git push failed.'; return } + + $fixedLines = ($NuGetFixed | ForEach-Object { "- NuGet $($_.Id): $($_.From) -> $($_.To) ($($_.Severity))" }) -join "`n" + $npmLines = ($NpmResults | Where-Object { $_.Fixed -gt 0 } | ForEach-Object { "- npm $($_.Project): fixed $($_.Fixed)" }) -join "`n" + $unfixed = ($NuGetUnfixed | ForEach-Object { "- $($_.Id) ($($_.Severity)): $($_.Reason)" }) -join "`n" + $body = @" +Automated remediation of moderate-and-above npm & NuGet vulnerabilities. + +## Fixed +$fixedLines +$npmLines + +## Needs manual review +$unfixed + +See the attached report ($([System.IO.Path]::GetFileName($ReportPath))) for full detail. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +"@ + & gh pr create --base dev --head $branch --title 'fix(deps): remediate moderate+ npm & NuGet vulnerabilities' --body $body + if($LASTEXITCODE -ne 0){ Write-Err 'gh pr create failed.' } else { Write-Info 'PR opened against dev.' } +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +$stamp = (Get-Date).ToString('yyyy-MM-dd-HHmmss') +Write-Info "update-vulnerable-deps (minSeverity=$MinSeverity, dryRun=$DryRun, npm=$doNpm, nuget=$doNuget)" + +if($doNuget){ Invoke-NuGetScan } +if($doNpm){ Invoke-NpmScan } + +$reportPath = Write-Report -Stamp $stamp + +# Determine unfixed count for exit code. +$npmRemaining = ($NpmResults | Measure-Object -Property After -Sum).Sum +if($null -eq $npmRemaining){ $npmRemaining = 0 } +$unfixedCount = $NuGetUnfixed.Count + [int]$npmRemaining + +Write-Host '' +Write-Host '================ SUMMARY ================' -ForegroundColor Cyan +Write-Host (" NuGet fixed : {0}" -f $NuGetFixed.Count) +Write-Host (" NuGet skipped : {0}" -f $NuGetSkipped.Count) +Write-Host (" NuGet unfixed : {0}" -f $NuGetUnfixed.Count) +Write-Host (" npm projects : {0}" -f $NpmResults.Count) +Write-Host (" npm remaining : {0}" -f $npmRemaining) +Write-Host (" scan errors : {0}" -f $ScanErrors.Count) +Write-Host '========================================' -ForegroundColor Cyan + +if($CreatePR -and -not $DryRun){ + if($NuGetFixed.Count -gt 0 -or ($NpmResults | Where-Object { $_.Fixed -gt 0 })){ + Invoke-CreatePR -ReportPath $reportPath + } else { + Write-Warn 'No fixes applied; skipping PR creation.' + } +} + +if($ScanErrors.Count -gt 0){ Write-Warn "$($ScanErrors.Count) scan error(s) - see report." } + +if($unfixedCount -gt 0){ + Write-Warn "$unfixedCount vulnerability/vulnerabilities remain unfixed." + exit 1 +} +Write-Info 'No outstanding vulnerabilities at/above threshold.' +exit 0 diff --git a/src/traversals/apax/apax.yml b/src/traversals/apax/apax.yml new file mode 100644 index 000000000..53140784a --- /dev/null +++ b/src/traversals/apax/apax.yml @@ -0,0 +1,51 @@ +name: "apax.traversal" +version: "0.0.0-dev.0" +type: "app" +targets: +- "1500" +registries: + '@inxton': "https://npm.pkg.github.com/" +devDependencies: + '@inxton/ax-sdk': "0.0.0-dev.0" +dependencies: + "@inxton/axopen.abstractions": "0.0.0-dev.0" + "@inxton/ax.axopen.app": "0.0.0-dev.0" + "@inxton/ax.axopen.hwlibrary": "0.0.0-dev.0" + "@inxton/ax.axopen.min": "0.0.0-dev.0" + "@inxton/ax.catalog": "0.0.51" + "@inxton/axopen.components.abb.robotics": "0.0.0-dev.0" + "@inxton/axopen.components.abstractions": "0.0.0-dev.0" + "@inxton/axopen.components.balluff.identification": "0.0.0-dev.0" + "@inxton/axopen.components.cognex.vision": "0.0.0-dev.0" + "@inxton/axopen.components.desoutter.tightening": "0.0.0-dev.0" + "@inxton/axopen.components.drives": "0.0.0-dev.0" + "@inxton/axopen.components.dukane.welders": "0.0.0-dev.0" + "@inxton/axopen.components.elements": "0.0.0-dev.0" + "@inxton/axopen.components.festo.drives": "0.0.0-dev.0" + "@inxton/axopen.components.keyence.vision": "0.0.0-dev.0" + "@inxton/axopen.components.kuka.robotics": "0.0.0-dev.0" + "@inxton/axopen.components.mitsubishi.robotics": "0.0.0-dev.0" + "@inxton/axopen.components.pneumatics": "0.0.0-dev.0" + "@inxton/axopen.components.rexroth.drives": "0.0.0-dev.0" + "@inxton/axopen.components.rexroth.press": "0.0.0-dev.0" + "@inxton/axopen.components.rexroth.tightening": "0.0.0-dev.0" + "@inxton/axopen.components.robotics": "0.0.0-dev.0" + "@inxton/axopen.components.siem.communication": "0.0.0-dev.0" + "@inxton/axopen.components.siem.identification": "0.0.0-dev.0" + "@inxton/axopen.components.ur.robotics": "0.0.0-dev.0" + "@inxton/axopen.components.zebra.vision": "0.0.0-dev.0" + "@inxton/axopen.core": "0.0.0-dev.0" + "@inxton/axopen.data": "0.0.0-dev.0" + "axopen_data_distributed_tests_l4": "0.0.0-dev.0" + "axopen.data.tests_l1": "0.0.0-dev.0" + "axopen.integration.tests_l4": "0.0.0-dev.0" + "@inxton/axopen.inspectors": "0.0.0-dev.0" + "@inxton/axopen.io": "0.0.0-dev.0" + "@inxton/axopen.probers": "0.0.0-dev.0" + "showcase": "0.0.0-dev.0" + "@inxton/axopen.simatic1500": "0.0.0-dev.0" + "@inxton/apaxlibname": "0.0.0-dev.0" + "@inxton/axopen.timers": "0.0.0-dev.0" + "@inxton/axopen.utils": "0.0.0-dev.0" +installStrategy: "overridable" +... From ae16242b754046bbbe4ad9ed3466a07a330ccf46 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 17:29:21 +0200 Subject: [PATCH 06/23] Refactor code structure for improved readability and maintainability --- src/styling/src/package-lock.json | 218 +++++++++-------------- src/styling/src/package.json | 8 +- src/styling/src/wwwroot/css/momentum.css | 4 +- 3 files changed, 88 insertions(+), 142 deletions(-) diff --git a/src/styling/src/package-lock.json b/src/styling/src/package-lock.json index 498ed0899..41135c11f 100644 --- a/src/styling/src/package-lock.json +++ b/src/styling/src/package-lock.json @@ -5,8 +5,8 @@ "packages": { "": { "dependencies": { - "@tailwindcss/cli": "^4.2.2", - "tailwindcss": "^4.2.2" + "@tailwindcss/cli": "^4.3.0", + "tailwindcss": "^4.3.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -350,65 +350,65 @@ } }, "node_modules/@tailwindcss/cli": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.2.tgz", - "integrity": "sha512-iJS+8kAFZ8HPqnh0O5DHCLjo4L6dD97DBQEkrhfSO4V96xeefUus2jqsBs1dUMt3OU9Ks4qIkiY0mpL5UW+4LQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.0.tgz", + "integrity": "sha512-X9kdlqyMopO9fewbgHsEeuy31YzMHbdZ9VsKt004tB+mxSg1CNbyhZYCzvhciN0AM4R4b5lvIprPjtNq7iQxpQ==", "license": "MIT", "dependencies": { "@parcel/watcher": "^2.5.1", - "@tailwindcss/node": "4.2.2", - "@tailwindcss/oxide": "4.2.2", - "enhanced-resolve": "^5.19.0", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "enhanced-resolve": "^5.21.0", "mri": "^1.2.0", "picocolors": "^1.1.1", - "tailwindcss": "4.2.2" + "tailwindcss": "4.3.0" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "node_modules/@tailwindcss/node": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", - "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", + "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.2" + "tailwindcss": "4.3.0" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", - "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-x64": "4.2.2", - "@tailwindcss/oxide-freebsd-x64": "4.2.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-x64-musl": "4.2.2", - "@tailwindcss/oxide-wasm32-wasi": "4.2.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", - "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", "cpu": [ "arm64" ], @@ -422,9 +422,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", - "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", "cpu": [ "arm64" ], @@ -438,9 +438,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", - "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", "cpu": [ "x64" ], @@ -454,9 +454,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", "cpu": [ "x64" ], @@ -470,9 +470,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", "cpu": [ "arm" ], @@ -486,9 +486,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", "cpu": [ "arm64" ], @@ -502,9 +502,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", "cpu": [ "arm64" ], @@ -518,9 +518,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", "cpu": [ "x64" ], @@ -534,9 +534,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", "cpu": [ "x64" ], @@ -550,9 +550,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -567,10 +567,10 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, @@ -578,64 +578,10 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.7.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.7.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", "cpu": [ "arm64" ], @@ -649,9 +595,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", "cpu": [ "x64" ], @@ -674,13 +620,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", + "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -714,9 +660,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -1023,15 +969,15 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", "engines": { "node": ">=6" diff --git a/src/styling/src/package.json b/src/styling/src/package.json index 5acf123ef..7a25bdcf0 100644 --- a/src/styling/src/package.json +++ b/src/styling/src/package.json @@ -1,10 +1,10 @@ { "scripts": { - "build:css": "npx @tailwindcss/cli -i ./wwwroot/tailwindroot.css -o ./wwwroot/css/axopenstyling.css --minify", - "watch:css": "npx @tailwindcss/cli -i ./wwwroot/tailwindroot.css -o ./wwwroot/css/axopenstyling.css --watch" + "build:css": "npx @tailwindcss/cli -i ./wwwroot/css/tailwind.css -o ./wwwroot/css/momentum.css --minify --content \"..\\..\\..\\**\\*.{html,js,jsx,ts,tsx,vue,razor}\"", + "watch:css": "npx @tailwindcss/cli -i ./wwwroot/css/tailwind.css -o ./wwwroot/css/momentum.css --watch --content \"..\\..\\..\\**\\*.{html,js,jsx,ts,tsx,vue,razor}\"" }, "dependencies": { - "@tailwindcss/cli": "^4.2.2", - "tailwindcss": "^4.2.2" + "@tailwindcss/cli": "^4.3.0", + "tailwindcss": "^4.3.0" } } diff --git a/src/styling/src/wwwroot/css/momentum.css b/src/styling/src/wwwroot/css/momentum.css index 1ca14ac2b..4ac1215be 100644 --- a/src/styling/src/wwwroot/css/momentum.css +++ b/src/styling/src/wwwroot/css/momentum.css @@ -1,2 +1,2 @@ -/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-orange-50:oklch(98% .016 73.684);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-lime-400:oklch(84.1% .238 128.85);--color-green-500:oklch(72.3% .219 149.579);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-900:oklch(39.8% .07 227.392);--color-sky-400:oklch(74.6% .16 232.661);--color-blue-500:oklch(62.3% .214 259.815);--color-purple-500:oklch(62.7% .265 303.9);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--container-xl:36rem;--container-3xl:48rem;--container-4xl:56rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base,components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-2{top:calc(var(--spacing) * 2)}.right-2{right:calc(var(--spacing) * 2)}.bottom-0{bottom:calc(var(--spacing) * 0)}.-z-1{z-index:calc(1 * -1)}.z-10{z-index:10}.z-\[600\]{z-index:600}.col-span-2{grid-column:span 2/span 2}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.\!m-0{margin:calc(var(--spacing) * 0)!important}.m-0{margin:calc(var(--spacing) * 0)}.m-0\!{margin:calc(var(--spacing) * 0)!important}.m-1{margin:calc(var(--spacing) * 1)}.m-2{margin:calc(var(--spacing) * 2)}.m-4{margin:calc(var(--spacing) * 4)}.-mx-2{margin-inline:calc(var(--spacing) * -2)}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing) * 1)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.my-auto{margin-block:auto}.ms-1{margin-inline-start:calc(var(--spacing) * 1)}.ms-2{margin-inline-start:calc(var(--spacing) * 2)}.ms-4{margin-inline-start:calc(var(--spacing) * 4)}.ms-auto{margin-inline-start:auto}.me-1{margin-inline-end:calc(var(--spacing) * 1)}.me-2{margin-inline-end:calc(var(--spacing) * 2)}.me-4{margin-inline-end:calc(var(--spacing) * 4)}.me-6{margin-inline-end:calc(var(--spacing) * 6)}.me-auto{margin-inline-end:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-\[15vh\]{margin-top:15vh}.mt-auto{margin-top:auto}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-5{margin-left:calc(var(--spacing) * 5)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.flex\!{display:flex!important}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-15{height:calc(var(--spacing) * 15)}.h-auto{height:auto}.h-full{height:100%}.max-h-\[50vh\]{max-height:50vh}.max-h-\[70vh\]{max-height:70vh}.min-h-40{min-height:calc(var(--spacing) * 40)}.w-1\/3{width:33.3333%}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-15{width:calc(var(--spacing) * 15)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-50{width:calc(var(--spacing) * 50)}.w-75{width:calc(var(--spacing) * 75)}.w-100{width:calc(var(--spacing) * 100)}.w-125{width:calc(var(--spacing) * 125)}.w-\[1px\]{width:1px}.w-auto{width:auto}.w-full{width:100%}.w-md{width:var(--container-md)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-200{max-width:calc(var(--spacing) * 200)}.max-w-none{max-width:none}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-20{min-width:calc(var(--spacing) * 20)}.min-w-32{min-width:calc(var(--spacing) * 32)}.min-w-\[10rem\]{min-width:10rem}.min-w-\[12rem\]{min-width:12rem}.flex-1{flex:1}.flex-\[2\]{flex:2}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow-1,.grow,.grow-1{flex-grow:1}.basis-1\/3{flex-basis:33.3333%}.basis-2\/3{flex-basis:66.6667%}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-move{cursor:move}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-row\!{flex-direction:row!important}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-center\!{align-items:center!important}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0{gap:calc(var(--spacing) * 0)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-1{row-gap:calc(var(--spacing) * 1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}.self-center{align-self:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-visible{overflow-y:visible}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-b,.border-b-1{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-solid{--tw-border-style:solid;border-style:solid}.border-blue-500\/50{border-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.border-blue-500\/50{border-color:color-mix(in oklab, var(--color-blue-500) 50%, transparent)}}.border-current{border-color:currentColor}.border-cyan-200{border-color:var(--color-cyan-200)}.border-cyan-400{border-color:var(--color-cyan-400)}.border-green-500\/50{border-color:#00c75880}@supports (color:color-mix(in lab, red, red)){.border-green-500\/50{border-color:color-mix(in oklab, var(--color-green-500) 50%, transparent)}}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-400\/50{border-color:#ff8b1a80}@supports (color:color-mix(in lab, red, red)){.border-orange-400\/50{border-color:color-mix(in oklab, var(--color-orange-400) 50%, transparent)}}.border-red-500\/50{border-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.border-red-500\/50{border-color:color-mix(in oklab, var(--color-red-500) 50%, transparent)}}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-400\/35{border-color:#90a1b959}@supports (color:color-mix(in lab, red, red)){.border-slate-400\/35{border-color:color-mix(in oklab, var(--color-slate-400) 35%, transparent)}}.border-slate-500\/40{border-color:#62748e66}@supports (color:color-mix(in lab, red, red)){.border-slate-500\/40{border-color:color-mix(in oklab, var(--color-slate-500) 40%, transparent)}}.border-yellow-500\/50{border-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/50{border-color:color-mix(in oklab, var(--color-yellow-500) 50%, transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-current{background-color:currentColor}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-cyan-100\/40{background-color:#cefafe66}@supports (color:color-mix(in lab, red, red)){.bg-cyan-100\/40{background-color:color-mix(in oklab, var(--color-cyan-100) 40%, transparent)}}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-green-500{background-color:var(--color-green-500)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-red-500{background-color:var(--color-red-500)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-400\/20{background-color:#90a1b933}@supports (color:color-mix(in lab, red, red)){.bg-slate-400\/20{background-color:color-mix(in oklab, var(--color-slate-400) 20%, transparent)}}.bg-slate-500{background-color:var(--color-slate-500)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-slate-700\/40{background-color:#31415866}@supports (color:color-mix(in lab, red, red)){.bg-slate-700\/40{background-color:color-mix(in oklab, var(--color-slate-700) 40%, transparent)}}.bg-slate-800\/60{background-color:#1d293d99}@supports (color:color-mix(in lab, red, red)){.bg-slate-800\/60{background-color:color-mix(in oklab, var(--color-slate-800) 60%, transparent)}}.bg-slate-900\/35{background-color:#0f172b59}@supports (color:color-mix(in lab, red, red)){.bg-slate-900\/35{background-color:color-mix(in oklab, var(--color-slate-900) 35%, transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab, var(--color-white) 60%, transparent)}}.bg-white\/80{background-color:#fffc}@supports (color:color-mix(in lab, red, red)){.bg-white\/80{background-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-linear-to-br{--tw-gradient-position:to bottom right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-linear-to-br{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-cyan-50{--tw-gradient-from:var(--color-cyan-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-emerald-50{--tw-gradient-from:var(--color-emerald-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-500{--tw-gradient-from:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-500{--tw-gradient-from:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-400{--tw-gradient-from:var(--color-yellow-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-0\%{--tw-gradient-from-position:0%}.via-amber-400{--tw-gradient-via:var(--color-amber-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-cyan-500{--tw-gradient-via:var(--color-cyan-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-lime-400{--tw-gradient-via:var(--color-lime-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-purple-500{--tw-gradient-via:var(--color-purple-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-sky-400{--tw-gradient-via:var(--color-sky-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-yellow-400{--tw-gradient-via:var(--color-yellow-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-blue-500{--tw-gradient-to:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-green-500{--tw-gradient-to:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-50\%{--tw-gradient-to-position:50%}.\!p-0{padding:calc(var(--spacing) * 0)!important}.p-0{padding:calc(var(--spacing) * 0)}.p-0\!{padding:calc(var(--spacing) * 0)!important}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\!{padding-inline:calc(var(--spacing) * 1)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\!{padding-inline:calc(var(--spacing) * 2)!important}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\!{padding-block:calc(var(--spacing) * 1)!important}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\!{padding-block:calc(var(--spacing) * 2)!important}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.ps-3{padding-inline-start:calc(var(--spacing) * 3)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.85rem\]{font-size:.85rem}.text-\[0\.95rem\]{font-size:.95rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-balance{text-wrap:balance}.text-nowrap{text-wrap:nowrap}.wrap-anywhere{overflow-wrap:anywhere}.whitespace-nowrap{white-space:nowrap}.text-amber-400{color:var(--color-amber-400)}.text-amber-600\/80{color:#dd7400cc}@supports (color:color-mix(in lab, red, red)){.text-amber-600\/80{color:color-mix(in oklab, var(--color-amber-600) 80%, transparent)}}.text-amber-700{color:var(--color-amber-700)}.text-blue-500{color:var(--color-blue-500)}.text-cyan-700{color:var(--color-cyan-700)}.text-cyan-900{color:var(--color-cyan-900)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-700{color:var(--color-emerald-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-inherit{color:inherit}.text-inherit\!{color:inherit!important}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-slate-100\/95{color:#f1f5f9f2}@supports (color:color-mix(in lab, red, red)){.text-slate-100\/95{color:color-mix(in oklab, var(--color-slate-100) 95%, transparent)}}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-400\/90{color:#90a1b9e6}@supports (color:color-mix(in lab, red, red)){.text-slate-400\/90{color:color-mix(in oklab, var(--color-slate-400) 90%, transparent)}}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-900\/70{color:#0f172bb3}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/70{color:color-mix(in oklab, var(--color-slate-900) 70%, transparent)}}.text-slate-900\/85{color:#0f172bd9}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/85{color:color-mix(in oklab, var(--color-slate-900) 85%, transparent)}}.text-slate-900\/90{color:#0f172be6}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/90{color:color-mix(in oklab, var(--color-slate-900) 90%, transparent)}}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.uppercase{text-transform:uppercase}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.placeholder-slate-400::placeholder{color:var(--color-slate-400)}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_8px_rgba\(34\,197\,94\,0\.6\)\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#22c55e99);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-blue-500\/50{--tw-shadow-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.shadow-blue-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-blue-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-green-500\/50{--tw-shadow-color:#00c75880}@supports (color:color-mix(in lab, red, red)){.shadow-green-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-green-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-orange-400\/40{--tw-shadow-color:#ff8b1a66}@supports (color:color-mix(in lab, red, red)){.shadow-orange-400\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-orange-400) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-red-500\/50{--tw-shadow-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.shadow-red-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-red-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-yellow-500\/50{--tw-shadow-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.shadow-yellow-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-yellow-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-400{--tw-duration:.4s;transition-duration:.4s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[assembly\:InternalsVisibleTo\(\"axopen\.inspectors_tests\"\)\]{assembly:InternalsVisibleTo("axopen.inspectors tests")}.\[assembly\:InternalsVisibleTo\(\"axopen_core_tests\"\)\]{assembly:InternalsVisibleTo("axopen core tests")}.\[assembly\:InternalsVisibleTo\(\"axopen_core_tests_L1\"\)\]{assembly:InternalsVisibleTo("axopen core tests L1")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsabbrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsabbrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsballuffidentification_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsballuffidentification tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentscognexvision_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentscognexvision tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsdesouttertightening_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsdesouttertightening tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsdrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsdrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsfestodrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsfestodrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentskeyencevision_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentskeyencevision tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentskukarobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentskukarobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsmitsubishirobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsmitsubishirobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrexrothdrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrexrothdrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrexrothpress_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrexrothpress tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentssiemidentification_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentssiemidentification tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsurrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsurrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopenio_tests\"\)\]{assembly:InternalsVisibleTo("axopenio tests")}.\[assembly\:InternalsVisibleTo\(\"components\.dukane\.welders_tests\"\)\]{assembly:InternalsVisibleTo("components.dukane.welders tests")}.\[assembly\:InternalsVisibleTo\(\"components\.rexroth\.tightening_tests\"\)\]{assembly:InternalsVisibleTo("components.rexroth.tightening tests")}.\[assembly\:InternalsVisibleTo\(\"components\.siem\.communication_tests\"\)\]{assembly:InternalsVisibleTo("components.siem.communication tests")}.\[assembly\:InternalsVisibleTo\(\"components\.zebra\.vision_tests\"\)\]{assembly:InternalsVisibleTo("components.zebra.vision tests")}.\[assembly\:InternalsVisibleTo\(\"elementscomponents_tests\"\)\]{assembly:InternalsVisibleTo("elementscomponents tests")}.\[assembly\:InternalsVisibleTo\(\"librarytemplate_tests\"\)\]{assembly:InternalsVisibleTo("librarytemplate tests")}.\[assembly\:InternalsVisibleTo\(\"pneumaticcomponents_tests\"\)\]{assembly:InternalsVisibleTo("pneumaticcomponents tests")}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-slate-300:hover{border-color:var(--color-slate-300)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.hover\:text-slate-700:hover{color:var(--color-slate-700)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-cyan-500:focus{border-color:var(--color-cyan-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-cyan-200:focus{--tw-ring-color:var(--color-cyan-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-between{justify-content:space-between}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}@media (min-width:80rem){.xl\:col-span-1{grid-column:span 1/span 1}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} \ No newline at end of file +/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-orange-50:oklch(98% .016 73.684);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-lime-400:oklch(84.1% .238 128.85);--color-green-500:oklch(72.3% .219 149.579);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-900:oklch(39.8% .07 227.392);--color-sky-400:oklch(74.6% .16 232.661);--color-blue-500:oklch(62.3% .214 259.815);--color-purple-500:oklch(62.7% .265 303.9);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--container-xl:36rem;--container-3xl:48rem;--container-4xl:56rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base,components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.top-2{top:calc(var(--spacing) * 2)}.right-2{right:calc(var(--spacing) * 2)}.bottom-0{bottom:calc(var(--spacing) * 0)}.-z-1{z-index:calc(1 * -1)}.z-10{z-index:10}.z-\[600\]{z-index:600}.col-span-2{grid-column:span 2/span 2}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.\!m-0{margin:calc(var(--spacing) * 0)!important}.m-0{margin:calc(var(--spacing) * 0)}.m-0\!{margin:calc(var(--spacing) * 0)!important}.m-1{margin:calc(var(--spacing) * 1)}.m-2{margin:calc(var(--spacing) * 2)}.m-4{margin:calc(var(--spacing) * 4)}.-mx-2{margin-inline:calc(var(--spacing) * -2)}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing) * 1)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.my-auto{margin-block:auto}.ms-1{margin-inline-start:calc(var(--spacing) * 1)}.ms-2{margin-inline-start:calc(var(--spacing) * 2)}.ms-4{margin-inline-start:calc(var(--spacing) * 4)}.ms-auto{margin-inline-start:auto}.me-1{margin-inline-end:calc(var(--spacing) * 1)}.me-2{margin-inline-end:calc(var(--spacing) * 2)}.me-4{margin-inline-end:calc(var(--spacing) * 4)}.me-6{margin-inline-end:calc(var(--spacing) * 6)}.me-auto{margin-inline-end:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-\[15vh\]{margin-top:15vh}.mt-auto{margin-top:auto}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-5{margin-left:calc(var(--spacing) * 5)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.flex\!{display:flex!important}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-15{height:calc(var(--spacing) * 15)}.h-auto{height:auto}.h-full{height:100%}.max-h-\[50vh\]{max-height:50vh}.max-h-\[70vh\]{max-height:70vh}.min-h-40{min-height:calc(var(--spacing) * 40)}.w-1\/3{width:33.3333%}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-15{width:calc(var(--spacing) * 15)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-50{width:calc(var(--spacing) * 50)}.w-75{width:calc(var(--spacing) * 75)}.w-100{width:calc(var(--spacing) * 100)}.w-125{width:calc(var(--spacing) * 125)}.w-\[1px\]{width:1px}.w-auto{width:auto}.w-full{width:100%}.w-md{width:var(--container-md)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-200{max-width:calc(var(--spacing) * 200)}.max-w-none{max-width:none}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-20{min-width:calc(var(--spacing) * 20)}.min-w-32{min-width:calc(var(--spacing) * 32)}.min-w-\[10rem\]{min-width:10rem}.min-w-\[12rem\]{min-width:12rem}.flex-1{flex:1}.flex-\[2\]{flex:2}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow-1,.grow,.grow-1{flex-grow:1}.basis-1\/3{flex-basis:33.3333%}.basis-2\/3{flex-basis:66.6667%}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-move{cursor:move}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-row\!{flex-direction:row!important}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-center\!{align-items:center!important}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0{gap:calc(var(--spacing) * 0)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-1{row-gap:calc(var(--spacing) * 1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}.self-center{align-self:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-visible{overflow-y:visible}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-b,.border-b-1{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-solid{--tw-border-style:solid;border-style:solid}.border-blue-500\/50{border-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.border-blue-500\/50{border-color:color-mix(in oklab, var(--color-blue-500) 50%, transparent)}}.border-current{border-color:currentColor}.border-cyan-200{border-color:var(--color-cyan-200)}.border-cyan-400{border-color:var(--color-cyan-400)}.border-green-500\/50{border-color:#00c75880}@supports (color:color-mix(in lab, red, red)){.border-green-500\/50{border-color:color-mix(in oklab, var(--color-green-500) 50%, transparent)}}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-400\/50{border-color:#ff8b1a80}@supports (color:color-mix(in lab, red, red)){.border-orange-400\/50{border-color:color-mix(in oklab, var(--color-orange-400) 50%, transparent)}}.border-red-500\/50{border-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.border-red-500\/50{border-color:color-mix(in oklab, var(--color-red-500) 50%, transparent)}}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-400\/35{border-color:#90a1b959}@supports (color:color-mix(in lab, red, red)){.border-slate-400\/35{border-color:color-mix(in oklab, var(--color-slate-400) 35%, transparent)}}.border-slate-500\/40{border-color:#62748e66}@supports (color:color-mix(in lab, red, red)){.border-slate-500\/40{border-color:color-mix(in oklab, var(--color-slate-500) 40%, transparent)}}.border-yellow-500\/50{border-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/50{border-color:color-mix(in oklab, var(--color-yellow-500) 50%, transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-current{background-color:currentColor}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-cyan-100\/40{background-color:#cefafe66}@supports (color:color-mix(in lab, red, red)){.bg-cyan-100\/40{background-color:color-mix(in oklab, var(--color-cyan-100) 40%, transparent)}}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-green-500{background-color:var(--color-green-500)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-red-500{background-color:var(--color-red-500)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-400\/20{background-color:#90a1b933}@supports (color:color-mix(in lab, red, red)){.bg-slate-400\/20{background-color:color-mix(in oklab, var(--color-slate-400) 20%, transparent)}}.bg-slate-500{background-color:var(--color-slate-500)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-slate-700\/40{background-color:#31415866}@supports (color:color-mix(in lab, red, red)){.bg-slate-700\/40{background-color:color-mix(in oklab, var(--color-slate-700) 40%, transparent)}}.bg-slate-800\/60{background-color:#1d293d99}@supports (color:color-mix(in lab, red, red)){.bg-slate-800\/60{background-color:color-mix(in oklab, var(--color-slate-800) 60%, transparent)}}.bg-slate-900\/35{background-color:#0f172b59}@supports (color:color-mix(in lab, red, red)){.bg-slate-900\/35{background-color:color-mix(in oklab, var(--color-slate-900) 35%, transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab, var(--color-white) 60%, transparent)}}.bg-white\/80{background-color:#fffc}@supports (color:color-mix(in lab, red, red)){.bg-white\/80{background-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-linear-to-br{--tw-gradient-position:to bottom right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-linear-to-br{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-cyan-50{--tw-gradient-from:var(--color-cyan-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-emerald-50{--tw-gradient-from:var(--color-emerald-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-500{--tw-gradient-from:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-500{--tw-gradient-from:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-400{--tw-gradient-from:var(--color-yellow-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-0\%{--tw-gradient-from-position:0%}.via-amber-400{--tw-gradient-via:var(--color-amber-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-cyan-500{--tw-gradient-via:var(--color-cyan-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-lime-400{--tw-gradient-via:var(--color-lime-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-purple-500{--tw-gradient-via:var(--color-purple-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-sky-400{--tw-gradient-via:var(--color-sky-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-yellow-400{--tw-gradient-via:var(--color-yellow-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-blue-500{--tw-gradient-to:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-green-500{--tw-gradient-to:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-50\%{--tw-gradient-to-position:50%}.\!p-0{padding:calc(var(--spacing) * 0)!important}.p-0{padding:calc(var(--spacing) * 0)}.p-0\!{padding:calc(var(--spacing) * 0)!important}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\!{padding-inline:calc(var(--spacing) * 1)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\!{padding-inline:calc(var(--spacing) * 2)!important}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\!{padding-block:calc(var(--spacing) * 1)!important}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\!{padding-block:calc(var(--spacing) * 2)!important}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.ps-3{padding-inline-start:calc(var(--spacing) * 3)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.85rem\]{font-size:.85rem}.text-\[0\.95rem\]{font-size:.95rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-balance{text-wrap:balance}.text-nowrap{text-wrap:nowrap}.wrap-anywhere{overflow-wrap:anywhere}.whitespace-nowrap{white-space:nowrap}.text-amber-400{color:var(--color-amber-400)}.text-amber-600\/80{color:#dd7400cc}@supports (color:color-mix(in lab, red, red)){.text-amber-600\/80{color:color-mix(in oklab, var(--color-amber-600) 80%, transparent)}}.text-amber-700{color:var(--color-amber-700)}.text-blue-500{color:var(--color-blue-500)}.text-cyan-700{color:var(--color-cyan-700)}.text-cyan-900{color:var(--color-cyan-900)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-700{color:var(--color-emerald-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-inherit{color:inherit}.text-inherit\!{color:inherit!important}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-slate-100\/95{color:#f1f5f9f2}@supports (color:color-mix(in lab, red, red)){.text-slate-100\/95{color:color-mix(in oklab, var(--color-slate-100) 95%, transparent)}}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-400\/90{color:#90a1b9e6}@supports (color:color-mix(in lab, red, red)){.text-slate-400\/90{color:color-mix(in oklab, var(--color-slate-400) 90%, transparent)}}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-900\/70{color:#0f172bb3}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/70{color:color-mix(in oklab, var(--color-slate-900) 70%, transparent)}}.text-slate-900\/85{color:#0f172bd9}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/85{color:color-mix(in oklab, var(--color-slate-900) 85%, transparent)}}.text-slate-900\/90{color:#0f172be6}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/90{color:color-mix(in oklab, var(--color-slate-900) 90%, transparent)}}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.uppercase{text-transform:uppercase}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.placeholder-slate-400::placeholder{color:var(--color-slate-400)}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_8px_rgba\(34\,197\,94\,0\.6\)\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#22c55e99);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-blue-500\/50{--tw-shadow-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.shadow-blue-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-blue-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-green-500\/50{--tw-shadow-color:#00c75880}@supports (color:color-mix(in lab, red, red)){.shadow-green-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-green-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-orange-400\/40{--tw-shadow-color:#ff8b1a66}@supports (color:color-mix(in lab, red, red)){.shadow-orange-400\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-orange-400) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-red-500\/50{--tw-shadow-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.shadow-red-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-red-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-yellow-500\/50{--tw-shadow-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.shadow-yellow-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-yellow-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-400{--tw-duration:.4s;transition-duration:.4s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[assembly\:InternalsVisibleTo\(\"axopen\.inspectors_tests\"\)\]{assembly:InternalsVisibleTo("axopen.inspectors tests")}.\[assembly\:InternalsVisibleTo\(\"axopen_core_tests\"\)\]{assembly:InternalsVisibleTo("axopen core tests")}.\[assembly\:InternalsVisibleTo\(\"axopen_core_tests_L1\"\)\]{assembly:InternalsVisibleTo("axopen core tests L1")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsabbrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsabbrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsballuffidentification_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsballuffidentification tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentscognexvision_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentscognexvision tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsdesouttertightening_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsdesouttertightening tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsdrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsdrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsfestodrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsfestodrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentskeyencevision_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentskeyencevision tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentskukarobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentskukarobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsmitsubishirobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsmitsubishirobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrexrothdrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrexrothdrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrexrothpress_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrexrothpress tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentssiemidentification_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentssiemidentification tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsurrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsurrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopenio_tests\"\)\]{assembly:InternalsVisibleTo("axopenio tests")}.\[assembly\:InternalsVisibleTo\(\"components\.dukane\.welders_tests\"\)\]{assembly:InternalsVisibleTo("components.dukane.welders tests")}.\[assembly\:InternalsVisibleTo\(\"components\.rexroth\.tightening_tests\"\)\]{assembly:InternalsVisibleTo("components.rexroth.tightening tests")}.\[assembly\:InternalsVisibleTo\(\"components\.siem\.communication_tests\"\)\]{assembly:InternalsVisibleTo("components.siem.communication tests")}.\[assembly\:InternalsVisibleTo\(\"components\.zebra\.vision_tests\"\)\]{assembly:InternalsVisibleTo("components.zebra.vision tests")}.\[assembly\:InternalsVisibleTo\(\"elementscomponents_tests\"\)\]{assembly:InternalsVisibleTo("elementscomponents tests")}.\[assembly\:InternalsVisibleTo\(\"librarytemplate_tests\"\)\]{assembly:InternalsVisibleTo("librarytemplate tests")}.\[assembly\:InternalsVisibleTo\(\"pneumaticcomponents_tests\"\)\]{assembly:InternalsVisibleTo("pneumaticcomponents tests")}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-slate-300:hover{border-color:var(--color-slate-300)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.hover\:text-slate-700:hover{color:var(--color-slate-700)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-cyan-500:focus{border-color:var(--color-cyan-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-cyan-200:focus{--tw-ring-color:var(--color-cyan-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-between{justify-content:space-between}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}@media (min-width:80rem){.xl\:col-span-1{grid-column:span 1/span 1}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} \ No newline at end of file From 334cd2625608b059c3103481326c6050af3fdd16 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 18:02:26 +0200 Subject: [PATCH 07/23] feat(deps): add script to update all non-AXSharp dependencies to latest stable versions --- scripts/update-latest-deps.ps1 | 682 +++++++++++++++++++++++++++++++++ 1 file changed, 682 insertions(+) create mode 100644 scripts/update-latest-deps.ps1 diff --git a/scripts/update-latest-deps.ps1 b/scripts/update-latest-deps.ps1 new file mode 100644 index 000000000..8ba494dde --- /dev/null +++ b/scripts/update-latest-deps.ps1 @@ -0,0 +1,682 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS +Updates all (non-AXSharp) dependencies to their latest STABLE versions across NuGet, npm and dotnet tools. + +.DESCRIPTION +One command to bring the repo up to the latest stable third-party versions. It complements - and never +overlaps with - the two narrower updaters: + + * scripts/update_axsharp_versions.ps1 - owns AXSharp.*/Inxton.Operon.* (always skipped here) + * scripts/update-vulnerable-deps.ps1 - owns the "Security pins" ItemGroup (always skipped here) + +Scope: + + NuGet - rewrites entries (and the GitVersion.MsBuild ) in + Directory.Packages.props to the latest STABLE version from nuget.org. AXSharp/Operon/AXOpen + packages, the security-pin entries, and framework-tied packages (Microsoft.AspNetCore.*, + Microsoft.EntityFrameworkCore.*, Microsoft.Extensions.*, System.*, Microsoft.NET.ILLink.Tasks, + Microsoft.VisualStudio.Web.CodeGeneration.Design) are FROZEN to stay aligned with net10.0. + + npm - for the source package.json projects, rewrites declared ranges (preserving the ^/~ prefix) to + the latest stable from `npm outdated`, then runs `npm install` to refresh package-lock.json. + + tools - rewrites .config/dotnet-tools.json tool versions to latest stable (AXSharp.ix* skipped). + +GitVersion.MsBuild and gitversion.tool are resolved together and pinned to the SAME latest stable. + +"Latest" means latest STABLE: alpha/beta/rc are skipped. Major-version bumps ARE applied but flagged +prominently in the report (use -SkipMajor to exclude them). + +Default action is a DRY RUN - pass -Apply to write changes. With -Apply (and unless -SkipBuild) the full +cake build runs as verification; on failure the offending bump(s) are bisected out and only the green +remainder is kept. A timestamped Markdown + JSON report is written to scripts/reports/. With -CreatePR the +changes are committed to branch 'chore/update-latest-deps' off origin/dev and a PR is opened against dev. + +.PARAMETER Apply +Write changes. Without it the script previews only (dry run) and touches nothing. + +.PARAMETER NuGetOnly +Process NuGet (Directory.Packages.props) only. + +.PARAMETER NpmOnly +Process npm only. + +.PARAMETER ToolsOnly +Process dotnet tools only. + +.PARAMETER SkipMajor +Exclude major-version bumps (default: apply them and flag them in the report). + +.PARAMETER SkipBuild +Skip the cake build verification step (fast path; relies on manual review). + +.PARAMETER RollbackAllOnFailure +On build failure, revert every change instead of bisecting for a green subset. + +.PARAMETER MaxBisectBuilds +Cap on the number of full cake builds spent bisecting after an initial failure. Default 6. + +.PARAMETER CreatePR +Commit changes to branch 'chore/update-latest-deps' off origin/dev and open a PR against dev. Implies -Apply. + +.PARAMETER Source +NuGet v3 feed used to look up available versions. Default nuget.org. + +.PARAMETER Token +Token for the NuGet feed (private feeds). Falls back to NUGET_TOKEN / GITHUB_PACKAGES_TOKEN / +GITHUB_TOKEN / GH_TOKEN. + +.PARAMETER Detailed +Verbose logging. + +.EXAMPLE +./update-latest-deps.ps1 # dry run, all ecosystems + +.EXAMPLE +./update-latest-deps.ps1 -NuGetOnly # dry run, NuGet only + +.EXAMPLE +./update-latest-deps.ps1 -Apply -SkipBuild # write changes, skip the (slow) cake build + +.EXAMPLE +./update-latest-deps.ps1 -CreatePR # apply, build-verify, open PR against dev +#> + +[CmdletBinding()] +param( + [switch]$Apply, + [switch]$NuGetOnly, + [switch]$NpmOnly, + [switch]$ToolsOnly, + [switch]$SkipMajor, + [switch]$SkipBuild, + [switch]$RollbackAllOnFailure, + [int]$MaxBisectBuilds = 6, + [switch]$CreatePR, + [string]$Source = 'https://api.nuget.org/v3/index.json', + [string]$Token, + [switch]$Detailed +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptRoot/_deps-common.ps1" + +$repoRoot = Split-Path -Parent $scriptRoot +$propsPath = Join-Path $repoRoot 'Directory.Packages.props' +$toolsPath = Join-Path $repoRoot '.config/dotnet-tools.json' +$cakeProj = Join-Path $repoRoot 'cake/Build.csproj' + +# --------------------------------------------------------------------------- +# Validation & constants +# --------------------------------------------------------------------------- +if($CreatePR){ $Apply = $true } # -CreatePR implies writing changes +$DryRun = -not $Apply + +$onlyCount = @($NuGetOnly,$NpmOnly,$ToolsOnly | Where-Object { $_ }).Count +if($onlyCount -gt 1){ Write-Err '-NuGetOnly / -NpmOnly / -ToolsOnly are mutually exclusive.'; exit 1 } + +$doNuget = (-not $NpmOnly) -and (-not $ToolsOnly) +$doNpm = (-not $NuGetOnly) -and (-not $ToolsOnly) +$doTools = (-not $NuGetOnly) -and (-not $NpmOnly) + +# Feed auth: never send a token to the public nuget.org CDN (it 403s on authed cached responses). +# Resolve a token only for an explicitly-provided private -Source (or explicit -Token). +$IsPublicNuGet = ($Source -match 'api\.nuget\.org') +$feedToken = if($Token){ $Token } elseif(-not $IsPublicNuGet){ Resolve-FeedToken -Token $Token -Detailed:$Detailed } else { $null } + +# Owned by update_axsharp_versions.ps1 - never touched. +$AxSharpSkipPattern = '^(AXSharp|Inxton\.Operon|AXOpen)\b' +# Owned by update-vulnerable-deps.ps1 ("Security pins" ItemGroup) - never touched. +$SecurityPinIds = @('Snappier','System.Security.Cryptography.Xml') +# Frozen to stay aligned with net10.0 (see plan / interview). +$FrameworkFreezeExact = @('Microsoft.NET.ILLink.Tasks','Microsoft.VisualStudio.Web.CodeGeneration.Design') +$FrameworkFreezePrefixes = @('Microsoft.AspNetCore.','Microsoft.EntityFrameworkCore.','Microsoft.Extensions.','System.') +# GitVersion packages/tool are kept in sync with one another. +$GitVersionNuGetId = 'GitVersion.MsBuild' +$GitVersionToolId = 'gitversion.tool' + +# Source npm projects (explicit list - avoids bin/obj/ctrl/.apax generated copies). +$NpmProjects = @( + 'src/components.abb.robotics/package.json' + 'src/components.abstractions/package.json' + 'src/data/package.json' + 'src/inspectors/package.json' + 'src/showcase/app/ix-blazor/showcase.blazor/package.json' + 'src/styling/src/package.json' +) | ForEach-Object { Join-Path $repoRoot $_ } + +# Accumulators for the report. +$Changes = New-Object System.Collections.ArrayList # applied/previewed bumps +$Frozen = New-Object System.Collections.ArrayList # id + reason +$Reverted = New-Object System.Collections.ArrayList # bumps removed by bisection +$ScanErrors = New-Object System.Collections.ArrayList +$BuildResult = 'not-run' + +# Captured original file contents (for bisection replay). file path -> original raw text. +$OriginalContent = @{} + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +function Test-CommandAvailable { param([string]$Name) $null -ne (Get-Command $Name -ErrorAction SilentlyContinue) } + +function Test-IsMajorBump { + param([string]$From,[string]$To) + $f = ConvertTo-VersionRecord $From + $t = ConvertTo-VersionRecord $To + return ($t.Major -gt $f.Major) +} + +function Get-LatestStableVersion { + # Highest non-prerelease version of $PackageId on the feed, or $null. + param([string]$PackageId,$FeedCtx) + $versions = @() + try { $versions = Get-PackageVersionsFromFeed -PkgBase $FeedCtx.PkgBase -Headers $FeedCtx.Headers -PackageId $PackageId } + catch { if($Detailed){ Write-Warn "feed lookup failed for ${PackageId}: $($_.Exception.Message)" }; return $null } + if(-not $versions -or @($versions).Count -eq 0){ return $null } + $stable = @($versions | Where-Object { -not (Test-IsPrerelease $_) }) + if($stable.Count -eq 0){ return $null } + $best = $stable[0] + foreach($v in $stable){ if(Test-VersionGreater $v $best){ $best = $v } } + return $best +} + +function Get-NuGetDisposition { + # Returns 'update' or a 'freeze-*' / 'skip-*' reason for a package id. + param([string]$Id) + if($Id -match $AxSharpSkipPattern){ return 'skip-axsharp (owned by update_axsharp_versions.ps1)' } + if($SecurityPinIds -contains $Id){ return 'skip-security-pin (owned by update-vulnerable-deps.ps1)' } + if($FrameworkFreezeExact -contains $Id){ return 'freeze-framework (net10.0-tied)' } + foreach($p in $FrameworkFreezePrefixes){ if($Id.StartsWith($p)){ return 'freeze-framework (net10.0-tied)' } } + return 'update' +} + +# --------------------------------------------------------------------------- +# Change application (replayable from captured original content) +# --------------------------------------------------------------------------- +function Apply-ChangeToContent { + # Applies a single change to the supplied file content and returns the new content. + param([string]$Content,$Change) + $idEsc = [regex]::Escape($Change.Id) + switch($Change.Kind){ + 'props-packageversion' { + $rx = "(]*\bInclude=`"$idEsc`"[^>]*\bVersion=`")[^`"]*(`")" + return [regex]::Replace($Content,$rx,"`${1}$($Change.To)`${2}",1) + } + 'props-globalref' { + $rx = "(]*\bInclude=`"$idEsc`"[^>]*\bVersion=`")[^`"]*(`")" + return [regex]::Replace($Content,$rx,"`${1}$($Change.To)`${2}",1) + } + 'tool' { + $rx = "(`"$idEsc`"\s*:\s*\{[^{}]*?`"version`"\s*:\s*`")[^`"]+(`")" + return [System.Text.RegularExpressions.Regex]::Replace( + $Content,$rx,"`${1}$($Change.To)`${2}", + [System.Text.RegularExpressions.RegexOptions]::Singleline) + } + 'npm' { + # Preserve any leading range operator (^, ~, >=, etc.) on the declared range. + $rx = "(`"$idEsc`"\s*:\s*`")([^`"]*)(`")" + return [regex]::Replace($Content,$rx,{ + param($m) + $old = $m.Groups[2].Value + $prefix = '' + if($old -match '^([\^~>=<\s]*)'){ $prefix = $Matches[1] } + $m.Groups[1].Value + $prefix + $Change.To + $m.Groups[3].Value + },1) + } + default { return $Content } + } +} + +function Write-FileSet { + # Rebuilds every affected file from its captured ORIGINAL content, applying only $ActiveChanges. + # Returns the list of npm directories whose package.json changed (caller refreshes their lock). + param($ActiveChanges) + $byFile = $ActiveChanges | Group-Object -Property File + $npmDirs = New-Object System.Collections.ArrayList + foreach($g in $byFile){ + $file = $g.Name + if(-not $OriginalContent.ContainsKey($file)){ continue } + $content = $OriginalContent[$file] + foreach($c in $g.Group){ + $content = Apply-ChangeToContent -Content $content -Change $c + if($c.Ecosystem -eq 'npm'){ $dir = Split-Path -Parent $file; if(-not $npmDirs.Contains($dir)){ [void]$npmDirs.Add($dir) } } + } + Write-Utf8NoBom-LF -Path $file -Content $content + } + # Files that had ALL their changes removed must be restored to original too. + foreach($file in $OriginalContent.Keys){ + if(-not ($byFile | Where-Object { $_.Name -eq $file })){ + Write-Utf8NoBom-LF -Path $file -Content $OriginalContent[$file] + } + } + return $npmDirs +} + +function Update-NpmLockfiles { + param($NpmDirs) + foreach($dir in $NpmDirs){ + if($Detailed){ Write-Info " npm install (refresh lockfile): $dir" } + Push-Location $dir + try { & npm install --no-audit --no-fund *> $null } + catch { [void]$ScanErrors.Add("npm install failed in ${dir}: $($_.Exception.Message)") } + finally { Pop-Location } + } +} + +# --------------------------------------------------------------------------- +# Scanners (populate $Changes / $Frozen; capture originals) +# --------------------------------------------------------------------------- +function Invoke-NuGetScan { + param($FeedCtx) + Write-Info 'Scanning NuGet (Directory.Packages.props)...' + if(-not (Test-Path $propsPath)){ [void]$ScanErrors.Add('Directory.Packages.props not found'); return } + $content = Get-Content -LiteralPath $propsPath -Raw + $OriginalContent[$propsPath] = $content + + $pvMatches = [regex]::Matches($content,']*\bInclude="([^"]+)"[^>]*\bVersion="([^"]+)"') + $grMatches = [regex]::Matches($content,']*\bInclude="([^"]+)"[^>]*\bVersion="([^"]+)"') + + foreach($m in @($pvMatches) + @($grMatches)){ + $id = $m.Groups[1].Value + $cur = $m.Groups[2].Value + $kind = if($m.Value -like '*GlobalPackageReference*'){ 'props-globalref' } else { 'props-packageversion' } + + # GitVersion handled together below. + if($id -eq $GitVersionNuGetId){ continue } + + $disp = Get-NuGetDisposition $id + if($disp -ne 'update'){ [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='nuget'; Id=$id; Current=$cur; Reason=$disp }); continue } + + $latest = Get-LatestStableVersion -PackageId $id -FeedCtx $FeedCtx + if(-not $latest){ [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='nuget'; Id=$id; Current=$cur; Reason='no stable version on feed' }); continue } + if(-not (Test-VersionGreater $latest $cur)){ if($Detailed){ Write-Info " $id already latest ($cur)" }; continue } + + $isMajor = Test-IsMajorBump $cur $latest + if($isMajor -and $SkipMajor){ [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='nuget'; Id=$id; Current=$cur; Reason="major bump to $latest skipped (-SkipMajor)" }); continue } + + [void]$Changes.Add([PSCustomObject]@{ Ecosystem='nuget'; Id=$id; From=$cur; To=$latest; IsMajor=$isMajor; Kind=$kind; File=$propsPath }) + Write-Info (" {0}: {1} -> {2}{3}" -f $id,$cur,$latest,$(if($isMajor){' [MAJOR]'}else{''})) + } +} + +function Resolve-GitVersionSync { + # Resolves one latest-stable GitVersion across MsBuild pkg + dotnet tool and queues both bumps. + param($FeedCtx) + if(-not (Test-Path $propsPath)){ return } + $propsRaw = $OriginalContent[$propsPath] + if(-not $propsRaw){ $propsRaw = Get-Content -LiteralPath $propsPath -Raw; $OriginalContent[$propsPath] = $propsRaw } + + $curMsb = $null + $m = [regex]::Match($propsRaw,"]*\bInclude=`"$([regex]::Escape($GitVersionNuGetId))`"[^>]*\bVersion=`"([^`"]+)`"") + if($m.Success){ $curMsb = $m.Groups[1].Value } + + $curTool = $null + if(Test-Path $toolsPath){ + $toolsRaw = $OriginalContent[$toolsPath] + if(-not $toolsRaw){ $toolsRaw = Get-Content -LiteralPath $toolsPath -Raw; $OriginalContent[$toolsPath] = $toolsRaw } + $tm = [regex]::Match($toolsRaw,"`"$([regex]::Escape($GitVersionToolId))`"\s*:\s*\{[^{}]*?`"version`"\s*:\s*`"([^`"]+)`"",[System.Text.RegularExpressions.RegexOptions]::Singleline) + if($tm.Success){ $curTool = $tm.Groups[1].Value } + } + + $latestMsb = Get-LatestStableVersion -PackageId $GitVersionNuGetId -FeedCtx $FeedCtx + $latestTool = Get-LatestStableVersion -PackageId $GitVersionToolId -FeedCtx $FeedCtx + $synced = $latestMsb + if($latestTool -and (Test-VersionGreater $latestTool $synced)){ $synced = $latestTool } + if(-not $synced){ Write-Warn 'Could not resolve a latest stable GitVersion; leaving as-is.'; return } + + if($doNuget -and $curMsb -and (Test-VersionGreater $synced $curMsb)){ + $isMajor = Test-IsMajorBump $curMsb $synced + if(-not ($isMajor -and $SkipMajor)){ + [void]$Changes.Add([PSCustomObject]@{ Ecosystem='nuget'; Id=$GitVersionNuGetId; From=$curMsb; To=$synced; IsMajor=$isMajor; Kind='props-globalref'; File=$propsPath }) + Write-Info (" {0}: {1} -> {2} (GitVersion sync){3}" -f $GitVersionNuGetId,$curMsb,$synced,$(if($isMajor){' [MAJOR]'}else{''})) + } else { [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='nuget'; Id=$GitVersionNuGetId; Current=$curMsb; Reason="major bump to $synced skipped (-SkipMajor)" }) } + } + if($doTools -and $curTool -and (Test-VersionGreater $synced $curTool)){ + $isMajor = Test-IsMajorBump $curTool $synced + if(-not ($isMajor -and $SkipMajor)){ + [void]$Changes.Add([PSCustomObject]@{ Ecosystem='tool'; Id=$GitVersionToolId; From=$curTool; To=$synced; IsMajor=$isMajor; Kind='tool'; File=$toolsPath }) + Write-Info (" {0}: {1} -> {2} (GitVersion sync){3}" -f $GitVersionToolId,$curTool,$synced,$(if($isMajor){' [MAJOR]'}else{''})) + } else { [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='tool'; Id=$GitVersionToolId; Current=$curTool; Reason="major bump to $synced skipped (-SkipMajor)" }) } + } +} + +function Invoke-ToolsScan { + param($FeedCtx) + Write-Info 'Scanning dotnet tools (.config/dotnet-tools.json)...' + if(-not (Test-Path $toolsPath)){ [void]$ScanErrors.Add('.config/dotnet-tools.json not found'); return } + $raw = $OriginalContent[$toolsPath] + if(-not $raw){ $raw = Get-Content -LiteralPath $toolsPath -Raw; $OriginalContent[$toolsPath] = $raw } + try { $obj = $raw | ConvertFrom-Json -ErrorAction Stop } catch { [void]$ScanErrors.Add('tools JSON parse failed'); return } + if(-not $obj.tools){ [void]$ScanErrors.Add("tools JSON missing 'tools'"); return } + + foreach($name in $obj.tools.PSObject.Properties.Name){ + if($name -like 'AXSharp.*' -or $name -like 'Inxton.Operon.*'){ [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='tool'; Id=$name; Current=$obj.tools.$name.version; Reason='skip-axsharp (owned by update_axsharp_versions.ps1)' }); continue } + if($name -eq $GitVersionToolId){ continue } # handled by Resolve-GitVersionSync + $cur = $obj.tools.$name.version + $latest = Get-LatestStableVersion -PackageId $name -FeedCtx $FeedCtx + if(-not $latest){ [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='tool'; Id=$name; Current=$cur; Reason='no stable version on feed' }); continue } + if(-not (Test-VersionGreater $latest $cur)){ if($Detailed){ Write-Info " $name already latest ($cur)" }; continue } + $isMajor = Test-IsMajorBump $cur $latest + if($isMajor -and $SkipMajor){ [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='tool'; Id=$name; Current=$cur; Reason="major bump to $latest skipped (-SkipMajor)" }); continue } + [void]$Changes.Add([PSCustomObject]@{ Ecosystem='tool'; Id=$name; From=$cur; To=$latest; IsMajor=$isMajor; Kind='tool'; File=$toolsPath }) + Write-Info (" {0}: {1} -> {2}{3}" -f $name,$cur,$latest,$(if($isMajor){' [MAJOR]'}else{''})) + } +} + +function Invoke-NpmScan { + Write-Info 'Scanning npm projects...' + if(-not (Test-CommandAvailable 'npm')){ Write-Err 'npm not found on PATH.'; [void]$ScanErrors.Add('npm not found'); return } + foreach($pkgJson in $NpmProjects){ + if(-not (Test-Path -LiteralPath $pkgJson)){ if($Detailed){ Write-Warn "missing: $pkgJson" }; continue } + $dir = Split-Path -Parent $pkgJson + $name = (Resolve-Path -LiteralPath $dir).Path.Replace((Resolve-Path $repoRoot).Path,'').TrimStart('\','/') + Write-Info " $name" + $OriginalContent[$pkgJson] = Get-Content -LiteralPath $pkgJson -Raw + Push-Location $dir + try { + if(-not (Test-Path 'node_modules')){ + if($Detailed){ Write-Info " npm install (no node_modules)" } + & npm install --no-audit --no-fund *> $null + } + $raw = & npm outdated --json 2>$null | Out-String + if(-not $raw.Trim()){ if($Detailed){ Write-Info " up to date" }; Pop-Location; continue } + $parsed = $null; try { $parsed = $raw | ConvertFrom-Json } catch { [void]$ScanErrors.Add("npm outdated unparseable: $name"); Pop-Location; continue } + if(-not $parsed){ Pop-Location; continue } + foreach($p in $parsed.PSObject.Properties){ + $pkgName = $p.Name + $info = $p.Value + if($info -is [System.Object[]]){ $info = $info[0] } # multiple entries -> take first + $cur = if($info.PSObject.Properties.Name -contains 'current'){ $info.current } else { $null } + $latest = if($info.PSObject.Properties.Name -contains 'latest'){ $info.latest } else { $null } + if(-not $cur -or -not $latest){ continue } # not installed / no candidate + if(Test-IsPrerelease $latest){ continue } # stable only + if(-not (Test-VersionGreater $latest $cur)){ continue } + $isMajor = Test-IsMajorBump $cur $latest + if($isMajor -and $SkipMajor){ [void]$Frozen.Add([PSCustomObject]@{ Ecosystem='npm'; Id="$name/$pkgName"; Current=$cur; Reason="major bump to $latest skipped (-SkipMajor)" }); continue } + [void]$Changes.Add([PSCustomObject]@{ Ecosystem='npm'; Id=$pkgName; Project=$name; From=$cur; To=$latest; IsMajor=$isMajor; Kind='npm'; File=$pkgJson }) + Write-Info (" {0}: {1} -> {2}{3}" -f $pkgName,$cur,$latest,$(if($isMajor){' [MAJOR]'}else{''})) + } + } catch { [void]$ScanErrors.Add("npm scan error in ${name}: $($_.Exception.Message)") } + finally { Pop-Location } + } +} + +# --------------------------------------------------------------------------- +# Build verification + bisection +# --------------------------------------------------------------------------- +function Invoke-CakeBuild { + if(-not (Test-CommandAvailable 'dotnet')){ Write-Err 'dotnet CLI not found; cannot build.'; return $false } + Write-Info 'Running full cake build (dotnet run --project cake/Build.csproj) ...' + & dotnet run --project $cakeProj + return ($LASTEXITCODE -eq 0) +} + +function Invoke-BisectGreenSubset { + # ddmin-style: remove minimal change subsets until the build is green, bounded by $MaxBisectBuilds. + # Mutates the working tree to the green subset; records removed changes in $script:Reverted. + $builds = 0 + $active = @($Changes) + $n = 2 + Write-Warn "Build failed with all $($active.Count) change(s). Bisecting for a green subset (max $MaxBisectBuilds builds)..." + while($builds -lt $MaxBisectBuilds -and $active.Count -gt 1){ + $chunkSize = [Math]::Ceiling($active.Count / $n) + $removedSomething = $false + for($i=0; $i -lt $active.Count; $i += $chunkSize){ + $chunk = $active[$i..([Math]::Min($i+$chunkSize-1,$active.Count-1))] + $trial = $active | Where-Object { $chunk -notcontains $_ } + if(@($trial).Count -eq 0){ continue } + $dirs = Write-FileSet -ActiveChanges $trial + Update-NpmLockfiles -NpmDirs $dirs + $builds++ + Write-Info " bisect build $builds/$MaxBisectBuilds : trying $((@($trial)).Count) of $($active.Count) changes" + if(Invoke-CakeBuild){ $active = @($trial); $n = [Math]::Max($n-1,2); $removedSomething = $true; break } + if($builds -ge $MaxBisectBuilds){ break } + } + if(-not $removedSomething){ + if($n -ge $active.Count){ break } + $n = [Math]::Min($n*2,$active.Count) + } + } + # Record what got dropped and lock in the surviving subset. + $kept = @($active) + foreach($c in $Changes){ if($kept -notcontains $c){ [void]$Reverted.Add($c) } } + $dirs = Write-FileSet -ActiveChanges $kept + Update-NpmLockfiles -NpmDirs $dirs + if($kept.Count -gt 0 -and $builds -lt $MaxBisectBuilds){ + $builds++ + Write-Info " bisect build $builds : confirming surviving subset ($($kept.Count) change(s))" + $script:BuildResult = if(Invoke-CakeBuild){ 'pass (after bisection)' } else { 'FAIL (bisection exhausted - manual review)' } + } else { + $script:BuildResult = "indeterminate (bisection hit -MaxBisectBuilds=$MaxBisectBuilds)" + } +} + +# --------------------------------------------------------------------------- +# Report +# --------------------------------------------------------------------------- +function Write-Report { + param([string]$Stamp) + $reportsDir = Join-Path $scriptRoot 'reports' + if(-not (Test-Path $reportsDir)){ New-Item -ItemType Directory -Path $reportsDir -Force | Out-Null } + $mdPath = Join-Path $reportsDir "latest-deps-report-$Stamp.md" + $jsonPath = Join-Path $reportsDir "latest-deps-report-$Stamp.json" + + $applied = @($Changes | Where-Object { $Reverted -notcontains $_ }) + $majors = @($applied | Where-Object { $_.IsMajor }) + + $payload = [PSCustomObject]@{ + timestamp = $Stamp + dryRun = [bool]$DryRun + skipMajor = [bool]$SkipMajor + ecosystems = @{ nuget=[bool]$doNuget; npm=[bool]$doNpm; tools=[bool]$doTools } + build = $BuildResult + applied = $applied + reverted = $Reverted + frozen = $Frozen + majors = $majors + scanErrors = $ScanErrors + } + Write-Utf8NoBom-LF -Path $jsonPath -Content ($payload | ConvertTo-Json -Depth 8) + + $sb = New-Object System.Text.StringBuilder + [void]$sb.AppendLine('# Latest-dependency update report') + [void]$sb.AppendLine('') + [void]$sb.AppendLine("- Generated: $Stamp") + [void]$sb.AppendLine("- Dry run: $([bool]$DryRun)") + [void]$sb.AppendLine("- Skip major: $([bool]$SkipMajor)") + [void]$sb.AppendLine("- Ecosystems: NuGet=$doNuget, npm=$doNpm, tools=$doTools") + [void]$sb.AppendLine("- Build result: **$BuildResult**") + [void]$sb.AppendLine('') + + if($majors.Count -gt 0){ + [void]$sb.AppendLine("## [!] Major-version bumps ($($majors.Count)) - review carefully") + [void]$sb.AppendLine('| Ecosystem | Package | From | To |') + [void]$sb.AppendLine('|---|---|---|---|') + foreach($c in $majors){ [void]$sb.AppendLine("| $($c.Ecosystem) | $($c.Id) | $($c.From) | $($c.To) |") } + [void]$sb.AppendLine('') + } + + [void]$sb.AppendLine("## NuGet bumped ($(@($applied | Where-Object { $_.Ecosystem -eq 'nuget' }).Count))") + [void]$sb.AppendLine('| Package | From | To | Major? |') + [void]$sb.AppendLine('|---|---|---|---|') + foreach($c in ($applied | Where-Object { $_.Ecosystem -eq 'nuget' })){ [void]$sb.AppendLine("| $($c.Id) | $($c.From) | $($c.To) | $(if($c.IsMajor){'YES'}else{''}) |") } + [void]$sb.AppendLine('') + + [void]$sb.AppendLine("## npm bumped ($(@($applied | Where-Object { $_.Ecosystem -eq 'npm' }).Count))") + [void]$sb.AppendLine('| Project | Package | From | To | Major? |') + [void]$sb.AppendLine('|---|---|---|---|---|') + foreach($c in ($applied | Where-Object { $_.Ecosystem -eq 'npm' })){ [void]$sb.AppendLine("| $($c.Project) | $($c.Id) | $($c.From) | $($c.To) | $(if($c.IsMajor){'YES'}else{''}) |") } + [void]$sb.AppendLine('') + + [void]$sb.AppendLine("## Tools bumped ($(@($applied | Where-Object { $_.Ecosystem -eq 'tool' }).Count))") + [void]$sb.AppendLine('| Tool | From | To |') + [void]$sb.AppendLine('|---|---|---|') + foreach($c in ($applied | Where-Object { $_.Ecosystem -eq 'tool' })){ [void]$sb.AppendLine("| $($c.Id) | $($c.From) | $($c.To) |") } + [void]$sb.AppendLine('') + + if($Reverted.Count -gt 0){ + [void]$sb.AppendLine("## Reverted by build bisection ($($Reverted.Count))") + [void]$sb.AppendLine('| Ecosystem | Package | From | To |') + [void]$sb.AppendLine('|---|---|---|---|') + foreach($c in $Reverted){ [void]$sb.AppendLine("| $($c.Ecosystem) | $($c.Id) | $($c.From) | $($c.To) |") } + [void]$sb.AppendLine('') + } + + [void]$sb.AppendLine("## Frozen / skipped ($($Frozen.Count))") + [void]$sb.AppendLine('| Ecosystem | Package | Current | Reason |') + [void]$sb.AppendLine('|---|---|---|---|') + foreach($f in $Frozen){ [void]$sb.AppendLine("| $($f.Ecosystem) | $($f.Id) | $($f.Current) | $($f.Reason) |") } + [void]$sb.AppendLine('') + + if($ScanErrors.Count -gt 0){ + [void]$sb.AppendLine('## Scan errors') + foreach($e in $ScanErrors){ [void]$sb.AppendLine("- $e") } + } + Write-Utf8NoBom-LF -Path $mdPath -Content $sb.ToString() + Write-Info "Report: $mdPath" + return $mdPath +} + +# --------------------------------------------------------------------------- +# Commit + PR +# --------------------------------------------------------------------------- +function Invoke-CreatePR { + param([string]$ReportPath) + if(-not (Test-CommandAvailable 'git')){ Write-Err 'git not found; cannot create PR.'; return } + if(-not (Test-CommandAvailable 'gh')){ Write-Err 'gh CLI not found; cannot open PR.'; return } + + $branch = 'chore/update-latest-deps' + Write-Info "Creating branch '$branch' off origin/dev and opening PR..." + + & git -C $repoRoot fetch origin dev --quiet + & git -C $repoRoot stash push -u -m 'latest-deps-wip' *> $null + $stashed = ($LASTEXITCODE -eq 0) + & git -C $repoRoot switch -C $branch origin/dev + if($LASTEXITCODE -ne 0){ Write-Err "Failed to create branch $branch."; if($stashed){ & git -C $repoRoot stash pop *> $null }; return } + if($stashed){ + & git -C $repoRoot stash pop + if($LASTEXITCODE -ne 0){ Write-Err 'Stash pop conflicted; resolve manually. Aborting PR.'; return } + } + + & git -C $repoRoot add -- 'Directory.Packages.props' '.config/dotnet-tools.json' '**/package.json' '**/package-lock.json' + & git -C $repoRoot commit -m @' +chore(deps): update dependencies to latest stable + +Automated by scripts/update-latest-deps.ps1. + +Co-Authored-By: Claude Opus 4.8 (1M context) +'@ + if($LASTEXITCODE -ne 0){ Write-Warn 'Nothing to commit (no changes staged). Skipping PR.'; return } + + & git -C $repoRoot push -u origin $branch + if($LASTEXITCODE -ne 0){ Write-Err 'git push failed.'; return } + + $applied = @($Changes | Where-Object { $Reverted -notcontains $_ }) + $bumpLines = ($applied | ForEach-Object { "- $($_.Ecosystem) $($_.Id): $($_.From) -> $($_.To)$(if($_.IsMajor){' (MAJOR)'}else{''})" }) -join "`n" + $majorList = ($applied | Where-Object { $_.IsMajor } | ForEach-Object { "- $($_.Ecosystem) $($_.Id): $($_.From) -> $($_.To)" }) -join "`n" + $revLines = ($Reverted | ForEach-Object { "- $($_.Ecosystem) $($_.Id): $($_.From) -> $($_.To)" }) -join "`n" + $body = @" +Automated update of (non-AXSharp) dependencies to their latest stable versions. + +Build verification: $BuildResult + +## Bumped ($($applied.Count)) +$bumpLines + +## [!] Major-version bumps +$(if($majorList){ $majorList } else { '_none_' }) + +## Reverted by build bisection +$(if($revLines){ $revLines } else { '_none_' }) + +See the attached report ($([System.IO.Path]::GetFileName($ReportPath))) for frozen/skipped detail. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +"@ + & gh pr create --base dev --head $branch --title 'chore(deps): update dependencies to latest stable' --body $body + if($LASTEXITCODE -ne 0){ Write-Err 'gh pr create failed.' } else { Write-Info 'PR opened against dev.' } +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +$stamp = (Get-Date).ToString('yyyy-MM-dd-HHmmss') +Write-Info "update-latest-deps (apply=$Apply, dryRun=$DryRun, nuget=$doNuget, npm=$doNpm, tools=$doTools, skipMajor=$SkipMajor)" +if($DryRun){ Write-Warn 'DRY RUN - no files will be changed. Pass -Apply to write.' } + +$feedCtx = $null +if($doNuget -or $doTools){ + try { $feedCtx = Get-FeedContext -Feed $Source -User $null -Tok $feedToken } catch { Write-Err "Could not initialise NuGet feed: $($_.Exception.Message)"; exit 2 } +} + +if($doNuget){ Invoke-NuGetScan -FeedCtx $feedCtx } +if($doTools){ Invoke-ToolsScan -FeedCtx $feedCtx } +if($doNuget -or $doTools){ Resolve-GitVersionSync -FeedCtx $feedCtx } +if($doNpm){ Invoke-NpmScan } + +Write-Host '' +Write-Host '================ PLAN ================' -ForegroundColor Cyan +Write-Host (" Changes : {0}" -f $Changes.Count) +Write-Host (" Major : {0}" -f @($Changes | Where-Object { $_.IsMajor }).Count) +Write-Host (" Frozen : {0}" -f $Frozen.Count) +Write-Host (" Errors : {0}" -f $ScanErrors.Count) +Write-Host '=====================================' -ForegroundColor Cyan + +if($DryRun){ + Write-Warn '[DRY RUN] No changes written. Review the report below.' + $reportPath = Write-Report -Stamp $stamp + if($ScanErrors.Count -gt 0){ Write-Warn "$($ScanErrors.Count) scan error(s) - see report." } + exit 0 +} + +if($Changes.Count -eq 0){ + Write-Info 'Nothing to update; everything already at latest stable.' + Write-Report -Stamp $stamp | Out-Null + exit 0 +} + +# Apply everything, then verify. +$dirs = Write-FileSet -ActiveChanges @($Changes) +Update-NpmLockfiles -NpmDirs $dirs +Write-Info "Applied $($Changes.Count) change(s) to the working tree." + +if(-not $SkipBuild){ + if(Invoke-CakeBuild){ + $BuildResult = 'pass' + Write-Info 'Build passed with all changes.' + } elseif($RollbackAllOnFailure){ + Write-Warn 'Build failed; -RollbackAllOnFailure set, reverting everything.' + foreach($c in $Changes){ [void]$Reverted.Add($c) } + Write-FileSet -ActiveChanges @() | Out-Null + $BuildResult = 'FAIL (all changes rolled back)' + } else { + Invoke-BisectGreenSubset + } +} else { + $BuildResult = 'skipped (-SkipBuild)' + Write-Warn 'Build verification skipped (-SkipBuild).' +} + +$reportPath = Write-Report -Stamp $stamp + +Write-Host '' +Write-Host '================ SUMMARY ================' -ForegroundColor Cyan +Write-Host (" Applied : {0}" -f @($Changes | Where-Object { $Reverted -notcontains $_ }).Count) +Write-Host (" Reverted : {0}" -f $Reverted.Count) +Write-Host (" Frozen : {0}" -f $Frozen.Count) +Write-Host (" Build : {0}" -f $BuildResult) +Write-Host (" Errors : {0}" -f $ScanErrors.Count) +Write-Host '========================================' -ForegroundColor Cyan + +if($CreatePR){ + if(@($Changes | Where-Object { $Reverted -notcontains $_ }).Count -gt 0){ Invoke-CreatePR -ReportPath $reportPath } + else { Write-Warn 'No surviving changes; skipping PR creation.' } +} + +if($BuildResult -like 'FAIL*'){ exit 1 } +exit 0 From caf3e4ba7941649e9f4238dd11bfd244b24044d0 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 18:02:37 +0200 Subject: [PATCH 08/23] Remove unused package.json and apax.yml files to clean up the project structure. --- src/components.abb.robotics/package-lock.json | 1174 ----------------- src/components.abb.robotics/package.json | 6 - src/components.abstractions/package-lock.json | 1174 ----------------- src/components.abstractions/package.json | 6 - src/data/package-lock.json | 1174 ----------------- src/data/package.json | 6 - src/inspectors/package-lock.json | 1174 ----------------- src/inspectors/package.json | 6 - src/traversals/apax/apax.yml | 51 - 9 files changed, 4771 deletions(-) delete mode 100644 src/components.abb.robotics/package-lock.json delete mode 100644 src/components.abb.robotics/package.json delete mode 100644 src/components.abstractions/package-lock.json delete mode 100644 src/components.abstractions/package.json delete mode 100644 src/data/package-lock.json delete mode 100644 src/data/package.json delete mode 100644 src/inspectors/package-lock.json delete mode 100644 src/inspectors/package.json delete mode 100644 src/traversals/apax/apax.yml diff --git a/src/components.abb.robotics/package-lock.json b/src/components.abb.robotics/package-lock.json deleted file mode 100644 index 13ea2dbbc..000000000 --- a/src/components.abb.robotics/package-lock.json +++ /dev/null @@ -1,1174 +0,0 @@ -{ - "name": "components.abb.robotics", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@tailwindcss/cli": "^4.1.11", - "tailwindcss": "^4.1.11" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.11.tgz", - "integrity": "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ==", - "license": "MIT", - "dependencies": { - "@parcel/watcher": "^2.5.1", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "enhanced-resolve": "^5.18.1", - "mri": "^1.2.0", - "picocolors": "^1.1.1", - "tailwindcss": "4.1.11" - }, - "bin": { - "tailwindcss": "dist/index.mjs" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - } - } -} diff --git a/src/components.abb.robotics/package.json b/src/components.abb.robotics/package.json deleted file mode 100644 index 59aeb296f..000000000 --- a/src/components.abb.robotics/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@tailwindcss/cli": "^4.1.11", - "tailwindcss": "^4.1.11" - } -} diff --git a/src/components.abstractions/package-lock.json b/src/components.abstractions/package-lock.json deleted file mode 100644 index c180f8e4c..000000000 --- a/src/components.abstractions/package-lock.json +++ /dev/null @@ -1,1174 +0,0 @@ -{ - "name": "components.abstractions", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@tailwindcss/cli": "^4.1.11", - "tailwindcss": "^4.1.11" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.11.tgz", - "integrity": "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ==", - "license": "MIT", - "dependencies": { - "@parcel/watcher": "^2.5.1", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "enhanced-resolve": "^5.18.1", - "mri": "^1.2.0", - "picocolors": "^1.1.1", - "tailwindcss": "4.1.11" - }, - "bin": { - "tailwindcss": "dist/index.mjs" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - } - } -} diff --git a/src/components.abstractions/package.json b/src/components.abstractions/package.json deleted file mode 100644 index 59aeb296f..000000000 --- a/src/components.abstractions/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@tailwindcss/cli": "^4.1.11", - "tailwindcss": "^4.1.11" - } -} diff --git a/src/data/package-lock.json b/src/data/package-lock.json deleted file mode 100644 index a4f78bc8b..000000000 --- a/src/data/package-lock.json +++ /dev/null @@ -1,1174 +0,0 @@ -{ - "name": "data", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@tailwindcss/cli": "^4.1.11", - "tailwindcss": "^4.1.11" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.11.tgz", - "integrity": "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ==", - "license": "MIT", - "dependencies": { - "@parcel/watcher": "^2.5.1", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "enhanced-resolve": "^5.18.1", - "mri": "^1.2.0", - "picocolors": "^1.1.1", - "tailwindcss": "4.1.11" - }, - "bin": { - "tailwindcss": "dist/index.mjs" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - } - } -} diff --git a/src/data/package.json b/src/data/package.json deleted file mode 100644 index 59aeb296f..000000000 --- a/src/data/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@tailwindcss/cli": "^4.1.11", - "tailwindcss": "^4.1.11" - } -} diff --git a/src/inspectors/package-lock.json b/src/inspectors/package-lock.json deleted file mode 100644 index 75f8225f6..000000000 --- a/src/inspectors/package-lock.json +++ /dev/null @@ -1,1174 +0,0 @@ -{ - "name": "inspectors", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@tailwindcss/cli": "^4.1.11", - "tailwindcss": "^4.1.11" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@tailwindcss/cli": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.11.tgz", - "integrity": "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ==", - "license": "MIT", - "dependencies": { - "@parcel/watcher": "^2.5.1", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "enhanced-resolve": "^5.18.1", - "mri": "^1.2.0", - "picocolors": "^1.1.1", - "tailwindcss": "4.1.11" - }, - "bin": { - "tailwindcss": "dist/index.mjs" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - } - } -} diff --git a/src/inspectors/package.json b/src/inspectors/package.json deleted file mode 100644 index 59aeb296f..000000000 --- a/src/inspectors/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@tailwindcss/cli": "^4.1.11", - "tailwindcss": "^4.1.11" - } -} diff --git a/src/traversals/apax/apax.yml b/src/traversals/apax/apax.yml deleted file mode 100644 index 53140784a..000000000 --- a/src/traversals/apax/apax.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: "apax.traversal" -version: "0.0.0-dev.0" -type: "app" -targets: -- "1500" -registries: - '@inxton': "https://npm.pkg.github.com/" -devDependencies: - '@inxton/ax-sdk': "0.0.0-dev.0" -dependencies: - "@inxton/axopen.abstractions": "0.0.0-dev.0" - "@inxton/ax.axopen.app": "0.0.0-dev.0" - "@inxton/ax.axopen.hwlibrary": "0.0.0-dev.0" - "@inxton/ax.axopen.min": "0.0.0-dev.0" - "@inxton/ax.catalog": "0.0.51" - "@inxton/axopen.components.abb.robotics": "0.0.0-dev.0" - "@inxton/axopen.components.abstractions": "0.0.0-dev.0" - "@inxton/axopen.components.balluff.identification": "0.0.0-dev.0" - "@inxton/axopen.components.cognex.vision": "0.0.0-dev.0" - "@inxton/axopen.components.desoutter.tightening": "0.0.0-dev.0" - "@inxton/axopen.components.drives": "0.0.0-dev.0" - "@inxton/axopen.components.dukane.welders": "0.0.0-dev.0" - "@inxton/axopen.components.elements": "0.0.0-dev.0" - "@inxton/axopen.components.festo.drives": "0.0.0-dev.0" - "@inxton/axopen.components.keyence.vision": "0.0.0-dev.0" - "@inxton/axopen.components.kuka.robotics": "0.0.0-dev.0" - "@inxton/axopen.components.mitsubishi.robotics": "0.0.0-dev.0" - "@inxton/axopen.components.pneumatics": "0.0.0-dev.0" - "@inxton/axopen.components.rexroth.drives": "0.0.0-dev.0" - "@inxton/axopen.components.rexroth.press": "0.0.0-dev.0" - "@inxton/axopen.components.rexroth.tightening": "0.0.0-dev.0" - "@inxton/axopen.components.robotics": "0.0.0-dev.0" - "@inxton/axopen.components.siem.communication": "0.0.0-dev.0" - "@inxton/axopen.components.siem.identification": "0.0.0-dev.0" - "@inxton/axopen.components.ur.robotics": "0.0.0-dev.0" - "@inxton/axopen.components.zebra.vision": "0.0.0-dev.0" - "@inxton/axopen.core": "0.0.0-dev.0" - "@inxton/axopen.data": "0.0.0-dev.0" - "axopen_data_distributed_tests_l4": "0.0.0-dev.0" - "axopen.data.tests_l1": "0.0.0-dev.0" - "axopen.integration.tests_l4": "0.0.0-dev.0" - "@inxton/axopen.inspectors": "0.0.0-dev.0" - "@inxton/axopen.io": "0.0.0-dev.0" - "@inxton/axopen.probers": "0.0.0-dev.0" - "showcase": "0.0.0-dev.0" - "@inxton/axopen.simatic1500": "0.0.0-dev.0" - "@inxton/apaxlibname": "0.0.0-dev.0" - "@inxton/axopen.timers": "0.0.0-dev.0" - "@inxton/axopen.utils": "0.0.0-dev.0" -installStrategy: "overridable" -... From 59fc0c12636cec48ecad85e2df809d1066130c0f Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Fri, 29 May 2026 18:15:38 +0200 Subject: [PATCH 09/23] docs(changelog): add deps-update entry; bump next-version 0.56.3 -> 0.56.4 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 24 ++++++++++++++++++++++++ GitVersion.yml | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8654c355..ea863b69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +### [BUILD] Dependency-maintenance tooling + AXSharp `0.47.0-alpha.484` bump + +**Note:** Build/CI tooling and dependency maintenance. No public-API change, no PLC source change. Branch: `deps-update`. + +- feat: `scripts/update-latest-deps.ps1` — bumps all non-AXSharp dependencies (NuGet + npm) to their latest stable versions, sharing common helpers via `scripts/_deps-common.ps1`. +- feat: `scripts/update-vulnerable-deps.ps1` — scans npm and NuGet dependencies for known vulnerabilities and emits a report. +- chore: AXSharp packages bumped to `0.47.0-alpha.484` in `Directory.Packages.props`, with transitive dependencies reconciled. `.config/dotnet-tools.json` updated to match. +- chore: Added `.claude/skills/update-axsharp-version/SKILL.md` — skill for updating AXSharp and Inxton.Operon package versions. +- chore: Removed obsolete `package.json` / `package-lock.json` files across `src/components.abb.robotics`, `src/components.abstractions`, `src/data`, `src/data/src/AXOpen.Data.Blazor`, `src/inspectors`, and a stray `apax.yml`, to clean up the project structure. +- chore: `develop` branch GitVersion mode changed to `ContinuousDeployment`. +- chore: Styling dependencies refreshed (`src/styling/src/package.json` / lock; `momentum.css` regenerated). + +**Impact:** +- Routine dependency bumps and vulnerability scanning are now scriptable and reproducible. +- AXSharp consumers build against `0.47.0-alpha.484`. +- Dead npm lockfiles no longer pollute the tree or trigger spurious tooling. + +**Risks/Review:** +- Dependency version bumps can introduce behavioural drift; verify a full `dotnet build` and the styling render after pulling. + +**Testing:** +- Run `scripts/update-latest-deps.ps1` and `scripts/update-vulnerable-deps.ps1` end-to-end (exit code 0). +- `dotnet build` from solution root succeeds against the bumped package set. + ### [CORE] AxoSequencer step-timeout alarm — does not fall after timeout clears **Note:** PLC bug fix in `src/core/ctrl/src/AxoCoordination/AxoSequencer/AxoSequencer.st` (`AxoStepTimedOutMessenger.Activate`). No public-API change. Branch: `fix-issue-when-timeout-sequencer-alarm-doesnot-fall`. diff --git a/GitVersion.yml b/GitVersion.yml index c4aeb37fd..458569c2d 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,5 +1,5 @@ mode: ContinuousDeployment -next-version: 0.56.3 +next-version: 0.56.4 branches: main: regex: ^master$|^main$ From 1c93135ccfa84513d057d09fadd282392c8f506f Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sat, 30 May 2026 08:10:48 +0200 Subject: [PATCH 10/23] feat(deps): update NuGet.Packaging to 6.14.3 and add NuGet.Protocol dependency; enhance npm project discovery in update scripts --- Directory.Packages.props | 3 +- scripts/update-latest-deps.ps1 | 30 +++-- scripts/update-vulnerable-deps-npm-app.ps1 | 140 --------------------- scripts/update-vulnerable-deps.ps1 | 17 ++- 4 files changed, 30 insertions(+), 160 deletions(-) delete mode 100644 scripts/update-vulnerable-deps-npm-app.ps1 diff --git a/Directory.Packages.props b/Directory.Packages.props index 6ccee4040..73c8c8c39 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + @@ -90,6 +90,7 @@ + diff --git a/scripts/update-latest-deps.ps1 b/scripts/update-latest-deps.ps1 index 8ba494dde..191802890 100644 --- a/scripts/update-latest-deps.ps1 +++ b/scripts/update-latest-deps.ps1 @@ -60,6 +60,10 @@ Cap on the number of full cake builds spent bisecting after an initial failure. .PARAMETER CreatePR Commit changes to branch 'chore/update-latest-deps' off origin/dev and open a PR against dev. Implies -Apply. +.PARAMETER NpmProjects +Explicit list of package.json paths to process (relative to repo root or absolute). When omitted, +projects are discovered automatically under src/ (skipping bin/obj/ctrl/.apax/node_modules/wwwroot/dist). + .PARAMETER Source NuGet v3 feed used to look up available versions. Default nuget.org. @@ -94,6 +98,7 @@ param( [switch]$RollbackAllOnFailure, [int]$MaxBisectBuilds = 6, [switch]$CreatePR, + [string[]]$NpmProjects, [string]$Source = 'https://api.nuget.org/v3/index.json', [string]$Token, [switch]$Detailed @@ -131,7 +136,7 @@ $feedToken = if($Token){ $Token } elseif(-not $IsPublicNuGet){ Resolve-FeedToken # Owned by update_axsharp_versions.ps1 - never touched. $AxSharpSkipPattern = '^(AXSharp|Inxton\.Operon|AXOpen)\b' # Owned by update-vulnerable-deps.ps1 ("Security pins" ItemGroup) - never touched. -$SecurityPinIds = @('Snappier','System.Security.Cryptography.Xml') +$SecurityPinIds = @('') #@('Snappier','System.Security.Cryptography.Xml') # Frozen to stay aligned with net10.0 (see plan / interview). $FrameworkFreezeExact = @('Microsoft.NET.ILLink.Tasks','Microsoft.VisualStudio.Web.CodeGeneration.Design') $FrameworkFreezePrefixes = @('Microsoft.AspNetCore.','Microsoft.EntityFrameworkCore.','Microsoft.Extensions.','System.') @@ -139,15 +144,20 @@ $FrameworkFreezePrefixes = @('Microsoft.AspNetCore.','Microsoft.EntityFrameworkC $GitVersionNuGetId = 'GitVersion.MsBuild' $GitVersionToolId = 'gitversion.tool' -# Source npm projects (explicit list - avoids bin/obj/ctrl/.apax generated copies). -$NpmProjects = @( - 'src/components.abb.robotics/package.json' - 'src/components.abstractions/package.json' - 'src/data/package.json' - 'src/inspectors/package.json' - 'src/showcase/app/ix-blazor/showcase.blazor/package.json' - 'src/styling/src/package.json' -) | ForEach-Object { Join-Path $repoRoot $_ } +# Source npm projects - discovered dynamically under src/, skipping generated/dependency +# copies (bin/obj/ctrl/.apax/node_modules/wwwroot). Override with -NpmProjects to be explicit. +function Get-NpmProjects { + $srcRoot = Join-Path $repoRoot 'src' + if(-not (Test-Path -LiteralPath $srcRoot)){ return @() } + $excludeRx = '[\\/](bin|obj|ctrl|\.apax|node_modules|wwwroot|dist|\.git)[\\/]' + Get-ChildItem -LiteralPath $srcRoot -Recurse -File -Filter 'package.json' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch $excludeRx } | + Select-Object -ExpandProperty FullName | + Sort-Object +} + +$NpmProjects = if($NpmProjects){ $NpmProjects | ForEach-Object { if([System.IO.Path]::IsPathRooted($_)){ $_ } else { Join-Path $repoRoot $_ } } } + else { Get-NpmProjects } # Accumulators for the report. $Changes = New-Object System.Collections.ArrayList # applied/previewed bumps diff --git a/scripts/update-vulnerable-deps-npm-app.ps1 b/scripts/update-vulnerable-deps-npm-app.ps1 deleted file mode 100644 index b2ee3c31f..000000000 --- a/scripts/update-vulnerable-deps-npm-app.ps1 +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS -Updates vulnerable npm dependencies in all directories matching the pattern **/app/ix-blazor/ - -.DESCRIPTION -This script finds all directories matching the pattern **/app/ix-blazor/ and runs npm audit fix -to automatically fix identified vulnerabilities. - -.PARAMETER DryRun -If specified, only shows what would be done without making actual changes. - -.PARAMETER Force -If specified, bypasses confirmation prompts. - -.EXAMPLE -./update-vulnerable-deps.ps1 -./update-vulnerable-deps.ps1 -DryRun -./update-vulnerable-deps.ps1 -Force -#> - -param( - [switch]$DryRun, - [switch]$Force -) - -$ErrorActionPreference = "Stop" - -# Get the script's directory -$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path - -# Find all directories matching **/app/ix-blazor/ -Write-Host "Searching for directories matching pattern '**/app/ix-blazor/'..." -ForegroundColor Cyan - -$targetDirs = @() -$appDirs = Get-ChildItem -Path $scriptRoot -Recurse -Filter "app" -Directory -ErrorAction SilentlyContinue - -foreach ($appDir in $appDirs) { - $ixBlazerPath = Join-Path -Path $appDir.FullName -ChildPath "ix-blazor" - - if (Test-Path -Path $ixBlazerPath -PathType Container) { - $packageJsonPath = Join-Path -Path $ixBlazerPath -ChildPath "package.json" - - if (Test-Path -Path $packageJsonPath -PathType Leaf) { - $targetDirs += @{ - Path = $ixBlazerPath - PackageJson = $packageJsonPath - } - } - } -} - -if ($targetDirs.Count -eq 0) { - Write-Host "No directories matching '**/app/ix-blazor/' with package.json found." -ForegroundColor Yellow - exit 0 -} - -Write-Host "Found $($targetDirs.Count) directory/directories to process:" -ForegroundColor Green -foreach ($dir in $targetDirs) { - Write-Host " - $($dir.Path)" -ForegroundColor Gray -} - -if (-not $Force) { - Write-Host "" - $confirmation = Read-Host "Continue with npm audit fix? (Y/n)" - if ($confirmation -and $confirmation.ToLower() -ne 'y') { - Write-Host "Operation cancelled." -ForegroundColor Yellow - exit 0 - } -} - -# Process each directory -$successCount = 0 -$failureCount = 0 - -foreach ($dir in $targetDirs) { - $dirPath = $dir.Path - Write-Host "" - Write-Host "Processing: $dirPath" -ForegroundColor Cyan - - try { - Push-Location -Path $dirPath - - # Check if node_modules exists, install if not - if (-not (Test-Path -Path "node_modules" -PathType Container)) { - Write-Host " Installing dependencies..." -ForegroundColor Gray - if ($DryRun) { - Write-Host " [DRY RUN] Would run: npm install" -ForegroundColor Yellow - } - else { - npm install - if ($LASTEXITCODE -ne 0) { - Write-Host " Failed to install dependencies" -ForegroundColor Red - $failureCount++ - Pop-Location - continue - } - } - } - - # Run npm audit to show vulnerabilities - Write-Host " Running npm audit..." -ForegroundColor Gray - if ($DryRun) { - Write-Host " [DRY RUN] Would run: npm audit fix" -ForegroundColor Yellow - npm audit - } - else { - npm audit fix - if ($LASTEXITCODE -ne 0) { - Write-Host " Warning: npm audit fix completed with exit code $($LASTEXITCODE)" -ForegroundColor Yellow - } - else { - Write-Host " Successfully updated vulnerable dependencies" -ForegroundColor Green - $successCount++ - } - } - - Pop-Location - } - catch { - Write-Host " Error processing directory: $_" -ForegroundColor Red - $failureCount++ - Pop-Location - } -} - -# Summary -Write-Host "" -Write-Host "================================" -ForegroundColor Cyan -Write-Host "Summary:" -ForegroundColor Cyan -Write-Host " Processed: $($targetDirs.Count)" -ForegroundColor Gray -Write-Host " Successful: $successCount" -ForegroundColor Green -Write-Host " Failed: $failureCount" -ForegroundColor $(if ($failureCount -gt 0) { "Red" } else { "Green" }) -Write-Host "================================" -ForegroundColor Cyan - -if ($DryRun) { - Write-Host "[DRY RUN] No changes were made." -ForegroundColor Yellow -} - -exit $(if ($failureCount -gt 0) { 1 } else { 0 }) diff --git a/scripts/update-vulnerable-deps.ps1 b/scripts/update-vulnerable-deps.ps1 index c2d68c6a3..477b28330 100644 --- a/scripts/update-vulnerable-deps.ps1 +++ b/scripts/update-vulnerable-deps.ps1 @@ -65,7 +65,7 @@ param( [switch]$NuGetOnly, [switch]$CreatePR, [ValidateSet('low','moderate','high','critical')] - [string]$MinSeverity = 'moderate', + [string]$MinSeverity = 'low', [string]$Source = 'https://api.nuget.org/v3/index.json', [string]$Token, [switch]$Detailed @@ -97,15 +97,14 @@ $SkipPattern = '^(AXSharp|Inxton\.Operon|AXOpen)\b' $Token = Resolve-FeedToken -Token $Token -Detailed:$Detailed -# Source npm projects (explicit list - avoids bin/obj/ctrl generated copies). +# Source npm projects - discovered dynamically under src/ (skips node_modules and +# bin/obj/ctrl generated copies so only first-party source manifests are audited). $NpmProjects = @( - 'src/components.abb.robotics/package.json' - 'src/components.abstractions/package.json' - 'src/data/package.json' - 'src/inspectors/package.json' - 'src/showcase/app/ix-blazor/showcase.blazor/package.json' - 'src/styling/src/package.json' -) | ForEach-Object { Join-Path $repoRoot $_ } + Get-ChildItem -Path (Join-Path $repoRoot 'src') -Recurse -File -Filter 'package.json' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '[\\/](node_modules|bin|obj|ctrl)[\\/]' } | + ForEach-Object { $_.FullName } | + Sort-Object +) # Accumulators for the report. $NuGetFixed = New-Object System.Collections.ArrayList From 7dc678a32def97187805ba1df4a6c45a7dc6f514 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sat, 30 May 2026 10:31:59 +0200 Subject: [PATCH 11/23] chore(catalog): update @ax deps + bump ax.catalog Automated by scripts/update-ax-catalog.ps1. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ax.axopen.app/ctrl/apax.yml | 6 ++-- src/ax.axopen.hwlibrary/ctrl/apax.yml | 2 +- src/ax.axopen.min/ctrl/apax.yml | 2 +- src/ax.catalog/apax.yml | 36 +++++++++---------- .../ctrl/apax.yml | 2 +- src/sdk-ax/ctrl/apax.yml | 28 +++++++-------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/ax.axopen.app/ctrl/apax.yml b/src/ax.axopen.app/ctrl/apax.yml index c222e84a2..948050959 100644 --- a/src/ax.axopen.app/ctrl/apax.yml +++ b/src/ax.axopen.app/ctrl/apax.yml @@ -6,7 +6,7 @@ files: registries: "@inxton": "https://npm.pkg.github.com/" catalogs: - "@inxton/ax.catalog": '0.0.51' + "@inxton/ax.catalog": '0.0.52' devDependencies: "@inxton/ax-sdk": '0.0.0-dev.0' dependencies: @@ -14,8 +14,8 @@ dependencies: "@ax/system-bitaccess": 10.4.16 "@ax/system-math": 10.4.16 "@ax/system-serde": 10.4.16 - "@ax/dcp-utility": 1.2.0 - "@ax/hardware-diagnostics": 1.1.1 + "@ax/dcp-utility": 1.2.1 + "@ax/hardware-diagnostics": 1.2.0 "@ax/simatic-tasks": 11.0.2 installStrategy: strict apaxVersion: 3.5.0 diff --git a/src/ax.axopen.hwlibrary/ctrl/apax.yml b/src/ax.axopen.hwlibrary/ctrl/apax.yml index 17d822ac4..5e3b2a176 100644 --- a/src/ax.axopen.hwlibrary/ctrl/apax.yml +++ b/src/ax.axopen.hwlibrary/ctrl/apax.yml @@ -6,7 +6,7 @@ files: registries: "@inxton": "https://npm.pkg.github.com/" catalogs: - "@inxton/ax.catalog": '0.0.51' + "@inxton/ax.catalog": '0.0.52' devDependencies: "@inxton/ax-sdk": '0.0.0-dev.0' dependencies: diff --git a/src/ax.axopen.min/ctrl/apax.yml b/src/ax.axopen.min/ctrl/apax.yml index 7d48cec69..1fd659fd6 100644 --- a/src/ax.axopen.min/ctrl/apax.yml +++ b/src/ax.axopen.min/ctrl/apax.yml @@ -6,7 +6,7 @@ files: registries: "@inxton": "https://npm.pkg.github.com/" catalogs: - "@inxton/ax.catalog": '0.0.51' + "@inxton/ax.catalog": '0.0.52' devDependencies: "@inxton/ax-sdk": '0.0.0-dev.0' dependencies: diff --git a/src/ax.catalog/apax.yml b/src/ax.catalog/apax.yml index 5da316ada..5d03afb50 100644 --- a/src/ax.catalog/apax.yml +++ b/src/ax.catalog/apax.yml @@ -1,5 +1,5 @@ name: "@inxton/ax.catalog" -version: '0.0.51' +version: '0.0.52' registries: "@inxton": "https://npm.pkg.github.com/" type: catalog @@ -12,21 +12,21 @@ catalogDependencies: "@ax/axunitst": 9.4.8 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 9.4.8 "@ax/axunitst-library": 9.4.8 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 9.4.8 "@ax/build-native": 16.1.51 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 16.1.51 - "@ax/certificate-management": 2.0.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 2.0.0 - "@ax/dcp-utility": 1.2.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.2.0 + "@ax/certificate-management": 2.0.1 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 2.0.0 + "@ax/dcp-utility": 1.2.1 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.2.0 "@ax/debug-st-ls-plugin": 1.0.10 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.0.10 - "@ax/diagnostic-buffer": 2.1.1 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 2.1.1 - "@ax/hardware-diagnostics": 1.1.1 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.1.1 + "@ax/diagnostic-buffer": 2.2.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 2.1.1 + "@ax/hardware-diagnostics": 1.2.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.1.1 "@ax/hw-et200sp": 4.4.1 # @ax/simatic-ax@2510.12.0 contained at the time of creation this catalog (25.5.2026) version 4.4.1 "@ax/hw-s7-1500": 4.4.1 # @ax/simatic-ax@2510.12.0 contained at the time of creation this catalog (25.5.2026) version 4.4.1 "@ax/hwc": 4.4.1 # @ax/simatic-ax@2510.12.0 contained at the time of creation this catalog (25.5.2026) version 4.4.1 - "@ax/hwld": 3.4.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 3.4.0 - "@ax/mod": 1.13.21 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.13.21 - "@ax/mon": 1.13.21 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.13.21 - "@ax/plc-control": 1.6.3 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.6.3 - "@ax/plc-info": 4.1.1 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 4.1.1 + "@ax/hwld": 3.5.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 3.4.0 + "@ax/mod": 1.14.13 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.13.21 + "@ax/mon": 1.14.13 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.13.21 + "@ax/plc-control": 1.7.22 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.6.3 + "@ax/plc-info": 4.2.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 4.1.1 "@ax/plc-web-app-manager": 1.2.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.2.0 - "@ax/sdb": 1.13.21 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.13.21 + "@ax/sdb": 1.14.13 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.13.21 "@ax/sdk": 2510.12.0 # @ax/simatic-ax@2510.12.0 contained at the time of creation this catalog (25.5.2026) version 2510.12.0 "@ax/simatic-alarming": 5.1.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 5.1.0 "@ax/simatic-clocks": 11.0.116 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.0.116 @@ -58,12 +58,12 @@ catalogDependencies: "@ax/simatic-pointtopoint": 4.0.54 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 4.0.54 "@ax/simatic-tasks": 11.0.2 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.0.2 "@ax/simatic-technology-objects": 4.0.43 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 4.0.43 - "@ax/sld": 3.6.3 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 3.6.3 - "@ax/st-lang-contrib-xlad": 1.0.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.0.0 - "@ax/st-ls": 11.4.57 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.4.57 + "@ax/sld": 3.8.3 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 3.6.3 + "@ax/st-lang-contrib-xlad": 1.1.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 1.0.0 + "@ax/st-ls": 11.5.53 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.4.57 "@ax/st-opcua.stc-plugin": 2.0.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 2.0.0 "@ax/st-resources.stc-plugin": 4.0.3 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 4.0.3 - "@ax/stc": 11.4.57 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.4.57 + "@ax/stc": 11.5.53 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.4.57 "@ax/system": 10.4.16 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 10.4.16 "@ax/system-bistable": 10.4.16 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 10.4.16 "@ax/system-bitaccess": 10.4.16 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 10.4.16 @@ -78,8 +78,8 @@ catalogDependencies: "@ax/system-serde": 10.4.16 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 10.4.16 "@ax/system-strings": 10.4.16 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 10.4.16 "@ax/system-timer": 10.4.16 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 10.4.16 - "@ax/target-llvm": 11.4.57 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.4.57 - "@ax/target-mc7plus": 11.4.57 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.4.57 + "@ax/target-llvm": 11.5.53 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.4.57 + "@ax/target-mc7plus": 11.5.53 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 11.4.57 "@ax/tia2st": 4.3.5 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 4.3.5 - "@ax/trace": 3.3.1 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 3.3.1 + "@ax/trace": 3.4.0 # @ax/simatic-ax@2510.11.0 contained at the time of creation this catalog (16.5.2026) version 3.3.1 "@ax/xlad-service": 1.1.0 # @ax/simatic-ax@2510.12.0 contained at the time of creation this catalog (25.5.2026) version 1.1.0 diff --git a/src/components.siem.communication/ctrl/apax.yml b/src/components.siem.communication/ctrl/apax.yml index 2d44d0a1b..391b1e229 100644 --- a/src/components.siem.communication/ctrl/apax.yml +++ b/src/components.siem.communication/ctrl/apax.yml @@ -9,7 +9,7 @@ files: registries: "@inxton": "https://npm.pkg.github.com/" catalogs: - "@inxton/ax.catalog": '0.0.51' + "@inxton/ax.catalog": '0.0.52' devDependencies: "@inxton/ax-sdk": '0.0.0-dev.0' dependencies: diff --git a/src/sdk-ax/ctrl/apax.yml b/src/sdk-ax/ctrl/apax.yml index a4eb51863..bb2646427 100644 --- a/src/sdk-ax/ctrl/apax.yml +++ b/src/sdk-ax/ctrl/apax.yml @@ -6,27 +6,27 @@ files: registries: "@inxton": "https://npm.pkg.github.com/" catalogs: - "@inxton/ax.catalog": '0.0.51' + "@inxton/ax.catalog": '0.0.52' dependencies: '@ax/apax-build': 2.2.60 '@ax/axunitst': 9.4.8 - '@ax/certificate-management': 2.0.0 - '@ax/diagnostic-buffer': 2.1.1 + '@ax/certificate-management': 2.0.1 + '@ax/diagnostic-buffer': 2.2.0 '@ax/hw-s7-1500': 4.4.1 '@ax/hwc': 4.4.1 - '@ax/hwld': 3.4.0 - '@ax/mod': 1.13.21 - '@ax/mon': 1.13.21 - '@ax/plc-info': 4.1.1 - '@ax/sdb': 1.13.21 + '@ax/hwld': 3.5.0 + '@ax/mod': 1.14.13 + '@ax/mon': 1.14.13 + '@ax/plc-info': 4.2.0 + '@ax/sdb': 1.14.13 '@ax/simatic-package-tool': 2.0.17 - '@ax/sld': 3.6.3 - '@ax/st-ls': 11.4.57 + '@ax/sld': 3.8.3 + '@ax/st-ls': 11.5.53 '@ax/st-resources.stc-plugin': 4.0.3 - '@ax/stc': 11.4.57 - '@ax/target-llvm': 11.4.57 - '@ax/target-mc7plus': 11.4.57 - '@ax/trace': 3.3.1 + '@ax/stc': 11.5.53 + '@ax/target-llvm': 11.5.53 + '@ax/target-mc7plus': 11.5.53 + '@ax/trace': 3.4.0 installStrategy: strict apaxVersion: 3.5.0 scripts: From a889c87a974f9d58326c2bb8bdbf51676d9914fa Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sat, 30 May 2026 10:32:10 +0200 Subject: [PATCH 12/23] feat: add update-ax-catalog.ps1 script for managing @ax/* dependencies - Implemented a PowerShell script to update @ax/* dependencies in the local apax catalog. - The script includes two phases: Phase 1 for updating and publishing the catalog, and Phase 2 for verifying the build. - Added parameters for controlling behavior such as -AfterPublish, -Prerelease, -CatalogVersion, -NoPublish, -DryRun, and -Detailed. - Introduced functions for dependency resolution, consumer synchronization, and report generation. - Ensured safe handling of git operations to maintain a clean working directory during updates. --- scripts/update-ax-catalog.ps1 | 731 ++++++++++++++++++++++++++++++++++ 1 file changed, 731 insertions(+) create mode 100644 scripts/update-ax-catalog.ps1 diff --git a/scripts/update-ax-catalog.ps1 b/scripts/update-ax-catalog.ps1 new file mode 100644 index 000000000..a920c2fd2 --- /dev/null +++ b/scripts/update-ax-catalog.ps1 @@ -0,0 +1,731 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS +Updates the @ax/* dependencies of the local apax catalog (@inxton/ax.catalog) to the latest versions, +bumps the catalog version, syncs every catalog consumer, and publishes the catalog (Phase 1); then +verifies the build (Phase 2, -AfterPublish). + +.DESCRIPTION +The repo pins all Simatic AX (@ax/*) package versions centrally in the apax catalog at +src/ax.catalog/apax.yml (package @inxton/ax.catalog). The catalog CONSUMERS are the per-library +manifests src//ctrl/apax.yml that declare a `catalogs:` block referencing @inxton/ax.catalog and +re-pin a subset of @ax/* packages in their own `dependencies:`. Consumers are discovered dynamically. + +NOTE on ctrl/ and build noise: the consumer manifest lives at src//ctrl/apax.yml (git-tracked) and +is the file apax install reads, so this script edits it directly. During `apax pack`, cake stamps the +GitVersion SemVer into each manifest's `version` and @inxton/* deps (it leaves @ax/* and the .catalog +reference alone - see cake BuildContext.UpdateApaxVersion/UpdateApaxDependencies). Those stamps are +unwanted build noise, so Phase 2 commits our edits, runs the build, then restores every build-touched +tracked file back to that commit and un-commits - leaving ONLY our @ax/catalog edits staged. + +Editing files needs no published catalog - only the cake VERIFY build (which runs apax install) does. +So all editing + publishing happens in Phase 1, and verification is the separate Phase 2 (because a +published catalog version cannot be cleanly un-published, the build verification is deliberately AFTER +the publish - run it and inspect before relying on the new catalog): + + PHASE 1 (default) - refresh catalog + sync consumers + publish: + 1. Capture the current @ax/* versions. + 2. Resolve each @ax/* dep's highest non-deprecated version from the AX registry via + `apax list -p ` (stable; -Prerelease widens to prereleases) and rewrite the catalog's + catalogDependencies. (`apax update` is NOT used - it ignores a catalog's catalogDependencies.) + 3. If (and only if) at least one @ax/* dep changed: auto-bump the catalog `version` (patch, or + -CatalogVersion), and sync every discovered consumer (src//ctrl/apax.yml) - their explicit + @ax/* pins to the new catalog versions and their `catalogs: "@inxton/ax.catalog"` reference. + 4. Write a report under scripts/reports/. + 5. Publish the catalog by invoking scripts/_pack_and_publish_catalog.ps1 (unless -NoPublish, or a + -DryRun, or nothing changed). Then STOP - run -AfterPublish to verify. + + PHASE 2 (-AfterPublish) - verify: + 1. Re-assert consumer sync as an idempotent guard (normally a no-op). + 2. Pre-build commit ONLY our edited manifests (catalog + consumers). + 3. Verify with cake TESTS at level 2: `dotnet run --project cake/Build.csproj --do-test + --test-level 2 -n` (cake runs apax install --catalog --strict + build per consumer, then the L2 + .NET test suite). Not pack - so cake does not stamp SemVer into the manifests. + 4. Discard build-produced changes WITHOUT touching your other uncommitted work: restore build-changed + tracked files (excluding files that were already dirty before this run) back to the commit, then + `git reset --soft HEAD~1` so our edits remain STAGED. No `git clean` (protects untracked files). + +Phase 1 publishes automatically when there are changes; pass -NoPublish to keep the old manual gate. + +.PARAMETER AfterPublish +Run Phase 2 (cake verify, with an idempotent consumer-sync guard). Use after Phase 1 has published. + +.PARAMETER Prerelease +Include prerelease versions when resolving "latest" (otherwise the highest non-deprecated stable version +is used). Default: stable only. + +.PARAMETER CatalogVersion +Explicit new catalog version (overrides the automatic patch bump). Phase 1 only. + +.PARAMETER NoPublish +Phase 1 only: update + sync but do NOT auto-publish; print the manual publish/verify steps instead. + +.PARAMETER DryRun +Preview everything; write no files, make no git changes, publish nothing, run no cake build. + +.PARAMETER Detailed +Verbose logging. + +.EXAMPLE +./update-ax-catalog.ps1 -DryRun -Detailed # Phase 1 preview: @ax old->new, version, consumer edits + +.EXAMPLE +./update-ax-catalog.ps1 # Phase 1: refresh + bump + sync + auto-publish + +.EXAMPLE +./update-ax-catalog.ps1 -NoPublish # Phase 1: refresh + bump + sync, but DON'T publish + +.EXAMPLE +./update-ax-catalog.ps1 -AfterPublish # Phase 2: cake-verify, discard build noise +#> + +[CmdletBinding()] +param( + [switch]$AfterPublish, + [switch]$Prerelease, + [string]$CatalogVersion, + [switch]$NoPublish, + [switch]$DryRun, + [switch]$Detailed +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptRoot/_deps-common.ps1" + +$repoRoot = Split-Path -Parent $scriptRoot +$catalogDir = Join-Path $repoRoot 'src/ax.catalog' +$catalogYml = Join-Path $catalogDir 'apax.yml' +$cakeProj = Join-Path $repoRoot 'cake/Build.csproj' +$catalogPkg = '@inxton/ax.catalog' +$publishScript = Join-Path $scriptRoot '_pack_and_publish_catalog.ps1' + +# Catalog consumers are DISCOVERED dynamically (see Get-CatalogConsumers): any apax.yml whose +# `catalogs:` block references @inxton/ax.catalog. This is the same marker cake's ApaxCatalogInstall +# keys on, so new libraries that adopt the catalog are picked up automatically. + +# --------------------------------------------------------------------------- +# Small helpers +# --------------------------------------------------------------------------- +function Test-CommandAvailable { param([string]$Name) $null -ne (Get-Command $Name -ErrorAction SilentlyContinue) } + +# Repo-root-relative, forward-slash path (matches `git status --porcelain` output). +function ConvertTo-RepoRelative { + param([string]$Path) + $full = [System.IO.Path]::GetFullPath($Path) + $root = [System.IO.Path]::GetFullPath($repoRoot) + if($full.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)){ + $full = $full.Substring($root.Length).TrimStart('\','/') + } + return ($full -replace '\\','/') +} + +# Dependency-line regexes. Tolerant of single/double quoting, any indentation, and an optional +# trailing comment (the catalog pins carry `# ...` provenance notes). Group layout for the dep +# regexes: 1=indent+openquote 2=key 3=closequote+colon+space 4=value-openquote 5=version +# 6=value-closequote 7=trailing (whitespace + optional #comment), preserved on rewrite. +$AxDepRx = '^(\s+[''"]?)(@ax/[^''":\s]+)([''"]?\s*:\s*)([''"]?)([^''"\s#]+)([''"]?)(\s*(?:#.*)?)$' +$CatalogRefRx = '^(\s+[''"]?)(@inxton/ax\.catalog)([''"]?\s*:\s*)([''"]?)([^''"\s#]+)([''"]?)(\s*(?:#.*)?)$' +$VersionRx = '^(version\s*:\s*)([''"]?)([^''"\s#]+)([''"]?)(\s*(?:#.*)?)$' + +# Parse a top-level section of `key: value` deps and return @ax/* entries as an ordered map. +function Get-AxDepsInSection { + param([string[]]$Lines, [string]$Section) + $map = [ordered]@{} + $cur = $null + foreach($ln in $Lines){ + if($ln -match '^[A-Za-z]'){ # zero-indent => a top-level key + if($ln -match '^([A-Za-z][\w\.]*)\s*:'){ $cur = $Matches[1] } + continue + } + if($cur -ne $Section){ continue } + if($ln -match $AxDepRx){ $map[$Matches[2]] = $Matches[5] } + } + return $map +} + +# True when these apax.yml lines declare a `catalogs:` block that references the catalog package. +function Test-IsCatalogConsumer { + param([string[]]$Lines) + $cur = $null + foreach($ln in $Lines){ + if($ln -match '^[A-Za-z]'){ + if($ln -match '^([A-Za-z][\w\.]*)\s*:'){ $cur = $Matches[1] } + continue + } + if($cur -eq 'catalogs' -and $ln -match $CatalogRefRx){ return $true } + } + return $false +} + +# Discover every catalog consumer under src/: an apax.yml whose `catalogs:` block references +# @inxton/ax.catalog. In this repo the per-library manifest lives at src//ctrl/apax.yml (tracked +# in git) - that IS the file we edit and that apax install reads. We exclude only the dependency-copy +# / build-output dirs (.apax cache, bin, obj, node_modules), NOT ctrl. Sorted, absolute. +function Get-CatalogConsumers { + $srcRoot = Join-Path $repoRoot 'src' + if(-not (Test-Path -LiteralPath $srcRoot)){ return @() } + $excludeRx = '[\\/](bin|obj|\.apax|node_modules|wwwroot|dist|\.git|\.vs)[\\/]' + $catalogFull = [System.IO.Path]::GetFullPath($catalogYml) + Get-ChildItem -LiteralPath $srcRoot -Recurse -File -Filter 'apax.yml' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch $excludeRx } | + Where-Object { [System.IO.Path]::GetFullPath($_.FullName) -ne $catalogFull } | + Where-Object { Test-IsCatalogConsumer -Lines (Get-Content -LiteralPath $_.FullName) } | + Select-Object -ExpandProperty FullName | + Sort-Object +} + +# Read the top-level `version:` value from catalog content. +function Get-CatalogVersion { + param([string[]]$Lines) + foreach($ln in $Lines){ if($ln -match $VersionRx){ return $Matches[3] } } + return $null +} + +# Patch-bump a semver string (x.y.z -> x.y.(z+1)). +function Get-NextPatchVersion { + param([string]$Version) + $rec = ConvertTo-VersionRecord $Version + return ('{0}.{1}.{2}' -f $rec.Major, $rec.Minor, ($rec.Patch + 1)) +} + +# Strip ANSI/VT escape sequences from apax's coloured output so it can be parsed. +function Remove-Ansi { param([string]$Text) return ([regex]::Replace($Text, "\x1B\[[0-9;]*[A-Za-z]", '')) } + +# Query the AX registry for one package via `apax list -p ` and return its latest dist-tag plus the +# full version list (each flagged deprecated/prerelease). apax has no JSON mode and writes to stderr, so +# we merge streams, strip ANSI, and parse the "Versions" / "Tags(latest:)" sections. $LASTEXITCODE is +# unreliable here (apax emits info banners on stderr), so success is judged by whether we parsed a tag +# or any versions. Returns $null when the package can't be read. +function Get-AxPackageInfo { + param([string]$Package, [int]$Retries = 2) + # apax list can occasionally return an empty/partial response (registry warmup). Retry a couple of + # times until we get output that contains a Versions/Tags section before giving up. + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + $clean = '' + try { + for($attempt = 0; $attempt -le $Retries; $attempt++){ + $raw = '' + try { $raw = (& apax list -p $Package 2>&1 | Out-String) } catch { $raw = '' } + $clean = Remove-Ansi $raw + if($clean -match '(?m)^\s*(Versions|Tags)\s*$'){ break } + } + } finally { $ErrorActionPreference = $prevEap } + $latest = $null + $versions = New-Object System.Collections.ArrayList + $inVersions = $false; $inTags = $false + foreach($ln in ($clean -split "`r?`n")){ + $t = $ln.Trim() + if($t -eq 'Versions'){ $inVersions = $true; $inTags = $false; continue } + if($t -eq 'Tags'){ $inTags = $true; $inVersions = $false; continue } + # A non-list, non-section header line ends the current section. + if($t -match '^[A-Za-z]' -and $t -notmatch '^-\s'){ $inVersions = $false; $inTags = $false } + if($inVersions -and $t -match '^-\s*([0-9][^\s]*)'){ + $v = $Matches[1] + [void]$versions.Add([PSCustomObject]@{ Version=$v; Deprecated=($t -match 'deprecated'); Prerelease=(Test-IsPrerelease $v) }) + } + if($inTags -and $t -match 'latest:\s*([^\s]+)'){ $latest = $Matches[1] } + } + if(-not $latest -and $versions.Count -eq 0){ return $null } + return [PSCustomObject]@{ Package=$Package; Latest=$latest; Versions=@($versions) } +} + +# Pick the version to move a package to: the HIGHEST non-deprecated published version. We deliberately +# do NOT use the registry 'latest' dist-tag, because on the AX registry it can point below the highest +# published version (e.g. @ax/ax2tia latest=11.1.25 while 12.2.8 is published) - taking it would +# downgrade a pin. Stable mode excludes prereleases; -Prerelease includes them. Returns $null if none. +function Select-TargetVersion { + param($Info) + if(-not $Info){ return $null } + # Wrap the whole if-expression in @() - assigning a one-element array via `if` would unwrap it to a + # scalar, breaking the .Count check under StrictMode. + $cand = @(if($Prerelease){ $Info.Versions | Where-Object { -not $_.Deprecated } } + else { $Info.Versions | Where-Object { -not $_.Deprecated -and -not $_.Prerelease } }) + if($cand.Count -eq 0){ return $null } + $best = $cand[0].Version + foreach($c in $cand){ if(Test-VersionGreater $c.Version $best){ $best = $c.Version } } + return $best +} + +# True when moving From -> To crosses a major-version boundary (used to flag, not block). +function Test-IsMajorBump { + param([string]$From,[string]$To) + if([string]::IsNullOrWhiteSpace($From) -or [string]::IsNullOrWhiteSpace($To)){ return $false } + return ((ConvertTo-VersionRecord $To).Major -gt (ConvertTo-VersionRecord $From).Major) +} + +# Resolve the latest target version for every @ax/* key in $OldMap by querying the registry. Returns an +# ordered map (same key order as $OldMap). NEVER downgrades: if the resolved highest version is not +# strictly greater than the current pin, the current pin is kept (covers a lagging 'latest', a yanked +# higher version, or an already-current pin). Unresolvable keys keep their current version with a +# warning. Pure read - touches no file. +function Resolve-LatestAxVersions { + param($OldMap) + $newMap = [ordered]@{} + $i = 0; $total = $OldMap.Count + $updated = 0; $kept = 0; $failed = 0 + foreach($pkg in $OldMap.Keys){ + $i++ + $cur = $OldMap[$pkg] + # Live progress: a progress bar plus a single rewriting status line, so 77 sequential registry + # queries don't look frozen. (Write-Progress is suppressed in non-interactive hosts; the inline + # counter still shows. -Detailed additionally logs the per-package outcome below.) + $pct = [int](($i-1) * 100 / [Math]::Max($total,1)) + Write-Progress -Activity 'Resolving @ax/* latest versions' -Status ("[{0}/{1}] {2}" -f $i,$total,$pkg) -PercentComplete $pct + Write-Host ("`r [{0,2}/{1}] querying {2,-45}" -f $i,$total,$pkg) -NoNewline -ForegroundColor DarkGray + + $info = Get-AxPackageInfo -Package $pkg + $target = Select-TargetVersion -Info $info + if(-not $target){ + $failed++; $newMap[$pkg] = $cur + Write-Host "`r" -NoNewline + Write-Warn (" [{0}/{1}] {2}: could not resolve latest - keeping {3}" -f $i,$total,$pkg,$cur) + continue + } + if(Test-VersionGreater $target $cur){ + $updated++; $newMap[$pkg] = $target + if($Detailed){ Write-Host "`r" -NoNewline; Write-Info (" [{0}/{1}] {2}: {3} -> {4}" -f $i,$total,$pkg,$cur,$target) } + } else { + # Resolved version is <= current pin: keep current (no downgrade). Note when the registry's + # highest is actually lower than what we have pinned. + $kept++; $newMap[$pkg] = $cur + if($Detailed){ + Write-Host "`r" -NoNewline + if(Test-VersionGreater $cur $target){ Write-Info (" [{0}/{1}] {2}: {3} (registry highest {4} is lower - kept)" -f $i,$total,$pkg,$cur,$target) } + else { Write-Info (" [{0}/{1}] {2}: {3} (already latest)" -f $i,$total,$pkg,$cur) } + } + } + } + Write-Host ("`r{0,-72}" -f '') -NoNewline; Write-Host "`r" -NoNewline # clear the status line + Write-Progress -Activity 'Resolving @ax/* latest versions' -Completed + Write-Info ("Resolved {0} package(s): {1} to update, {2} already current, {3} unresolved." -f $total,$updated,$kept,$failed) + return $newMap +} + +# Rewrite the catalogDependencies section of the catalog file in place from $NewMap, preserving line +# formatting and trailing comments. Only @ax/* lines whose version changed are touched. +function Update-CatalogDepsFile { + param([string]$Path, $NewMap) + $lines = Get-Content -LiteralPath $Path + $cur = $null + $out = foreach($ln in $lines){ + if($ln -match '^[A-Za-z]'){ + if($ln -match '^([A-Za-z][\w\.]*)\s*:'){ $cur = $Matches[1] } + $ln; continue + } + if($cur -eq 'catalogDependencies' -and $ln -match $AxDepRx){ + $pkg = $Matches[2]; $oldv = $Matches[5] + if($NewMap.Contains($pkg) -and $NewMap[$pkg] -ne $oldv){ + "$($Matches[1])$pkg$($Matches[3])$($Matches[4])$($NewMap[$pkg])$($Matches[6])$($Matches[7])".TrimEnd(); continue + } + $ln; continue + } + $ln + } + Write-Utf8NoBom-LF -Path $Path -Content (($out -join "`n") + "`n") +} + +# Pack + publish the catalog to the @inxton registry by invoking the existing pack/publish script. +# Returns $true on success. Requires APAX_KEY (and GH_USER/GH_TOKEN if the registry login isn't cached). +function Invoke-CatalogPublish { + if(-not (Test-Path -LiteralPath $publishScript)){ Write-Err "Publish script not found: $publishScript"; return $false } + if(-not $env:APAX_KEY){ Write-Err 'APAX_KEY is not set - cannot pack/publish the catalog. Set APAX_KEY (and GH_USER/GH_TOKEN) and re-run, or pass -NoPublish.'; return $false } + Write-Info "Publishing catalog: $publishScript" + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { & $publishScript } finally { $ErrorActionPreference = $prevEap } + if($LASTEXITCODE -and $LASTEXITCODE -ne 0){ Write-Err "Publish script exited with code $LASTEXITCODE."; return $false } + return $true +} + +# --------------------------------------------------------------------------- +# Report +# --------------------------------------------------------------------------- +function Write-Report { + param([string]$Stamp, [string]$Phase, $Payload, [string]$MarkdownBody) + $reportsDir = Join-Path $scriptRoot 'reports' + if(-not (Test-Path $reportsDir)){ New-Item -ItemType Directory -Path $reportsDir -Force | Out-Null } + $base = "ax-catalog-$Phase-$Stamp" + $mdPath = Join-Path $reportsDir "$base.md" + $jsonPath = Join-Path $reportsDir "$base.json" + Write-Utf8NoBom-LF -Path $jsonPath -Content ($Payload | ConvertTo-Json -Depth 8) + Write-Utf8NoBom-LF -Path $mdPath -Content $MarkdownBody + Write-Info "Report: $mdPath" + return $mdPath +} + +function Format-DiffTable { + # Builds a markdown table of @ax old->new from two ordered maps. + param($OldMap, $NewMap) + $sb = New-Object System.Text.StringBuilder + [void]$sb.AppendLine('| Package | From | To |') + [void]$sb.AppendLine('|---|---|---|') + $keys = @($OldMap.Keys) + @($NewMap.Keys) | Select-Object -Unique | Sort-Object + foreach($k in $keys){ + $from = if($OldMap.Contains($k)){ $OldMap[$k] } else { '(new)' } + $to = if($NewMap.Contains($k)){ $NewMap[$k] } else { '(removed)' } + if($from -ne $to){ [void]$sb.AppendLine("| $k | $from | $to |") } + } + return $sb.ToString() +} + +# =========================================================================== +# PHASE 1 - refresh catalog +# =========================================================================== +function Invoke-Phase1 { + param([string]$Stamp) + Write-Info '== PHASE 1: refresh @inxton/ax.catalog ==' + if(-not (Test-CommandAvailable 'apax')){ Write-Err 'apax CLI not found on PATH.'; exit 2 } + if(-not (Test-Path -LiteralPath $catalogYml)){ Write-Err "Catalog not found: $catalogYml"; exit 2 } + + $origLines = Get-Content -LiteralPath $catalogYml + $oldMap = Get-AxDepsInSection -Lines $origLines -Section 'catalogDependencies' + $oldVersion = Get-CatalogVersion -Lines $origLines + if(-not $oldVersion){ Write-Err 'Could not read catalog version field.'; exit 2 } + Write-Info "Catalog at $oldVersion ($($oldMap.Count) @ax/* deps). Resolving latest versions..." + + # Resolve the latest version of every @ax/* dep by querying the AX registry directly with + # `apax list -p `. NOTE: `apax update` only touches dependencies/devDependencies "from the + # apax.yml" - it does NOT update a catalog's catalogDependencies (verified: it reports "No updates + # available" on this catalog), which is why we resolve each pin ourselves. This is a pure read; the + # file is rewritten below only when something actually changed. + $newMap = Resolve-LatestAxVersions -OldMap $oldMap + + # Diff summary (version-changed @ax/* deps; the set of keys is fixed by the catalog). Major bumps are + # flagged for review but still applied. + $changed = @() + foreach($k in $newMap.Keys){ + if($oldMap[$k] -ne $newMap[$k]){ $changed += [PSCustomObject]@{ Package=$k; From=$oldMap[$k]; To=$newMap[$k]; IsMajor=(Test-IsMajorBump $oldMap[$k] $newMap[$k]) } } + } + $majors = @($changed | Where-Object { $_.IsMajor }) + + # Bump the catalog version ONLY when something changed - no dependency change means no new version + # to publish. On a real run we rewrite the catalogDependencies and the version in place; dry run + # reports what WOULD change and writes nothing. + $hasChanges = ($changed.Count -gt 0) + $newVersion = if($hasChanges){ if($CatalogVersion){ $CatalogVersion } else { Get-NextPatchVersion $oldVersion } } else { $oldVersion } + if($hasChanges -and -not $DryRun){ + Update-CatalogDepsFile -Path $catalogYml -NewMap $newMap + $afterLines = Get-Content -LiteralPath $catalogYml + $bumped = $afterLines | ForEach-Object { + if($_ -match $VersionRx){ "$($Matches[1])$($Matches[2])$newVersion$($Matches[4])$($Matches[5])".TrimEnd() } else { $_ } + } + Write-Utf8NoBom-LF -Path $catalogYml -Content (($bumped -join "`n") + "`n") + Write-Info "Updated $($changed.Count) @ax/* dep(s); bumped catalog version $oldVersion -> $newVersion." + } elseif(-not $hasChanges){ + Write-Info 'No @ax/* dependency changes - catalog left unchanged (version NOT bumped).' + } + + Write-Host '' + Write-Host '================ CATALOG PLAN ================' -ForegroundColor Cyan + Write-Host (" @ax deps changed : {0} / {1}" -f $changed.Count, $newMap.Count) + Write-Host (" Major bumps : {0}" -f $majors.Count) + if($hasChanges){ Write-Host (" Catalog version : {0} -> {1}" -f $oldVersion, $newVersion) } + else { Write-Host (" Catalog version : {0} (unchanged - no dep updates)" -f $oldVersion) } + Write-Host '=============================================' -ForegroundColor Cyan + foreach($c in $changed){ Write-Info (" {0}: {1} -> {2}{3}" -f $c.Package, $c.From, $c.To, $(if($c.IsMajor){' [MAJOR]'}else{''})) } + if($majors.Count -gt 0){ Write-Warn "$($majors.Count) MAJOR-version bump(s) included - review before publishing." } + + # Sync consumers in the SAME phase - editing files needs no published catalog (only the cake verify + # build in Phase 2 does), so the catalog + all consumer edits land as one reviewable diff before + # publish. We pass the freshly-resolved map/version so dry-run preview is accurate even though the + # catalog file isn't written yet. Skipped entirely when nothing changed. + $sync = $null + if($hasChanges){ + Write-Host '' + $sync = Invoke-ConsumerSync -CatMap $newMap -CatVersion $newVersion + } + + # Report. + $md = New-Object System.Text.StringBuilder + [void]$md.AppendLine('# @ax catalog update - Phase 1 (refresh + sync)') + [void]$md.AppendLine('') + [void]$md.AppendLine("- Generated: $Stamp") + [void]$md.AppendLine("- Dry run: $([bool]$DryRun)") + [void]$md.AppendLine("- Prerelease: $([bool]$Prerelease)") + [void]$md.AppendLine("- Catalog version: $oldVersion -> $newVersion") + [void]$md.AppendLine("- @ax deps changed: $($changed.Count) of $($newMap.Count)") + [void]$md.AppendLine("- Major-version bumps: $($majors.Count)") + [void]$md.AppendLine("- Consumer edits: $(if($sync){$sync.TotalEdits}else{0})") + [void]$md.AppendLine('') + if($majors.Count -gt 0){ + [void]$md.AppendLine("## [!] Major-version bumps ($($majors.Count)) - review carefully") + [void]$md.AppendLine('| Package | From | To |') + [void]$md.AppendLine('|---|---|---|') + foreach($c in $majors){ [void]$md.AppendLine("| $($c.Package) | $($c.From) | $($c.To) |") } + [void]$md.AppendLine('') + } + [void]$md.AppendLine('## Changed @ax dependencies') + [void]$md.Append((Format-DiffTable -OldMap $oldMap -NewMap $newMap)) + if($sync -and $sync.TotalEdits -gt 0){ + [void]$md.AppendLine('') + [void]$md.AppendLine('## Consumer edits') + Add-ConsumerDiffMarkdown -Sb $md -Results $sync.Results + } + $payload = [PSCustomObject]@{ + phase='1-refresh-sync'; timestamp=$Stamp; dryRun=[bool]$DryRun; prerelease=[bool]$Prerelease + catalogVersionFrom=$oldVersion; catalogVersionTo=$newVersion; changed=$changed + consumerEdits=$(if($sync){$sync.TotalEdits}else{0}) + consumers=@(if($sync){ $sync.Results | ForEach-Object { [PSCustomObject]@{ file=(ConvertTo-RepoRelative $_.File); edits=$_.Edits } } }) + } + Write-Report -Stamp $Stamp -Phase 'phase1' -Payload $payload -MarkdownBody $md.ToString() | Out-Null + + Write-Host '' + if(-not $hasChanges){ + Write-Info 'All @ax/* dependencies already at their latest. Nothing to publish; no Phase 2 needed.' + return + } + if($DryRun){ + Write-Warn "[DRY RUN] $($changed.Count) @ax/* dep(s) would change; catalog would bump $oldVersion -> $newVersion; $($sync.TotalEdits) consumer edit(s)." + Write-Warn "[DRY RUN] Would then publish the catalog$(if($NoPublish){' (SKIPPED: -NoPublish)'}else{''}) and stop for build verification. Re-run without -DryRun to apply." + return + } + + # Real run with changes: publish the catalog automatically (per user request), unless -NoPublish. + if($NoPublish){ + Write-Info 'Catalog + consumers updated. Publish skipped (-NoPublish). NEXT STEPS:' + Write-Info " 1. Review the diff: git diff -- src/ax.catalog/apax.yml 'src/**/ctrl/apax.yml'" + Write-Info " 2. Publish the catalog: pwsh scripts/_pack_and_publish_catalog.ps1 (needs APAX_KEY, GH_USER, GH_TOKEN)" + Write-Info " 3. Verify the build: pwsh scripts/update-ax-catalog.ps1 -AfterPublish" + return + } + + Write-Host '' + if(Invoke-CatalogPublish){ + Write-Info "Catalog @inxton/ax.catalog $newVersion published." + Write-Info 'NEXT STEP - verify the build: pwsh scripts/update-ax-catalog.ps1 -AfterPublish' + } else { + Write-Err 'Automatic publish FAILED. The catalog + consumer edits are on disk but NOT published.' + Write-Err "Fix the cause (e.g. set APAX_KEY/GH_USER/GH_TOKEN), then run: pwsh scripts/_pack_and_publish_catalog.ps1" + Write-Err 'After a successful publish, run: pwsh scripts/update-ax-catalog.ps1 -AfterPublish' + exit 4 + } +} + +# =========================================================================== +# PHASE 2 - sync consumers + verify +# =========================================================================== + +# Rewrite one consumer file in place (or preview): sync @ax/* pins + catalog ref to $CatVersion/$CatMap. +# Returns a PSCustomObject with the file's applied edits. +function Update-ConsumerFile { + param([string]$Path, $CatMap, [string]$CatVersion) + $lines = Get-Content -LiteralPath $Path + $edits = New-Object System.Collections.ArrayList + $cur = $null + $out = for($i=0; $i -lt $lines.Count; $i++){ + $ln = $lines[$i] + if($ln -match '^[A-Za-z]'){ + if($ln -match '^([A-Za-z][\w\.]*)\s*:'){ $cur = $Matches[1] } + $ln; continue + } + if($cur -eq 'dependencies' -and $ln -match $AxDepRx){ + $pkg = $Matches[2]; $oldv = $Matches[5] + if($CatMap.Contains($pkg) -and $CatMap[$pkg] -ne $oldv){ + [void]$edits.Add([PSCustomObject]@{ Package=$pkg; From=$oldv; To=$CatMap[$pkg] }) + "$($Matches[1])$pkg$($Matches[3])$($Matches[4])$($CatMap[$pkg])$($Matches[6])$($Matches[7])".TrimEnd(); continue + } + $ln; continue + } + if($cur -eq 'catalogs' -and $ln -match $CatalogRefRx){ + $oldv = $Matches[5] + if($oldv -ne $CatVersion){ + [void]$edits.Add([PSCustomObject]@{ Package=$catalogPkg; From=$oldv; To=$CatVersion }) + "$($Matches[1])$($Matches[2])$($Matches[3])$($Matches[4])$CatVersion$($Matches[6])$($Matches[7])".TrimEnd(); continue + } + $ln; continue + } + $ln + } + if($edits.Count -gt 0 -and -not $DryRun){ + Write-Utf8NoBom-LF -Path $Path -Content (($out -join "`n") + "`n") + } + return [PSCustomObject]@{ File=$Path; Edits=@($edits) } +} + +# Discover catalog consumers and sync each one's @ax/* pins + `catalogs:` ref to the catalog. Applies +# edits in place (preview only when -DryRun). $CatMap/$CatVersion may be supplied explicitly (Phase 1 +# passes the freshly-resolved values so dry-run preview is accurate even though the catalog file is not +# written yet); when omitted they are read from the catalog file (Phase 2, post-publish). Returns the +# discovered consumers, per-file edit results, and the totals. +function Invoke-ConsumerSync { + param($CatMap, [string]$CatVersion) + if(-not $CatMap -or -not $CatVersion){ + $catLines = Get-Content -LiteralPath $catalogYml + if(-not $CatMap){ $CatMap = Get-AxDepsInSection -Lines $catLines -Section 'catalogDependencies' } + if(-not $CatVersion){ $CatVersion = Get-CatalogVersion -Lines $catLines } + } + if(-not $CatVersion){ Write-Err 'Could not determine catalog version for consumer sync.'; exit 2 } + + $consumerYmls = @(Get-CatalogConsumers) + if($consumerYmls.Count -eq 0){ Write-Err 'No catalog consumers discovered (no apax.yml references @inxton/ax.catalog).'; exit 2 } + Write-Info "Syncing $($consumerYmls.Count) consumer(s) to catalog @ $CatVersion ($($CatMap.Count) @ax/* deps):" + $results = foreach($c in $consumerYmls){ + $r = Update-ConsumerFile -Path $c -CatMap $CatMap -CatVersion $CatVersion + $rel = ConvertTo-RepoRelative $c + if($r.Edits.Count -gt 0){ + Write-Info " $rel : $($r.Edits.Count) edit(s)" + foreach($e in $r.Edits){ if($Detailed){ Write-Info (" {0}: {1} -> {2}" -f $e.Package,$e.From,$e.To) } } + } elseif($Detailed){ Write-Info " $rel : already in sync" } + $r + } + $totalEdits = (@($results) | ForEach-Object { $_.Edits.Count } | Measure-Object -Sum).Sum + return [PSCustomObject]@{ Consumers=$consumerYmls; Results=@($results); CatVersion=$CatVersion; TotalEdits=$totalEdits } +} + +# Append a per-consumer edit table section to a markdown StringBuilder. +function Add-ConsumerDiffMarkdown { + param([System.Text.StringBuilder]$Sb, $Results) + foreach($r in @($Results)){ + if($r.Edits.Count -eq 0){ continue } + [void]$Sb.AppendLine("### $(ConvertTo-RepoRelative $r.File) ($($r.Edits.Count))") + [void]$Sb.AppendLine('| Package | From | To |') + [void]$Sb.AppendLine('|---|---|---|') + foreach($e in $r.Edits){ [void]$Sb.AppendLine("| $($e.Package) | $($e.From) | $($e.To) |") } + [void]$Sb.AppendLine('') + } +} + +function Invoke-Phase2 { + param([string]$Stamp) + Write-Info '== PHASE 2: verify (consumers synced in Phase 1) ==' + if(-not (Test-Path -LiteralPath $catalogYml)){ Write-Err "Catalog not found: $catalogYml"; exit 2 } + + # Re-assert consumer sync as an idempotent guard: Phase 1 already synced them, but the catalog may + # have moved (e.g. a re-publish) or a new consumer may have appeared. Reads map/version from the + # (now-published) catalog file. Normally a no-op. + $sync = Invoke-ConsumerSync + if($sync.TotalEdits -gt 0){ Write-Warn "$($sync.TotalEdits) consumer edit(s) applied in Phase 2 (Phase 1 sync was incomplete or stale)." } + else { Write-Info 'Consumers already in sync with the catalog.' } + + $buildResult = if($DryRun){ 'not-run (dry run)' } else { Invoke-VerifyAndDiscardBuildNoise -Consumers $sync.Consumers } + + # Report. + $md = New-Object System.Text.StringBuilder + [void]$md.AppendLine('# @ax catalog update - Phase 2 (verify)') + [void]$md.AppendLine('') + [void]$md.AppendLine("- Generated: $Stamp") + [void]$md.AppendLine("- Dry run: $([bool]$DryRun)") + [void]$md.AppendLine("- Catalog version: $($sync.CatVersion)") + [void]$md.AppendLine("- Consumer edits (this phase): $($sync.TotalEdits)") + [void]$md.AppendLine("- Build result: **$buildResult**") + [void]$md.AppendLine('') + if($sync.TotalEdits -gt 0){ + [void]$md.AppendLine('## Consumer edits (Phase 2 guard)') + Add-ConsumerDiffMarkdown -Sb $md -Results $sync.Results + } + $payload = [PSCustomObject]@{ + phase='2-verify'; timestamp=$Stamp; dryRun=[bool]$DryRun; catalogVersion=$sync.CatVersion + build=$buildResult; consumerEditsThisPhase=$sync.TotalEdits + consumers=@($sync.Results | ForEach-Object { [PSCustomObject]@{ file=(ConvertTo-RepoRelative $_.File); edits=$_.Edits } }) + } + Write-Report -Stamp $Stamp -Phase 'phase2' -Payload $payload -MarkdownBody $md.ToString() | Out-Null + + Write-Host '' + Write-Host '================ PHASE 2 SUMMARY ================' -ForegroundColor Cyan + Write-Host (" Consumer edits : {0}" -f $sync.TotalEdits) + Write-Host (" Build : {0}" -f $buildResult) + Write-Host '=================================================' -ForegroundColor Cyan + + if($DryRun){ Write-Warn '[DRY RUN] Nothing written, no build run.'; return } + if($buildResult -like 'FAIL*'){ Write-Err 'Build verification FAILED - edits left staged for inspection.'; exit 1 } +} + +# Pre-build commit our files, run cake, then surgically discard build-produced changes and un-commit +# so our edits remain staged. Returns a build-result string. Never touches files that were already +# dirty before this run, and never runs `git clean`. +function Invoke-VerifyAndDiscardBuildNoise { + param([string[]]$Consumers) + if(-not (Test-CommandAvailable 'git')){ Write-Err 'git not found; cannot safely verify.'; return 'FAIL (git unavailable)' } + if(-not (Test-CommandAvailable 'dotnet')){ Write-Err 'dotnet not found; cannot build.'; return 'FAIL (dotnet unavailable)' } + + # Refuse to run mid-merge / detached HEAD. + $headRef = (& git -C $repoRoot symbolic-ref --quiet HEAD 2>$null) + if($LASTEXITCODE -ne 0){ Write-Err 'Detached HEAD or no branch; aborting Phase 2 git steps.'; return 'FAIL (detached HEAD)' } + if(Test-Path (Join-Path $repoRoot '.git/MERGE_HEAD')){ Write-Err 'Merge in progress; aborting.'; return 'FAIL (merge in progress)' } + + # Files that were already dirty BEFORE this run (excluding our own allowlist) must never be restored. + # Our edits are the source manifests only; ctrl/apax.yml and apax-lock.json are gitignored generated + # output and never appear in git status, so they need no special handling. + $allowlist = New-Object System.Collections.Generic.HashSet[string] ([StringComparer]::OrdinalIgnoreCase) + foreach($f in (@($catalogYml) + $Consumers)){ [void]$allowlist.Add((ConvertTo-RepoRelative $f)) } + + $preExistingDirty = New-Object System.Collections.Generic.HashSet[string] ([StringComparer]::OrdinalIgnoreCase) + foreach($line in (& git -C $repoRoot status --porcelain)){ + if($line.Length -lt 4){ continue } + $p = $line.Substring(3).Trim('"') + if($p -match ' -> '){ $p = ($p -split ' -> ')[-1].Trim('"') } # rename: keep destination + if(-not $allowlist.Contains($p)){ [void]$preExistingDirty.Add($p) } + } + if($Detailed -and $preExistingDirty.Count -gt 0){ + Write-Info "Protecting $($preExistingDirty.Count) pre-existing dirty path(s) from discard:" + foreach($p in $preExistingDirty){ Write-Info " $p" } + } + + # Pre-build commit of ONLY our files. + $addPaths = @($catalogYml) + $Consumers | ForEach-Object { ConvertTo-RepoRelative $_ } + & git -C $repoRoot add -- $addPaths + & git -C $repoRoot diff --cached --quiet + $committed = ($LASTEXITCODE -ne 0) # non-zero => there ARE staged changes + if($committed){ + & git -C $repoRoot commit -q -m @" +chore(catalog): update @ax deps + bump ax.catalog + +Automated by scripts/update-ax-catalog.ps1. + +Co-Authored-By: Claude Opus 4.8 (1M context) +"@ + if($LASTEXITCODE -ne 0){ Write-Err 'Pre-build commit failed.'; return 'FAIL (pre-build commit)' } + Write-Info 'Committed catalog + consumer edits (temporary; un-committed after verification).' + } else { + Write-Warn 'No catalog/consumer changes to commit; running build verification anyway.' + } + + # Verify with cake TESTS at level 2 (NOT pack). Level-2 runs apax install --catalog --strict + + # build per consumer and the L2 .NET test suite, which is what we want to prove the new catalog + # resolves and builds. We deliberately do NOT pack (--do-pack), so cake also won't stamp GitVersion + # SemVer into the manifests - the only remaining noise is apax install churn, handled below. + Write-Info 'Verifying: dotnet run --project cake/Build.csproj --do-test --test-level 2 -n' + & dotnet run --project $cakeProj -- --do-test --test-level 2 -n + $buildOk = ($LASTEXITCODE -eq 0) + + # Discard build/test-produced changes to TRACKED files (everything dirty vs HEAD that we did not + # already flag as pre-existing) - e.g. apax install churn during the test run. We never `git clean` + # (untracked files - incl. yours - are left alone). + $toRestore = New-Object System.Collections.ArrayList + foreach($p in (& git -C $repoRoot diff --name-only HEAD)){ + $pp = $p.Trim('"') + if(-not $preExistingDirty.Contains($pp)){ [void]$toRestore.Add($pp) } + } + if($toRestore.Count -gt 0){ + Write-Info "Discarding $($toRestore.Count) build-produced change(s) to tracked files." + & git -C $repoRoot checkout -- $toRestore + } + $newUntracked = @(& git -C $repoRoot ls-files --others --exclude-standard) | Where-Object { -not $preExistingDirty.Contains($_.Trim('"')) } + if($newUntracked.Count -gt 0){ + Write-Warn "$($newUntracked.Count) new untracked file(s) left by the build (NOT removed - clean manually if unwanted):" + foreach($u in ($newUntracked | Select-Object -First 20)){ Write-Warn " $u" } + } + + # Un-commit so our edits remain STAGED (build noise already discarded). + if($committed){ + & git -C $repoRoot reset --soft HEAD~1 + if($LASTEXITCODE -ne 0){ Write-Warn 'Could not soft-reset; the pre-build commit remains in history.' } + else { Write-Info 'Un-committed: catalog + consumer edits are now staged.' } + } + + if($buildOk){ return 'pass (test-level 2)' } else { return 'FAIL (cake test-level 2 - edits left staged)' } +} + +# =========================================================================== +# Main +# =========================================================================== +$stamp = (Get-Date).ToString('yyyy-MM-dd-HHmmss') +Write-Info "update-ax-catalog (afterPublish=$AfterPublish, dryRun=$DryRun, prerelease=$Prerelease)" +if($DryRun){ Write-Warn 'DRY RUN - no files, no git, no build.' } + +if($AfterPublish){ Invoke-Phase2 -Stamp $stamp } else { Invoke-Phase1 -Stamp $stamp } +exit 0 From 2d820d1366471285eb81d0ca67c775999d9127eb Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sat, 30 May 2026 11:30:38 +0200 Subject: [PATCH 13/23] feat: enhance testing capabilities with level 4 integration and cleanup of unused workflows --- .github/workflows/nightly.yml | 2 +- .github/workflows/single_app_run.yml | 127 ----------- cake/AppsRunTaskHelpers.cs | 324 +++++++-------------------- cake/BuildParameters.cs | 8 +- cake/DotNetCmd.cs | 128 +++++++++++ cake/Program.cs | 137 +---------- cake/Properties/launchSettings.json | 122 +--------- scripts/test_L4.ps1 | 4 + 8 files changed, 226 insertions(+), 626 deletions(-) delete mode 100644 .github/workflows/single_app_run.yml create mode 100644 scripts/test_L4.ps1 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index cee3d166c..6cd271afe 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -91,7 +91,7 @@ jobs: GH_TOKEN : ${{ secrets.GH_TOKEN }} GH_USER : ${{ secrets.GH_USER }} run: | - dotnet run --project cake/Build.csproj --apps-run + dotnet run --project cake/Build.csproj --do-test --test-level 4 "TEST_EXIT_CODE=$LASTEXITCODE" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 shell: pwsh diff --git a/.github/workflows/single_app_run.yml b/.github/workflows/single_app_run.yml deleted file mode 100644 index dd2d2fb88..000000000 --- a/.github/workflows/single_app_run.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: single_app_run -permissions: - contents: read -on: - workflow_dispatch: - inputs: - folder: - description: "Select library folder" - required: true - default: "template.axolibrary" - type: choice - options: - - abstractions - - components.abb.robotics - - components.abstractions - - components.balluff.identification - - components.cognex.vision - - components.desoutter.tightening - - components.drives - - components.elements - - components.festo.drives - - components.kuka.robotics - - components.keyence.vision - - components.mitsubishi.robotics - - components.pneumatics - - components.rexroth.drives - - components.rexroth.press - - components.robotics - - components.siem.identification - - components.ur.robotics - - core - - data - - inspectors - - integrations - - io - - probers - - simatic1500 - - template.axolibrary - - timers - - utils - -jobs: - app-run-manual-with-selected-folder: - runs-on: [self-hosted, Windows, X64, L3, AX, APP] - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: '0' - - - name: Get Branch Name - run: | - if ($env:GITHUB_REF -like "refs/pull/*") { - $BRANCH_NAME = "${{ github.event.pull_request.head.ref }}" - } else { - $BRANCH_NAME = $env:GITHUB_REF -replace 'refs/heads/', '' - } - Write-Host "Triggered on branch: $BRANCH_NAME" - "BRANCH_NAME=$BRANCH_NAME" | Out-File -FilePath $env:GITHUB_ENV -Append - shell: pwsh - - - name: Get Short Commit SHA - run: | - $currentSHA = git rev-parse --short HEAD - "CURRENT_SHA=$currentSHA" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - shell: pwsh - - - name: Setup - run: | - "TEST_EXIT_CODE=123" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - shell: pwsh - - - name: Print and store library folder - run: | - $APP_FOLDER = "${{ inputs.folder }}" - Write-Host "Selected folder: $APP_FOLDER" - "APP_FOLDER=$APP_FOLDER" | Out-File -FilePath $env:GITHUB_ENV -Append - shell: pwsh - - - name: "Build cake" - run: dotnet build cake/Build.csproj - shell: pwsh - - - name: "Run cake with single app" - continue-on-error: true - env: - GH_TOKEN : ${{ secrets.GH_TOKEN }} - GH_USER : ${{ secrets.GH_USER }} - run: | - dotnet run --project cake/Build.csproj --skip-build --apps-run --single-app-run-folder-name $env:APP_FOLDER - "TEST_EXIT_CODE=$LASTEXITCODE" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - shell: pwsh - - - name: Display workflow details in Summary - run: | - if($env:TEST_EXIT_CODE -eq "123") - { - $TEST_RESULT = "SKIPPED" - } - elseif($env:TEST_EXIT_CODE -eq "0") - { - $TEST_RESULT = "PASSED" - } - else - { - $TEST_RESULT = "FAILED" - } - "### Runner name: $env:RUNNER_NAME ::: Branch name: $env:BRANCH_NAME ::: Library folder: $env:APP_FOLDER ::: Commit SHA: $env:CURRENT_SHA ::: Test result: $TEST_RESULT" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - shell: pwsh - - - name: Evaluate final result - run: | - if($env:TEST_EXIT_CODE -eq "123") - { - echo "Tests skipped." - } - elseif($env:TEST_EXIT_CODE -eq "0") - { - echo "Tests passed." - } - else - { - echo "Tests failed." - Exit 1 - } - shell: pwsh - diff --git a/cake/AppsRunTaskHelpers.cs b/cake/AppsRunTaskHelpers.cs index c1a9bd7cd..cb9a0ae81 100644 --- a/cake/AppsRunTaskHelpers.cs +++ b/cake/AppsRunTaskHelpers.cs @@ -1,4 +1,4 @@ -using Cake.Common.Diagnostics; +using Cake.Common.Diagnostics; using Cake.Common.IO; using Cake.Core.Diagnostics; using System; @@ -17,276 +17,110 @@ internal static class AppsRunTaskHelpers { - - public static void BuildAndLoadPlc(BuildContext context, string appYamlFile, string appName, string logFilePath, ref bool summaryResult) + /// + /// Test level 4 integration step: loads the entire showcase PLC (incl. hardware configuration) onto + /// PLCSIM Advanced, starts the Blazor server and asserts it serves traffic over HTTPS. + /// Fails the build (non-zero exit) on any error and always cleans up the spawned processes. + /// + public static void RunShowcaseIntegration(BuildContext context, string appYamlFile) { - string plcName = context.PlcName; - string plcIpAddress = context.PlcIpAddress; - - // Validate application YAML file - if (string.IsNullOrWhiteSpace(appYamlFile)) - { - context.Log.Error("The provided YAML of the application is empty."); - return; - } - - if (!File.Exists(appYamlFile)) - { - context.Log.Error($"The provided application file does not exist: {appYamlFile}"); - return; - } - - if (string.IsNullOrWhiteSpace(appName)) - { - context.Log.Error("The provided application name is empty."); - return; - } - - string appFolder = Path.GetDirectoryName(appYamlFile); - if (string.IsNullOrWhiteSpace(appFolder) || !Directory.Exists(appFolder)) - { - context.Log.Error($"The provided path for the application does not exist: {appFolder}"); - return; - } - - // Run "apax install" - ApaxCmd.ApaxInstall(context, appFolder); - - // Run "apax plcsim" - string plcSimResult = ApaxCmd.ApaxPlcSim(context, appFolder, ref summaryResult); - WriteResult(context, plcSimResult, logFilePath, appendToSameLine: true); - - // Run "apax hwu" - string hwuResult = ApaxCmd.ApaxHwu(context, appFolder, ref summaryResult); - WriteResult(context, hwuResult, logFilePath, appendToSameLine: true); - - // Run "apax swfd" - string swfdResult = ApaxCmd.ApaxSwfd(context, appFolder, ref summaryResult); - WriteResult(context, swfdResult, logFilePath, appendToSameLine: true); - } - public static void AppRunDetailed(BuildContext context, string appYamlFile, string appName, string logFilePath, ref bool summaryResult) - { - string plcName = context.PlcName; - string plcIpAddress = context.PlcIpAddress; - - // Validate application YAML file - if (string.IsNullOrWhiteSpace(appYamlFile)) - { - context.Log.Error("The provided YAML of the application is empty."); - return; - } + bool summaryResult = true; - if (!File.Exists(appYamlFile)) + if (string.IsNullOrWhiteSpace(appYamlFile) || !File.Exists(appYamlFile)) { - context.Log.Error($"The provided application file does not exist: {appYamlFile}"); + context.Log.Error($"Showcase application file does not exist: {appYamlFile}"); + Environment.Exit(1); return; } - if (string.IsNullOrWhiteSpace(appName)) - { - context.Log.Error("The provided application name is empty."); - return; - } - - string appFolder = Path.GetDirectoryName(appYamlFile); - if (string.IsNullOrWhiteSpace(appFolder) || !Directory.Exists(appFolder)) - { - context.Log.Error($"The provided path for the application does not exist: {appFolder}"); - return; - } - - // Run "apax install" - string result = ApaxCmd.ApaxCommand(context, appFolder, "install", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax plcsim" - result = ApaxCmd.ApaxPlcSim(context, appFolder, ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax gsd" # copy and install all gsdml files from libraries - result = ApaxCmd.ApaxCommand(context, appFolder, "gsd", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax hwl" # copy all templates from libraries - result = ApaxCmd.ApaxCommand(context, appFolder, "hwl", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax hwcc" # compile hardware configuration - result = ApaxCmd.ApaxCommand(context, appFolder, "hwcc", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax hwid" # copy the generated HwIds from global constants into the type definition, matching the format as the TIA2AX tool creates - result = ApaxCmd.ApaxCommand(context, appFolder, "hwid", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax hwadr" # copy the generated IoAddresses - result = ApaxCmd.ApaxCommand(context, appFolder, "hwadr", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax hwdo" # download HW only using certificate - result = ApaxCmd.ApaxCommand(context, appFolder, "hwdo", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax build " - result = ApaxCmd.ApaxCommand(context, appFolder, "build", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "dotnet ixc" - result = DotNetCmd.DotNetIxc(context, appFolder, ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - // Run "apax swfdo" # software full download only - result = ApaxCmd.ApaxCommand(context, appFolder, "swfdo", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - + string appFolder = Path.GetFullPath(Path.GetDirectoryName(appYamlFile)); + string appName = context.GetApplicationName(appYamlFile); - // Recreate solution file by running the slngen script - string slnGenPath = Path.GetFullPath(Path.GetFullPath(Path.Combine(appFolder, "..", "./slngen.ps1"))); - result = DotNetCmd.RunPowershellScript(context, slnGenPath, "", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); + context.Log.Information("###################################################"); + context.Log.Information($"Test level 4 showcase integration: {appName}"); + context.Log.Information($"Application file: {appYamlFile}"); + context.Log.Information("###################################################"); - // Clean solution - string solutionFile = Path.GetFullPath(Path.Combine(appFolder, "../this.sln")); - result = DotNetCmd.DotNetClean(context, solutionFile, "-c Debug", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); + // Make sure no stale simulator UI is running before we start. + KillProcess(context, "Siemens.Simatic.PlcSim.Advanced.UserInterface"); - //########################## template.axolibrary => ######################// - if (appFolder.Contains("template.axolibrary")) + try { - string dot_g_folder = Path.GetFullPath(Path.Combine(appFolder, "ix//.g")); - context.CleanDirectory(dot_g_folder, new CleanDirectorySettings() { Force = true }); - - string dot_meta_folder = Path.GetFullPath(Path.Combine(appFolder, "ix//.meta")); - context.CleanDirectory(dot_meta_folder, new CleanDirectorySettings() { Force = true }); - - // Run "dotnet ixc" - result = DotNetCmd.DotNetIxc(context, appFolder, ref summaryResult); - } - - //########################## <= template.axolibrary ######################// + // Clean state from previous runs. + DeleteJsonReposFolder(context, appFolder); - //// Build solution - //result = DotNetCmd.DotNetBuildWithResult(context, solutionFile, "-c Debug", ref summaryResult); - //WriteResult(context, result, logFilePath, appendToSameLine: true); + // Prepare a fresh PLCSIM Advanced virtual memory card instance. + InitializePlcSimInstance(context, appName); - //// Get blazor projects - //var blazorFiles = Directory.GetFiles(appFolder, "*.csproj", SearchOption.AllDirectories).Where(file => file.Contains("blazor")).ToList(); + // Provide the certificate / security configuration used by the secure download. + OverwriteSecurityFiles(context, appYamlFile, context.PlcName); - // Get blazor projects - var blazorFiles = Directory.GetFiles(appFolder, "*.csproj", SearchOption.AllDirectories).Where(file => file.Contains("blazor")).ToList(); + // Full first-download of hardware + software onto the simulator. + LoadShowcasePlc(context, appFolder, ref summaryResult); - if (blazorFiles.Any()) - { - foreach (var blazorFile in blazorFiles) + if (!summaryResult) { - // Build solution - result = DotNetCmd.DotNetBuildWithResult(context, blazorFile, "-c Debug", ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); - - context.Log.Information($"Application 'blazor' file: {blazorFile}"); + context.Log.Error("Showcase PLC load failed."); + } + else + { + // Build and run the Blazor server, then probe it over HTTPS. + var blazorFile = Directory + .GetFiles(appFolder, "*.csproj", SearchOption.AllDirectories) + .FirstOrDefault(file => file.Contains("blazor") && !File.ReadAllText(file).Contains("")); - // Filter out libraries by checking for in the project file - string csprojContent = File.ReadAllText(blazorFile); - if (!csprojContent.Contains("")) + if (string.IsNullOrEmpty(blazorFile)) { - result = DotNetCmd.DotNetRunWithResult(context, blazorFile, "-c Debug --framework net9.0", 60, ref summaryResult); - WriteResult(context, result, logFilePath, appendToSameLine: true); + context.Log.Error("No runnable Blazor project (*blazor*.csproj without ) was found."); + summaryResult = false; + } + else + { + DotNetCmd.DotNetBuildWithResult(context, blazorFile, "-c Debug", ref summaryResult); + + DotNetCmd.DotNetRunWithHealthCheck( + context, + blazorFile, + "-c Debug --launch-profile https", + "https://localhost:7290", + 120, + ref summaryResult); } } } - else - { - context.Log.Information("No files containing 'blazor' in the filename and ending with '.csproj' were found."); - } - } - - public static void BuildAndStartHmi(BuildContext context, string appYamlFile, string appName, string logFilePath, ref bool summaryResult) - { - // Validate application YAML file - if (string.IsNullOrWhiteSpace(appYamlFile)) - { - context.Log.Error("The provided YAML of the application is empty."); - return; - } - - if (!context.FileExists(appYamlFile)) - { - context.Log.Error($"The provided application file does not exist: {appYamlFile}"); - return; - } - - if (string.IsNullOrWhiteSpace(appName)) - { - context.Log.Error("The provided application name is empty."); - return; - } - - string appFolder = Path.GetFullPath(Path.GetDirectoryName(appYamlFile)); - if (string.IsNullOrWhiteSpace(appFolder) || !context.DirectoryExists(appFolder)) + finally { - context.Log.Error($"The provided path for the application does not exist: {appFolder}"); - return; + // Always tear down the Blazor server and the simulator, even on success. + KillProcess(context, "dotnet"); + KillProcess(context, "Siemens.Simatic.PlcSim.Advanced.UserInterface"); } - // Recreate solution file by running the slngen script - string slnGenPath = Path.GetFullPath(Path.GetFullPath(Path.Combine(appFolder, "..", "./slngen.ps1"))); - DotNetCmd.RunPowershellScript(context, slnGenPath, ""); - - // Clean solution - string solutionFile = Path.GetFullPath(Path.Combine(appFolder, "../this.sln")); - DotNetCmd.DotNetClean(context, solutionFile, "-c Debug"); - - - //########################## template.axolibrary => ######################// - if (appFolder.Contains("template.axolibrary")) + if (!summaryResult) { - string dot_g_folder = Path.GetFullPath(Path.Combine(appFolder, "ix//.g")); - context.CleanDirectory(dot_g_folder, new CleanDirectorySettings() { Force = true }); - - string dot_meta_folder = Path.GetFullPath(Path.Combine(appFolder, "ix//.meta")); - context.CleanDirectory(dot_meta_folder, new CleanDirectorySettings() { Force = true }); - - // Run "dotnet ixc" - DotNetCmd.DotNetIxc(context, appFolder, ref summaryResult); + context.Log.Error("Showcase integration (test level 4) failed."); + Environment.Exit(1); } - //########################## <= template.axolibrary ######################// - - //// Build solution - //string buildResult = DotNetCmd.DotNetBuildWithResult(context, solutionFile, "-c Debug", ref summaryResult); - //WriteResult(context, buildResult, logFilePath, appendToSameLine: true); - - //// Get blazor projects - //var blazorFiles = Directory.GetFiles(appFolder, "*.csproj", SearchOption.AllDirectories).Where(file => file.Contains("blazor")).ToList(); - - // Get blazor projects - var blazorFiles = Directory.GetFiles(appFolder, "*.csproj", SearchOption.AllDirectories).Where(file => file.Contains("blazor")).ToList(); - - - - if (blazorFiles.Any()) - { - foreach (var blazorFile in blazorFiles) - { - // Build solution - string buildResult = DotNetCmd.DotNetBuildWithResult(context, blazorFile, "-c Debug", ref summaryResult); - WriteResult(context, buildResult, logFilePath, appendToSameLine: true); - - context.Log.Information($"Application 'blazor' file: {blazorFile}"); + context.Log.Information("Showcase integration (test level 4) done."); + } - // Filter out libraries by checking for in the project file - string csprojContent = File.ReadAllText(blazorFile); - if (!csprojContent.Contains("")) - { - string runResult = DotNetCmd.DotNetRunWithResult(context, blazorFile, "-c Debug --framework net9.0", 60, ref summaryResult); - WriteResult(context, runResult, logFilePath, appendToSameLine: true); - } - } - } - else - { - context.Log.Information("No files containing 'blazor' in the filename and ending with '.csproj' were found."); - } + /// + /// Performs the full first-download apax sequence for the showcase application onto PLCSIM Advanced: + /// install -> plcsim -> gsd -> hwl -> hwcc -> hwid -> hwadr -> hwdo -> build -> dotnet ixc -> swfdo. + /// + public static void LoadShowcasePlc(BuildContext context, string appFolder, ref bool summaryResult) + { + ApaxCmd.ApaxCommand(context, appFolder, "install", ref summaryResult); // install dependencies + ApaxCmd.ApaxPlcSim(context, appFolder, ref summaryResult); // start PLCSIM Advanced instance + ApaxCmd.ApaxCommand(context, appFolder, "gsd", ref summaryResult); // copy & install GSDML files + ApaxCmd.ApaxCommand(context, appFolder, "hwl", ref summaryResult); // copy hardware templates + ApaxCmd.ApaxCommand(context, appFolder, "hwcc", ref summaryResult); // compile hardware configuration + ApaxCmd.ApaxCommand(context, appFolder, "hwid", ref summaryResult); // copy generated HwIds + ApaxCmd.ApaxCommand(context, appFolder, "hwadr", ref summaryResult); // copy generated IO addresses + ApaxCmd.ApaxCommand(context, appFolder, "hwdo", ref summaryResult); // download hardware only + ApaxCmd.ApaxCommand(context, appFolder, "build", ref summaryResult); // compile SIMATIC AX code + DotNetCmd.DotNetIxc(context, appFolder, ref summaryResult); // generate IXC twin controller + ApaxCmd.ApaxCommand(context, appFolder, "swfdo", ref summaryResult); // software full download only } public static (bool Success, string FilePath) CreateLogFile(BuildContext context, string fileNamePrefix = "app_test_result") @@ -646,4 +480,4 @@ public static void WriteResult(BuildContext context, string textToWrite, string context.Log.Error($"An error occurred while writing to the file: {ex.Message}"); } } -} \ No newline at end of file +} diff --git a/cake/BuildParameters.cs b/cake/BuildParameters.cs index ad7f1b675..250de1689 100644 --- a/cake/BuildParameters.cs +++ b/cake/BuildParameters.cs @@ -28,7 +28,7 @@ public class BuildParameters [Option('v', "verbosity", Required = false, Default = DotNetVerbosity.Minimal, HelpText = "Verbosity (default Quiet)")] public DotNetVerbosity Verbosity { get; set; } - [Option('l', "test-level", Required = false, Default = 1, HelpText = "Test level 1 - 3")] + [Option('l', "test-level", Required = false, Default = 1, HelpText = "Test level 1 - 4")] public int TestLevel { get; set; } [Option('r', "do-publish-release", Required = false, Default = false, HelpText = "Publishes release on GH")] @@ -49,12 +49,6 @@ public class BuildParameters [Option('b', "skip-build", Required = false, Default = false, HelpText = "Does not run build steps")] public bool NoBuild { get; set; } - [Option('a', "apps-run", Required = false, Default = false, HelpText = "Download to PLC and run apps ")] - public bool AppsRun{ get; set; } - [Option('o', "do-publish-only", Required = false, Default = false, HelpText = "Skips all steps and publishes from pre-build artefacts.")] public bool PublishOnly { get; set; } - - [Option('s', "single-app-run-folder-name", Required = false, Default = "", HelpText = "Download to PLC and run just app from folder")] - public string AppRunOnlyFolderName { get; set; } } \ No newline at end of file diff --git a/cake/DotNetCmd.cs b/cake/DotNetCmd.cs index fc2d29e8c..2af679783 100644 --- a/cake/DotNetCmd.cs +++ b/cake/DotNetCmd.cs @@ -13,6 +13,7 @@ using System.IO; using System.Linq; using System.Management.Automation; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using Cake.Common.IO; @@ -487,4 +488,131 @@ public static string DotNetRunWithResult(this BuildContext context, string proje return retVal; } } + + /// + /// Runs a project via "dotnet run" and probes until it returns a + /// success status code or elapses. The probe accepts self-signed + /// development certificates. The spawned process is always terminated before returning. + /// Returns ",OK" on a healthy response, ",NOK" otherwise (and sets to false). + /// + public static string DotNetRunWithHealthCheck(this BuildContext context, string projectPath, string arguments, string healthUrl, int maxWaitSeconds, ref bool summaryResult) + { + string workDir = Path.GetFullPath(Path.Combine(projectPath, "..")); + string args = $"run --project \"{projectPath}\" {arguments}"; + context.Log.Information($"Dotnet run (health check) started with project: {projectPath}"); + + string retVal = ",NOK"; + + var processStartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = args, + WorkingDirectory = workDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = Process.Start(processStartInfo)) + { + if (process == null) + { + summaryResult = false; + throw new Exception("Failed to start the process."); + } + + // Asynchronously drain the output and error streams. + Task outputTask = Task.Run(() => + { + while (!process.StandardOutput.EndOfStream) + { + var line = process.StandardOutput.ReadLine(); + if (!string.IsNullOrEmpty(line)) + { + context.Log.Information($"[Output] {line}"); + } + } + }); + + Task errorTask = Task.Run(() => + { + while (!process.StandardError.EndOfStream) + { + var line = process.StandardError.ReadLine(); + if (!string.IsNullOrEmpty(line)) + { + context.Log.Error($"[Error] {line}"); + } + } + }); + + try + { + // Probe the server, accepting the self-signed development certificate. + using (var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }) + using (var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) }) + { + bool healthy = false; + int waited = 0; + + while (waited < maxWaitSeconds) + { + if (process.HasExited) + { + context.Log.Error($"The application '{projectPath}' exited before serving (exit code {process.ExitCode})."); + break; + } + + try + { + var response = client.GetAsync(healthUrl).GetAwaiter().GetResult(); + if (response.IsSuccessStatusCode) + { + context.Log.Information($"Health check OK: {healthUrl} returned {(int)response.StatusCode}."); + healthy = true; + break; + } + + context.Log.Information($"Health check: {healthUrl} returned {(int)response.StatusCode}, retrying..."); + } + catch (Exception ex) + { + context.Log.Information($"Health check: {healthUrl} not ready yet ({ex.Message}), retrying..."); + } + + Task.Delay(2000).Wait(); + waited += 2; + } + + if (healthy) + { + retVal = ",OK"; + } + else + { + summaryResult = false; + context.Log.Error($"Health check FAILED: no success response from {healthUrl} within {maxWaitSeconds}s."); + } + } + } + finally + { + // Always stop the server. + if (!process.HasExited) + { + context.Log.Information($"Terminating the application '{projectPath}'..."); + process.Kill(true); + } + + Task.WhenAll(outputTask, errorTask); + context.Log.Information($"Process exited with code: {process.ExitCode}"); + } + + return retVal; + } + } } \ No newline at end of file diff --git a/cake/Program.cs b/cake/Program.cs index 0e00dc628..a069a94cd 100644 --- a/cake/Program.cs +++ b/cake/Program.cs @@ -399,142 +399,21 @@ public override void Run(BuildContext context) context.DotNetTest(Path.Combine(context.RootDir, package.folder, "tmp_L3_.proj"), context.DotNetTestSettings); } } - - context.Log.Information("Tests done."); - } -} - -[TaskName("AppsRun")] -[IsDependentOn(typeof(TestsTask))] -public sealed class AppsRunTask : FrostingTask -{ - // Tasks can be asynchronous - public override void Run(BuildContext context) - { - if (!context.BuildParameters.AppsRun) + if (context.BuildParameters.TestLevel >= 4) { - context.Log.Warning($"Skipping apps run"); - return; + // Full end-to-end smoke test of the consolidated showcase app: load the entire PLC + // (incl. hardware configuration) onto PLCSIM Advanced, then run the Blazor server and + // assert it serves traffic. Fails the build on any error. + var showcaseApp = Path.Combine(context.RootDir, "showcase", "app", "apax.yml"); + AppsRunTaskHelpers.RunShowcaseIntegration(context, showcaseApp); } - AppsRunTaskHelpers.KillProcess(context,"Siemens.Simatic.PlcSim.Advanced.UserInterface"); - - bool summaryResult = true; - - if (string.IsNullOrEmpty(context.BuildParameters.AppRunOnlyFolderName)) - { - var createResult = AppsRunTaskHelpers.CreateLogFile(context, "app_test_result"); - - if (createResult.Success) - { - string logFilePath = createResult.FilePath; - AppsRunTaskHelpers.WriteResult(context, "AppName,PlcSim,PlcHw,PlcSw,DotnetBuild,DotnetRun", logFilePath); - - foreach (var library in context.Libraries) - { - if (library.app_run) - { - string appFolder = context.GetAppFolder(library); - string appFile = context.GetApaxFile(appFolder); - string appName = context.GetApplicationName(appFile); - - if (!string.IsNullOrEmpty(appFolder) && context.DirectoryExists(appFolder) && !string.IsNullOrEmpty(appFile) && context.FileExists(appFile) && !string.IsNullOrEmpty(appName)) - { - // Display the file details - context.Log.Information($"###################################################"); - context.Log.Information($"Starting the application: {appName}"); - context.Log.Information($"File of the application: {appFile}"); - context.Log.Information($"###################################################"); - AppsRunTaskHelpers.WriteResult(context, " ", logFilePath); - AppsRunTaskHelpers.WriteResult(context, appName, logFilePath, appendToSameLine: true); - - // Cleanup JSONREPOS - AppsRunTaskHelpers.DeleteJsonReposFolder(context, appFolder); - - // Initialize PLC Sim instance - AppsRunTaskHelpers.InitializePlcSimInstance(context, appName); - - // Overwrite security files - AppsRunTaskHelpers.OverwriteSecurityFiles(context, appFile, context.PlcName); - - // Build and load PLC - AppsRunTaskHelpers.BuildAndLoadPlc(context, appFile, appName, logFilePath, ref summaryResult); - - // Build and start HMI - AppsRunTaskHelpers.BuildAndStartHmi(context, appFile, appName, logFilePath, ref summaryResult); - - - } - } - } - - if (!summaryResult) - { - context.Log.Error($"App run failed for some of the applications."); - context.Log.Error($"Good luck with finding out the reason :-)."); - Environment.Exit(1); - } - } - else - { - Console.Error.WriteLine("Failed to create the log file."); - } - } - else - { - var createResult = AppsRunTaskHelpers.CreateLogFile(context, "single_app_test_result"); - - if (createResult.Success) - { - string logFilePath = createResult.FilePath; - AppsRunTaskHelpers.WriteResult(context, "AppName,ApaxInstall,ApaxPlcSim,ApaxGsd,ApaxHwl,ApaxHwcc,ApaxHwid,ApaxHwadr,ApaxHwdo,ApaxBuild,DotnetIxc,ApaxSlfdo,Slngen,DotnetClean,DotnetBuild,DotnetRun", logFilePath); - - string appFolder = Path.Combine(context.RootDir, context.BuildParameters.AppRunOnlyFolderName); - string appFile = context.GetApaxFile(appFolder); - string appName = context.GetApplicationName(appFile); - - if (!string.IsNullOrEmpty(appFolder) && context.DirectoryExists(appFolder) && !string.IsNullOrEmpty(appFile) && context.FileExists(appFile) && !string.IsNullOrEmpty(appName)) - { - // Display the file details - context.Log.Information($"###################################################"); - context.Log.Information($"Starting the application: {appName}"); - context.Log.Information($"File of the application: {appFile}"); - context.Log.Information($"###################################################"); - AppsRunTaskHelpers.WriteResult(context, " ", logFilePath); - AppsRunTaskHelpers.WriteResult(context, appName, logFilePath, appendToSameLine: true); - - // Cleanup JSONREPOS - AppsRunTaskHelpers.DeleteJsonReposFolder(context, appFolder); - - // Initialize PLC Sim instance - AppsRunTaskHelpers.InitializePlcSimInstance(context, appName); - - // Overwrite security files - AppsRunTaskHelpers.OverwriteSecurityFiles(context, appFile, context.PlcName); - - // Build and load PLC, build and start HMI with more grannular evaluation - AppsRunTaskHelpers.AppRunDetailed(context, appFile, appName, logFilePath, ref summaryResult); - - if (!summaryResult) - { - context.Log.Error($"App run failed for the application name: '{appName}', application file: '{appFile}' in folder: '{appFolder}'"); - Environment.Exit(1); - } - } - } - else - { - Console.Error.WriteLine("Failed to create the log file."); - } - } - - AppsRunTaskHelpers.KillProcess(context, "Siemens.Simatic.PlcSim.Advanced.UserInterface"); - context.Log.Information("Apps run done."); + context.Log.Information("Tests done."); } } [TaskName("CreateArtifacts")] -[IsDependentOn(typeof(AppsRunTask))] +[IsDependentOn(typeof(TestsTask))] public sealed class CreateArtifactsTask : FrostingTask { public override void Run(BuildContext context) diff --git a/cake/Properties/launchSettings.json b/cake/Properties/launchSettings.json index 53666dfa1..131b89751 100644 --- a/cake/Properties/launchSettings.json +++ b/cake/Properties/launchSettings.json @@ -20,6 +20,10 @@ "commandName": "Project", "commandLineArgs": "--do-test --test-level 10" }, + "test-L4": { + "commandName": "Project", + "commandLineArgs": "--do-test --test-level 4" + }, "no-build": { "commandName": "Project", "commandLineArgs": "--skip-build" @@ -31,122 +35,6 @@ "build": { "commandName": "Project", "commandLineArgs": "" - }, - "test-L3-apps_run": { - "commandName": "Project", - "commandLineArgs": "--do-test --test-level 10 --apps-run" - }, - "apps_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run" - }, - "abstractions_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name abstractions" - }, - "components.abb.robotics_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.abb.robotics" - }, - "components.abstractions_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.abstractions" - }, - "components.balluff.identification_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.balluff.identification" - }, - "components.cognex.vision_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.cognex.vision" - }, - "components.desoutter.tightening_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.desoutter.tightening" - }, - "components.drives_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.drives" - }, - "components.elements_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.elements" - }, - "components.festo.drives_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.festo.drives" - }, - "components.kuka.robotics_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.kuka.robotics" - }, - "components.keyence.vision_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.keyence.vision" - }, - "components.mitsubishi.robotics_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.mitsubishi.robotics" - }, - "components.pneumatics_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.pneumatics" - }, - "components.rexroth.drives_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.rexroth.drives" - }, - "components.rexroth.press_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.rexroth.press" - }, - "components.robotics_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.robotics" - }, - "components.siem.identification_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.siem.identification" - }, - "components.ur.robotics_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name components.ur.robotics" - }, - "core_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name core" - }, - "data_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name data" - }, - "inspectors_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name inspectors" - }, - "io_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name io" - }, - "probers_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name probers" - }, - "simatic1500_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name simatic1500" - }, - "template.axolibrary_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name template.axolibrary" - }, - "timers_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name timers" - }, - "utils_app_run_only": { - "commandName": "Project", - "commandLineArgs": "--skip-build --apps-run --single-app-run-folder-name utils" } } -} \ No newline at end of file +} diff --git a/scripts/test_L4.ps1 b/scripts/test_L4.ps1 new file mode 100644 index 000000000..f76ea7585 --- /dev/null +++ b/scripts/test_L4.ps1 @@ -0,0 +1,4 @@ +# run build + +dotnet run --project cake/Build.csproj --do-test --test-level 4 -n +exit $LASTEXITCODE; \ No newline at end of file From 3fe533f21e1cdc42cdef249f1cce1dfb6ecaaa7a Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sat, 30 May 2026 18:04:03 +0200 Subject: [PATCH 14/23] feat(scripts): port src/scripts shell workflow to C# (AXOpen.Dev + axdev) Port the SIMATIC-AX developer workflow from src/scripts/*.sh (+2 .ps1) and scripts/check_requisites.ps1 / copy-ctrl-folders.ps1 into a TDD-tested C# library reused by a packed dotnet tool and the in-repo apax aliases. - AXOpen.Dev: all logic behind IProcessRunner (apax/dotnet/openssl wrappers, cert SHA1 compare, HwIdentifiers/IoAddresses generators verified byte-identical, asset discovery, scaffolding, validators, compare exit-code mapping, orchestrators, full faithful check_requisites + copy-ctrl-folders). - AXOpen.Dev.Tool: Spectre.Console.Cli verbs (descriptive name + apax alias), packs as dotnet tool 'axdev' via AxdevApp.Build(). - AXOpen.Dev.Tests: 168 xUnit tests (red-first pure logic + golden files). - AXOpen.Dev.E2E.Tests: optional env-gated PLC/data end-to-end tests. - src/scripts/dev.cs: file-based dispatcher (#:project the tool) sharing AxdevApp.Build; src/scripts/Directory.Packages.props disables CPM there. - src/showcase/app/apax.yml: all 24 script aliases rewired to 'dotnet run ../../scripts/dev.cs -- '; password via AX_TARGET_PWD env. Verified: apax hdl end-to-end against the PLC; full check_requisites report runs green on Windows. .sh files retained pending manual removal. Co-Authored-By: Claude Opus 4.8 (1M context) --- Directory.Packages.props | 2 + .../AXOpen.Dev.E2E.Tests.csproj | 25 + src/axopen.dev/AXOpen.Dev.E2E.Tests/E2E.cs | 66 ++ .../GeneratorDataE2ETests.cs | 38 ++ .../AXOpen.Dev.E2E.Tests/GlobalUsings.cs | 5 + .../PlcProvisioningE2ETests.cs | 48 ++ .../PlcReadOnlyE2ETests.cs | 37 ++ src/axopen.dev/AXOpen.Dev.E2E.Tests/README.md | 49 ++ .../AXOpen.Dev.Tests/AXOpen.Dev.Tests.csproj | 21 + .../Assets/AssetDiscoveryTests.cs | 93 +++ .../Commands/PlcControlCommandTests.cs | 97 +++ .../Commands/SwCommandTests.cs | 40 ++ .../Diagnostics/CompareResultTests.cs | 37 ++ .../Fakes/FakeProcessRunner.cs | 41 ++ .../AXOpen.Dev.Tests/GlobalUsings.cs | 1 + .../Hardware/HwIdentifiersGeneratorTests.cs | 79 +++ .../Hardware/IoAddressesGeneratorTests.cs | 110 ++++ .../Requisites/RequisiteCheckerTests.cs | 26 + .../Requisites/RequisiteParsingTests.cs | 53 ++ .../Requisites/SecretMaskingTests.cs | 20 + .../Requisites/VersionComparerTests.cs | 52 ++ .../Scaffolding/ComponentScaffolderTests.cs | 70 +++ .../Scaffolding/NamespaceConverterTests.cs | 30 + .../Utils/CtrlFolderCopierTests.cs | 70 +++ .../Validation/ArgumentGuardsTests.cs | 45 ++ .../Validation/IpValidatorTests.cs | 45 ++ .../Validation/MacValidatorTests.cs | 21 + .../Validation/PasswordValidatorTests.cs | 25 + .../AXOpen.Dev.Tool/AXOpen.Dev.Tool.csproj | 25 + src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs | 140 +++++ .../Commands/DiagnosticsVerbs.cs | 34 ++ .../Commands/FirstSetupVerbs.cs | 98 +++ .../Commands/HardwareGenVerbs.cs | 33 + .../Commands/OrchestratorVerbs.cs | 95 +++ .../Commands/PlcCommandSettings.cs | 27 + .../AXOpen.Dev.Tool/Commands/ResetPlcVerbs.cs | 25 + .../Commands/RestartPlcVerb.cs | 24 + .../AXOpen.Dev.Tool/Commands/UtilityVerbs.cs | 51 ++ .../Commands/ValidateIpCommand.cs | 28 + .../AXOpen.Dev.Tool/Commands/WrapperVerbs.cs | 76 +++ src/axopen.dev/AXOpen.Dev.Tool/Program.cs | 3 + src/axopen.dev/AXOpen.Dev/AXOpen.Dev.csproj | 16 + src/axopen.dev/AXOpen.Dev/Apax/ApaxClient.cs | 307 ++++++++++ .../AXOpen.Dev/Assets/AssetDiscovery.cs | 70 +++ .../AXOpen.Dev/Commands/AssetCopyCommands.cs | 103 ++++ .../Commands/CertHashCheckCommand.cs | 71 +++ .../AXOpen.Dev/Commands/CompareAllCommand.cs | 71 +++ .../Commands/CopyCtrlFoldersCommand.cs | 40 ++ .../Commands/CopyHardwareIdsCommand.cs | 47 ++ .../Commands/CopyIoAddressesCommand.cs | 56 ++ .../AXOpen.Dev/Commands/DcpCommands.cs | 47 ++ .../AXOpen.Dev/Commands/FirstSetupCommands.cs | 214 +++++++ .../AXOpen.Dev/Commands/HwDiagListCommand.cs | 42 ++ .../Commands/HwDownloadOnlyCommand.cs | 64 ++ .../AXOpen.Dev/Commands/HwUpdateCommands.cs | 74 +++ .../Commands/OrchestratorCommands.cs | 296 +++++++++ .../AXOpen.Dev/Commands/PlcSimCommand.cs | 49 ++ .../AXOpen.Dev/Commands/ResetPlcCommand.cs | 42 ++ .../AXOpen.Dev/Commands/RestartPlcCommand.cs | 61 ++ .../SetupSecureCommunicationCommand.cs | 149 +++++ .../AXOpen.Dev/Commands/SwDeltaCommands.cs | 71 +++ .../AXOpen.Dev/Diagnostics/CompareResult.cs | 36 ++ .../Hardware/HwIdentifiersGenerator.cs | 106 ++++ .../Hardware/IoAddressesGenerator.cs | 179 ++++++ src/axopen.dev/AXOpen.Dev/Hardware/StFile.cs | 24 + .../AXOpen.Dev/Observability/Output.cs | 13 + src/axopen.dev/AXOpen.Dev/Plc/PlcTarget.cs | 8 + .../AXOpen.Dev/Process/IProcessRunner.cs | 33 + .../AXOpen.Dev/Process/ProcessRunner.cs | 49 ++ .../AXOpen.Dev/Requisites/IFileDownloader.cs | 19 + .../AXOpen.Dev/Requisites/IUserPrompt.cs | 27 + .../AXOpen.Dev/Requisites/RequisiteChecker.cs | 139 +++++ .../AXOpen.Dev/Requisites/RequisiteParsing.cs | 72 +++ .../AXOpen.Dev/Requisites/RequisitesConfig.cs | 55 ++ .../AXOpen.Dev/Requisites/SecretMasking.cs | 26 + .../Requisites/SystemEnvironment.cs | 67 ++ .../Requisites/SystemRequisitesChecker.cs | 576 ++++++++++++++++++ .../AXOpen.Dev/Requisites/VersionComparer.cs | 48 ++ .../Scaffolding/ComponentScaffolder.cs | 123 ++++ .../Scaffolding/NamespaceConverter.cs | 48 ++ .../AXOpen.Dev/Tools/DotnetClient.cs | 21 + .../AXOpen.Dev/Tools/OpensslClient.cs | 38 ++ .../AXOpen.Dev/Utils/CtrlFolderCopier.cs | 52 ++ .../AXOpen.Dev/Validation/ArgumentGuards.cs | 34 ++ .../AXOpen.Dev/Validation/IpValidator.cs | 50 ++ .../AXOpen.Dev/Validation/MacValidator.cs | 12 + .../Validation/PasswordValidator.cs | 29 + src/scripts/Directory.Packages.props | 12 + src/scripts/dev.cs | 9 + src/showcase/app/apax.yml | 66 +- 90 files changed, 5732 insertions(+), 34 deletions(-) create mode 100644 src/axopen.dev/AXOpen.Dev.E2E.Tests/AXOpen.Dev.E2E.Tests.csproj create mode 100644 src/axopen.dev/AXOpen.Dev.E2E.Tests/E2E.cs create mode 100644 src/axopen.dev/AXOpen.Dev.E2E.Tests/GeneratorDataE2ETests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.E2E.Tests/GlobalUsings.cs create mode 100644 src/axopen.dev/AXOpen.Dev.E2E.Tests/PlcProvisioningE2ETests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.E2E.Tests/PlcReadOnlyE2ETests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.E2E.Tests/README.md create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/AXOpen.Dev.Tests.csproj create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Assets/AssetDiscoveryTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Commands/PlcControlCommandTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Commands/SwCommandTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Diagnostics/CompareResultTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Fakes/FakeProcessRunner.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/GlobalUsings.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Hardware/HwIdentifiersGeneratorTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Hardware/IoAddressesGeneratorTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Requisites/RequisiteCheckerTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Requisites/RequisiteParsingTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Requisites/SecretMaskingTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Requisites/VersionComparerTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Scaffolding/ComponentScaffolderTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Scaffolding/NamespaceConverterTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Utils/CtrlFolderCopierTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Validation/ArgumentGuardsTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Validation/IpValidatorTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Validation/MacValidatorTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Validation/PasswordValidatorTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/AXOpen.Dev.Tool.csproj create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/DiagnosticsVerbs.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/FirstSetupVerbs.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/HardwareGenVerbs.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/OrchestratorVerbs.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/PlcCommandSettings.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/ResetPlcVerbs.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/RestartPlcVerb.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/UtilityVerbs.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/ValidateIpCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Commands/WrapperVerbs.cs create mode 100644 src/axopen.dev/AXOpen.Dev.Tool/Program.cs create mode 100644 src/axopen.dev/AXOpen.Dev/AXOpen.Dev.csproj create mode 100644 src/axopen.dev/AXOpen.Dev/Apax/ApaxClient.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Assets/AssetDiscovery.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/AssetCopyCommands.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/CertHashCheckCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/CompareAllCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/CopyCtrlFoldersCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/CopyHardwareIdsCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/CopyIoAddressesCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/DcpCommands.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/FirstSetupCommands.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/HwDiagListCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/HwDownloadOnlyCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/HwUpdateCommands.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/OrchestratorCommands.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/PlcSimCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/ResetPlcCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/RestartPlcCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/SetupSecureCommunicationCommand.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Commands/SwDeltaCommands.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Diagnostics/CompareResult.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Hardware/HwIdentifiersGenerator.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Hardware/IoAddressesGenerator.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Hardware/StFile.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Observability/Output.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Plc/PlcTarget.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Process/IProcessRunner.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Process/ProcessRunner.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/IFileDownloader.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/IUserPrompt.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/RequisiteChecker.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/RequisiteParsing.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/RequisitesConfig.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/SecretMasking.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/SystemEnvironment.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/SystemRequisitesChecker.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Requisites/VersionComparer.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Scaffolding/ComponentScaffolder.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Scaffolding/NamespaceConverter.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Tools/DotnetClient.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Tools/OpensslClient.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Utils/CtrlFolderCopier.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Validation/ArgumentGuards.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Validation/IpValidator.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Validation/MacValidator.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Validation/PasswordValidator.cs create mode 100644 src/scripts/Directory.Packages.props create mode 100644 src/scripts/dev.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 73c8c8c39..d8217ca64 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -66,6 +66,8 @@ + + diff --git a/src/axopen.dev/AXOpen.Dev.E2E.Tests/AXOpen.Dev.E2E.Tests.csproj b/src/axopen.dev/AXOpen.Dev.E2E.Tests/AXOpen.Dev.E2E.Tests.csproj new file mode 100644 index 000000000..b8c3289de --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.E2E.Tests/AXOpen.Dev.E2E.Tests.csproj @@ -0,0 +1,25 @@ + + + + enable + enable + false + AXOpen.Dev.E2E.Tests + + + + + + + + + + + + + + diff --git a/src/axopen.dev/AXOpen.Dev.E2E.Tests/E2E.cs b/src/axopen.dev/AXOpen.Dev.E2E.Tests/E2E.cs new file mode 100644 index 000000000..7fd3b9c3a --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.E2E.Tests/E2E.cs @@ -0,0 +1,66 @@ +namespace AXOpen.Dev.E2E.Tests; + +/// +/// Environment-driven configuration + gating for the optional end-to-end tests. All tests are +/// skipped unless their flag env var is set; PLC tests additionally need connection settings. +/// Defaults target the showcase app + its PLCSIM Advanced instance. +/// +internal static class E2E +{ + // Gating flags (set to "1"/"true" to enable that tier): + public const string DataFlag = "AXDEV_E2E_DATA"; // read-only, needs only the app dir + public const string PlcFlag = "AXDEV_E2E_PLC"; // read-only, needs a provisioned PLC + public const string ProvisionFlag = "AXDEV_E2E_PLC_DESTRUCTIVE"; // provisions/overwrites the PLC + + public static bool Enabled(string flag) => + Environment.GetEnvironmentVariable(flag) is "1" or "true" or "TRUE"; + + public static string Target => Get("AX_TARGET", "192.168.100.1"); + public static string PlcName => Get("AX_PLC_NAME", "plc_line"); + public static string Username => Get("AX_USERNAME", "admin"); + public static string Password => Environment.GetEnvironmentVariable("AX_TARGET_PWD") ?? string.Empty; + public static string Namespace => Get("AX_NAMESPACE", "AXOpen.Showcase"); + public static string Platform => Get("AX_PLATFORM", @".\bin\1500\"); + + /// The apax app directory the commands run in (relative paths resolve against it). + public static string AppDirectory => Get("AX_APP_DIR", DiscoverAppDirectory()); + + /// Set the process CWD to the app directory (commands use ./certs, ./hwc, etc.). + public static void EnterAppDirectory() => Directory.SetCurrentDirectory(AppDirectory); + + private static string Get(string key, string fallback) + { + var value = Environment.GetEnvironmentVariable(key); + return string.IsNullOrEmpty(value) ? fallback : value; + } + + private static string DiscoverAppDirectory() + { + // Walk up from the test output directory until we find src/showcase/app/apax.yml. + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null) + { + var candidate = Path.Combine(dir.FullName, "src", "showcase", "app"); + if (File.Exists(Path.Combine(candidate, "apax.yml"))) + { + return candidate; + } + + dir = dir.Parent; + } + + return Directory.GetCurrentDirectory(); + } +} + +/// A that skips unless its gating env flag is enabled. +public sealed class E2EFactAttribute : FactAttribute +{ + public E2EFactAttribute(string flag) + { + if (!E2E.Enabled(flag)) + { + Skip = $"End-to-end test disabled. Set {flag}=1 (and the AX_* connection env vars) to run it."; + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev.E2E.Tests/GeneratorDataE2ETests.cs b/src/axopen.dev/AXOpen.Dev.E2E.Tests/GeneratorDataE2ETests.cs new file mode 100644 index 000000000..a88543c74 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.E2E.Tests/GeneratorDataE2ETests.cs @@ -0,0 +1,38 @@ +using AXOpen.Dev.Hardware; + +namespace AXOpen.Dev.E2E.Tests; + +/// +/// Read-only end-to-end check: the ST generators reproduce the app's shipped src/IO/*.st +/// byte-for-byte from the compiled SystemConstants/*.st. Needs only the app directory +/// (no apax, no PLC). Enable with AXDEV_E2E_DATA=1. +/// +public class GeneratorDataE2ETests +{ + private static string App => E2E.AppDirectory; + + private static string ReadExpected(string relative) => + File.ReadAllText(Path.Combine(App, "src", "IO", relative)).Replace("\r\n", "\n"); + + [E2EFact(E2E.DataFlag)] + public void HwIdentifier_files_are_byte_identical() + { + var input = File.ReadAllText(Path.Combine(App, "SystemConstants", $"{E2E.PlcName}_HwIdentifiers.st")); + var (identifiers, list) = HwIdentifiersGenerator.Generate(E2E.Namespace, input); + + Assert.Equal(ReadExpected("HwIdentifiers.st"), identifiers); + Assert.Equal(ReadExpected("HwIdentifierList.st"), list); + } + + [E2EFact(E2E.DataFlag)] + public void IoAddress_files_are_byte_identical() + { + var input = File.ReadAllText(Path.Combine(App, "SystemConstants", $"{E2E.PlcName}_IoAddresses.st")); + var (inputs, outputs, structures) = IoAddressesGenerator.Generate(E2E.Namespace, input); + + // The shipped files carry the PowerShell Set-Content trailing newline. + Assert.Equal(ReadExpected("Inputs.st"), inputs + "\n"); + Assert.Equal(ReadExpected("Outputs.st"), outputs + "\n"); + Assert.Equal(ReadExpected("IoStructures.st"), structures + "\n"); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.E2E.Tests/GlobalUsings.cs b/src/axopen.dev/AXOpen.Dev.E2E.Tests/GlobalUsings.cs new file mode 100644 index 000000000..5c88c1ca0 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.E2E.Tests/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using Xunit; + +// End-to-end tests change the process working directory (commands use relative paths), so they +// must not run in parallel with each other. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/src/axopen.dev/AXOpen.Dev.E2E.Tests/PlcProvisioningE2ETests.cs b/src/axopen.dev/AXOpen.Dev.E2E.Tests/PlcProvisioningE2ETests.cs new file mode 100644 index 000000000..06099e29c --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.E2E.Tests/PlcProvisioningE2ETests.cs @@ -0,0 +1,48 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Process; +using AXOpen.Dev.Tools; + +namespace AXOpen.Dev.E2E.Tests; + +/// +/// DESTRUCTIVE end-to-end provisioning of a blank PLC: secure-comm setup + first HW download + +/// SW build/download/restart. Overwrites the PLC and the local certs. Enable with +/// AXDEV_E2E_PLC_DESTRUCTIVE=1 plus the AX_* connection env vars. +/// +public class PlcProvisioningE2ETests +{ + private static ApaxClient Apax() => new(new ProcessRunner()); + + [E2EFact(E2E.ProvisionFlag)] + public async Task Full_first_setup_provisions_the_plc() + { + E2E.EnterAppDirectory(); + + // Force-clean prior security artifacts so setup-secure-communication can run fresh. + var certsDir = Path.Combine("certs", E2E.PlcName); + if (Directory.Exists(certsDir)) + { + Directory.Delete(certsDir, recursive: true); + } + + var securityConfig = Path.Combine("hwc", "hwc.gen", $"{E2E.PlcName}.SecurityConfiguration.json"); + if (File.Exists(securityConfig)) + { + File.Delete(securityConfig); + } + + // HW: gsd + templates + secure comms + compile + first download + cert pull. + var hw = await new HwFirstDownloadCommand(Apax(), new OpensslClient(new ProcessRunner())) + .ExecuteAsync(E2E.Namespace, E2E.PlcName, E2E.Target, E2E.Username, E2E.Password); + Assert.Equal(0, hw); + + Assert.True(File.Exists(Path.Combine(certsDir, $"{E2E.PlcName}.cer")), + "The PLC certificate should have been pulled to ./certs//.cer."); + + // SW: apax build + dotnet ixc + full download + restart. + var sw = await new SwBuildDownloadFullCommand(Apax(), new DotnetClient(new ProcessRunner())) + .ExecuteAsync(E2E.PlcName, E2E.Target, E2E.Platform, E2E.Username, E2E.Password); + Assert.Equal(0, sw); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.E2E.Tests/PlcReadOnlyE2ETests.cs b/src/axopen.dev/AXOpen.Dev.E2E.Tests/PlcReadOnlyE2ETests.cs new file mode 100644 index 000000000..cbbbd87ea --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.E2E.Tests/PlcReadOnlyE2ETests.cs @@ -0,0 +1,37 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Process; + +namespace AXOpen.Dev.E2E.Tests; + +/// +/// Read-only end-to-end checks against a PROVISIONED PLC (certificate already present locally and +/// on the device). Enable with AXDEV_E2E_PLC=1 plus AX_TARGET / AX_PLC_NAME / AX_USERNAME / +/// AX_TARGET_PWD. +/// +/// +/// apax shares one PLC connection session across commands run in the same process. The first +/// command establishes that session, so the authenticated command (hw-diag) must run before the +/// unauthenticated one (plc-cert / cert-check); otherwise hw-diag would reuse a credential-less +/// shared session and be denied. The two checks therefore live in one ordered test. +/// +public class PlcReadOnlyE2ETests +{ + private static PlcTarget Target() => new(E2E.Target, E2E.PlcName, E2E.Username, E2E.Password); + + private static ApaxClient Apax() => new(new ProcessRunner()); + + [E2EFact(E2E.PlcFlag)] + public async Task HwDiag_then_cert_check_succeed() + { + E2E.EnterAppDirectory(); + + // Authenticated command first (establishes the shared connection session). + var diag = await new HwDiagListCommand(Apax()).ExecuteAsync(Target()); + Assert.Equal(0, diag); + + var cert = await new CertHashCheckCommand(Apax()).ExecuteAsync(Target()); + Assert.Equal(0, cert); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.E2E.Tests/README.md b/src/axopen.dev/AXOpen.Dev.E2E.Tests/README.md new file mode 100644 index 000000000..cf896fa54 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.E2E.Tests/README.md @@ -0,0 +1,49 @@ +# AXOpen.Dev end-to-end tests (optional) + +These tests drive the real `apax` CLI / `openssl` / a live PLC. They are **skipped by default** — +each is gated by an environment flag — so a normal `dotnet test` run and CI report them as +*skipped*, never failed. + +Run them explicitly from the **app directory's** context (defaults to `src/showcase/app`, override +with `AX_APP_DIR`). Connection settings default to the showcase values; override via env. + +## Tiers + +| Flag | Scope | Needs | +|------|-------|-------| +| `AXDEV_E2E_DATA=1` | Read-only: ST generators reproduce `src/IO/*.st` byte-for-byte | app dir only | +| `AXDEV_E2E_PLC=1` | Read-only on a **provisioned** PLC: `hw-diag`, `cert-check` | apax + live PLC + local cert | +| `AXDEV_E2E_PLC_DESTRUCTIVE=1` | **Provisions/overwrites** the PLC: secure-comm + HW + SW download | apax + openssl + live PLC | + +> **Note:** these read `AX_USERNAME` etc. from the environment. If your shell has a stray +> `AX_USERNAME` (e.g. `adm`), set it explicitly (`AX_USERNAME=admin`) or the PLC will deny access. +> apax also shares one PLC connection session per process, so the PLC read-only test runs the +> authenticated command (`hw-diag`) before the unauthenticated one (`cert-check`). + +## Connection env (defaults shown) + +``` +AX_TARGET=192.168.100.1 +AX_PLC_NAME=plc_line +AX_USERNAME=admin +AX_TARGET_PWD=... # required for PLC tiers (no default) +AX_NAMESPACE=AXOpen.Showcase +AX_PLATFORM=.\bin\1500\ +AX_APP_DIR=/src/showcase/app +``` + +## Examples + +```powershell +# Safe, read-only data check +$env:AXDEV_E2E_DATA=1 +dotnet test src/axopen.dev/AXOpen.Dev.E2E.Tests + +# Read-only PLC check (PLC already provisioned) +$env:AXDEV_E2E_PLC=1; $env:AX_TARGET_PWD='...' +dotnet test src/axopen.dev/AXOpen.Dev.E2E.Tests --filter PlcReadOnly + +# Full provisioning of a blank PLC (DESTRUCTIVE) +$env:AXDEV_E2E_PLC_DESTRUCTIVE=1; $env:AX_TARGET_PWD='...' +dotnet test src/axopen.dev/AXOpen.Dev.E2E.Tests --filter PlcProvisioning +``` diff --git a/src/axopen.dev/AXOpen.Dev.Tests/AXOpen.Dev.Tests.csproj b/src/axopen.dev/AXOpen.Dev.Tests/AXOpen.Dev.Tests.csproj new file mode 100644 index 000000000..17373f422 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/AXOpen.Dev.Tests.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + false + AXOpen.Dev.Tests + + + + + + + + + + + + + + diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Assets/AssetDiscoveryTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Assets/AssetDiscoveryTests.cs new file mode 100644 index 000000000..d0c620828 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Assets/AssetDiscoveryTests.cs @@ -0,0 +1,93 @@ +using AXOpen.Dev.Assets; + +namespace AXOpen.Dev.Tests.Assets; + +public sealed class AssetDiscoveryTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), $"axdev-assets-{Guid.NewGuid():N}"); + + public AssetDiscoveryTests() + { + // .apax/pkgA/assets/GSDML-foo.xml + // .apax/pkgA/assets/sub/GSDML-bar.XML (case-insensitive match) + // .apax/pkgA/assets/templates/a.hwl.json + // .apax/pkgA/assets/b.hwl.yml + // .apax/pkgA/assets/ignore.txt (ignored by both) + // .apax/pkgB/nested/assets/GSDML-foo.xml (flat-collision with pkgA) + Make("pkgA/assets/GSDML-foo.xml"); + Make("pkgA/assets/sub/GSDML-bar.XML"); + Make("pkgA/assets/templates/a.hwl.json"); + Make("pkgA/assets/b.hwl.yml"); + Make("pkgA/assets/ignore.txt"); + Make("pkgB/nested/assets/GSDML-foo.xml"); + } + + private string ApaxDir => Path.Combine(_root, ".apax"); + + private void Make(string relativeUnderApax) + { + var full = Path.Combine(ApaxDir, relativeUnderApax.Replace('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(Path.GetDirectoryName(full)!); + File.WriteAllText(full, "x"); + } + + [Fact] + public void Finds_all_assets_directories() + { + var dirs = AssetDiscovery.FindAssetsDirectories(ApaxDir); + Assert.Equal(2, dirs.Count); + Assert.All(dirs, d => Assert.Equal("assets", Path.GetFileName(d))); + } + + [Fact] + public void Gsd_plan_is_flat_and_matches_gsdml_xml_case_insensitively() + { + var dest = Path.Combine(_root, "gsd", "source"); + var ops = AssetDiscovery.PlanGsdCopies(ApaxDir, dest); + + // GSDML-foo.xml (pkgA), GSDML-bar.XML (pkgA/sub), GSDML-foo.xml (pkgB) => 3 sources + Assert.Equal(3, ops.Count); + Assert.All(ops, op => Assert.Equal(dest, Path.GetDirectoryName(op.Destination))); + Assert.DoesNotContain(ops, op => op.Source.EndsWith("ignore.txt", StringComparison.Ordinal)); + Assert.Contains(ops, op => Path.GetFileName(op.Destination) == "GSDML-bar.XML"); + } + + [Fact] + public void Gsd_flat_collisions_are_detected_by_destination_basename() + { + var dest = Path.Combine(_root, "gsd", "source"); + var ops = AssetDiscovery.PlanGsdCopies(ApaxDir, dest); + var collisions = AssetDiscovery.FlatCollisions(ops); + + Assert.Contains("GSDML-foo.xml", collisions); + Assert.DoesNotContain("GSDML-bar.XML", collisions); + } + + [Fact] + public void Hwl_plan_preserves_relative_subfolders() + { + var dest = Path.Combine(_root, "hwc", "library_templates"); + var ops = AssetDiscovery.PlanHwlCopies(ApaxDir, dest); + + Assert.Equal(2, ops.Count); + Assert.Contains(ops, op => op.Destination == Path.Combine(dest, "templates", "a.hwl.json")); + Assert.Contains(ops, op => op.Destination == Path.Combine(dest, "b.hwl.yml")); + } + + [Fact] + public void Missing_apax_dir_yields_empty_plans() + { + var missing = Path.Combine(_root, "does-not-exist"); + Assert.Empty(AssetDiscovery.FindAssetsDirectories(missing)); + Assert.Empty(AssetDiscovery.PlanGsdCopies(missing, "dest")); + Assert.Empty(AssetDiscovery.PlanHwlCopies(missing, "dest")); + } + + public void Dispose() + { + if (Directory.Exists(_root)) + { + Directory.Delete(_root, recursive: true); + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Commands/PlcControlCommandTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Commands/PlcControlCommandTests.cs new file mode 100644 index 000000000..720796a9c --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Commands/PlcControlCommandTests.cs @@ -0,0 +1,97 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Process; +using AXOpen.Dev.Tests.Fakes; + +namespace AXOpen.Dev.Tests.Commands; + +public class PlcControlCommandTests +{ + private static readonly ProcessResult Ok = new(0, string.Empty, string.Empty); + private static readonly ProcessResult Fail = new(1, string.Empty, "boom"); + + [Fact] + public async Task Restart_issues_stop_then_run_with_certificate() + { + var fake = new FakeProcessRunner { Default = Ok }; + var target = new PlcTarget("10.10.10.120", "PLC1", "user", "pass"); + var cmd = new RestartPlcCommand(new ApaxClient(fake), fileExists: _ => true); + + var exit = await cmd.ExecuteAsync(target); + + Assert.Equal(0, exit); + Assert.Equal(2, fake.Invocations.Count); + Assert.All(fake.Invocations, r => Assert.Equal("apax", r.Executable)); + Assert.Contains("STOP", fake.Invocations[0].Arguments); + Assert.Contains("RUN", fake.Invocations[1].Arguments); + Assert.Contains(target.CertificatePath, fake.Invocations[0].Arguments); + Assert.Contains("--no-input", fake.Invocations[0].Arguments); + } + + [Fact] + public async Task Restart_stops_after_failed_stop_and_returns_1() + { + var fake = new FakeProcessRunner(); + fake.When(r => r.Arguments.Contains("STOP"), Fail); + var cmd = new RestartPlcCommand(new ApaxClient(fake), fileExists: _ => true); + + var exit = await cmd.ExecuteAsync(new PlcTarget("1.2.3.4", "PLC1", "u", "p")); + + Assert.Equal(1, exit); + Assert.Single(fake.Invocations); // never attempts RUN + } + + [Fact] + public async Task Restart_rejects_invalid_ip_without_calling_apax() + { + var fake = new FakeProcessRunner { Default = Ok }; + var cmd = new RestartPlcCommand(new ApaxClient(fake), fileExists: _ => true); + + var exit = await cmd.ExecuteAsync(new PlcTarget("999.1.1.1", "PLC1", "u", "p")); + + Assert.Equal(1, exit); + Assert.Empty(fake.Invocations); + } + + [Fact] + public async Task Restart_fails_when_certificate_missing() + { + var fake = new FakeProcessRunner { Default = Ok }; + var cmd = new RestartPlcCommand(new ApaxClient(fake), fileExists: _ => false); + + var exit = await cmd.ExecuteAsync(new PlcTarget("1.2.3.4", "PLC1", "u", "p")); + + Assert.Equal(1, exit); + Assert.Empty(fake.Invocations); + } + + [Theory] + [InlineData(ResetScope.KeepOnlyIp, "KeepOnlyIP")] + [InlineData(ResetScope.All, "All")] + public async Task Reset_passes_correct_scope_and_pipes_confirmation(ResetScope scope, string expectedFlag) + { + var fake = new FakeProcessRunner { Default = Ok }; + var cmd = new ResetPlcCommand(new ApaxClient(fake)); + + var exit = await cmd.ExecuteAsync(scope, "10.10.10.120", "user", "pass"); + + Assert.Equal(0, exit); + var call = Assert.Single(fake.Invocations); + Assert.Equal("apax", call.Executable); + Assert.Contains("hwld", call.Arguments); + Assert.Contains(expectedFlag, call.Arguments); + Assert.Equal("y\n", call.StandardInput); + } + + [Fact] + public async Task Reset_maps_apax_failure_to_exit_1() + { + var fake = new FakeProcessRunner { Default = Fail }; + var cmd = new ResetPlcCommand(new ApaxClient(fake)); + + var exit = await cmd.ExecuteAsync(ResetScope.All, "1.2.3.4", "u", "p"); + + Assert.Equal(1, exit); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Commands/SwCommandTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Commands/SwCommandTests.cs new file mode 100644 index 000000000..594d13503 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Commands/SwCommandTests.cs @@ -0,0 +1,40 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Tools; +using AXOpen.Dev.Tests.Fakes; + +namespace AXOpen.Dev.Tests.Commands; + +public class SwCommandTests +{ + [Fact] + public async Task SwBuildDownloadFull_runs_build_then_ixc_before_download() + { + var fake = new FakeProcessRunner(); // default success + var cmd = new SwBuildDownloadFullCommand(new ApaxClient(fake), new DotnetClient(fake)); + + // Certificate is absent in the test working directory, so the download step stops early — + // but build + ixc must have run first, in order. + var exit = await cmd.ExecuteAsync("plc_line", "192.168.100.1", ".\\bin\\1500\\", "admin", "pwd"); + + Assert.Equal(1, exit); + Assert.True(fake.Invocations.Count >= 2); + Assert.Equal("apax", fake.Invocations[0].Executable); + Assert.Contains("build", fake.Invocations[0].Arguments); + Assert.Equal("dotnet", fake.Invocations[1].Executable); + Assert.Contains("ixc", fake.Invocations[1].Arguments); + } + + [Fact] + public async Task SwBuildDownloadFull_stops_when_build_fails() + { + var fake = new FakeProcessRunner(); + fake.When(r => r.Executable == "apax" && r.Arguments.Contains("build"), new(1, "", "err")); + var cmd = new SwBuildDownloadFullCommand(new ApaxClient(fake), new DotnetClient(fake)); + + var exit = await cmd.ExecuteAsync("plc_line", "192.168.100.1", ".\\bin\\1500\\", "admin", "pwd"); + + Assert.Equal(1, exit); + Assert.Single(fake.Invocations); // never reaches dotnet ixc + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Diagnostics/CompareResultTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Diagnostics/CompareResultTests.cs new file mode 100644 index 000000000..839fd6e3c --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Diagnostics/CompareResultTests.cs @@ -0,0 +1,37 @@ +using AXOpen.Dev.Diagnostics; + +namespace AXOpen.Dev.Tests.Diagnostics; + +public class CompareResultTests +{ + [Theory] + [InlineData(0, CompareOutcome.Identical, 0)] + [InlineData(9, CompareOutcome.CodeBlocksDiffer, 9)] + [InlineData(10, CompareOutcome.DataBlocksDiffer, 10)] + [InlineData(11, CompareOutcome.CodeAndDataBlocksDiffer, 11)] + public void Known_apax_codes_map_and_propagate(int apax, CompareOutcome outcome, int processExit) + { + var result = CompareResult.FromApaxExitCode(apax); + Assert.Equal(outcome, result.Outcome); + Assert.Equal(processExit, result.ProcessExitCode); + Assert.False(string.IsNullOrWhiteSpace(result.Message)); + } + + [Theory] + [InlineData(1)] + [InlineData(7)] + [InlineData(255)] + public void Unknown_codes_are_unspecified_and_fail_with_1(int apax) + { + var result = CompareResult.FromApaxExitCode(apax); + Assert.Equal(CompareOutcome.Unspecified, result.Outcome); + Assert.Equal(1, result.ProcessExitCode); + } + + [Fact] + public void Identical_is_the_only_success() + { + Assert.True(CompareResult.FromApaxExitCode(0).IsIdentical); + Assert.False(CompareResult.FromApaxExitCode(9).IsIdentical); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Fakes/FakeProcessRunner.cs b/src/axopen.dev/AXOpen.Dev.Tests/Fakes/FakeProcessRunner.cs new file mode 100644 index 000000000..487bde36c --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Fakes/FakeProcessRunner.cs @@ -0,0 +1,41 @@ +using AXOpen.Dev.Process; + +namespace AXOpen.Dev.Tests.Fakes; + +/// +/// Test double for . Records every request and returns +/// scripted results matched by executable + first argument, falling back to a default. +/// +public sealed class FakeProcessRunner : IProcessRunner +{ + private readonly List<(Func Match, ProcessResult Result)> _rules = new(); + + public List Invocations { get; } = new(); + + public ProcessResult Default { get; set; } = new(0, string.Empty, string.Empty); + + /// Return for requests matching . + public FakeProcessRunner When(Func predicate, ProcessResult result) + { + _rules.Add((predicate, result)); + return this; + } + + /// Match on executable name and (optionally) the first argument. + public FakeProcessRunner When(string executable, ProcessResult result, string? firstArg = null) + => When(r => r.Executable == executable && (firstArg is null || (r.Arguments.Count > 0 && r.Arguments[0] == firstArg)), result); + + public Task RunAsync(ProcessRequest request, CancellationToken cancellationToken = default) + { + Invocations.Add(request); + foreach (var (match, result) in _rules) + { + if (match(request)) + { + return Task.FromResult(result); + } + } + + return Task.FromResult(Default); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/GlobalUsings.cs b/src/axopen.dev/AXOpen.Dev.Tests/GlobalUsings.cs new file mode 100644 index 000000000..c802f4480 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Hardware/HwIdentifiersGeneratorTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Hardware/HwIdentifiersGeneratorTests.cs new file mode 100644 index 000000000..17c7189ff --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Hardware/HwIdentifiersGeneratorTests.cs @@ -0,0 +1,79 @@ +using AXOpen.Dev.Hardware; + +namespace AXOpen.Dev.Tests.Hardware; + +public class HwIdentifiersGeneratorTests +{ + // Input deliberately unsorted by value, with surrounding namespace + a non-constant + // block that must be ignored. \r mixed in to exercise CR stripping. + private const string Input = + "NAMESPACE Ignored.Input.Ns\r\n" + + " VAR_GLOBAL\r\n" + + " ShouldBeIgnored : UINT := UINT#999;\r\n" + + " END_VAR\r\n" + + " VAR_GLOBAL CONSTANT\r\n" + + " Device_1 : UINT := UINT#258;\r\n" + + " Local : UINT := UINT#0;\r\n" + + " PN_IO : UINT := UINT#257;\r\n" + + " PROFINET_IO_System : UINT := UINT#256;\r\n" + + " END_VAR\r\n" + + "END_NAMESPACE\r\n"; + + [Fact] + public void Generates_enum_style_identifiers_sorted_by_value() + { + var (identifiers, _) = HwIdentifiersGenerator.Generate("My.Ns", Input); + + const string expected = + "NAMESPACE My.Ns\n" + + " TYPE\n" + + " HwIdentifiers : UINT\n" + + " (\n" + + " Local := UINT#0,\n" + + " PROFINET_IO_System := UINT#256,\n" + + " PN_IO := UINT#257,\n" + + " Device_1 := UINT#258\n" + + " );\n" + + " END_TYPE\n" + + "END_NAMESPACE\n\n"; + + Assert.Equal(expected, identifiers); + } + + [Fact] + public void Generates_array_list_sorted_by_value() + { + var (_, list) = HwIdentifiersGenerator.Generate("My.Ns", Input); + + const string expected = + "NAMESPACE My.Ns\n" + + " TYPE HwIdentifierList : ARRAY[0..3] OF UINT :=\n" + + " [\n" + + " UINT#0,\n" + + " UINT#256,\n" + + " UINT#257,\n" + + " UINT#258\n" + + " ];\n" + + "END_TYPE\n" + + "END_NAMESPACE\n\n"; + + Assert.Equal(expected, list); + } + + [Fact] + public void Empty_constant_block_emits_NONE() + { + var (identifiers, _) = HwIdentifiersGenerator.Generate("My.Ns", + "VAR_GLOBAL CONSTANT\r\nEND_VAR\r\n"); + + Assert.Contains(" NONE := UINT#0\n", identifiers); + } + + [Fact] + public void Uses_lf_line_endings_only() + { + var (identifiers, list) = HwIdentifiersGenerator.Generate("My.Ns", Input); + Assert.DoesNotContain('\r', identifiers); + Assert.DoesNotContain('\r', list); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Hardware/IoAddressesGeneratorTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Hardware/IoAddressesGeneratorTests.cs new file mode 100644 index 000000000..79ca47920 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Hardware/IoAddressesGeneratorTests.cs @@ -0,0 +1,110 @@ +using AXOpen.Dev.Hardware; + +namespace AXOpen.Dev.Tests.Hardware; + +public class IoAddressesGeneratorTests +{ + private const string Input = + "NAMESPACE Hwc.Input\n" + + " VAR_GLOBAL\n" + + " // first input\n" + + " Station1_DI AT %IB0 : BYTE;\n" + + " Station1_DO AT %QB0 : BYTE;\n" + + " NotAnIo : INT := 5;\n" + + " END_VAR\n" + + " TYPE\n" + + " IoStruct : STRUCT\n" + + " a : BOOL;\n" + + " END_STRUCT;\n" + + " END_TYPE\n" + + "END_NAMESPACE\n"; + + [Fact] + public void Inputs_struct_strips_direction_letter_and_keeps_preceding_comment() + { + var result = IoAddressesGenerator.Generate("My.Ns", Input); + + const string expected = + "NAMESPACE My.Ns\n" + + " TYPE\n" + + " {S7.extern=ReadWrite}\n" + + " {#ix-attr:[Container(Layout.Wrap)]}\n" + + " Inputs : STRUCT\n" + + "\t // first input\n" + + "\t Station1_DI AT %B0 : BYTE;\n" + + "\n" + // blank line after each declaration block (faithful to the PowerShell source) + " END_STRUCT;\n" + + " END_TYPE\n" + + "END_NAMESPACE\n"; + + Assert.Equal(expected, result.Inputs); + } + + [Fact] + public void Outputs_struct_strips_direction_letter() + { + var result = IoAddressesGenerator.Generate("My.Ns", Input); + + const string expected = + "NAMESPACE My.Ns\n" + + " TYPE\n" + + " {S7.extern=ReadWrite}\n" + + " {#ix-attr:[Container(Layout.Wrap)]}\n" + + " Outputs : STRUCT\n" + + "\t Station1_DO AT %B0 : BYTE;\n" + + "\n" + // blank line after each declaration block (faithful to the PowerShell source) + " END_STRUCT;\n" + + " END_TYPE\n" + + "END_NAMESPACE\n"; + + Assert.Equal(expected, result.Outputs); + } + + [Fact] + public void Structures_reemit_type_section_with_injected_attributes() + { + var result = IoAddressesGenerator.Generate("My.Ns", Input); + + const string expected = + "NAMESPACE My.Ns\n" + + "\t TYPE\n" + + " {S7.extern=ReadWrite}\n" + + " {#ix-attr:[Container(Layout.Wrap)]}\n" + + "\t IoStruct : STRUCT\n" + + "\t a : BOOL;\n" + + "\t END_STRUCT;\n" + + "\t END_TYPE\n" + + "\tEND_NAMESPACE\n" + + "END_NAMESPACE\n"; + + Assert.Equal(expected, result.Structures); + } + + [Fact] + public void No_io_emits_placeholder_members() + { + var input = + " VAR_GLOBAL\n" + + " OnlyData : INT := 1;\n" + + " END_VAR\n"; + + var result = IoAddressesGenerator.Generate("My.Ns", input); + + Assert.Contains(" noInputsFoundInTheHwConfig AT %B0: BYTE;\n", result.Inputs); + Assert.Contains(" noOutputsFoundInTheHwConfig AT %B0: BYTE;\n", result.Outputs); + } + + [Fact] + public void Missing_var_global_block_throws() + => Assert.Throws( + () => IoAddressesGenerator.Generate("My.Ns", "TYPE\nEND_TYPE\n")); + + [Fact] + public void Output_is_lf_only() + { + var result = IoAddressesGenerator.Generate("My.Ns", Input); + Assert.DoesNotContain('\r', result.Inputs); + Assert.DoesNotContain('\r', result.Outputs); + Assert.DoesNotContain('\r', result.Structures); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Requisites/RequisiteCheckerTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Requisites/RequisiteCheckerTests.cs new file mode 100644 index 000000000..8b23443a8 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Requisites/RequisiteCheckerTests.cs @@ -0,0 +1,26 @@ +using AXOpen.Dev.Requisites; + +namespace AXOpen.Dev.Tests.Requisites; + +public class RequisiteCheckerTests +{ + private const string Url = "https://npm.pkg.github.com/"; + + [Fact] + public void AuthJson_with_registry_and_token_is_detected() + { + var json = $$""" + { "{{Url}}": { "registryToken": "ghp_abc123", "userName": "someone" } } + """; + Assert.True(RequisiteChecker.AuthJsonHasRegistry(json, Url)); + } + + [Theory] + [InlineData("{ \"https://other/\": { \"registryToken\": \"x\" } }")] // different registry + [InlineData("{ \"https://npm.pkg.github.com/\": { \"registryToken\": \"\" } }")] // empty token + [InlineData("{ \"https://npm.pkg.github.com/\": { \"userName\": \"x\" } }")] // no token + [InlineData("not json")] + [InlineData("{}")] + public void AuthJson_without_valid_registry_is_rejected(string json) + => Assert.False(RequisiteChecker.AuthJsonHasRegistry(json, Url)); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Requisites/RequisiteParsingTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Requisites/RequisiteParsingTests.cs new file mode 100644 index 000000000..a29e7ad8f --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Requisites/RequisiteParsingTests.cs @@ -0,0 +1,53 @@ +using AXOpen.Dev.Requisites; + +namespace AXOpen.Dev.Tests.Requisites; + +public sealed class RequisiteParsingTests +{ + [Theory] + [InlineData("v23.7.0", "23.7.0")] + [InlineData("23.7.0\n", "23.7.0")] + public void ParseNodeVersion(string input, string expected) + => Assert.Equal(expected, RequisiteParsing.ParseNodeVersion(input)); + + [Theory] + [InlineData("git version 2.44.0.windows.1", "2.44.0")] + [InlineData("git version 2.45.1", "2.45.1")] + [InlineData("no version here", null)] + public void ExtractGitVersion(string input, string? expected) + => Assert.Equal(expected, RequisiteParsing.ExtractGitVersion(input)); + + [Fact] + public void DotnetSdkListed_matches_exact_leading_version() + { + var lines = new[] + { + "8.0.404 [C:\\Program Files\\dotnet\\sdk]", + "10.0.100 [C:\\Program Files\\dotnet\\sdk]", + }; + Assert.True(RequisiteParsing.DotnetSdkListed(lines, "10.0.100")); + Assert.False(RequisiteParsing.DotnetSdkListed(lines, "10.0.200")); + // must be a leading match, not a substring + Assert.False(RequisiteParsing.DotnetSdkListed(new[] { "110.0.100 [x]" }, "10.0.100")); + } + + [Fact] + public void HasDesktopRuntime_checks_windowsdesktop_app_versions() + { + var lines = new[] + { + "Microsoft.AspNetCore.App 8.0.22 [C:\\x]", + "Microsoft.WindowsDesktop.App 8.0.30 [C:\\x]", + "Microsoft.NETCore.App 8.0.30 [C:\\x]", + }; + Assert.True(RequisiteParsing.HasDesktopRuntime(lines, "8.0.22")); + Assert.False(RequisiteParsing.HasDesktopRuntime(lines, "8.0.40")); + Assert.False(RequisiteParsing.HasDesktopRuntime(lines, "9.0.0")); + } + + [Theory] + [InlineData("\r\n Version REG_SZ 14.38.33135.0\r\n", "14.38.33135.0")] + [InlineData(" Installed REG_DWORD 0x1\r\n", "0x1")] + public void ExtractRegValue(string output, string expected) + => Assert.Equal(expected, RequisiteParsing.ExtractRegValue(output, expected.StartsWith("0x") ? "Installed" : "Version")); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Requisites/SecretMaskingTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Requisites/SecretMaskingTests.cs new file mode 100644 index 000000000..6a87c52cd --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Requisites/SecretMaskingTests.cs @@ -0,0 +1,20 @@ +using AXOpen.Dev.Requisites; + +namespace AXOpen.Dev.Tests.Requisites; + +public sealed class SecretMaskingTests +{ + [Theory] + [InlineData("ghp_ABCDEFGH", "ghp_A******H")] // len 12 > 6: first5 + 6 stars + last1 + [InlineData("123456", "******")] // len 6: all stars + [InlineData("", "")] + public void MaskToken(string token, string expected) + => Assert.Equal(expected, SecretMasking.MaskToken(token)); + + [Theory] + [InlineData("octocat", "o*****t")] // len 7 > 2: first1 + 5 stars + last1 + [InlineData("ab", "**")] // len 2: all stars + [InlineData("a", "*")] + public void MaskUserName(string userName, string expected) + => Assert.Equal(expected, SecretMasking.MaskUserName(userName)); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Requisites/VersionComparerTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Requisites/VersionComparerTests.cs new file mode 100644 index 000000000..0f55e8c66 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Requisites/VersionComparerTests.cs @@ -0,0 +1,52 @@ +using AXOpen.Dev.Requisites; + +namespace AXOpen.Dev.Tests.Requisites; + +public sealed class VersionComparerTests +{ + [Theory] + [InlineData("8.0", true)] + [InlineData("8.0.2", true)] + [InlineData("16.11.36631.11", true)] + [InlineData("8", false)] + [InlineData("v8.0", false)] + [InlineData("8.0.2.3.4", false)] + [InlineData("", false)] + public void IsValidRequiredVersion_matches_powershell_regex(string version, bool expected) + => Assert.Equal(expected, VersionComparer.IsValidRequiredVersion(version)); + + [Theory] + [InlineData("4.3.0", "4.3.0", true)] + [InlineData("4.3.1", "4.3.0", false)] + public void Equal_requires_exact_match(string actual, string required, bool expected) + => Assert.Equal(expected, VersionComparer.Equal(actual, required)); + + [Theory] + [InlineData("23.7.0", "23.7.0", true)] + [InlineData("23.8.0", "23.7.0", true)] + [InlineData("23.6.9", "23.7.0", false)] + [InlineData("2.44.1", "2.44.0", true)] + public void EqualOrHigher_allows_same_or_newer(string actual, string required, bool expected) + => Assert.Equal(expected, VersionComparer.EqualOrHigher(actual, required)); + + // PowerShell [version] semantics: unspecified components are -1, so "8.0" < "8.0.0". + [Fact] + public void EqualOrHigher_respects_unspecified_component_semantics() + => Assert.False(VersionComparer.EqualOrHigher("8.0", "8.0.0")); + + [Theory] + [InlineData("8.0.22", "8.0.22", true)] // desktop runtime exact + [InlineData("8.0.30", "8.0.22", true)] // higher build + [InlineData("8.0.10", "8.0.22", false)] // lower build + [InlineData("9.0.22", "8.0.22", false)] // major differs + public void MajorMinorEqual_BuildRevisionEqualOrHigher(string actual, string required, bool expected) + => Assert.Equal(expected, VersionComparer.MajorMinorEqualBuildRevisionEqualOrHigher(actual, required)); + + [Theory] + [InlineData("16.11.36631.11", "16.11.36631.11", true)] + [InlineData("16.11.36631.20", "16.11.36631.11", true)] // higher revision + [InlineData("16.11.36631.5", "16.11.36631.11", false)] // lower revision + [InlineData("16.11.40000.11", "16.11.36631.11", false)] // build differs + public void MajorMinorBuildEqual_RevisionEqualOrHigher(string actual, string required, bool expected) + => Assert.Equal(expected, VersionComparer.MajorMinorBuildEqualRevisionEqualOrHigher(actual, required)); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Scaffolding/ComponentScaffolderTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Scaffolding/ComponentScaffolderTests.cs new file mode 100644 index 000000000..2bb0dc860 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Scaffolding/ComponentScaffolderTests.cs @@ -0,0 +1,70 @@ +using AXOpen.Dev.Scaffolding; + +namespace AXOpen.Dev.Tests.Scaffolding; + +public sealed class ComponentScaffolderTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), $"axdev-scaffold-{Guid.NewGuid():N}"); + private readonly string _template; + + public ComponentScaffolderTests() + { + _template = Path.Combine(_root, "template", "TemplateComponent"); + Write("TemplateComponent.st", "NAMESPACE Template.Axolibrary\nCLASS TemplateComponent\nEND_CLASS\n"); + Write(Path.Combine("sub", "TemplateComponentThing.cs"), + "namespace Template.Axolibrary;\npublic class TemplateComponentThing {}\n"); + Write("static.txt", "no markers here"); + } + + private void Write(string relative, string content) + { + var full = Path.Combine(_template, relative); + Directory.CreateDirectory(Path.GetDirectoryName(full)!); + File.WriteAllText(full, content); + } + + [Fact] + public void Scaffold_renames_tree_and_replaces_tokens() + { + var dest = Path.Combine(_root, "out"); + var final = ComponentScaffolder.Scaffold(_template, dest, "MyComp", "AXOpen.MyLib"); + + Assert.Equal(Path.Combine(dest, "MyComp"), final); + Assert.True(File.Exists(Path.Combine(final, "MyComp.st"))); + Assert.True(File.Exists(Path.Combine(final, "sub", "MyCompThing.cs"))); + Assert.False(Directory.Exists(Path.Combine(dest, "TemplateComponent"))); + + var st = File.ReadAllText(Path.Combine(final, "MyComp.st")); + Assert.Contains("NAMESPACE AXOpen.MyLib", st); + Assert.Contains("CLASS MyComp", st); + Assert.DoesNotContain("TemplateComponent", st); + + var cs = File.ReadAllText(Path.Combine(final, "sub", "MyCompThing.cs")); + Assert.Contains("namespace AXOpen.MyLib;", cs); + Assert.Contains("class MyCompThing", cs); + + Assert.Equal("no markers here", File.ReadAllText(Path.Combine(final, "static.txt"))); + } + + [Fact] + public void Scaffold_throws_when_source_missing() + => Assert.Throws( + () => ComponentScaffolder.Scaffold(Path.Combine(_root, "nope"), Path.Combine(_root, "out"), "MyComp", "AXOpen.MyLib")); + + [Fact] + public void Scaffold_throws_when_destination_exists() + { + var dest = Path.Combine(_root, "out"); + Directory.CreateDirectory(Path.Combine(dest, "MyComp")); + Assert.Throws( + () => ComponentScaffolder.Scaffold(_template, dest, "MyComp", "AXOpen.MyLib")); + } + + public void Dispose() + { + if (Directory.Exists(_root)) + { + Directory.Delete(_root, recursive: true); + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Scaffolding/NamespaceConverterTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Scaffolding/NamespaceConverterTests.cs new file mode 100644 index 000000000..55d38487e --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Scaffolding/NamespaceConverterTests.cs @@ -0,0 +1,30 @@ +using AXOpen.Dev.Scaffolding; + +namespace AXOpen.Dev.Tests.Scaffolding; + +public class NamespaceConverterTests +{ + [Theory] + [InlineData("org.inxton.mylib.components", "Org.Inxton.Mylib.Components")] + [InlineData("@inxton/axopen.components.foo", "AXOpen.Components.Foo")] + [InlineData("x/AxOpen", "AXOpen")] + [InlineData("axopen", "AXOpen")] + [InlineData("vendor/myLib.Thing", "MyLib.Thing")] // first letter capitalized, rest preserved + public void Converts_to_component_namespace(string input, string expected) + => Assert.Equal(expected, NamespaceConverter.ToComponentNamespace(input)); + + [Theory] + [InlineData("Foo")] + [InlineData("Foo1_bar")] + [InlineData("F")] + public void Valid_component_names(string name) => Assert.True(ComponentName.IsValid(name)); + + [Theory] + [InlineData("foo")] // must start upper + [InlineData("1Foo")] + [InlineData("Foo Bar")] + [InlineData("Foo-Bar")] + [InlineData("")] + [InlineData(null)] + public void Invalid_component_names(string? name) => Assert.False(ComponentName.IsValid(name)); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Utils/CtrlFolderCopierTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Utils/CtrlFolderCopierTests.cs new file mode 100644 index 000000000..ccc7de479 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Utils/CtrlFolderCopierTests.cs @@ -0,0 +1,70 @@ +using AXOpen.Dev.Utils; + +namespace AXOpen.Dev.Tests.Utils; + +public sealed class CtrlFolderCopierTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), $"axdev-ctrl-{Guid.NewGuid():N}"); + + public CtrlFolderCopierTests() + { + // src/LibA/ctrl/a.st + // src/LibA/ctrl/sub/b.st + // src/LibB/inner/ctrl/c.st (preserve LibB/inner relative path) + // src/LibC/Ctrl/d.st (case-insensitive match) + // src/LibD/notctrl/e.st (ignored) + Make("src/LibA/ctrl/a.st"); + Make("src/LibA/ctrl/sub/b.st"); + Make("src/LibB/inner/ctrl/c.st"); + Make("src/LibC/Ctrl/d.st"); + Make("src/LibD/notctrl/e.st"); + } + + private string SourceRoot => Path.Combine(_root, "src"); + + private void Make(string relative) + { + var full = Path.Combine(_root, relative.Replace('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(Path.GetDirectoryName(full)!); + File.WriteAllText(full, "x"); + } + + [Fact] + public void Plan_finds_ctrl_directories_case_insensitively_preserving_structure() + { + var dest = Path.Combine(_root, "out"); + var ops = CtrlFolderCopier.Plan(SourceRoot, dest); + + Assert.Equal(3, ops.Count); + Assert.All(ops, op => Assert.Equal("ctrl", Path.GetFileName(op.Source), ignoreCase: true)); + Assert.Contains(ops, op => op.Destination == Path.Combine(dest, "LibA", "ctrl")); + Assert.Contains(ops, op => op.Destination == Path.Combine(dest, "LibB", "inner", "ctrl")); + Assert.Contains(ops, op => op.Destination == Path.Combine(dest, "LibC", "Ctrl")); + } + + [Fact] + public void Plan_returns_empty_when_source_missing() + => Assert.Empty(CtrlFolderCopier.Plan(Path.Combine(_root, "nope"), Path.Combine(_root, "out"))); + + [Fact] + public void Copy_replicates_files_preserving_nested_structure() + { + var dest = Path.Combine(_root, "out"); + var count = CtrlFolderCopier.Copy(SourceRoot, dest); + + Assert.Equal(3, count); + Assert.True(File.Exists(Path.Combine(dest, "LibA", "ctrl", "a.st"))); + Assert.True(File.Exists(Path.Combine(dest, "LibA", "ctrl", "sub", "b.st"))); + Assert.True(File.Exists(Path.Combine(dest, "LibB", "inner", "ctrl", "c.st"))); + Assert.True(File.Exists(Path.Combine(dest, "LibC", "Ctrl", "d.st"))); + Assert.False(Directory.Exists(Path.Combine(dest, "LibD"))); + } + + public void Dispose() + { + if (Directory.Exists(_root)) + { + Directory.Delete(_root, recursive: true); + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Validation/ArgumentGuardsTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Validation/ArgumentGuardsTests.cs new file mode 100644 index 000000000..5ec704080 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Validation/ArgumentGuardsTests.cs @@ -0,0 +1,45 @@ +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Tests.Validation; + +public class ArgumentGuardsTests +{ + [Theory] + [InlineData("value")] + [InlineData("p@ss$word`with|specials;")] // no special-char rejection exists in the source scripts + public void NotEmpty_accepts_non_empty(string value) + => ArgumentGuards.EnsureNotEmpty("ARG", value); // does not throw + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void NotEmpty_rejects_empty_or_whitespace(string? value) + => Assert.Throws(() => ArgumentGuards.EnsureNotEmpty("ARG", value)); + + [Theory] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("TRUE", true)] + [InlineData("false", false)] + [InlineData("FALSE", false)] + public void PlcSim_flag_parses_case_insensitively(string value, bool expected) + => Assert.Equal(expected, ArgumentGuards.ParsePlcSim(value)); + + [Theory] + [InlineData("yes")] + [InlineData("1")] + [InlineData("")] + [InlineData(null)] + public void PlcSim_flag_rejects_other_values(string? value) + => Assert.Throws(() => ArgumentGuards.ParsePlcSim(value)); + + [Theory] + [InlineData("true", true)] + [InlineData("True", false)] // bash `[ "$8" = "true" ]` is case-sensitive + [InlineData("false", false)] + [InlineData("anything", false)] + [InlineData(null, false)] + public void Force_flag_is_exact_lowercase_true(string? value, bool expected) + => Assert.Equal(expected, ArgumentGuards.ParseForce(value)); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Validation/IpValidatorTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Validation/IpValidatorTests.cs new file mode 100644 index 000000000..a46bfb4ec --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Validation/IpValidatorTests.cs @@ -0,0 +1,45 @@ +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Tests.Validation; + +public class IpValidatorTests +{ + [Theory] + [InlineData("0.0.0.0")] + [InlineData("192.168.1.1")] + [InlineData("10.10.10.120")] + [InlineData("255.255.255.255")] + [InlineData("1.2.3.4")] + public void Valid_ipv4_addresses_pass(string ip) + => Assert.True(IpValidator.IsValidIp(ip)); + + [Theory] + [InlineData("256.0.0.1")] // octet > 255 + [InlineData("192.168.1")] // only 3 octets + [InlineData("192.168.1.1.1")] // 5 octets + [InlineData("192.168.1.1234")] // 4-digit group + [InlineData("abc.def.ghi.jkl")] + [InlineData("192.168.1.")] + [InlineData("")] + [InlineData("192.168.1.1/24")] // CIDR is not a bare IP + public void Invalid_ipv4_addresses_fail(string ip) + => Assert.False(IpValidator.IsValidIp(ip)); + + [Theory] + [InlineData("192.168.1.0/24")] + [InlineData("10.0.0.0/8")] + [InlineData("1.2.3.4/0")] + [InlineData("1.2.3.4/32")] + [InlineData("172.16.0.0/29")] + public void Valid_cidr_pass(string cidr) + => Assert.True(IpValidator.IsValidCidr(cidr)); + + [Theory] + [InlineData("192.168.1.0/33")] // prefix > 32 + [InlineData("192.168.1.0")] // no prefix + [InlineData("256.1.1.1/24")] // bad octet + [InlineData("192.168.1.0/")] + [InlineData("192.168.1.0/-1")] + public void Invalid_cidr_fail(string cidr) + => Assert.False(IpValidator.IsValidCidr(cidr)); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Validation/MacValidatorTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Validation/MacValidatorTests.cs new file mode 100644 index 000000000..9eb2cddda --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Validation/MacValidatorTests.cs @@ -0,0 +1,21 @@ +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Tests.Validation; + +public class MacValidatorTests +{ + [Theory] + [InlineData("00:1B:44:11:3A:B7")] + [InlineData("00-1b-44-11-3a-b7")] + [InlineData("aa:bb:cc:dd:ee:ff")] + public void Valid_macs(string mac) => Assert.True(MacValidator.IsValid(mac)); + + [Theory] + [InlineData("00:1B:44:11:3A")] // too short + [InlineData("00:1B:44:11:3A:B7:C9")] // too long + [InlineData("ZZ:1B:44:11:3A:B7")] // non-hex + [InlineData("001B.4411.3AB7")] // wrong separators + [InlineData("")] + [InlineData(null)] + public void Invalid_macs(string? mac) => Assert.False(MacValidator.IsValid(mac)); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Validation/PasswordValidatorTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Validation/PasswordValidatorTests.cs new file mode 100644 index 000000000..66b8d7058 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Validation/PasswordValidatorTests.cs @@ -0,0 +1,25 @@ +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Tests.Validation; + +public class PasswordValidatorTests +{ + [Theory] + [InlineData("Qwerty123456+")] // the showcase password — '+' is allowed + [InlineData("Abc123-_=.~")] + [InlineData("PlainPassword1")] + public void Safe_passwords_pass(string pwd) => Assert.True(PasswordValidator.IsSafe(pwd)); + + [Theory] + [InlineData("has space")] + [InlineData("dollar$ign")] + [InlineData("back`tick")] + [InlineData("pipe|d")] + [InlineData("semi;colon")] + [InlineData("quote\"d")] + [InlineData("star*")] + [InlineData("brace{}")] + [InlineData("")] + [InlineData(null)] + public void Unsafe_passwords_fail(string? pwd) => Assert.False(PasswordValidator.IsSafe(pwd)); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/AXOpen.Dev.Tool.csproj b/src/axopen.dev/AXOpen.Dev.Tool/AXOpen.Dev.Tool.csproj new file mode 100644 index 000000000..23f9208fa --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/AXOpen.Dev.Tool.csproj @@ -0,0 +1,25 @@ + + + + Exe + enable + enable + AXOpen.Dev.Tool + axdev + + true + axdev + AXOpen.Dev.Tool + AXOpen developer CLI (axdev): SIMATIC-AX hardware/software workflow, secure communication, diagnostics and scaffolding. + + + + + + + + + + + + diff --git a/src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs b/src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs new file mode 100644 index 000000000..834b4d8a0 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs @@ -0,0 +1,140 @@ +using AXOpen.Dev.Tool.Commands; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool; + +/// +/// Builds the shared used by both the packed dotnet tool +/// (axdev) and the in-repo file-based dispatcher (src/scripts/axdev.cs). +/// Descriptive verb names are canonical; the apax aliases are real invokable aliases. +/// +public static class AxdevApp +{ + public static CommandApp Build() + { + var app = new CommandApp(); + app.Configure(config => + { + config.SetApplicationName("axdev"); + + config.AddCommand("validate-ip") + .WithDescription("Validate an IPv4 address."); + + config.AddCommand("restart-plc") + .WithAlias("r") + .WithDescription("Restart the PLC (STOP then RUN). apax alias: r"); + + config.AddCommand("clean-plc") + .WithAlias("clean_plc") + .WithDescription("Reset the PLC keeping only its IP. apax alias: clean_plc"); + + config.AddCommand("reset-plc") + .WithAlias("reset_plc") + .WithDescription("Full factory reset of the PLC. apax alias: reset_plc"); + + config.AddCommand("hw-diag") + .WithAlias("hdl") + .WithDescription("List hardware diagnostics from the PLC. apax alias: hdl"); + + config.AddCommand("cert-check") + .WithDescription("Compare stored certificate SHA1 with the PLC's certificate."); + + config.AddCommand("copy-hardware-ids") + .WithAlias("hwid") + .WithDescription("Generate HwIdentifiers.st + HwIdentifierList.st. apax alias: hwid"); + + config.AddCommand("copy-io-addresses") + .WithAlias("hwadr") + .WithDescription("Generate Inputs.st + Outputs.st + IoStructures.st. apax alias: hwadr"); + + config.AddCommand("hw-compile") + .WithAlias("hwcc") + .WithDescription("Compile the hardware configuration. apax alias: hwcc"); + + config.AddCommand("install-gsd") + .WithAlias("gsd") + .WithDescription("Copy and install GSDML files. apax alias: gsd"); + + config.AddCommand("copy-hwl-templates") + .WithAlias("hwl") + .WithDescription("Copy hardware library templates. apax alias: hwl"); + + config.AddCommand("setup-secure-communication") + .WithAlias("ssc") + .WithDescription("Set up secure communication + certificates. apax alias: ssc"); + + config.AddCommand("hw-first-download") + .WithAlias("hwfd") + .WithDescription("First-time HW provisioning + download. apax alias: hwfd"); + + config.AddCommand("hw-first-download-only") + .WithAlias("hwfdo") + .WithDescription("First HW download with master password + pull cert. apax alias: hwfdo"); + + config.AddCommand("sw-download-full") + .WithAlias("swfdo") + .WithDescription("Full software download via certificate. apax alias: swfdo"); + + config.AddCommand("sw-build-download-full") + .WithAlias("swfd") + .WithDescription("Build + full software download. apax alias: swfd"); + + config.AddCommand("sw-download-delta") + .WithAlias("swddo") + .WithDescription("Delta software download via certificate. apax alias: swddo"); + + config.AddCommand("sw-build-download-delta") + .WithAlias("swdd") + .WithDescription("Build + delta software download. apax alias: swdd"); + + config.AddCommand("hw-download-only") + .WithAlias("hwdo") + .WithDescription("Download compiled HW using certificate. apax alias: hwdo"); + + config.AddCommand("dcp-discover") + .WithAlias("dcpd") + .WithDescription("Discover PNIO devices from a source MAC. apax alias: dcpd"); + + config.AddCommand("dcp-list-interfaces") + .WithAlias("dcpli") + .WithDescription("List network interfaces. apax alias: dcpli"); + + config.AddCommand("plcsim") + .WithDescription("Start PLCSIM Advanced (Windows only). apax alias: plcsim"); + + config.AddCommand("hw-update") + .WithAlias("hwu") + .WithDescription("Update HW: gsd + templates + compile + cert download. apax alias: hwu"); + + config.AddCommand("compare-all") + .WithAlias("cpa") + .WithDescription("Compare online vs offline software (exit 0/9/10/11). apax alias: cpa"); + + config.AddCommand("check-requisites") + .WithDescription("Check apax / NuGet / custom registry prerequisites."); + + config.AddCommand("copy-ctrl-folders") + .WithDescription("Copy every 'ctrl' directory under a source tree into a destination."); + + config.AddCommand("check-system-requisites") + .WithDescription("Verify/install developer-machine prerequisites (full check_requisites.ps1 port)."); + + config.AddCommand("compile-all") + .WithAlias("cla") + .WithDescription("Compile HW + SW and pull certificate. apax alias: cla"); + + config.AddCommand("compile-all-compare-all") + .WithAlias("cca") + .WithDescription("Compile everything then compare. apax alias: cca"); + + config.AddCommand("all-first") + .WithAlias("alf") + .WithDescription("Full initial PLC bring-up (HW + SW). apax alias: alf"); + + config.AddCommand("all") + .WithAlias("a") + .WithDescription("Smart update (first-setup / fast update / regenerate). apax alias: a"); + }); + return app; + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/DiagnosticsVerbs.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/DiagnosticsVerbs.cs new file mode 100644 index 000000000..548746d18 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/DiagnosticsVerbs.cs @@ -0,0 +1,34 @@ +using System.ComponentModel; +using AXOpen.Dev.Apax; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Process; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// Settings for verbs that also need the PLC name (to locate its certificate). +public class NamedPlcSettings : PlcCommandSettings +{ + [CommandOption("-n|--name ")] + [Description("PLC name (locates ./certs//.cer).")] + public string Name { get; init; } = string.Empty; +} + +/// axdev hw-diag — port of hw_diag_list.sh. apax alias: hdl. +[Description("List hardware diagnostic information from the PLC (apax hw-diag list).")] +public sealed class HwDiagVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, NamedPlcSettings settings) + => new HwDiagListCommand(new ApaxClient(new ProcessRunner())) + .ExecuteAsync(new PlcTarget(settings.IpAddress, settings.Name, settings.Username, settings.ResolvePassword())); +} + +/// axdev cert-check — port of is_cert_hash_sha1_equal.sh. +[Description("Compare the stored certificate SHA1 against the certificate on the PLC.")] +public sealed class CertCheckVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, NamedPlcSettings settings) + => new CertHashCheckCommand(new ApaxClient(new ProcessRunner())) + .ExecuteAsync(new PlcTarget(settings.IpAddress, settings.Name, settings.Username, settings.ResolvePassword())); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/FirstSetupVerbs.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/FirstSetupVerbs.cs new file mode 100644 index 000000000..f5b8d2915 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/FirstSetupVerbs.cs @@ -0,0 +1,98 @@ +using System.ComponentModel; +using AXOpen.Dev.Apax; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Process; +using AXOpen.Dev.Tools; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +internal static class Clients +{ + public static ApaxClient Apax() => new(new ProcessRunner()); + public static OpensslClient Openssl() => new(new ProcessRunner()); + public static DotnetClient Dotnet() => new(new ProcessRunner()); +} + +/// axdev hw-compile — apax hwc compile. apax alias: hwcc. +[Description("Compile the hardware configuration (apax hwc compile). apax alias: hwcc")] +public sealed class HwCompileVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context) + => new HwCompileCommand(Clients.Apax()).ExecuteAsync(); +} + +/// axdev install-gsd — copy + install GSDML files. apax alias: gsd. +[Description("Copy and install GSDML files from library assets. apax alias: gsd")] +public sealed class GsdVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context) + => new GsdInstallCommand(Clients.Apax()).ExecuteAsync(); +} + +/// axdev copy-hwl-templates — copy HWL templates. apax alias: hwl. +[Description("Copy hardware library templates from library assets. apax alias: hwl")] +public sealed class HwlVerb : Command +{ + public override int Execute(CommandContext context) => new HwlCopyCommand().Execute(); +} + +/// axdev setup-secure-communication — port of setup_secure_communication.sh. apax alias: ssc. +[Description("Create certificates and configure secure communication on the PLC. apax alias: ssc")] +public sealed class SetupSecureCommVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, NamedPlcSettings settings) + => new SetupSecureCommunicationCommand(Clients.Apax(), Clients.Openssl()) + .ExecuteAsync(settings.Name, settings.Username, settings.ResolvePassword(), settings.IpAddress); +} + +/// axdev hw-first-download — port of hw_first_download.sh. apax alias: hwfd. +[Description("First-time HW provisioning: gsd + templates + secure comms + compile + download + cert. apax alias: hwfd")] +public sealed class HwFirstDownloadVerb : AsyncCommand +{ + public sealed class Settings : NamedPlcSettings + { + [CommandOption("--namespace ")] + [Description("Default namespace (DEFAULT_NAMESPACE).")] + public string Namespace { get; init; } = string.Empty; + } + + public override Task ExecuteAsync(CommandContext context, Settings settings) + => new HwFirstDownloadCommand(Clients.Apax(), Clients.Openssl()) + .ExecuteAsync(settings.Namespace, settings.Name, settings.IpAddress, settings.Username, settings.ResolvePassword()); +} + +/// axdev hw-first-download-only — port of hw_first_download_only.sh. apax alias: hwfdo. +[Description("First HW download with master password + pull certificate. apax alias: hwfdo")] +public sealed class HwFirstDownloadOnlyVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, NamedPlcSettings settings) + => new HwFirstDownloadOnlyCommand(Clients.Apax()) + .ExecuteAsync(settings.Name, settings.IpAddress, settings.ResolvePassword()); +} + +/// axdev sw-download-full — port of sw_download_full.sh. apax alias: swfdo. +[Description("Full software download via certificate (apax sld load --mode FULL --restart). apax alias: swfdo")] +public sealed class SwDownloadFullVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, SwVerbSettings settings) + => new SwDownloadFullCommand(Clients.Apax()) + .ExecuteAsync(settings.Name, settings.IpAddress, settings.Platform, settings.Username, settings.ResolvePassword()); +} + +/// axdev sw-build-download-full — port of sw_build_and_download_full.sh. apax alias: swfd. +[Description("Build software and full-download it to the PLC. apax alias: swfd")] +public sealed class SwBuildDownloadFullVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, SwVerbSettings settings) + => new SwBuildDownloadFullCommand(Clients.Apax(), Clients.Dotnet()) + .ExecuteAsync(settings.Name, settings.IpAddress, settings.Platform, settings.Username, settings.ResolvePassword()); +} + +/// Settings for software download verbs (adds platform input path). +public class SwVerbSettings : NamedPlcSettings +{ + [CommandOption("--platform ")] + [Description("Platform output directory (AXTARGETPLATFORMINPUT, e.g. .\\bin\\1500\\).")] + public string Platform { get; init; } = string.Empty; +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/HardwareGenVerbs.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/HardwareGenVerbs.cs new file mode 100644 index 000000000..0501f4451 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/HardwareGenVerbs.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using AXOpen.Dev.Commands; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// Settings carrying the namespace + PLC name used by the ST generators. +public class HardwareGenSettings : CommandSettings +{ + [CommandArgument(0, "")] + [Description("Default namespace for the generated types (DEFAULT_NAMESPACE).")] + public string Namespace { get; init; } = string.Empty; + + [CommandArgument(1, "")] + [Description("PLC name (selects SystemConstants/_*.st).")] + public string PlcName { get; init; } = string.Empty; +} + +/// axdev copy-hardware-ids — port of copy_hardware_ids.sh. apax alias: hwid. +[Description("Generate src/IO/HwIdentifiers.st + HwIdentifierList.st. apax alias: hwid")] +public sealed class CopyHardwareIdsVerb : Command +{ + public override int Execute(CommandContext context, HardwareGenSettings settings) + => new CopyHardwareIdsCommand().Execute(settings.Namespace, settings.PlcName); +} + +/// axdev copy-io-addresses — port of copy_io_addresses.sh. apax alias: hwadr. +[Description("Generate src/IO/Inputs.st + Outputs.st + IoStructures.st. apax alias: hwadr")] +public sealed class CopyIoAddressesVerb : Command +{ + public override int Execute(CommandContext context, HardwareGenSettings settings) + => new CopyIoAddressesCommand().Execute(settings.Namespace, settings.PlcName); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/OrchestratorVerbs.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/OrchestratorVerbs.cs new file mode 100644 index 000000000..8f98e002b --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/OrchestratorVerbs.cs @@ -0,0 +1,95 @@ +using System.ComponentModel; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Process; +using AXOpen.Dev.Requisites; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// Settings for the multi-step orchestrators. +public class OrchestratorSettings : SwVerbSettings +{ + [CommandOption("--namespace ")] + [Description("Default namespace (DEFAULT_NAMESPACE).")] + public string Namespace { get; init; } = string.Empty; + + [CommandOption("--use-plc-sim")] + [Description("Start PLCSIM Advanced first (USE_PLC_SIM_ADVANCED).")] + public bool UsePlcSim { get; init; } + + [CommandOption("--force")] + [Description("Force-regenerate certificates (delete ./certs and ./hwc/hwc.gen first).")] + public bool Force { get; init; } +} + +/// axdev all-first — port of all_first.sh. apax alias: alf. +[Description("Full initial bring-up of a PLC (HW + SW). apax alias: alf")] +public sealed class AllFirstVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, OrchestratorSettings s) + => new AllFirstCommand(Clients.Apax(), Clients.Openssl(), Clients.Dotnet()) + .ExecuteAsync(s.Namespace, s.Name, s.IpAddress, s.Platform, s.Username, s.ResolvePassword(), s.UsePlcSim, s.Force); +} + +/// axdev all — smart update dispatcher. Port of all.sh. apax alias: a. +[Description("Smart update: first-setup, fast update, or regenerate based on certificate state. apax alias: a")] +public sealed class AllVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, OrchestratorSettings s) + => new AllCommand(Clients.Apax(), Clients.Openssl(), Clients.Dotnet()) + .ExecuteAsync(s.Namespace, s.Name, s.IpAddress, s.Platform, s.Username, s.ResolvePassword(), s.UsePlcSim, s.Force); +} + +/// axdev compile-all — port of compile_all.sh. apax alias: ca. +[Description("Compile HW + SW and pull the PLC certificate (no download). apax alias: ca")] +public sealed class CompileAllVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, OrchestratorSettings s) + => new CompileAllCommand(Clients.Apax(), Clients.Openssl(), Clients.Dotnet()) + .ExecuteAsync(s.Namespace, s.Name, s.IpAddress, s.Platform, s.Username, s.ResolvePassword(), s.UsePlcSim); +} + +/// axdev compile-all-compare-all — port of compile_all_compare_all.sh. apax alias: cca. +[Description("Compile everything, then compare online vs offline. apax alias: cca")] +public sealed class CompileAllCompareAllVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, OrchestratorSettings s) + => new CompileAllCompareAllCommand(Clients.Apax(), Clients.Openssl(), Clients.Dotnet()) + .ExecuteAsync(s.Namespace, s.Name, s.IpAddress, s.Platform, s.Username, s.ResolvePassword(), s.UsePlcSim); +} + +/// axdev compare-all — port of compare_all.sh. +[Description("Compare online (PLC) vs offline (compiled) software. Exit 0/9/10/11.")] +public sealed class CompareAllVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, SwVerbSettings s) + => new CompareAllCommand(Clients.Apax()) + .ExecuteAsync(s.Name, s.IpAddress, s.Platform, s.Username, s.ResolvePassword()); +} + +/// axdev hw-update — port of hw_update.sh. apax alias: hwu. +[Description("Update HW: gsd + templates + compile + download via certificate. apax alias: hwu")] +public sealed class HwUpdateVerb : AsyncCommand +{ + public sealed class Settings : NamedPlcSettings + { + [CommandOption("--namespace ")] + public string Namespace { get; init; } = string.Empty; + } + + public override Task ExecuteAsync(CommandContext context, Settings s) + => new HwUpdateCommand(Clients.Apax()) + .ExecuteAsync(s.Namespace, s.Name, s.IpAddress, s.Username, s.ResolvePassword()); +} + +/// axdev check-requisites — apax/nuget/custom-registry prerequisite checks. +[Description("Check apax, NuGet feed and custom NPM registry prerequisites (report-only).")] +public sealed class CheckRequisitesVerb : AsyncCommand +{ + public override async Task ExecuteAsync(CommandContext context) + { + var checker = new RequisiteChecker(new ProcessRunner()); + var ok = await checker.CheckApaxAsync() & await checker.CheckNugetAsync() & checker.CheckCustomRegistry(); + return ok ? 0 : 1; + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/PlcCommandSettings.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/PlcCommandSettings.cs new file mode 100644 index 000000000..7ec1f1522 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/PlcCommandSettings.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// +/// Shared settings for PLC-targeting verbs. Passwords prefer the AX_TARGET_PWD environment +/// variable; the positional/option value is a deprecated back-compat fallback. +/// +public class PlcCommandSettings : CommandSettings +{ + [CommandOption("-t|--target ")] + [Description("PLC IP address (AXTARGET).")] + public string IpAddress { get; init; } = string.Empty; + + [CommandOption("-u|--username ")] + [Description("PLC username (AX_USERNAME).")] + public string Username { get; init; } = string.Empty; + + [CommandOption("-p|--password ")] + [Description("PLC password. Prefer the AX_TARGET_PWD environment variable; this option is a deprecated fallback.")] + public string? Password { get; init; } + + /// Resolves the password from AX_TARGET_PWD first, then the option value. + public string ResolvePassword() + => Environment.GetEnvironmentVariable("AX_TARGET_PWD") is { Length: > 0 } env ? env : Password ?? string.Empty; +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/ResetPlcVerbs.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/ResetPlcVerbs.cs new file mode 100644 index 000000000..3e5356d99 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/ResetPlcVerbs.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using AXOpen.Dev.Apax; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Process; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// axdev clean-plc — port of clean_plc.sh (reset keeping only the IP). +[Description("Reset the PLC keeping only its IP address (apax hwld --reset-plc KeepOnlyIP).")] +public sealed class CleanPlcVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, PlcCommandSettings settings) + => new ResetPlcCommand(new ApaxClient(new ProcessRunner())) + .ExecuteAsync(ResetScope.KeepOnlyIp, settings.IpAddress, settings.Username, settings.ResolvePassword()); +} + +/// axdev reset-plc — port of reset_plc.sh (full factory reset). +[Description("Full factory reset of the PLC including IP and name (apax hwld --reset-plc All).")] +public sealed class ResetPlcVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, PlcCommandSettings settings) + => new ResetPlcCommand(new ApaxClient(new ProcessRunner())) + .ExecuteAsync(ResetScope.All, settings.IpAddress, settings.Username, settings.ResolvePassword()); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/RestartPlcVerb.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/RestartPlcVerb.cs new file mode 100644 index 000000000..f6d2fed23 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/RestartPlcVerb.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using AXOpen.Dev.Apax; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Process; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// axdev restart-plc — port of restart_PLC.sh (STOP then RUN via certificate). +[Description("Restart the PLC by switching it to STOP then back to RUN, using certificate auth.")] +public sealed class RestartPlcVerb : AsyncCommand +{ + public sealed class Settings : PlcCommandSettings + { + [CommandOption("-n|--name ")] + [Description("PLC name (used to locate ./certs//.cer).")] + public string Name { get; init; } = string.Empty; + } + + public override Task ExecuteAsync(CommandContext context, Settings settings) + => new RestartPlcCommand(new ApaxClient(new ProcessRunner())) + .ExecuteAsync(new PlcTarget(settings.IpAddress, settings.Name, settings.Username, settings.ResolvePassword())); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/UtilityVerbs.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/UtilityVerbs.cs new file mode 100644 index 000000000..e66bf0c7c --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/UtilityVerbs.cs @@ -0,0 +1,51 @@ +using System.ComponentModel; +using AXOpen.Dev.Commands; +using AXOpen.Dev.Process; +using AXOpen.Dev.Requisites; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// axdev copy-ctrl-folders — port of scripts/copy-ctrl-folders.ps1. +[Description("Copy every 'ctrl' directory under a source tree into a destination, preserving structure.")] +public sealed class CopyCtrlFoldersVerb : Command +{ + public sealed class Settings : CommandSettings + { + [CommandArgument(0, "")] + [Description("Target root directory the ctrl folders are copied into.")] + public string Destination { get; init; } = string.Empty; + + [CommandOption("-s|--source ")] + [Description("Source root to scan (defaults to ./src).")] + public string Source { get; init; } = string.Empty; + } + + public override int Execute(CommandContext context, Settings settings) + => new CopyCtrlFoldersCommand().Execute(settings.Source, settings.Destination); +} + +/// axdev check-system-requisites — full faithful port of scripts/check_requisites.ps1. +[Description("Verify (and optionally install) the developer-machine prerequisites: Node, VC++, Git, .NET SDK + Desktop Runtime, VS Build Tools, AX Code, Apax, registries.")] +public sealed class CheckSystemRequisitesVerb : AsyncCommand +{ + public sealed class Settings : CommandSettings + { + [CommandOption("-y|--yes")] + [Description("Answer 'yes' to every install/set prompt (unattended install).")] + public bool AssumeYes { get; init; } + + [CommandOption("--non-interactive")] + [Description("Answer 'no' to every prompt (report-only; never installs).")] + public bool NonInteractive { get; init; } + } + + public override Task ExecuteAsync(CommandContext context, Settings settings) + { + IUserPrompt prompt = settings.AssumeYes ? new FixedUserPrompt(true) + : settings.NonInteractive ? new FixedUserPrompt(false) + : new ConsoleUserPrompt(); + + return new SystemRequisitesChecker(new ProcessRunner(), prompt, new HttpFileDownloader()).RunAsync(); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/ValidateIpCommand.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/ValidateIpCommand.cs new file mode 100644 index 000000000..87c093b1e --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/ValidateIpCommand.cs @@ -0,0 +1,28 @@ +using System.ComponentModel; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Validation; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// axdev validate-ip <ip> — port of validate_ip.sh. +public sealed class ValidateIpCommand : Command +{ + public sealed class Settings : CommandSettings + { + [CommandArgument(0, "")] + [Description("IPv4 address to validate.")] + public string Ip { get; init; } = string.Empty; + } + + public override int Execute(CommandContext context, Settings settings) + { + if (IpValidator.IsValidIp(settings.Ip)) + { + return 0; + } + + Output.Error($"The input parameter '{settings.Ip}' is not a valid IP address."); + return 1; + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Commands/WrapperVerbs.cs b/src/axopen.dev/AXOpen.Dev.Tool/Commands/WrapperVerbs.cs new file mode 100644 index 000000000..484992d88 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Commands/WrapperVerbs.cs @@ -0,0 +1,76 @@ +using System.ComponentModel; +using AXOpen.Dev.Commands; +using Spectre.Console.Cli; + +namespace AXOpen.Dev.Tool.Commands; + +/// axdev dcp-discover <mac> — port of dcp_utility_discover.sh. apax alias: dcpd. +[Description("Discover PNIO devices from a source MAC → ./dcp_export/devices.json. apax alias: dcpd")] +public sealed class DcpDiscoverVerb : AsyncCommand +{ + public sealed class Settings : CommandSettings + { + [CommandArgument(0, "")] + [Description("Source adapter MAC address.")] + public string Mac { get; init; } = string.Empty; + } + + public override Task ExecuteAsync(CommandContext context, Settings settings) + => new DcpDiscoverCommand(Clients.Apax()).ExecuteAsync(settings.Mac); +} + +/// axdev dcp-list-interfaces — port of dcp_utility_list_interfaces.sh. apax alias: dcpli. +[Description("List network interfaces → ./dcp_export/interfaces.json. apax alias: dcpli")] +public sealed class DcpListInterfacesVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context) + => new DcpListInterfacesCommand(Clients.Apax()).ExecuteAsync(); +} + +/// axdev hw-download-only — port of hw_download_only.sh. apax alias: hwdo. +[Description("Download compiled HW using certificate. apax alias: hwdo")] +public sealed class HwDownloadOnlyVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, NamedPlcSettings settings) + => new HwDownloadOnlyCommand(Clients.Apax()) + .ExecuteAsync(settings.Name, settings.IpAddress, settings.Username, settings.ResolvePassword()); +} + +/// axdev sw-download-delta — port of sw_download_delta.sh. apax alias: swddo. +[Description("Delta software download via certificate. apax alias: swddo")] +public sealed class SwDownloadDeltaVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, SwVerbSettings settings) + => new SwDownloadDeltaCommand(Clients.Apax()) + .ExecuteAsync(settings.Name, settings.IpAddress, settings.Platform, settings.Username, settings.ResolvePassword()); +} + +/// axdev sw-build-download-delta — port of sw_build_and_download_delta.sh. apax alias: swdd. +[Description("Build software and delta-download it to the PLC. apax alias: swdd")] +public sealed class SwBuildDownloadDeltaVerb : AsyncCommand +{ + public override Task ExecuteAsync(CommandContext context, SwVerbSettings settings) + => new SwBuildDownloadDeltaCommand(Clients.Apax(), Clients.Dotnet()) + .ExecuteAsync(settings.Name, settings.IpAddress, settings.Platform, settings.Username, settings.ResolvePassword()); +} + +/// axdev plcsim — port of plcsimadvanced.sh. apax alias: plcsim. +[Description("Start PLCSIM Advanced (Windows only; no-op elsewhere). apax alias: plcsim")] +public sealed class PlcSimVerb : AsyncCommand +{ + public sealed class Settings : CommandSettings + { + [CommandOption("-x|--instance ")] + [Description("Instance name (APAX_YML_NAME).")] + public string Instance { get; init; } = string.Empty; + + [CommandOption("-n|--name ")] + public string Name { get; init; } = string.Empty; + + [CommandOption("-t|--target ")] + public string Target { get; init; } = string.Empty; + } + + public override Task ExecuteAsync(CommandContext context, Settings settings) + => new PlcSimCommand(Clients.Dotnet()).ExecuteAsync(settings.Instance, settings.Name, settings.Target); +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Program.cs b/src/axopen.dev/AXOpen.Dev.Tool/Program.cs new file mode 100644 index 000000000..57257019a --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tool/Program.cs @@ -0,0 +1,3 @@ +using AXOpen.Dev.Tool; + +return AxdevApp.Build().Run(args); diff --git a/src/axopen.dev/AXOpen.Dev/AXOpen.Dev.csproj b/src/axopen.dev/AXOpen.Dev/AXOpen.Dev.csproj new file mode 100644 index 000000000..5c1be3ec3 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/AXOpen.Dev.csproj @@ -0,0 +1,16 @@ + + + + enable + enable + AXOpen.Dev + AXOpen.Dev + false + + + + + + + + diff --git a/src/axopen.dev/AXOpen.Dev/Apax/ApaxClient.cs b/src/axopen.dev/AXOpen.Dev/Apax/ApaxClient.cs new file mode 100644 index 000000000..9164a7dd0 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Apax/ApaxClient.cs @@ -0,0 +1,307 @@ +using AXOpen.Dev.Process; + +namespace AXOpen.Dev.Apax; + +public enum PlcMode +{ + Stop, + Run, +} + +public enum ResetScope +{ + /// apax --reset-plc KeepOnlyIP — clears everything except the IP. + KeepOnlyIp, + + /// apax --reset-plc All — full factory reset. + All, +} + +/// +/// Thin, cross-platform wrapper over the apax CLI. Each method maps 1:1 to the command +/// lines used by the bash scripts so behavior is preserved. Process execution is delegated to +/// so callers are unit-testable with a fake. +/// +public sealed class ApaxClient(IProcessRunner runner) +{ + public const string Executable = "apax"; + + /// The underlying process runner (used to build sibling clients like the requisite checker). + public IProcessRunner Runner => runner; + + /// apax plc-info set-mode {STOP|RUN} --target IP --username U --password P --certificate CERT --no-input + public Task SetModeAsync(PlcMode mode, string ipAddress, string username, string password, string certificatePath, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "plc-info", "set-mode", mode == PlcMode.Stop ? "STOP" : "RUN", + "--target", ipAddress, + "--username", username, + "--password", password, + "--certificate", certificatePath, + "--no-input", + }, + }, ct); + + /// apax hw-diag list --target IP --username U --password P --certificate CERT + public Task HwDiagListAsync(string ipAddress, string username, string password, string certificatePath, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "hw-diag", "list", + "--target", ipAddress, + "--username", username, + "--password", password, + "--certificate", certificatePath, + }, + }, ct); + + /// apax plc-cert -t IP -o OUTPUT — pulls the PLC's public certificate. + public Task PullCertificateAsync(string ipAddress, string outputPath, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] { "plc-cert", "-t", ipAddress, "-o", outputPath }, + }, ct); + + /// echo y | apax hwld load --target IP --reset-plc {KeepOnlyIP|All} --username U --password P --accept-security-disclaimer + public Task ResetAsync(ResetScope scope, string ipAddress, string username, string password, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "hwld", "load", + "--target", ipAddress, + "--reset-plc", scope == ResetScope.KeepOnlyIp ? "KeepOnlyIP" : "All", + "--username", username, + "--password", password, + "--accept-security-disclaimer", + }, + StandardInput = "y\n", + }, ct); + + // ---- Hardware configuration ------------------------------------------------------------ + + /// apax hwc compile -i .\hwc -o bin/hwc/ + public Task HwcCompileAsync(CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] { "hwc", "compile", "-i", ".\\hwc", "-o", "bin/hwc/" }, + }, ct); + + /// apax hwc install-gsd --input ./gsd + public Task HwcInstallGsdAsync(string gsdDirectory, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] { "hwc", "install-gsd", "--input", gsdDirectory }, + }, ct); + + // ---- Secure communication -------------------------------------------------------------- + + /// apax hwc setup-secure-communication --module-name X --no-input --input .\hwc --master-password P + public Task HwcSetupSecureCommunicationAsync(string plcName, string masterPassword, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "hwc", "setup-secure-communication", + "--module-name", plcName, + "--no-input", + "--input", ".\\hwc", + "--master-password", masterPassword, + }, + }, ct); + + /// apax hwc import-certificate --module-name X --input .\hwc --certificate P12 --passphrase P --purpose TLS|WebServer + public Task HwcImportCertificateAsync(string plcName, string certificatePath, string passphrase, string purpose, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "hwc", "import-certificate", + "--module-name", plcName, + "--input", ".\\hwc", + "--certificate", certificatePath, + "--passphrase", passphrase, + "--purpose", purpose, + }, + }, ct); + + /// apax hwc set-accessprotection-password --module-name X --input .\hwc --level FullAccess --password P + public Task HwcSetAccessProtectionPasswordAsync(string plcName, string password, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "hwc", "set-accessprotection-password", + "--module-name", plcName, + "--input", ".\\hwc", + "--level", "FullAccess", + "--password", password, + }, + }, ct); + + /// apax hwc manage-users --module-name X --input .\hwc set-password --username U --password P + public Task HwcSetUserPasswordAsync(string plcName, string username, string password, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "hwc", "manage-users", + "--module-name", plcName, + "--input", ".\\hwc", + "set-password", + "--username", username, + "--password", password, + }, + }, ct); + + // ---- Download / certificate ------------------------------------------------------------ + + /// echo y | apax hwld load --input bin/hwc/X --target IP --master-password P --accept-security-disclaimer --log Information + public Task HwldFirstDownloadAsync(string plcName, string ipAddress, string masterPassword, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "hwld", "load", + "--input", $"bin/hwc/{plcName}", + "--target", ipAddress, + "--master-password", masterPassword, + "--accept-security-disclaimer", + "--log", "Information", + }, + StandardInput = "y\n", + }, ct); + + /// apax plc-cert --target IP --output OUTPUT — retrieves the PLC certificate to a file. + public Task PullCertificateToFileAsync(string ipAddress, string outputPath, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] { "plc-cert", "--target", ipAddress, "--output", outputPath }, + }, ct); + + /// apax sld load --mode FULL --target IP --input PLATFORM --username U --password P --certificate CERT --restart --accept-security-disclaimer + public Task SldLoadFullAsync(string ipAddress, string platform, string username, string password, string certificatePath, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "sld", "load", + "--mode", "FULL", + "--target", ipAddress, + "--input", platform, + "--username", username, + "--password", password, + "--certificate", certificatePath, + "--restart", + "--accept-security-disclaimer", + }, + }, ct); + + /// apax sld load ... --accept-reinit-variables --restart --mode delta + public Task SldLoadDeltaAsync(string ipAddress, string platform, string username, string password, string certificatePath, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "sld", "load", + "--accept-security-disclaimer", + "--target", ipAddress, + "--input", platform, + "--username", username, + "--password", password, + "--certificate", certificatePath, + "--accept-reinit-variables", + "--restart", + "--mode", "delta", + }, + }, ct); + + /// apax hwld load --input bin/hwc/X --target IP --username U --password P --certificate CERT --no-input --accept-security-disclaimer --log Information --restart + public Task HwldDownloadAsync(string plcName, string ipAddress, string username, string password, string certificatePath, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "hwld", "load", + "--input", $"bin/hwc/{plcName}", + "--target", ipAddress, + "--username", username, + "--password", password, + "--certificate", certificatePath, + "--no-input", + "--accept-security-disclaimer", + "--log", "Information", + "--restart", + }, + }, ct); + + /// apax dcp-utility discover --source-mac MAC --timeout 30000 (stdout captured for export). + public Task DcpDiscoverAsync(string sourceMac, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] { "dcp-utility", "discover", "--source-mac", sourceMac, "--timeout", "30000" }, + EchoToConsole = false, + }, ct); + + /// apax dcp-utility list-interfaces -f JSON (stdout captured for export). + public Task DcpListInterfacesAsync(CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] { "dcp-utility", "list-interfaces", "-f", "JSON" }, + EchoToConsole = false, + }, ct); + + /// apax sld compare --mode all --target IP --input PLATFORM --username U --password P --certificate CERT --log Information + public Task SldCompareAllAsync(string ipAddress, string platform, string username, string password, string certificatePath, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] + { + "sld", "compare", + "--mode", "all", + "--target", ipAddress, + "--input", platform, + "--username", username, + "--password", password, + "--certificate", certificatePath, + "--log", "Information", + }, + }, ct); + + /// apax build + public Task BuildAsync(CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest { Executable = Executable, Arguments = new[] { "build" } }, ct); + + /// apax clean + public Task CleanAsync(CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest { Executable = Executable, Arguments = new[] { "clean" } }, ct); + + /// apax install [--catalog] + public Task InstallAsync(bool catalog = false, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = catalog ? new[] { "install", "--catalog" } : new[] { "install" }, + }, ct); +} diff --git a/src/axopen.dev/AXOpen.Dev/Assets/AssetDiscovery.cs b/src/axopen.dev/AXOpen.Dev/Assets/AssetDiscovery.cs new file mode 100644 index 000000000..573d92e35 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Assets/AssetDiscovery.cs @@ -0,0 +1,70 @@ +namespace AXOpen.Dev.Assets; + +/// A planned file copy (source absolute path → destination absolute path). +public sealed record CopyOperation(string Source, string Destination); + +/// +/// Discovers asset files under .apax/**/assets and plans their copies. +/// Pure port of the discovery in copy_and_install_gsd.sh (GSDML*.xml → flat layout) +/// and copy_hwl_templates.sh (*.hwl.json / *.hwl.yml → preserved subfolders). +/// Results are sorted by source path for deterministic behavior. +/// +public static class AssetDiscovery +{ + public static IReadOnlyList FindAssetsDirectories(string apaxDirectory) + { + if (!Directory.Exists(apaxDirectory)) + { + return Array.Empty(); + } + + return Directory + .EnumerateDirectories(apaxDirectory, "assets", SearchOption.AllDirectories) + .OrderBy(d => d, StringComparer.Ordinal) + .ToList(); + } + + /// Flat copy plan for GSDML*.xml (case-insensitive) into a single destination folder. + public static IReadOnlyList PlanGsdCopies(string apaxDirectory, string destinationDirectory) + => PlanCopies( + apaxDirectory, + name => name.StartsWith("GSDML", StringComparison.OrdinalIgnoreCase) + && name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase), + (_, file) => Path.Combine(destinationDirectory, Path.GetFileName(file))); + + /// Subfolder-preserving copy plan for *.hwl.json / *.hwl.yml. + public static IReadOnlyList PlanHwlCopies(string apaxDirectory, string destinationDirectory) + => PlanCopies( + apaxDirectory, + name => name.EndsWith(".hwl.json", StringComparison.OrdinalIgnoreCase) + || name.EndsWith(".hwl.yml", StringComparison.OrdinalIgnoreCase), + (assetsDir, file) => Path.Combine(destinationDirectory, Path.GetRelativePath(assetsDir, file))); + + /// Destination file names that more than one source maps to (overwrite warnings). + public static IReadOnlyCollection FlatCollisions(IReadOnlyList operations) + => operations + .GroupBy(op => Path.GetFileName(op.Destination), StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + private static IReadOnlyList PlanCopies( + string apaxDirectory, + Func nameMatches, + Func destinationFor) + { + var operations = new List(); + foreach (var assetsDir in FindAssetsDirectories(apaxDirectory)) + { + foreach (var file in Directory.EnumerateFiles(assetsDir, "*", SearchOption.AllDirectories)) + { + if (nameMatches(Path.GetFileName(file))) + { + operations.Add(new CopyOperation(file, destinationFor(assetsDir, file))); + } + } + } + + return operations.OrderBy(op => op.Source, StringComparer.Ordinal).ToList(); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/AssetCopyCommands.cs b/src/axopen.dev/AXOpen.Dev/Commands/AssetCopyCommands.cs new file mode 100644 index 000000000..90e4fcae3 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/AssetCopyCommands.cs @@ -0,0 +1,103 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Assets; +using AXOpen.Dev.Observability; + +namespace AXOpen.Dev.Commands; + +/// +/// Copies GSDML*.xml from .apax/**/assets into a flat ./gsd/source folder and +/// installs everything under ./gsd via apax. Port of copy_and_install_gsd.sh. +/// +public sealed class GsdInstallCommand(ApaxClient apax) +{ + private const string ApaxDir = "./.apax"; + private const string Destination = "./gsd/source"; + private const string GsdDir = "./gsd"; + + public async Task ExecuteAsync(CancellationToken ct = default) + { + if (Directory.Exists(ApaxDir)) + { + Directory.CreateDirectory(Destination); + var operations = AssetDiscovery.PlanGsdCopies(ApaxDir, Destination); + foreach (var collision in AssetDiscovery.FlatCollisions(operations)) + { + Output.Warning($"Warning: overwriting existing {collision} in {Destination}"); + } + + foreach (var op in operations) + { + File.Copy(op.Source, op.Destination, overwrite: true); + } + + Output.Success($"{operations.Count} file(s) copied to {Destination} (flat layout)."); + } + else + { + Output.Error("Directory ./.apax does not exist!!!"); + } + + Directory.CreateDirectory(Destination); + + var installable = Directory.Exists(GsdDir) + ? Directory.EnumerateFiles(GsdDir, "*", SearchOption.AllDirectories) + .Count(f => + { + var n = Path.GetFileName(f); + return n.StartsWith("GSDML", StringComparison.OrdinalIgnoreCase) + && n.EndsWith(".xml", StringComparison.OrdinalIgnoreCase); + }) + : 0; + + if (installable == 0) + { + Output.Warning($"No GSDML*.xml files found in '{GsdDir}'."); + return 0; + } + + var result = await apax.HwcInstallGsdAsync(GsdDir, ct); + if (!result.Success) + { + Output.Error("The installation of the gsdml files finished with an error!"); + return 1; + } + + Output.Success($"{installable} file(s) installed."); + return 0; + } +} + +/// +/// Copies *.hwl.json / *.hwl.yml from .apax/**/assets into ./hwc/library_templates +/// preserving subfolders. Port of copy_hwl_templates.sh. +/// +public sealed class HwlCopyCommand +{ + private const string ApaxDir = "./.apax"; + private const string Destination = "./hwc/library_templates"; + + public int Execute() + { + if (!Directory.Exists(ApaxDir)) + { + Output.Error("Directory ./.apax does not exist!"); + return 0; // mirrors the bash, which prints and exits 0 + } + + Directory.CreateDirectory(Destination); + var operations = AssetDiscovery.PlanHwlCopies(ApaxDir, Destination); + foreach (var op in operations) + { + var dir = Path.GetDirectoryName(op.Destination); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + + File.Copy(op.Source, op.Destination, overwrite: true); + } + + Output.Success($"{operations.Count} file(s) copied to {Destination} (subfolders preserved)."); + return 0; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/CertHashCheckCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/CertHashCheckCommand.cs new file mode 100644 index 000000000..4ba70efcd --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/CertHashCheckCommand.cs @@ -0,0 +1,71 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Certs; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Compares the SHA1 of the stored certificate against the certificate currently on the PLC. +/// Port of is_cert_hash_sha1_equal.sh (certutil replaced by .NET X509). +/// +public sealed class CertHashCheckCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(PlcTarget target, CancellationToken ct = default) + { + if (!IpValidator.IsValidIp(target.IpAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{target.IpAddress}' is not a valid IP address."); + return 1; + } + + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", target.Name); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!File.Exists(target.CertificatePath)) + { + Output.Error($"Certificate file {target.CertificatePath} does not exist!!!"); + return 1; + } + + var temp = Path.Combine(Path.GetTempPath(), $"axdev-plccert-{Guid.NewGuid():N}.cer"); + try + { + var pull = await apax.PullCertificateAsync(target.IpAddress, temp, ct); + if (!pull.Success || !File.Exists(temp)) + { + Output.Error("Failed to retrieve the certificate from the PLC."); + return 1; + } + + var storedHash = CertService.ComputeSha1Thumbprint(target.CertificatePath); + var plcHash = CertService.ComputeSha1Thumbprint(temp); + Console.WriteLine(storedHash); + Console.WriteLine(plcHash); + + if (CertService.AreEqual(storedHash, plcHash)) + { + Output.Success($"The hash of the stored certificate file and the certificate inside the PLC with IP address {target.IpAddress} are equal."); + return 0; + } + + Output.Error($"The hash of the stored certificate file and the certificate inside the PLC with IP address {target.IpAddress} are different."); + return 1; + } + finally + { + if (File.Exists(temp)) + { + File.Delete(temp); + } + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/CompareAllCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/CompareAllCommand.cs new file mode 100644 index 000000000..5e9eef06a --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/CompareAllCommand.cs @@ -0,0 +1,71 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Diagnostics; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Compares online (PLC) vs offline (compiled) software. Port of compare_all.sh: writes the +/// output to ./online_offline_compare_result.txt and maps the apax exit code to 0/9/10/11 +/// (see ). +/// +public sealed class CompareAllCommand(ApaxClient apax) +{ + public const string ResultFile = "./online_offline_compare_result.txt"; + + public async Task ExecuteAsync(string plcName, string ipAddress, string platform, string username, string password, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("PLATFORM", platform); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + var certFile = new PlcTarget(ipAddress, plcName, username, password).CertificatePath; + if (!File.Exists(certFile)) + { + Output.Error($"Certification file {certFile} does not exist!!!"); + return 1; + } + + if (File.Exists(ResultFile)) + { + File.Delete(ResultFile); + } + + var run = await apax.SldCompareAllAsync(ipAddress, platform, username, password, certFile, ct); + await File.WriteAllTextAsync(ResultFile, run.StandardOutput, ct); + Console.WriteLine($"exit_code: {run.ExitCode}"); + + var result = CompareResult.FromApaxExitCode(run.ExitCode); + if (result.IsIdentical) + { + Output.Success(result.Message); + } + else if (result.Outcome == CompareOutcome.Unspecified) + { + Output.Error(result.Message); + } + else + { + Output.Warning(result.Message); + } + + return result.ProcessExitCode; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/CopyCtrlFoldersCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/CopyCtrlFoldersCommand.cs new file mode 100644 index 000000000..71f9b1087 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/CopyCtrlFoldersCommand.cs @@ -0,0 +1,40 @@ +using AXOpen.Dev.Observability; +using AXOpen.Dev.Utils; + +namespace AXOpen.Dev.Commands; + +/// +/// Copies every ctrl directory under a source tree into a destination, preserving structure. +/// Port of scripts/copy-ctrl-folders.ps1. +/// +public sealed class CopyCtrlFoldersCommand +{ + public int Execute(string source, string destination) + { + if (string.IsNullOrWhiteSpace(destination)) + { + Output.Error("Destination must not be empty."); + return 1; + } + + var sourceRoot = Path.GetFullPath(string.IsNullOrWhiteSpace(source) ? "src" : source); + if (!Directory.Exists(sourceRoot)) + { + Output.Error($"Source directory '{sourceRoot}' does not exist."); + return 1; + } + + var destinationRoot = Path.GetFullPath(destination); + Directory.CreateDirectory(destinationRoot); + + var count = CtrlFolderCopier.Copy(sourceRoot, destinationRoot); + if (count == 0) + { + Output.Warning($"No directories named 'ctrl' were found under '{sourceRoot}'."); + return 0; + } + + Output.Success($"Copied {count} 'ctrl' directories to '{destinationRoot}'."); + return 0; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/CopyHardwareIdsCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/CopyHardwareIdsCommand.cs new file mode 100644 index 000000000..d516c2ebe --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/CopyHardwareIdsCommand.cs @@ -0,0 +1,47 @@ +using AXOpen.Dev.Hardware; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Generates src/IO/HwIdentifiers.st and src/IO/HwIdentifierList.st from the +/// compiled SystemConstants/<PLC>_HwIdentifiers.st. Port of copy_hardware_ids.sh. +/// Paths are relative to the current working directory (the app folder). +/// +public sealed class CopyHardwareIdsCommand +{ + public int Execute(string @namespace, string plcName) + { + try + { + ArgumentGuards.EnsureNotEmpty("NAMESPACE", @namespace); + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!Directory.Exists("./hwc")) + { + Output.Error("Directory \"./hwc\" does not exist!!!"); + return 1; + } + + var input = Path.Combine("SystemConstants", $"{plcName}_HwIdentifiers.st"); + if (!File.Exists(input)) + { + Output.Error($"File {input} does not exist!!!"); + return 1; + } + + var (identifiers, list) = HwIdentifiersGenerator.Generate(@namespace, File.ReadAllText(input)); + StFile.Write(Path.Combine("src", "IO", "HwIdentifiers.st"), identifiers); + StFile.Write(Path.Combine("src", "IO", "HwIdentifierList.st"), list); + + Output.Success("Generation complete: src/IO/HwIdentifiers.st, src/IO/HwIdentifierList.st"); + return 0; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/CopyIoAddressesCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/CopyIoAddressesCommand.cs new file mode 100644 index 000000000..97237c4e5 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/CopyIoAddressesCommand.cs @@ -0,0 +1,56 @@ +using AXOpen.Dev.Hardware; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Generates src/IO/Inputs.st, Outputs.st and IoStructures.st from the +/// compiled SystemConstants/<PLC>_IoAddresses.st. Port of copy_io_addresses.sh +/// (hwc >= 3.4.0 path). Files get a trailing newline to match PowerShell Set-Content exactly. +/// +public sealed class CopyIoAddressesCommand +{ + public int Execute(string @namespace, string plcName) + { + try + { + ArgumentGuards.EnsureNotEmpty("NAMESPACE", @namespace); + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!Directory.Exists("./hwc")) + { + Output.Error("Directory \"./hwc\" does not exist!!!"); + return 1; + } + + var input = Path.Combine("SystemConstants", $"{plcName}_IoAddresses.st"); + if (!File.Exists(input)) + { + Output.Error($"File {input} does not exist!!!"); + return 1; + } + + try + { + var (inputs, outputs, structures) = IoAddressesGenerator.Generate(@namespace, File.ReadAllText(input)); + StFile.Write(Path.Combine("src", "IO", "Inputs.st"), inputs, trailingNewline: true); + StFile.Write(Path.Combine("src", "IO", "Outputs.st"), outputs, trailingNewline: true); + StFile.Write(Path.Combine("src", "IO", "IoStructures.st"), structures, trailingNewline: true); + } + catch (IoAddressesFormatException ex) + { + Output.Error(ex.Message); + return 1; + } + + Output.Success("Generation complete: src/IO/Inputs.st, src/IO/Outputs.st, src/IO/IoStructures.st"); + return 0; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/DcpCommands.cs b/src/axopen.dev/AXOpen.Dev/Commands/DcpCommands.cs new file mode 100644 index 000000000..134d9e73e --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/DcpCommands.cs @@ -0,0 +1,47 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// apax dcp-utility discover → ./dcp_export/devices.json. Port of dcp_utility_discover.sh. +public sealed class DcpDiscoverCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(string sourceMac, CancellationToken ct = default) + { + if (!MacValidator.IsValid(sourceMac)) + { + Output.Error($"The {sourceMac} is not a valid MAC address."); + return 1; + } + + Directory.CreateDirectory("./dcp_export"); + var exportFile = Path.Combine("dcp_export", "devices.json"); + if (File.Exists(exportFile)) + { + File.Delete(exportFile); + } + + var result = await apax.DcpDiscoverAsync(sourceMac, ct); + await File.WriteAllTextAsync(exportFile, result.StandardOutput, ct); + return result.ExitCode; + } +} + +/// apax dcp-utility list-interfaces → ./dcp_export/interfaces.json. Port of dcp_utility_list_interfaces.sh. +public sealed class DcpListInterfacesCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(CancellationToken ct = default) + { + Directory.CreateDirectory("./dcp_export"); + var exportFile = Path.Combine("dcp_export", "interfaces.json"); + if (File.Exists(exportFile)) + { + File.Delete(exportFile); + } + + var result = await apax.DcpListInterfacesAsync(ct); + await File.WriteAllTextAsync(exportFile, result.StandardOutput, ct); + return result.ExitCode; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/FirstSetupCommands.cs b/src/axopen.dev/AXOpen.Dev/Commands/FirstSetupCommands.cs new file mode 100644 index 000000000..4399b4c0a --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/FirstSetupCommands.cs @@ -0,0 +1,214 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Tools; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// apax hwc compile. Port of hw_compile.sh. +public sealed class HwCompileCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(CancellationToken ct = default) + { + var result = await apax.HwcCompileAsync(ct); + if (!result.Success) + { + Output.Error("The compilation of the hardware configuration finished with an error!"); + return 1; + } + + Output.Success("Hardware configuration compiled successfully."); + return 0; + } +} + +/// +/// Compile HW, generate ST id/address files, first-download HW with the master password and pull +/// the PLC certificate. Port of hw_first_compile_and_first_download.sh. +/// +public sealed class HwFirstCompileAndDownloadCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(string @namespace, string plcName, string ipAddress, string password, CancellationToken ct = default) + { + if (!Directory.Exists("./hwc")) + { + Output.Error("Directory \"./hwc\" does not exist!!!"); + return 1; + } + + var hwlFile = Path.Combine("hwc", $"{plcName}.hwl.yml"); + if (!File.Exists(hwlFile)) + { + Output.Error($"Hardware configuration file {hwlFile} does not exist!!!"); + return 1; + } + + if (await new HwCompileCommand(apax).ExecuteAsync(ct) != 0) return 1; + if (new CopyHardwareIdsCommand().Execute(@namespace, plcName) != 0) return 1; + if (new CopyIoAddressesCommand().Execute(@namespace, plcName) != 0) return 1; + + var download = await apax.HwldFirstDownloadAsync(plcName, ipAddress, password, ct); + if (!download.Success) + { + Output.Error("Downloading of the hardware configuration finished with an error!"); + return 1; + } + + Output.Success("Hardware configuration has been successfully downloaded."); + + var certFile = $"./certs/{plcName}/{plcName}.cer"; + var pull = await apax.PullCertificateToFileAsync(ipAddress, certFile, ct); + if (!pull.Success) + { + Output.Error("Uploading of the security certificate finished with an error!"); + return 1; + } + + Output.Success("Security certificate has been successfully uploaded."); + return 0; + } +} + +/// +/// First HW download using the master password, then pull the PLC certificate. Assumes the HW is +/// already compiled and secure comms configured. Port of hw_first_download_only.sh. +/// +public sealed class HwFirstDownloadOnlyCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(string plcName, string ipAddress, string password, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + if (!Directory.Exists("./hwc")) + { + Output.Error("Directory \"./hwc\" does not exist!!!"); + return 1; + } + + var hwlFile = Path.Combine("hwc", $"{plcName}.hwl.yml"); + if (!File.Exists(hwlFile)) + { + Output.Error($"Hardware configuration file {hwlFile} does not exist!!!"); + return 1; + } + + var download = await apax.HwldFirstDownloadAsync(plcName, ipAddress, password, ct); + if (!download.Success) + { + Output.Error("Downloading of the hardware configuration finished with an error!"); + return 1; + } + + Output.Success("Hardware configuration has been successfully downloaded."); + + var certFile = $"./certs/{plcName}/{plcName}.cer"; + var pull = await apax.PullCertificateToFileAsync(ipAddress, certFile, ct); + if (!pull.Success) + { + Output.Error("Uploading of the security certificate finished with an error!"); + return 1; + } + + Output.Success("Security certificate has been successfully uploaded."); + return 0; + } +} + +/// +/// Full first-time HW provisioning: install GSD, copy HWL templates, set up secure communication, +/// then compile + first-download HW and pull the certificate. Port of hw_first_download.sh. +/// (Assumes apax dependencies are already installed.) +/// +public sealed class HwFirstDownloadCommand(ApaxClient apax, OpensslClient openssl) +{ + public async Task ExecuteAsync(string @namespace, string plcName, string ipAddress, string username, string password, CancellationToken ct = default) + { + if (await new GsdInstallCommand(apax).ExecuteAsync(ct) != 0) return 1; + if (new HwlCopyCommand().Execute() != 0) return 1; + if (await new SetupSecureCommunicationCommand(apax, openssl).ExecuteAsync(plcName, username, password, ipAddress, ct) != 0) return 1; + if (await new HwFirstCompileAndDownloadCommand(apax).ExecuteAsync(@namespace, plcName, ipAddress, password, ct) != 0) return 1; + + Output.Success("Hardware configuration has been successfully compiled and downloaded."); + return 0; + } +} + +/// apax sld load --mode FULL --restart, using certificate auth. Port of sw_download_full.sh. +public sealed class SwDownloadFullCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(string plcName, string ipAddress, string platform, string username, string password, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("PLATFORM", platform); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + var certFile = new PlcTarget(ipAddress, plcName, username, password).CertificatePath; + if (!File.Exists(certFile)) + { + Output.Error($"Certification file {certFile} does not exist!!!"); + return 1; + } + + var result = await apax.SldLoadFullAsync(ipAddress, platform, username, password, certFile, ct); + if (!result.Success) + { + Output.Error("Downloading of the software using security certificate finished with an error!"); + return 1; + } + + Output.Success("Software has been successfully downloaded using security certificate."); + return 0; + } +} + +/// apax build + dotnet ixc + full SW download. Port of sw_build_and_download_full.sh. +public sealed class SwBuildDownloadFullCommand(ApaxClient apax, DotnetClient dotnet) +{ + public async Task ExecuteAsync(string plcName, string ipAddress, string platform, string username, string password, CancellationToken ct = default) + { + if (!(await apax.BuildAsync(ct)).Success) + { + Output.Error("apax build finished with an error!"); + return 1; + } + + if (!(await dotnet.IxcAsync(ct)).Success) + { + Output.Error("dotnet ixc finished with an error!"); + return 1; + } + + return await new SwDownloadFullCommand(apax).ExecuteAsync(plcName, ipAddress, platform, username, password, ct); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/HwDiagListCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/HwDiagListCommand.cs new file mode 100644 index 000000000..7a4f506ed --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/HwDiagListCommand.cs @@ -0,0 +1,42 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// Lists hardware diagnostic info from a PLC. Port of hw_diag_list.sh. +public sealed class HwDiagListCommand(ApaxClient apax, Func? fileExists = null) +{ + private readonly Func _fileExists = fileExists ?? File.Exists; + + public async Task ExecuteAsync(PlcTarget target, CancellationToken ct = default) + { + if (!IpValidator.IsValidIp(target.IpAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{target.IpAddress}' is not a valid IP address."); + return 1; + } + + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", target.Name); + ArgumentGuards.EnsureNotEmpty("USERNAME", target.Username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", target.Password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!_fileExists(target.CertificatePath)) + { + Output.Error($"Certificate file {target.CertificatePath} not found!!!"); + return 1; + } + + var result = await apax.HwDiagListAsync(target.IpAddress, target.Username, target.Password, target.CertificatePath, ct); + return result.ExitCode; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/HwDownloadOnlyCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/HwDownloadOnlyCommand.cs new file mode 100644 index 000000000..0b1df17ad --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/HwDownloadOnlyCommand.cs @@ -0,0 +1,64 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Download an already-compiled HW configuration using certificate auth. Port of +/// hw_download_only.sh. +/// +public sealed class HwDownloadOnlyCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(string plcName, string ipAddress, string username, string password, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + if (!Directory.Exists("./hwc")) + { + Output.Error("Directory \"./hwc\" does not exist!!!"); + return 1; + } + + var hwlFile = Path.Combine("hwc", $"{plcName}.hwl.yml"); + if (!File.Exists(hwlFile)) + { + Output.Error($"Hardware configuration file {hwlFile} does not exist!!!"); + return 1; + } + + var certFile = new PlcTarget(ipAddress, plcName, username, password).CertificatePath; + if (!File.Exists(certFile)) + { + Output.Error($"Certification file {certFile} does not exist!!!"); + return 1; + } + + var result = await apax.HwldDownloadAsync(plcName, ipAddress, username, password, certFile, ct); + if (!result.Success) + { + Output.Error("Downloading of the hardware configuration finished with an error!"); + return 1; + } + + Output.Success("Hardware configuration has been successfully downloaded."); + return 0; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/HwUpdateCommands.cs b/src/axopen.dev/AXOpen.Dev/Commands/HwUpdateCommands.cs new file mode 100644 index 000000000..2c4393ded --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/HwUpdateCommands.cs @@ -0,0 +1,74 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Compile HW, regenerate ST id/address files, then download with certificate auth. +/// Port of hw_compile_and_download.sh. +/// +public sealed class HwCompileAndDownloadCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(string @namespace, string plcName, string ipAddress, string username, string password, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("NAMESPACE", @namespace); + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + if (!Directory.Exists("./hwc")) + { + Output.Error("Directory \"./hwc\" does not exist!!!"); + return 1; + } + + var hwlFile = Path.Combine("hwc", $"{plcName}.hwl.yml"); + if (!File.Exists(hwlFile)) + { + Output.Error($"Hardware configuration file {hwlFile} does not exist!!!"); + return 1; + } + + var certFile = new PlcTarget(ipAddress, plcName, username, password).CertificatePath; + if (!File.Exists(certFile)) + { + Output.Error($"Certification file {certFile} does not exist!!!"); + return 1; + } + + if (await new HwCompileCommand(apax).ExecuteAsync(ct) != 0) return 1; + if (new CopyHardwareIdsCommand().Execute(@namespace, plcName) != 0) return 1; + if (new CopyIoAddressesCommand().Execute(@namespace, plcName) != 0) return 1; + return await new HwDownloadOnlyCommand(apax).ExecuteAsync(plcName, ipAddress, username, password, ct); + } +} + +/// +/// Update HW: install GSD, copy templates, then compile + download with certificate. +/// Port of hw_update.sh. +/// +public sealed class HwUpdateCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(string @namespace, string plcName, string ipAddress, string username, string password, CancellationToken ct = default) + { + if (await new GsdInstallCommand(apax).ExecuteAsync(ct) != 0) return 1; + if (new HwlCopyCommand().Execute() != 0) return 1; + return await new HwCompileAndDownloadCommand(apax).ExecuteAsync(@namespace, plcName, ipAddress, username, password, ct); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/OrchestratorCommands.cs b/src/axopen.dev/AXOpen.Dev/Commands/OrchestratorCommands.cs new file mode 100644 index 000000000..f21e03939 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/OrchestratorCommands.cs @@ -0,0 +1,296 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Requisites; +using AXOpen.Dev.Tools; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// Shared helpers for the top-level orchestrators. +internal static class Orchestration +{ + public static async Task RequisitesOkAsync(RequisiteChecker checker, CancellationToken ct) + => await checker.CheckApaxAsync(ct) && await checker.CheckNugetAsync(ct) && checker.CheckCustomRegistry(); + + /// Delete the contents of a folder (like rm -rf "$folder/"*), keeping the folder. + public static void CleanContents(string directory) + { + if (!Directory.Exists(directory)) + { + return; + } + + foreach (var file in Directory.GetFiles(directory)) + { + File.Delete(file); + } + + foreach (var sub in Directory.GetDirectories(directory)) + { + Directory.Delete(sub, recursive: true); + } + } +} + +/// +/// Full initial bring-up of a (blank) PLC: requisites → optional PLCSIM → clean/install → +/// optional force-clean of certs → reset PLC → first HW download → SW build + full download. +/// Port of all_first.sh. +/// +public sealed class AllFirstCommand(ApaxClient apax, OpensslClient openssl, DotnetClient dotnet) +{ + public async Task ExecuteAsync(string @namespace, string plcName, string ipAddress, string platform, string username, string password, bool usePlcSim, bool force, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("NAMESPACE", @namespace); + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("PLATFORM", platform); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + if (!PasswordValidator.IsSafe(password)) + { + Output.Error("The PASSWORD contains problematic characters. Cannot use: $ ` \\ \" ' & | ; < > ( ) * ? [ ] { } or whitespace"); + return 1; + } + + if (!await Orchestration.RequisitesOkAsync(new RequisiteChecker(apax.Runner), ct)) + { + return 1; + } + + if (usePlcSim) + { + await new PlcSimCommand(dotnet).ExecuteAsync(@namespace, plcName, ipAddress, ct); + } + + await apax.CleanAsync(ct); + await apax.InstallAsync(catalog: true, ct); + await apax.InstallAsync(catalog: false, ct); + + if (force) + { + Orchestration.CleanContents("./certs"); + Orchestration.CleanContents("./hwc/hwc.gen"); + } + + // clean_plc (reset keeping only IP); the bash ignores its result. + await new ResetPlcCommand(apax).ExecuteAsync(ResetScope.KeepOnlyIp, ipAddress, username, password, ct); + + if (await new HwFirstDownloadCommand(apax, openssl).ExecuteAsync(@namespace, plcName, ipAddress, username, password, ct) != 0) + { + return 1; + } + + if (await new SwBuildDownloadFullCommand(apax, dotnet).ExecuteAsync(plcName, ipAddress, platform, username, password, ct) != 0) + { + return 1; + } + + Output.Success("Software has been successfully compiled and downloaded."); + return 0; + } +} + +/// +/// Compile everything (HW + SW) and pull the PLC certificate, without downloading to the PLC. +/// Port of compile_all.sh. +/// +/// +/// The bash calls setup_secure_communication with only 3 args (missing the IP), which makes that +/// script fail its argument check; this port passes the IP so the step actually works. +/// +public sealed class CompileAllCommand(ApaxClient apax, OpensslClient openssl, DotnetClient dotnet) +{ + public async Task ExecuteAsync(string @namespace, string plcName, string ipAddress, string platform, string username, string password, bool usePlcSim, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("NAMESPACE", @namespace); + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("PLATFORM", platform); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + await apax.InstallAsync(catalog: false, ct); + + if (!await Orchestration.RequisitesOkAsync(new RequisiteChecker(apax.Runner), ct)) + { + return 1; + } + + if (usePlcSim) + { + await new PlcSimCommand(dotnet).ExecuteAsync(@namespace, plcName, ipAddress, ct); + } + + await apax.CleanAsync(ct); + await apax.InstallAsync(catalog: true, ct); + await apax.InstallAsync(catalog: false, ct); + + if (await new GsdInstallCommand(apax).ExecuteAsync(ct) != 0) return 1; + if (new HwlCopyCommand().Execute() != 0) return 1; + if (await new SetupSecureCommunicationCommand(apax, openssl).ExecuteAsync(plcName, username, password, ipAddress, ct) != 0) return 1; + if (await new HwCompileCommand(apax).ExecuteAsync(ct) != 0) return 1; + if (new CopyHardwareIdsCommand().Execute(@namespace, plcName) != 0) return 1; + if (new CopyIoAddressesCommand().Execute(@namespace, plcName) != 0) return 1; + + var certFile = $"./certs/{plcName}/{plcName}.cer"; + Directory.CreateDirectory(Path.Combine("certs", plcName)); + if (!(await apax.PullCertificateToFileAsync(ipAddress, certFile, ct)).Success) + { + Output.Error("Uploading of the security certificate finished with an error!"); + return 1; + } + + if (!(await apax.BuildAsync(ct)).Success) + { + Output.Error("apax build finished with an error!"); + return 1; + } + + if (!(await dotnet.IxcAsync(ct)).Success) + { + Output.Error("dotnet ixc finished with an error!"); + return 1; + } + + return 0; + } +} + +/// +/// Smart update dispatcher. Port of all.sh: +/// +/// force = true: requires existing cert artifacts, then gsd + hwl + first-compile-download + SW. +/// force = false, no cert: full . +/// force = false, cert present but hash differs from PLC: full . +/// force = false, cert matches PLC: fast path — + SW. +/// +/// +/// +/// CAVEAT: the fast path runs the unauthenticated cert-hash check (apax plc-cert) before the +/// authenticated hw_update. The bash ran these as separate processes; in-process they share one +/// apax connection session, so the authenticated step can be denied (see the connection-sharing +/// note). Verify on hardware; prefer force/alf if the fast path is denied. +/// +public sealed class AllCommand(ApaxClient apax, OpensslClient openssl, DotnetClient dotnet) +{ + public async Task ExecuteAsync(string @namespace, string plcName, string ipAddress, string platform, string username, string password, bool usePlcSim, bool force, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("NAMESPACE", @namespace); + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("PLATFORM", platform); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + var certsDir = $"./certs/{plcName}"; + var certFile = $"{certsDir}/{plcName}.cer"; + var checker = new RequisiteChecker(apax.Runner); + + if (force) + { + foreach (var (path, label) in new[] + { + (certFile, "Certification file"), + ($"{certsDir}/containerWithPublicAndPrivateKeys_x509.p12", "Public/private key container file"), + ($"{certsDir}/reference_x509.crt", "Reference file"), + }) + { + if (!File.Exists(path)) + { + Output.Error($"{label} {path} does not exist."); + return 1; + } + } + + if (!await Orchestration.RequisitesOkAsync(checker, ct)) return 1; + if (usePlcSim) await new PlcSimCommand(dotnet).ExecuteAsync(@namespace, plcName, ipAddress, ct); + if (await new GsdInstallCommand(apax).ExecuteAsync(ct) != 0) return 1; + if (new HwlCopyCommand().Execute() != 0) return 1; + if (await new HwFirstCompileAndDownloadCommand(apax).ExecuteAsync(@namespace, plcName, ipAddress, password, ct) != 0) return 1; + return await new SwBuildDownloadFullCommand(apax, dotnet).ExecuteAsync(plcName, ipAddress, platform, username, password, ct); + } + + if (!File.Exists(certFile)) + { + return await new AllFirstCommand(apax, openssl, dotnet) + .ExecuteAsync(@namespace, plcName, ipAddress, platform, username, password, usePlcSim, force: false, ct); + } + + if (!await Orchestration.RequisitesOkAsync(checker, ct)) return 1; + if (usePlcSim) await new PlcSimCommand(dotnet).ExecuteAsync(@namespace, plcName, ipAddress, ct); + + await apax.CleanAsync(ct); + await apax.InstallAsync(catalog: true, ct); + await apax.InstallAsync(catalog: false, ct); + + var hashEqual = await new CertHashCheckCommand(apax) + .ExecuteAsync(new PlcTarget(ipAddress, plcName, username, password), ct) == 0; + + if (!hashEqual) + { + Output.Warning($"Certificate {certFile} exists but its SHA1 differs from the PLC's; regenerating via first setup."); + return await new AllFirstCommand(apax, openssl, dotnet) + .ExecuteAsync(@namespace, plcName, ipAddress, platform, username, password, usePlcSim, force: false, ct); + } + + Output.Success($"Certificate {certFile} matches the PLC; performing a fast update."); + if (await new HwUpdateCommand(apax).ExecuteAsync(@namespace, plcName, ipAddress, username, password, ct) != 0) return 1; + return await new SwBuildDownloadFullCommand(apax, dotnet).ExecuteAsync(plcName, ipAddress, platform, username, password, ct); + } +} + +/// Compile everything then compare online vs offline. Port of compile_all_compare_all.sh. +public sealed class CompileAllCompareAllCommand(ApaxClient apax, OpensslClient openssl, DotnetClient dotnet) +{ + public async Task ExecuteAsync(string @namespace, string plcName, string ipAddress, string platform, string username, string password, bool usePlcSim, CancellationToken ct = default) + { + if (await new CompileAllCommand(apax, openssl, dotnet).ExecuteAsync(@namespace, plcName, ipAddress, platform, username, password, usePlcSim, ct) != 0) + { + return 1; + } + + return await new CompareAllCommand(apax).ExecuteAsync(plcName, ipAddress, platform, username, password, ct); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/PlcSimCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/PlcSimCommand.cs new file mode 100644 index 000000000..d9890bb38 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/PlcSimCommand.cs @@ -0,0 +1,49 @@ +using System.Runtime.InteropServices; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Tools; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Starts the PLCSIM Advanced starter tool. Port of plcsimadvanced.sh. PLCSIM Advanced is +/// Windows-only, so on other platforms this is a no-op that warns and succeeds (so orchestrators +/// don't break). +/// +public sealed class PlcSimCommand(DotnetClient dotnet) +{ + private const string StarterProject = + @"..\..\tools\src\PlcSimAdvancedStarter\PlcSimAdvancedStarterTool\PlcSimAdvancedStarterTool.csproj"; + + public async Task ExecuteAsync(string instanceName, string plcName, string ipAddress, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("INSTANCE_NAME", instanceName); + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Output.Warning("PLCSIM Advanced is only supported on Windows; skipping 'plcsim' on this OS."); + return 0; + } + + var result = await dotnet.RunProjectAsync( + StarterProject, + new[] { "startplcsim", "-x", instanceName, "-n", plcName, "-t", ipAddress }, + ct); + return result.ExitCode; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/ResetPlcCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/ResetPlcCommand.cs new file mode 100644 index 000000000..4e352ba5c --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/ResetPlcCommand.cs @@ -0,0 +1,42 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Resets a PLC. ports clean_plc.sh; +/// ports reset_plc.sh. +/// +public sealed class ResetPlcCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(ResetScope scope, string ipAddress, string username, string password, CancellationToken ct = default) + { + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + try + { + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + var result = await apax.ResetAsync(scope, ipAddress, username, password, ct); + if (!result.Success) + { + Output.Error("Unable to reset the PLC! Please check the details above."); + return 1; + } + + Output.Success("PLC was reset successfully."); + return 0; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/RestartPlcCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/RestartPlcCommand.cs new file mode 100644 index 000000000..efdbc4894 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/RestartPlcCommand.cs @@ -0,0 +1,61 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Restarts a PLC by switching it to STOP then back to RUN, using certificate auth. +/// Port of restart_PLC.sh. +/// +public sealed class RestartPlcCommand(ApaxClient apax, Func? fileExists = null) +{ + private readonly Func _fileExists = fileExists ?? File.Exists; + + public async Task ExecuteAsync(PlcTarget target, CancellationToken ct = default) + { + if (!IpValidator.IsValidIp(target.IpAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{target.IpAddress}' is not a valid IP address."); + return 1; + } + + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", target.Name); + ArgumentGuards.EnsureNotEmpty("USERNAME", target.Username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", target.Password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!_fileExists(target.CertificatePath)) + { + Output.Error($"Certificate file {target.CertificatePath} not found!!!"); + return 1; + } + + var stop = await apax.SetModeAsync(PlcMode.Stop, target.IpAddress, target.Username, target.Password, target.CertificatePath, ct); + if (!stop.Success) + { + Output.Error("Unable to set the PLC to STOP mode! Please check the details above."); + return 1; + } + + Output.Success("PLC was successfully set to STOP mode."); + + var run = await apax.SetModeAsync(PlcMode.Run, target.IpAddress, target.Username, target.Password, target.CertificatePath, ct); + if (!run.Success) + { + Output.Error("Unable to set the PLC to RUN mode! Please check the details above."); + return 1; + } + + Output.Success("PLC was successfully set to RUN mode."); + return 0; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/SetupSecureCommunicationCommand.cs b/src/axopen.dev/AXOpen.Dev/Commands/SetupSecureCommunicationCommand.cs new file mode 100644 index 000000000..26caf39ee --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/SetupSecureCommunicationCommand.cs @@ -0,0 +1,149 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Process; +using AXOpen.Dev.Tools; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// +/// Generates a self-signed certificate (via openssl) and configures secure TLS communication on +/// the PLC's hardware configuration. Port of setup_secure_communication.sh. +/// Paths are relative to the current working directory (the app folder). +/// +public sealed class SetupSecureCommunicationCommand(ApaxClient apax, OpensslClient openssl) +{ + public async Task ExecuteAsync(string plcName, string username, string password, string ipAddress, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + if (!Directory.Exists("./hwc")) + { + Output.Error("Directory \"./hwc\" does not exist!!!"); + return 1; + } + + var hwlFile = Path.Combine("hwc", $"{plcName}.hwl.yml"); + if (!File.Exists(hwlFile)) + { + Output.Error($"Hardware configuration file {hwlFile} does not exist!!!"); + return 1; + } + + var certsDir = Path.Combine("certs", plcName); + const string alreadyConfigured = "If you want to change the security configuration, you must delete it manually before triggering this command."; + + var securityConfig = Path.Combine("hwc", "hwc.gen", $"{plcName}.SecurityConfiguration.json"); + foreach (var guard in new[] + { + securityConfig, + Path.Combine(certsDir, "containerWithPublicAndPrivateKeys_x509.p12"), + Path.Combine(certsDir, "reference_x509.crt"), + Path.Combine(certsDir, $"{plcName}.cer"), + }) + { + if (File.Exists(guard)) + { + Output.Warning($"File '{guard}' already exists. {alreadyConfigured}"); + return 1; + } + } + + Directory.CreateDirectory(certsDir); + const string keyFile = "privateKey.pem"; + const string certFile = "server.cert.pem"; + const string configFile = "server_cert_ext.cnf"; + const string p12File = "containerWithPublicAndPrivateKeys_x509.p12"; + const string crtFile = "reference_x509.crt"; + + foreach (var leftover in new[] { keyFile, certFile, configFile }) + { + var p = Path.Combine(certsDir, leftover); + if (File.Exists(p)) File.Delete(p); + } + + await File.WriteAllTextAsync(Path.Combine(certsDir, configFile), BuildOpenSslConfig(ipAddress), ct); + + // ---- openssl: key -> self-signed cert -> pkcs12 -> cert-only crt ---- + if (!await OkAsync(openssl.GenerateRsaKeyAsync(certsDir, keyFile, 2048, ct), "Generating private key")) return 1; + if (!await OkAsync(openssl.SelfSignAsync(certsDir, keyFile, certFile, configFile, ct), "Generating self-signed certificate")) return 1; + if (!await OkAsync(openssl.ExportPkcs12Async(certsDir, certFile, keyFile, p12File, password, ct), "Exporting to PKCS12")) return 1; + if (!await OkAsync(openssl.ExportCertificateOnlyAsync(certsDir, p12File, crtFile, password, ct), "Exporting certificate (crt)")) return 1; + + foreach (var leftover in new[] { keyFile, certFile, configFile }) + { + var p = Path.Combine(certsDir, leftover); + if (File.Exists(p)) File.Delete(p); + } + + // ---- apax hwc: setup secure comm, import cert (TLS + WebServer), passwords ---- + var p12Path = $"./certs/{plcName}/{p12File}"; + if (!await OkAsync(apax.HwcSetupSecureCommunicationAsync(plcName, password, ct), "apax hwc setup-secure-communication")) return 1; + if (!await OkAsync(apax.HwcImportCertificateAsync(plcName, p12Path, password, "TLS", ct), "apax hwc import-certificate (TLS)")) return 1; + if (!await OkAsync(apax.HwcImportCertificateAsync(plcName, p12Path, password, "WebServer", ct), "apax hwc import-certificate (WebServer)")) return 1; + if (!await OkAsync(apax.HwcSetAccessProtectionPasswordAsync(plcName, password, ct), "apax hwc set-accessprotection-password")) return 1; + if (!await OkAsync(apax.HwcSetUserPasswordAsync(plcName, username, password, ct), "apax hwc manage-users set-password")) return 1; + + Output.Success("Secure communication configured successfully."); + return 0; + } + + private static async Task OkAsync(Task task, string what) + { + var result = await task; + if (!result.Success) + { + Output.Error($"{what} failed. Please check the details above."); + return false; + } + + return true; + } + + // Mirrors the heredoc in setup_secure_communication.sh. DNSNAME and URI are intentionally + // empty (as in the source); CN falls back to "localhost". + private static string BuildOpenSslConfig(string ipAddress) => + "[ req ]\n" + + "default_bits = 2048\n" + + "default_md = sha256\n" + + "distinguished_name = dn\n" + + "x509_extensions = v3_req\n" + + "prompt = no\n" + + "\n" + + "[ dn ]\n" + + "C = XX\n" + + "ST = StateName\n" + + "L = CityName\n" + + "O = CompanyName\n" + + "OU = CompanySectionName\n" + + "CN = localhost\n" + + "\n" + + "[ v3_req ]\n" + + "basicConstraints = CA:FALSE\n" + + "keyUsage = critical, digitalSignature, nonRepudiation, keyCertSign, keyCertSign, keyEncipherment, dataEncipherment\n" + + "extendedKeyUsage = serverAuth,clientAuth\n" + + "subjectAltName = @alt_names\n" + + "subjectKeyIdentifier = hash\n" + + "authorityKeyIdentifier = keyid:always,issuer:always\n" + + "\n" + + "[ alt_names ]\n" + + "DNS.1 = \n" + + $"IP.1 = {ipAddress}\n" + + "URI.1 = \n"; +} diff --git a/src/axopen.dev/AXOpen.Dev/Commands/SwDeltaCommands.cs b/src/axopen.dev/AXOpen.Dev/Commands/SwDeltaCommands.cs new file mode 100644 index 000000000..7ae5506d4 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Commands/SwDeltaCommands.cs @@ -0,0 +1,71 @@ +using AXOpen.Dev.Apax; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Plc; +using AXOpen.Dev.Tools; +using AXOpen.Dev.Validation; + +namespace AXOpen.Dev.Commands; + +/// apax sld load --mode delta. Port of sw_download_delta.sh. +public sealed class SwDownloadDeltaCommand(ApaxClient apax) +{ + public async Task ExecuteAsync(string plcName, string ipAddress, string platform, string username, string password, CancellationToken ct = default) + { + try + { + ArgumentGuards.EnsureNotEmpty("PLC_NAME", plcName); + ArgumentGuards.EnsureNotEmpty("PLATFORM", platform); + ArgumentGuards.EnsureNotEmpty("USERNAME", username); + ArgumentGuards.EnsureNotEmpty("PASSWORD", password); + } + catch (ArgumentValidationException ex) + { + Output.Error(ex.Message); + return 1; + } + + if (!IpValidator.IsValidIp(ipAddress)) + { + Output.Error($"The PLC_IP_ADDRESS '{ipAddress}' is not a valid IP address."); + return 1; + } + + var certFile = new PlcTarget(ipAddress, plcName, username, password).CertificatePath; + if (!File.Exists(certFile)) + { + Output.Error($"Certification file {certFile} does not exist!!!"); + return 1; + } + + var result = await apax.SldLoadDeltaAsync(ipAddress, platform, username, password, certFile, ct); + if (!result.Success) + { + Output.Error("Downloading of the software using security certificate finished with an error!"); + return 1; + } + + Output.Success("Software has been successfully downloaded using security certificate."); + return 0; + } +} + +/// apax build + dotnet ixc + delta SW download. Port of sw_build_and_download_delta.sh. +public sealed class SwBuildDownloadDeltaCommand(ApaxClient apax, DotnetClient dotnet) +{ + public async Task ExecuteAsync(string plcName, string ipAddress, string platform, string username, string password, CancellationToken ct = default) + { + if (!(await apax.BuildAsync(ct)).Success) + { + Output.Error("apax build finished with an error!"); + return 1; + } + + if (!(await dotnet.IxcAsync(ct)).Success) + { + Output.Error("dotnet ixc finished with an error!"); + return 1; + } + + return await new SwDownloadDeltaCommand(apax).ExecuteAsync(plcName, ipAddress, platform, username, password, ct); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Diagnostics/CompareResult.cs b/src/axopen.dev/AXOpen.Dev/Diagnostics/CompareResult.cs new file mode 100644 index 000000000..bff9da7da --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Diagnostics/CompareResult.cs @@ -0,0 +1,36 @@ +namespace AXOpen.Dev.Diagnostics; + +/// Classification of an apax sld compare --mode all run. +public enum CompareOutcome +{ + Identical, + CodeBlocksDiffer, + DataBlocksDiffer, + CodeAndDataBlocksDiffer, + Unspecified, +} + +/// +/// Maps the apax compare exit codes to an outcome and a process exit code. +/// Ports compare_all.sh: 0 identical, 9 code differ, 10 data differ, 11 both differ. +/// Unlike the bash (which swallowed 9/10/11 and exited 0), the known codes are propagated +/// as the process exit code so callers can react; unknown codes fail with 1. +/// +public sealed record CompareResult(CompareOutcome Outcome, int ProcessExitCode, string Message) +{ + public bool IsIdentical => Outcome == CompareOutcome.Identical; + + public static CompareResult FromApaxExitCode(int apaxExitCode) => apaxExitCode switch + { + 0 => new(CompareOutcome.Identical, 0, + "The compiled software and loaded one are identical."), + 9 => new(CompareOutcome.CodeBlocksDiffer, 9, + "At least one code block is different between the compiled software and loaded one."), + 10 => new(CompareOutcome.DataBlocksDiffer, 10, + "At least one data block is different between the compiled software and loaded one."), + 11 => new(CompareOutcome.CodeAndDataBlocksDiffer, 11, + "At least one code block and one data block are different between the compiled software and loaded one."), + _ => new(CompareOutcome.Unspecified, 1, + "Unspecified return code during comparing! Please check the details above."), + }; +} diff --git a/src/axopen.dev/AXOpen.Dev/Hardware/HwIdentifiersGenerator.cs b/src/axopen.dev/AXOpen.Dev/Hardware/HwIdentifiersGenerator.cs new file mode 100644 index 000000000..92b6085fa --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Hardware/HwIdentifiersGenerator.cs @@ -0,0 +1,106 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace AXOpen.Dev.Hardware; + +/// +/// Generates HwIdentifiers.st (ENUM-style TYPE) and HwIdentifierList.st +/// (ARRAY of UINT) from a compiled <PLC>_HwIdentifiers.st constants file. +/// Pure port of the awk program in copy_hardware_ids.sh: entries are read from the +/// VAR_GLOBAL CONSTANT block and sorted by value ascending; output uses LF endings. +/// +public static partial class HwIdentifiersGenerator +{ + [GeneratedRegex(@"^\s*([A-Za-z0-9_]+)\s*:\s*UINT\s*:=\s*UINT#([0-9]+)\s*;")] + private static partial Regex ConstantLine(); + + public static (string Identifiers, string IdentifierList) Generate(string @namespace, string inputContent) + { + var items = Parse(inputContent); + + // asorti(..., "@val_num_asc"): order keys by associated numeric value ascending. + // Ties (not expected for HW ids) fall back to ordinal name order for determinism. + var sorted = items + .OrderBy(kvp => kvp.Value) + .ThenBy(kvp => kvp.Key, StringComparer.Ordinal) + .ToList(); + var n = sorted.Count; + + var identifiers = new StringBuilder(); + identifiers.Append("NAMESPACE ").Append(@namespace).Append('\n'); + identifiers.Append(" TYPE\n"); + identifiers.Append(" HwIdentifiers : UINT\n"); + identifiers.Append(" (\n"); + if (n == 0) + { + identifiers.Append(" NONE := UINT#0\n"); + } + else + { + for (var i = 0; i < n; i++) + { + var comma = i == n - 1 ? "" : ","; + identifiers.Append(" ").Append(sorted[i].Key) + .Append(" := UINT#").Append(sorted[i].Value).Append(comma).Append('\n'); + } + } + + identifiers.Append(" );\n"); + identifiers.Append(" END_TYPE\n"); + identifiers.Append("END_NAMESPACE\n"); + identifiers.Append('\n'); // awk print appends ORS after the assembled string + + var list = new StringBuilder(); + list.Append("NAMESPACE ").Append(@namespace).Append('\n'); + list.Append(" TYPE HwIdentifierList : ARRAY[0..").Append(n - 1).Append("] OF UINT :=\n"); + list.Append(" [\n"); + for (var i = 0; i < n; i++) + { + var comma = i == n - 1 ? "" : ","; + list.Append(" UINT#").Append(sorted[i].Value).Append(comma).Append('\n'); + } + + list.Append(" ];\n"); + list.Append("END_TYPE\n"); + list.Append("END_NAMESPACE\n"); + list.Append('\n'); + + return (identifiers.ToString(), list.ToString()); + } + + private static Dictionary Parse(string content) + { + var items = new Dictionary(StringComparer.Ordinal); + var inBlock = false; + + foreach (var raw in content.Split('\n')) + { + var line = raw.Replace("\r", string.Empty); + + if (line.Contains("VAR_GLOBAL CONSTANT", StringComparison.Ordinal)) + { + inBlock = true; + continue; + } + + if (line.Contains("END_VAR", StringComparison.Ordinal)) + { + inBlock = false; + continue; + } + + if (!inBlock) + { + continue; + } + + var match = ConstantLine().Match(line); + if (match.Success) + { + items[match.Groups[1].Value] = ulong.Parse(match.Groups[2].Value); + } + } + + return items; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Hardware/IoAddressesGenerator.cs b/src/axopen.dev/AXOpen.Dev/Hardware/IoAddressesGenerator.cs new file mode 100644 index 000000000..fce39ba3a --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Hardware/IoAddressesGenerator.cs @@ -0,0 +1,179 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace AXOpen.Dev.Hardware; + +/// Thrown when the IoAddresses input has no usable VAR_GLOBAL block. +public sealed class IoAddressesFormatException(string message) : Exception(message); + +/// +/// Generates Inputs.st, Outputs.st and IoStructures.st from a compiled +/// <PLC>_IoAddresses.st file. Pure C# port of copy_io_addresses_hwc_3_4_0.ps1 +/// (the hwc >= 3.4.0 path; the legacy < 3.4.0 awk path is intentionally dropped). +/// Output uses LF endings (the bash applied dos2unix afterwards). +/// Matches the source's case-insensitive regex semantics. +/// +public static class IoAddressesGenerator +{ + private const RegexOptions Opts = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + + public static (string Inputs, string Outputs, string Structures) Generate(string @namespace, string inputContent) + { + var all = SplitLines(inputContent); + + // Locate the first VAR_GLOBAL ... END_VAR region. + int? startVar = null; + int? endVar = null; + for (var i = 0; i < all.Length; i++) + { + if (startVar is null && Regex.IsMatch(all[i], @"^\s*VAR_GLOBAL\b", Opts)) + { + startVar = i; + continue; + } + + if (startVar is not null && Regex.IsMatch(all[i], @"^\s*END_VAR\b", Opts)) + { + endVar = i; + break; + } + } + + if (startVar is null || endVar is null || endVar <= startVar) + { + throw new IoAddressesFormatException( + "Could not locate a proper VAR_GLOBAL ... END_VAR block in the input."); + } + + var varLines = all[(startVar.Value + 1)..endVar.Value]; + + var sbIn = new StringBuilder() + .Append("NAMESPACE ").Append(@namespace).Append('\n') + .Append(" TYPE\n") + .Append(" {S7.extern=ReadWrite}\n") + .Append(" {#ix-attr:[Container(Layout.Wrap)]}\n") + .Append(" Inputs : STRUCT\n"); + var sbOut = new StringBuilder() + .Append("NAMESPACE ").Append(@namespace).Append('\n') + .Append(" TYPE\n") + .Append(" {S7.extern=ReadWrite}\n") + .Append(" {#ix-attr:[Container(Layout.Wrap)]}\n") + .Append(" Outputs : STRUCT\n"); + var sbStruct = new StringBuilder() + .Append("NAMESPACE ").Append(@namespace).Append('\n'); + + var containsInputs = false; + var containsOutputs = false; + + var idx = 0; + while (idx < varLines.Length) + { + var line = varLines[idx]; + if (Regex.IsMatch(line, @"^\s*$") || Regex.IsMatch(line, @"^\s*//")) + { + idx++; + continue; + } + + var decl = new StringBuilder(); + + // Attach the immediately preceding line if it is a comment. + var prevIdx = idx - 1; + if (prevIdx >= 0) + { + var prev = "\t" + varLines[prevIdx]; + if (Regex.IsMatch(prev, @"^\s*//")) + { + decl.Append(prev).Append('\n'); + } + } + + // Accumulate until a line containing a semicolon. + while (idx < varLines.Length) + { + var l = "\t" + varLines[idx]; + decl.Append(l).Append('\n'); + if (l.Contains(';')) + { + idx++; + break; + } + + idx++; + } + + var declText = decl.ToString(); + // The PowerShell source does `$sb.AppendLine($normalized)` where $normalized already + // ends with a newline, so each declaration block is followed by a blank line. Match + // that exactly (verified against the showcase's shipped Inputs.st/Outputs.st). + if (Regex.IsMatch(declText, @"AT\s*%I", Opts)) + { + sbIn.Append(Regex.Replace(declText, @"AT\s*%I", "AT %", Opts)).Append('\n'); + containsInputs = true; + } + else if (Regex.IsMatch(declText, @"AT\s*%Q", Opts)) + { + sbOut.Append(Regex.Replace(declText, @"AT\s*%Q", "AT %", Opts)).Append('\n'); + containsOutputs = true; + } + } + + if (!containsInputs) + { + sbIn.Append(" noInputsFoundInTheHwConfig AT %B0: BYTE;\n"); + } + + if (!containsOutputs) + { + sbOut.Append(" noOutputsFoundInTheHwConfig AT %B0: BYTE;\n"); + } + + // Re-emit the first TYPE section into the structures file, injecting the ix attributes + // immediately after each TYPE line. + int? startType = null; + for (var i = endVar.Value; i < all.Length; i++) + { + if (Regex.IsMatch(all[i], @"^\s*TYPE\b", Opts)) + { + startType = i; + break; + } + } + + if (startType is not null) + { + for (var i = startType.Value; i < all.Length; i++) + { + sbStruct.Append('\t').Append(all[i]).Append('\n'); + if (Regex.IsMatch(all[i], @"^\s*TYPE\b", Opts)) + { + sbStruct.Append(" {S7.extern=ReadWrite}\n"); + sbStruct.Append(" {#ix-attr:[Container(Layout.Wrap)]}\n"); + } + } + } + + sbIn.Append(" END_STRUCT;\n END_TYPE\nEND_NAMESPACE\n"); + sbOut.Append(" END_STRUCT;\n END_TYPE\nEND_NAMESPACE\n"); + sbStruct.Append("END_NAMESPACE\n"); + + return (sbIn.ToString(), sbOut.ToString(), sbStruct.ToString()); + } + + private static string[] SplitLines(string content) + { + var lines = content.Split('\n'); + for (var i = 0; i < lines.Length; i++) + { + lines[i] = lines[i].TrimEnd('\r'); + } + + // Get-Content drops a trailing empty line produced by a final newline. + if (lines.Length > 0 && lines[^1].Length == 0) + { + return lines[..^1]; + } + + return lines; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Hardware/StFile.cs b/src/axopen.dev/AXOpen.Dev/Hardware/StFile.cs new file mode 100644 index 000000000..482130e42 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Hardware/StFile.cs @@ -0,0 +1,24 @@ +using System.Text; + +namespace AXOpen.Dev.Hardware; + +/// Writes generated ST files with LF endings and no BOM (mirrors the bash dos2unix result). +public static class StFile +{ + private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); + + /// + /// When true, append a final newline to match PowerShell Set-Content (used for the + /// IoAddresses outputs so regenerated files are byte-identical to the shipped ones). + /// + public static void Write(string path, string content, bool trailingNewline = false) + { + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(path, trailingNewline ? content + "\n" : content, Utf8NoBom); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Observability/Output.cs b/src/axopen.dev/AXOpen.Dev/Observability/Output.cs new file mode 100644 index 000000000..79b4db17b --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Observability/Output.cs @@ -0,0 +1,13 @@ +using Spectre.Console; + +namespace AXOpen.Dev.Observability; + +/// Console output helpers. Replaces the bash colored printf messages. +public static class Output +{ + public static void Error(string message) => AnsiConsole.MarkupLineInterpolated($"[red]{message}[/]"); + + public static void Warning(string message) => AnsiConsole.MarkupLineInterpolated($"[yellow]{message}[/]"); + + public static void Success(string message) => AnsiConsole.MarkupLineInterpolated($"[green]{message}[/]"); +} diff --git a/src/axopen.dev/AXOpen.Dev/Plc/PlcTarget.cs b/src/axopen.dev/AXOpen.Dev/Plc/PlcTarget.cs new file mode 100644 index 000000000..85be2c90c --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Plc/PlcTarget.cs @@ -0,0 +1,8 @@ +namespace AXOpen.Dev.Plc; + +/// A PLC connection target. Certificate path mirrors the bash convention. +public sealed record PlcTarget(string IpAddress, string Name, string Username, string Password) +{ + /// ./certs/<name>/<name>.cer — the per-PLC certificate location used by the scripts. + public string CertificatePath => Path.Combine("./certs", Name, $"{Name}.cer"); +} diff --git a/src/axopen.dev/AXOpen.Dev/Process/IProcessRunner.cs b/src/axopen.dev/AXOpen.Dev/Process/IProcessRunner.cs new file mode 100644 index 000000000..f58b25a1f --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Process/IProcessRunner.cs @@ -0,0 +1,33 @@ +namespace AXOpen.Dev.Process; + +/// +/// Describes an external process invocation (apax, dotnet, openssl, …). +/// +public sealed record ProcessRequest +{ + public required string Executable { get; init; } + public IReadOnlyList Arguments { get; init; } = Array.Empty(); + public string? WorkingDirectory { get; init; } + public IReadOnlyDictionary? Environment { get; init; } + + /// Optional text piped to the process's standard input (e.g. the bash echo y | idiom). + public string? StandardInput { get; init; } + + /// When true, the process output is also echoed live to the console. + public bool EchoToConsole { get; init; } = true; +} + +/// Result of an external process invocation. +public sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError) +{ + public bool Success => ExitCode == 0; +} + +/// +/// Abstraction over external process execution so orchestration logic is unit-testable +/// (replace with a fake in tests) and the implementation is cross-platform. +/// +public interface IProcessRunner +{ + Task RunAsync(ProcessRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/axopen.dev/AXOpen.Dev/Process/ProcessRunner.cs b/src/axopen.dev/AXOpen.Dev/Process/ProcessRunner.cs new file mode 100644 index 000000000..8a3b0cfc2 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Process/ProcessRunner.cs @@ -0,0 +1,49 @@ +using System.Text; +using CliWrap; + +namespace AXOpen.Dev.Process; + +/// +/// Cross-platform built on CliWrap. Captures stdout/stderr +/// and, when requested, streams them live to the console. +/// +public sealed class ProcessRunner : IProcessRunner +{ + public async Task RunAsync(ProcessRequest request, CancellationToken cancellationToken = default) + { + var stdout = new StringBuilder(); + var stderr = new StringBuilder(); + + var outTarget = request.EchoToConsole + ? PipeTarget.Merge(PipeTarget.ToStringBuilder(stdout), PipeTarget.ToDelegate(Console.Out.WriteLine)) + : PipeTarget.ToStringBuilder(stdout); + + var errTarget = request.EchoToConsole + ? PipeTarget.Merge(PipeTarget.ToStringBuilder(stderr), PipeTarget.ToDelegate(Console.Error.WriteLine)) + : PipeTarget.ToStringBuilder(stderr); + + var command = Cli.Wrap(request.Executable) + .WithArguments(request.Arguments) + .WithValidation(CommandResultValidation.None) + .WithStandardOutputPipe(outTarget) + .WithStandardErrorPipe(errTarget); + + if (!string.IsNullOrEmpty(request.WorkingDirectory)) + { + command = command.WithWorkingDirectory(request.WorkingDirectory); + } + + if (request.Environment is not null) + { + command = command.WithEnvironmentVariables(request.Environment); + } + + if (request.StandardInput is not null) + { + command = command.WithStandardInputPipe(PipeSource.FromString(request.StandardInput)); + } + + var result = await command.ExecuteAsync(cancellationToken).ConfigureAwait(false); + return new ProcessResult(result.ExitCode, stdout.ToString(), stderr.ToString()); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/IFileDownloader.cs b/src/axopen.dev/AXOpen.Dev/Requisites/IFileDownloader.cs new file mode 100644 index 000000000..3f13995f8 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/IFileDownloader.cs @@ -0,0 +1,19 @@ +namespace AXOpen.Dev.Requisites; + +/// Abstracts Invoke-WebRequest -OutFile downloads used by the installers. +public interface IFileDownloader +{ + Task DownloadAsync(string url, string outputPath, CancellationToken ct = default); +} + +/// Default -based downloader. +public sealed class HttpFileDownloader : IFileDownloader +{ + public async Task DownloadAsync(string url, string outputPath, CancellationToken ct = default) + { + using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) }; + await using var source = await client.GetStreamAsync(url, ct); + await using var target = File.Create(outputPath); + await source.CopyToAsync(target, ct); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/IUserPrompt.cs b/src/axopen.dev/AXOpen.Dev/Requisites/IUserPrompt.cs new file mode 100644 index 000000000..3f33fd309 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/IUserPrompt.cs @@ -0,0 +1,27 @@ +using Spectre.Console; + +namespace AXOpen.Dev.Requisites; + +/// Abstracts the Read-Host "... (Y/N)" prompts so the checker is testable / scriptable. +public interface IUserPrompt +{ + /// Returns true when the user answers Y/y, mirroring the PowerShell prompts. + bool Confirm(string message); +} + +/// Interactive console prompt (default for the CLI). +public sealed class ConsoleUserPrompt : IUserPrompt +{ + public bool Confirm(string message) + { + AnsiConsole.Markup($"[yellow]{Markup.Escape(message)} (Y/N)[/] "); + var response = Console.ReadLine(); + return string.Equals(response?.Trim(), "Y", StringComparison.OrdinalIgnoreCase); + } +} + +/// Non-interactive prompt that always returns the same answer (for --yes / --non-interactive). +public sealed class FixedUserPrompt(bool answer) : IUserPrompt +{ + public bool Confirm(string message) => answer; +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/RequisiteChecker.cs b/src/axopen.dev/AXOpen.Dev/Requisites/RequisiteChecker.cs new file mode 100644 index 000000000..14368394e --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/RequisiteChecker.cs @@ -0,0 +1,139 @@ +using System.Text.Json; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Process; + +namespace AXOpen.Dev.Requisites; + +/// +/// Cross-platform, report-only prerequisite checks. Ports check_requisites_apax.sh, +/// check_requisites_nuget.sh and check_requisites_custom_registry.ps1. No auto-install. +/// +public sealed class RequisiteChecker(IProcessRunner runner) +{ + public const string ApaxSite = "https://console.simatic-ax.siemens.io/"; + public const string NugetFeed = "https://nuget.pkg.github.com/inxton/index.json"; + public const string NpmRegistry = "https://npm.pkg.github.com/"; + public const string ExpectedApaxVersion = "4.3.0"; + + public async Task CheckApaxAsync(CancellationToken ct = default) + { + var version = await runner.RunAsync(new ProcessRequest + { + Executable = "apax", + Arguments = new[] { "--version" }, + EchoToConsole = false, + }, ct); + + if (!version.Success) + { + Output.Error("Apax is not installed or not found in PATH. You need a valid SIMATIC-AX license."); + return false; + } + + var found = version.StandardOutput.Trim(); + if (found != ExpectedApaxVersion) + { + Output.Error($"Apax version mismatch. Expected {ExpectedApaxVersion} but found {found}. Run 'apax self-update {ExpectedApaxVersion}'."); + return false; + } + + Output.Success($"Apax {ExpectedApaxVersion} detected."); + + if (!await IsHttpReachableAsync(ApaxSite, ct)) + { + Output.Error($"Failed to access {ApaxSite}. Check your connection, firewall, credentials, etc."); + return false; + } + + Output.Success($"Feed {ApaxSite} accessible."); + + var scopes = await runner.RunAsync(new ProcessRequest + { + Executable = "apax", + Arguments = new[] { "info", "--ax-scopes" }, + EchoToConsole = false, + }, ct); + + if ((scopes.StandardOutput + scopes.StandardError).Contains("No access to the Simatic-AX registry", StringComparison.OrdinalIgnoreCase)) + { + Output.Error("Unable to access apax registries. Check your connection, firewall, credentials, etc."); + return false; + } + + Output.Success("Apax registries are accessible."); + return true; + } + + public async Task CheckNugetAsync(CancellationToken ct = default) + { + var sources = await runner.RunAsync(new ProcessRequest + { + Executable = "dotnet", + Arguments = new[] { "nuget", "list", "source" }, + EchoToConsole = false, + }, ct); + + if (!sources.StandardOutput.Contains(NugetFeed, StringComparison.OrdinalIgnoreCase)) + { + Output.Error($"The NuGet feed {NugetFeed} is not added. Add it manually (see src/README.md)."); + return false; + } + + Output.Success($"The NuGet feed {NugetFeed} is added."); + return true; + } + + public bool CheckCustomRegistry() + { + var authPath = Path.Combine(UserProfileDirectory(), ".apax", "auth.json"); + if (!File.Exists(authPath)) + { + Output.Error($"apax auth file not found at {authPath}. Run 'apax login' for the custom NPM registry."); + return false; + } + + if (AuthJsonHasRegistry(File.ReadAllText(authPath), NpmRegistry)) + { + Output.Success($"Registry {NpmRegistry} found in apax auth."); + return true; + } + + Output.Error($"Registry '{NpmRegistry}' not found in apax auth. Run 'apax login' for the custom NPM registry."); + return false; + } + + /// True when the auth json contains the registry with a non-empty registryToken. + public static bool AuthJsonHasRegistry(string json, string registryUrl) + { + try + { + using var doc = JsonDocument.Parse(json); + return doc.RootElement.TryGetProperty(registryUrl, out var registry) + && registry.TryGetProperty("registryToken", out var token) + && token.ValueKind == JsonValueKind.String + && !string.IsNullOrEmpty(token.GetString()); + } + catch (JsonException) + { + return false; + } + } + + private static string UserProfileDirectory() + => Environment.GetEnvironmentVariable("USERPROFILE") + ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + private static async Task IsHttpReachableAsync(string url, CancellationToken ct) + { + try + { + using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) }; + using var response = await client.GetAsync(url, ct); + return (int)response.StatusCode == 200; + } + catch + { + return false; + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/RequisiteParsing.cs b/src/axopen.dev/AXOpen.Dev/Requisites/RequisiteParsing.cs new file mode 100644 index 000000000..e75943556 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/RequisiteParsing.cs @@ -0,0 +1,72 @@ +using System.Text.RegularExpressions; + +namespace AXOpen.Dev.Requisites; + +/// +/// Pure parsing helpers for the tool outputs consumed by scripts/check_requisites.ps1 +/// (node -v, git --version, dotnet --list-sdks / --list-runtimes, reg query). +/// +public static partial class RequisiteParsing +{ + [GeneratedRegex(@"[0-9]+\.[0-9]+\.[0-9]+")] + private static partial Regex SemVerPattern(); + + [GeneratedRegex(@"^Microsoft\.WindowsDesktop\.App\s+([\d\.]+)\s")] + private static partial Regex DesktopRuntimePattern(); + + /// (node -v).TrimStart('v'). + public static string ParseNodeVersion(string nodeDashV) + => (nodeDashV ?? string.Empty).Trim().TrimStart('v'); + + /// First x.y.z match in git --version output, or null. + public static string? ExtractGitVersion(string gitVersionOutput) + { + var match = SemVerPattern().Match(gitVersionOutput ?? string.Empty); + return match.Success ? match.Value : null; + } + + /// True when any dotnet --list-sdks line starts with the required version followed by whitespace. + public static bool DotnetSdkListed(IEnumerable sdkLines, string requiredVersion) + { + var pattern = new Regex("^" + Regex.Escape(requiredVersion) + @"\s"); + return sdkLines.Any(line => pattern.IsMatch(line)); + } + + /// + /// True when dotnet --list-runtimes contains a Microsoft.WindowsDesktop.App whose version + /// satisfies against the requirement. + /// + public static bool HasDesktopRuntime(IEnumerable runtimeLines, string requiredVersion) + { + foreach (var line in runtimeLines) + { + var match = DesktopRuntimePattern().Match(line); + if (match.Success && VersionComparer.MajorMinorEqualBuildRevisionEqualOrHigher(match.Groups[1].Value, requiredVersion)) + { + return true; + } + } + + return false; + } + + /// Extracts a REG_SZ value from reg query ... /v Name output (the token after the type). + public static string? ExtractRegValue(string regQueryOutput, string valueName) + { + foreach (var raw in (regQueryOutput ?? string.Empty).Split('\n')) + { + var line = raw.Trim(); + if (line.StartsWith(valueName, StringComparison.OrdinalIgnoreCase) && line.Contains("REG_")) + { + // Format: "Name REG_SZ value" + var parts = Regex.Split(line, @"\s{2,}|\t+"); + if (parts.Length >= 3) + { + return parts[^1].Trim(); + } + } + } + + return null; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/RequisitesConfig.cs b/src/axopen.dev/AXOpen.Dev/Requisites/RequisitesConfig.cs new file mode 100644 index 000000000..156efb41f --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/RequisitesConfig.cs @@ -0,0 +1,55 @@ +namespace AXOpen.Dev.Requisites; + +/// +/// Constants from the top of scripts/check_requisites.ps1 (required versions, URLs, paths). +/// +public static class RequisitesConfig +{ + // Node + public const string NodeRequiredVersion = "23.7.0"; + public const string NodeWingetId = "OpenJS.NodeJS.LTS"; + + // Microsoft Visual C++ Redistributable + public const string VcpRequiredVersion = "14.38.33135.0"; + public const string VcpRegPath = @"HKLM\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64"; + public const string VcpUrl = "https://aka.ms/vs/17/release/vc_redist.x64.exe"; + + // Git + public const string GitRequiredVersion = "2.44.0"; + public const string GitWingetId = "Git.Git"; + + // .NET + public const string DotNetInstallScriptUrl = "https://dot.net/v1/dotnet-install.ps1"; + public const string DotNetSdkRequiredVersion = "10.0.100"; + public const string DotNetDesktopRuntimeRequiredVersion = "8.0.22"; + + // AX Code + public const string AxCodeRequiredVersion = "1.94.2"; + public const string AxCodeDownloadUrl = "https://console.simatic-ax.siemens.io/downloads"; + + // Apax + public const string ApaxRequiredVersion = "4.1.1"; + public const string ApaxUrl = "https://console.simatic-ax.siemens.io/"; + + // Inxton registry + NuGet feed + public const string InxtonRegistryUrl = "https://npm.pkg.github.com/"; + public const string NugetFeedUrl = "https://nuget.pkg.github.com/inxton/index.json"; + + // Visual Studio Build Tools + public const string VsBuildToolInstallationUrl = "https://aka.ms/vs/16/release/vs_buildtools.exe"; + public const string VsBuildToolRequiredVersion = "16.11.36631.11"; + public const string ExpectedVcToolsInstallDir = @"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Tools\MSVC\14.29.30133"; + + public static readonly string[] VsBuildToolInstallArgs = + { + "--wait", "--norestart", "--nocache", "--passive", + "--add", "Microsoft.VisualStudio.Workload.VCTools", + "--add", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "--add", "Microsoft.VisualStudio.Component.Windows10SDK", + "--add", "Microsoft.VisualStudio.Component.Windows10SDK.19041", + }; + + // Visual Studio (optional) + public const string VisualStudioRequiredVersionRange = "[17.8.0,19.0)"; + public const string VisualStudioDownloadUrl = "https://visualstudio.microsoft.com/vs/"; +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/SecretMasking.cs b/src/axopen.dev/AXOpen.Dev/Requisites/SecretMasking.cs new file mode 100644 index 000000000..680381cde --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/SecretMasking.cs @@ -0,0 +1,26 @@ +namespace AXOpen.Dev.Requisites; + +/// +/// Faithful port of the token/username masking in the @inxton registry section of +/// scripts/check_requisites.ps1. Used only for display. +/// +public static class SecretMasking +{ + /// first 5 + stars + last 1 when longer than 6 chars; otherwise all stars. + public static string MaskToken(string token) + { + token ??= string.Empty; + return token.Length > 6 + ? token[..5] + new string('*', token.Length - 6) + token[^1] + : new string('*', token.Length); + } + + /// first 1 + stars + last 1 when longer than 2 chars; otherwise all stars. + public static string MaskUserName(string userName) + { + userName ??= string.Empty; + return userName.Length > 2 + ? userName[..1] + new string('*', userName.Length - 2) + userName[^1] + : new string('*', userName.Length); + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/SystemEnvironment.cs b/src/axopen.dev/AXOpen.Dev/Requisites/SystemEnvironment.cs new file mode 100644 index 000000000..9720253c4 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/SystemEnvironment.cs @@ -0,0 +1,67 @@ +using System.Runtime.Versioning; +using SysProcess = System.Diagnostics.Process; +using ProcessStartInfo = System.Diagnostics.ProcessStartInfo; + +namespace AXOpen.Dev.Requisites; + +/// +/// Windows environment + elevated-process helpers used by the installers in the requisites checker. +/// Ports Refresh-Path, the user-PATH persistence, env-var set/get and Start-Process -Verb RunAs. +/// +[SupportedOSPlatform("windows")] +public static class SystemEnvironment +{ + /// Refresh-Path: rebuilds the process PATH from Machine + User scopes. + public static void RefreshPath() + { + var machine = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine); + var user = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.User); + Environment.SetEnvironmentVariable("Path", $"{machine};{user}"); + } + + /// Prepends to the user PATH (persisted) if absent, then refreshes. + public static void EnsureUserPath(string directory) + { + var existing = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty; + if (!existing.Contains(directory, StringComparison.OrdinalIgnoreCase)) + { + Environment.SetEnvironmentVariable("PATH", $"{directory};{existing}", EnvironmentVariableTarget.User); + RefreshPath(); + } + } + + public static string? GetUserEnv(string name) + => Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.User); + + public static void SetUserEnv(string name, string value) + => Environment.SetEnvironmentVariable(name, value, EnvironmentVariableTarget.User); + + /// Runs an installer elevated (-Verb RunAs) and waits; returns its exit code (or -1 if cancelled). + public static int RunElevated(string fileName, string arguments) + { + var psi = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = true, + Verb = "runas", + }; + + try + { + using var proc = SysProcess.Start(psi); + if (proc is null) + { + return -1; + } + + proc.WaitForExit(); + return proc.ExitCode; + } + catch (Exception) + { + // The UAC prompt was declined or the shell could not elevate. + return -1; + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/SystemRequisitesChecker.cs b/src/axopen.dev/AXOpen.Dev/Requisites/SystemRequisitesChecker.cs new file mode 100644 index 000000000..e8f36a52a --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/SystemRequisitesChecker.cs @@ -0,0 +1,576 @@ +using System.Runtime.Versioning; +using AXOpen.Dev.Observability; +using AXOpen.Dev.Process; +using Spectre.Console; + +namespace AXOpen.Dev.Requisites; + +/// +/// Faithful port of scripts/check_requisites.ps1: a Windows developer-machine bootstrap that +/// verifies (and optionally installs) Node, VC++ Redistributable, Git, the .NET SDK + Desktop Runtime, +/// VS Build Tools and the VCToolsInstallDir env var, then checks AX Code, Apax, the @inxton registry, +/// the GitHub NuGet feed and Visual Studio. Process calls go through ; +/// prompts through ; downloads through . +/// +public sealed class SystemRequisitesChecker(IProcessRunner runner, IUserPrompt prompt, IFileDownloader downloader) +{ + /// Runs the full sequence. Returns the process exit code (mirrors the script's exit points). + public async Task RunAsync(CancellationToken ct = default) + { + if (!OperatingSystem.IsWindows()) + { + Output.Warning("check-requisites is a Windows developer-machine bootstrap; running the portable subset only."); + var portable = new RequisiteChecker(runner); + var ok = await portable.CheckApaxAsync(ct) & await portable.CheckNugetAsync(ct) & portable.CheckCustomRegistry(); + return ok ? 0 : 1; + } + + return await RunWindowsAsync(ct); + } + + [SupportedOSPlatform("windows")] + private async Task RunWindowsAsync(CancellationToken ct) + { + SystemEnvironment.RefreshPath(); + + if (!await ToolAvailableAsync("winget", ct)) + { + Output.Error("winget is not available on this system."); + AnsiConsole.MarkupLine("[yellow] To install winget:\n 1. Proceed to: https://learn.microsoft.com/en-us/windows/package-manager/winget/.\n 2. Follow the on-site instructions to download and install winget.[/]"); + return 1; + } + + // Node + if (!await VerifyNodeAsync(ct) && prompt.Confirm($"Node.js {RequisitesConfig.NodeRequiredVersion} is not installed. Would you like to install it now?")) + { + await InstallViaWingetAsync(RequisitesConfig.NodeWingetId, "Node.js", ct); + } + + // Microsoft Visual C++ Redistributable + if (!await VerifyVcpAsync(ct) && prompt.Confirm($"Microsoft Visual C++ Redistributable {RequisitesConfig.VcpRequiredVersion} is not installed. Would you like to install it now?")) + { + await InstallVcpAsync(ct); + } + + // Git + if (!await VerifyGitAsync(ct) && prompt.Confirm($"Git {RequisitesConfig.GitRequiredVersion} is not installed. Would you like to install it now?")) + { + await InstallViaWingetAsync(RequisitesConfig.GitWingetId, "Git", ct); + } + + // .NET SDK + if (!await VerifyDotNetSdkAsync(ct) && prompt.Confirm($".NET {RequisitesConfig.DotNetSdkRequiredVersion} SDK is not installed. Would you like to install it now?")) + { + await InstallDotNetSdkAsync(ct); + } + + // .NET Desktop Runtime x86 + x64 + if (!await VerifyDotNetDesktopRuntimeAsync("x86", ct) && prompt.Confirm($".NET {RequisitesConfig.DotNetDesktopRuntimeRequiredVersion} runtime x86 is not installed. Would you like to install it now?")) + { + await InstallDotNetDesktopRuntimeAsync("x86", ct); + } + + if (!await VerifyDotNetDesktopRuntimeAsync("x64", ct) && prompt.Confirm($".NET {RequisitesConfig.DotNetDesktopRuntimeRequiredVersion} runtime x64 is not installed. Would you like to install it now?")) + { + await InstallDotNetDesktopRuntimeAsync("x64", ct); + } + + // AX Code (warn on mismatch; exit 1 when not installed) + var axCode = await RunAsync("axcode", new[] { "--version" }, ct); + if (!axCode.Success) + { + Output.Error("Error: Unable to determine the AXCode version. Ensure AXCode is correctly installed and accessible from the command line."); + AnsiConsole.MarkupLine($"[yellow]To install the AXCode:\n 1. Ensure that you have a valid SIMATIC AX license.\n 2. Verify that you have access to {RequisitesConfig.AxCodeDownloadUrl}.\n 3. Download AX Code for Windows.\n 4. Install it, restart your computer, and run this check again.[/]"); + return 1; + } + + var axVersion = axCode.StandardOutput.Split('\n').FirstOrDefault()?.Trim() ?? string.Empty; + if (!VersionComparer.IsValidRequiredVersion(axVersion) || !VersionComparer.EqualOrHigher(axVersion, RequisitesConfig.AxCodeRequiredVersion)) + { + Output.Error($"The AXCode version ({axVersion}) does not match the expected version: {RequisitesConfig.AxCodeRequiredVersion}. It's highly recommended to update it."); + } + else + { + Output.Success($"AX Code {axVersion} detected."); + } + + // Apax installation + var isApaxInstalled = false; + var apax = await RunAsync("apax", new[] { "--version" }, ct); + if (apax.Success && VersionComparer.IsValidRequiredVersion(apax.StandardOutput.Trim()) && + VersionComparer.EqualOrHigher(apax.StandardOutput.Trim(), RequisitesConfig.ApaxRequiredVersion)) + { + isApaxInstalled = true; + Output.Success($"APAX {apax.StandardOutput.Trim()} detected."); + } + else if (apax.Success) + { + Output.Error($"The APAX version does not match the expected version: {RequisitesConfig.ApaxRequiredVersion}. It's highly recommended to update it."); + } + + if (!isApaxInstalled) + { + Output.Warning($"Apax is not installed or not found in PATH. You need a valid SIMATIC-AX license.\nTo download Apax:\n 1. Proceed to: {RequisitesConfig.AxCodeDownloadUrl} in your browser.\n 2. Log in with your credentials.\n 3. Follow the on-site instructions to download and install Apax."); + } + + // Apax network + registry access + if (isApaxInstalled) + { + if (await IsHttpReachableAsync(RequisitesConfig.ApaxUrl, ct)) + { + Output.Success($"Feed: {RequisitesConfig.ApaxUrl} accessible by means of network."); + } + else + { + Output.Error($"Failed to access feed: {RequisitesConfig.ApaxUrl}. Check your connection, firewall settings, etc."); + } + } + + var scopes = await RunAsync("apax", new[] { "info", "--ax-scopes" }, ct); + if ((scopes.StandardOutput + scopes.StandardError).Contains("No access to the Simatic-AX registry", StringComparison.OrdinalIgnoreCase)) + { + Output.Error("Unable to access apax packages."); + AnsiConsole.MarkupLine($"[yellow] 1. Check your connections, firewall, credentials etc.\n 2. Proceed to: {RequisitesConfig.ApaxUrl} and verify it is accessible.\n 3. Log in, then run 'apax login' and choose 'AX (for Apax packages and IDE extensions)'.\n 4. Run this check again.[/]"); + return 1; + } + + Output.Success("Apax packages are accessible."); + + // @inxton registry (auth.json) + if (!CheckInxtonRegistry()) + { + return 1; + } + + // GitHub NuGet feed + var feedExit = await CheckNugetFeedAsync(ct); + if (feedExit != 0) + { + return feedExit; + } + + // VS Build Tools + VCToolsInstallDir env var + if (!await VerifyVsBuildToolsAsync(ct) && prompt.Confirm($"VSBuildTools {RequisitesConfig.VsBuildToolRequiredVersion} is not installed. Would you like to install it now?")) + { + await InstallVsBuildToolsAsync(ct); + } + + if (!VerifyVcToolsInstallDirEnvVar() && + prompt.Confirm($"VCToolsInstallDir environment variable is not set to: {RequisitesConfig.ExpectedVcToolsInstallDir} for the current user. Would you like to set it now?")) + { + SystemEnvironment.SetUserEnv("VCToolsInstallDir", RequisitesConfig.ExpectedVcToolsInstallDir); + } + + // Visual Studio (optional) + CheckVisualStudio(); + + return 0; + } + + // ---- Node ------------------------------------------------------------------------------ + + [SupportedOSPlatform("windows")] + private async Task VerifyNodeAsync(CancellationToken ct) + { + var node = await RunAsync("node", new[] { "-v" }, ct); + if (!node.Success) + { + Output.Error("Node.js is not installed or not found in PATH."); + return false; + } + + return LogVersion("Node.js", RequisiteParsing.ParseNodeVersion(node.StandardOutput), RequisitesConfig.NodeRequiredVersion, VersionComparer.EqualOrHigher); + } + + // ---- VC++ Redistributable (registry) --------------------------------------------------- + + [SupportedOSPlatform("windows")] + private async Task VerifyVcpAsync(CancellationToken ct) + { + var query = await RunAsync("reg", new[] { "query", RequisitesConfig.VcpRegPath }, ct); + var installed = RequisiteParsing.ExtractRegValue(query.StandardOutput, "Installed"); + var version = RequisiteParsing.ExtractRegValue(query.StandardOutput, "Version")?.TrimStart('v'); + + var ok = query.Success + && (installed == "0x1" || installed == "1") + && version is not null + && VersionComparer.IsValidRequiredVersion(version) + && VersionComparer.EqualOrHigher(version, RequisitesConfig.VcpRequiredVersion); + + if (ok) + { + Output.Success($"VC++ Redistributable {RequisitesConfig.VcpRequiredVersion} detected."); + } + else + { + Output.Error($"VC++ Redistributable {RequisitesConfig.VcpRequiredVersion} is not installed."); + } + + return ok; + } + + [SupportedOSPlatform("windows")] + private async Task InstallVcpAsync(CancellationToken ct) + { + var installer = Path.Combine(Path.GetTempPath(), "vc_redist.x64.exe"); + Output.Warning("Downloading VC++ Redistributable..."); + await downloader.DownloadAsync(RequisitesConfig.VcpUrl, installer, ct); + Output.Warning("Installing VC++ Redistributable..."); + await RunAsync(installer, new[] { "/install", "/quiet", "/norestart" }, ct); + if (!await VerifyVcpAsync(ct)) + { + Output.Error("Error installing VC++ Redistributable."); + } + } + + // ---- Git ------------------------------------------------------------------------------- + + [SupportedOSPlatform("windows")] + private async Task VerifyGitAsync(CancellationToken ct) + { + var git = await RunAsync("git", new[] { "--version" }, ct); + var version = git.Success ? RequisiteParsing.ExtractGitVersion(git.StandardOutput) : null; + if (version is null) + { + Output.Error("Git is not installed."); + return false; + } + + return LogVersion("Git", version, RequisitesConfig.GitRequiredVersion, VersionComparer.EqualOrHigher); + } + + // ---- .NET SDK -------------------------------------------------------------------------- + + [SupportedOSPlatform("windows")] + private async Task VerifyDotNetSdkAsync(CancellationToken ct) + { + var sdks = await RunAsync("dotnet", new[] { "--list-sdks" }, ct); + var ok = sdks.Success && RequisiteParsing.DotnetSdkListed(SplitLines(sdks.StandardOutput), RequisitesConfig.DotNetSdkRequiredVersion); + if (ok) + { + Output.Success($".NET {RequisitesConfig.DotNetSdkRequiredVersion} SDK detected."); + } + else + { + Output.Error($".NET {RequisitesConfig.DotNetSdkRequiredVersion} SDK is not installed."); + } + + return ok; + } + + [SupportedOSPlatform("windows")] + private async Task InstallDotNetSdkAsync(CancellationToken ct) + { + var script = Path.Combine(Path.GetTempPath(), "dotnet-install.ps1"); + var installDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "dotnet"); + try + { + Output.Warning("Downloading dotnet-install.ps1... The installer will request administrator rights."); + await downloader.DownloadAsync(RequisitesConfig.DotNetInstallScriptUrl, script, ct); + + var args = $"-NoProfile -ExecutionPolicy Bypass -File \"{script}\" -Version \"{RequisitesConfig.DotNetSdkRequiredVersion}\" -InstallDir \"{installDir}\" -NoPath"; + var exit = SystemEnvironment.RunElevated("powershell.exe", args); + if (exit != 0) + { + Output.Error($"dotnet-install.ps1 failed with exit code {exit}."); + return; + } + + SystemEnvironment.SetUserEnv("DOTNET_ROOT", installDir); + SystemEnvironment.EnsureUserPath(installDir); + + if (!await VerifyDotNetSdkAsync(ct)) + { + Output.Error("Error installing dotnet (or dotnet not visible in this session)."); + } + else + { + Output.Success($".NET SDK {RequisitesConfig.DotNetSdkRequiredVersion} installed successfully."); + } + } + finally + { + if (File.Exists(script)) + { + File.Delete(script); + } + } + } + + // ---- .NET Desktop Runtime -------------------------------------------------------------- + + [SupportedOSPlatform("windows")] + private async Task VerifyDotNetDesktopRuntimeAsync(string architecture, CancellationToken ct) + { + var runtimes = await RunAsync("dotnet", new[] { "--list-runtimes" }, ct); + var ok = runtimes.Success && RequisiteParsing.HasDesktopRuntime(SplitLines(runtimes.StandardOutput), RequisitesConfig.DotNetDesktopRuntimeRequiredVersion); + if (ok) + { + Output.Success($".NET {RequisitesConfig.DotNetDesktopRuntimeRequiredVersion} runtime {architecture} detected."); + } + else + { + Output.Error($".NET {RequisitesConfig.DotNetDesktopRuntimeRequiredVersion} runtime {architecture} is not installed."); + } + + return ok; + } + + [SupportedOSPlatform("windows")] + private async Task InstallDotNetDesktopRuntimeAsync(string architecture, CancellationToken ct) + { + var channel = string.Join('.', RequisitesConfig.DotNetDesktopRuntimeRequiredVersion.Split('.').Take(2)); + var url = $"https://aka.ms/dotnet/{channel}/windowsdesktop-runtime-win-{architecture}.exe"; + var installer = Path.Combine(Path.GetTempPath(), $"dotnet-desktop-runtime-{channel}-{architecture}.exe"); + + Output.Warning($"Downloading .NET Desktop Runtime {channel} ({architecture})... The installer will request administrator rights."); + await downloader.DownloadAsync(url, installer, ct); + Output.Warning($"Installing .NET Desktop Runtime {channel} ({architecture})..."); + var exit = SystemEnvironment.RunElevated(installer, "/install /quiet /norestart"); + if (exit != 0 || !await VerifyDotNetDesktopRuntimeAsync(architecture, ct)) + { + Output.Error($".NET Desktop Runtime {RequisitesConfig.DotNetDesktopRuntimeRequiredVersion} ({architecture}) installation failed or could not be verified."); + } + else + { + Output.Success($".NET Desktop Runtime {RequisitesConfig.DotNetDesktopRuntimeRequiredVersion} ({architecture}) installed successfully."); + } + } + + // ---- @inxton registry ------------------------------------------------------------------ + + [SupportedOSPlatform("windows")] + private bool CheckInxtonRegistry() + { + var userProfile = Environment.GetEnvironmentVariable("USERPROFILE") + ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var authPath = Path.Combine(userProfile, ".apax", "auth.json"); + var maskedPath = @"\.apax\auth.json"; + + if (!File.Exists(authPath)) + { + Output.Error($"Registry '{RequisitesConfig.InxtonRegistryUrl}' not found in {maskedPath}."); + WriteRegistryGuide(); + return false; + } + + if (!RequisiteChecker.AuthJsonHasRegistry(File.ReadAllText(authPath), RequisitesConfig.InxtonRegistryUrl)) + { + Output.Error($"Registry '{RequisitesConfig.InxtonRegistryUrl}' not found in {maskedPath}."); + WriteRegistryGuide(); + return false; + } + + Output.Success($"Registry {RequisitesConfig.InxtonRegistryUrl} found in {maskedPath}!"); + return true; + } + + private static void WriteRegistryGuide() + => AnsiConsole.MarkupLine("[yellow]You need to provide apax login to the external registry. Run 'apax login', choose 'Custom NPM registry', enter the URL https://npm.pkg.github.com/, your username and a GitHub PAT with at least 'read:packages'.[/]"); + + // ---- GitHub NuGet feed ----------------------------------------------------------------- + + private async Task CheckNugetFeedAsync(CancellationToken ct) + { + var sources = await RunAsync("dotnet", new[] { "nuget", "list", "source" }, ct); + var added = sources.StandardOutput.Contains(RequisitesConfig.NugetFeedUrl, StringComparison.OrdinalIgnoreCase); + + if (added) + { + Output.Success($"The NuGet feed with URL {RequisitesConfig.NugetFeedUrl} is already added."); + } + else + { + Output.Error($"The NuGet feed with URL {RequisitesConfig.NugetFeedUrl} is not added."); + } + + var accessible = added && await IsHttpReachableAsync(RequisitesConfig.NugetFeedUrl, ct); + if (added && accessible) + { + Output.Success($"Feed: {RequisitesConfig.NugetFeedUrl} accessible by means of network."); + } + + if (!(added && accessible)) + { + var guide = "You need to add the GitHub NuGet feed to your sources manually:\n" + + " dotnet nuget add source --username [YOUR_GITHUB_USERNAME] --password [YOUR_PAT] " + + $"--store-password-in-clear-text --name gh-packages-inxton {RequisitesConfig.NugetFeedUrl}"; + AnsiConsole.MarkupLineInterpolated($"[yellow]{guide}[/]"); + return 0; // the script exits 0 here (non-fatal guidance) + } + + return 0; + } + + // ---- VS Build Tools -------------------------------------------------------------------- + + [SupportedOSPlatform("windows")] + private async Task VerifyVsBuildToolsAsync(CancellationToken ct) + { + var vswhere = VsWherePath(); + if (!File.Exists(vswhere)) + { + Output.Error("vswhere.exe not found. Visual Studio Installer is missing."); + return false; + } + + var retval = false; + var path = await RunAsync(vswhere, new[] { "-products", "Microsoft.VisualStudio.Product.BuildTools", "-property", "installationPath" }, ct); + if (!string.IsNullOrWhiteSpace(path.StandardOutput)) + { + Output.Success($"Visual Studio Build Tools already installed at: {path.StandardOutput.Trim()}"); + var version = await RunAsync(vswhere, new[] { "-products", "Microsoft.VisualStudio.Product.BuildTools", "-property", "installationVersion" }, ct); + var found = version.StandardOutput.Trim(); + retval = VersionComparer.IsValidRequiredVersion(found) + && VersionComparer.MajorMinorEqualBuildRevisionEqualOrHigher(found, RequisitesConfig.VsBuildToolRequiredVersion); + } + + if (Directory.Exists(RequisitesConfig.ExpectedVcToolsInstallDir)) + { + Output.Success($"VSBuildTools default installation path exists: {RequisitesConfig.ExpectedVcToolsInstallDir}"); + } + else + { + Output.Error($"VSBuildTools default installation path could not be found: {RequisitesConfig.ExpectedVcToolsInstallDir}"); + return false; + } + + return retval; + } + + [SupportedOSPlatform("windows")] + private async Task InstallVsBuildToolsAsync(CancellationToken ct) + { + Output.Warning("VS Build Tools not found. Installing..."); + var installer = Path.Combine(Path.GetTempPath(), "vs_buildtools.exe"); + try + { + await downloader.DownloadAsync(RequisitesConfig.VsBuildToolInstallationUrl, installer, ct); + await RunAsync(installer, RequisitesConfig.VsBuildToolInstallArgs, ct); + + if (Directory.Exists(RequisitesConfig.ExpectedVcToolsInstallDir)) + { + Output.Success($"VSBuildTools default installation path exists: {RequisitesConfig.ExpectedVcToolsInstallDir}"); + SystemEnvironment.SetUserEnv("VCToolsInstallDir", RequisitesConfig.ExpectedVcToolsInstallDir); + } + else + { + Output.Error($"VSBuildTools default installation path could not be found: {RequisitesConfig.ExpectedVcToolsInstallDir}"); + } + } + catch (Exception) + { + Output.Error("VS Build Tools installation finished with an error."); + } + } + + [SupportedOSPlatform("windows")] + private bool VerifyVcToolsInstallDirEnvVar() + { + if (!Directory.Exists(RequisitesConfig.ExpectedVcToolsInstallDir)) + { + Output.Error($"VSBuildTools default installation path could not be found: {RequisitesConfig.ExpectedVcToolsInstallDir}"); + return false; + } + + var value = SystemEnvironment.GetUserEnv("VCToolsInstallDir"); + if (string.Equals(value, RequisitesConfig.ExpectedVcToolsInstallDir, StringComparison.OrdinalIgnoreCase)) + { + Output.Success($"VCToolsInstallDir is set to: {value}"); + return true; + } + + Output.Error("VCToolsInstallDir is not set correctly."); + return false; + } + + // ---- Visual Studio (optional) ---------------------------------------------------------- + + [SupportedOSPlatform("windows")] + private void CheckVisualStudio() + { + const string editorNote = "Visual Studio is optional; you can use VSCode, Rider, or AX Code to edit .NET files."; + var vswhere = VsWherePath(); + if (!File.Exists(vswhere)) + { + Output.Warning($"vswhere tool not found. Unable to determine if Visual Studio is installed. {editorNote}"); + return; + } + + var result = runner.RunAsync(new ProcessRequest + { + Executable = vswhere, + Arguments = new[] { "-version", RequisitesConfig.VisualStudioRequiredVersionRange, "-products", "*", "-property", "catalog_productDisplayVersion" }, + EchoToConsole = false, + }).GetAwaiter().GetResult(); + + if (string.IsNullOrWhiteSpace(result.StandardOutput)) + { + Output.Warning($"Visual Studio is not detected in the required version range {RequisitesConfig.VisualStudioRequiredVersionRange}. {editorNote}"); + if (prompt.Confirm("Visual Studio is not detected. Would you like to download it now?")) + { + SystemEnvironment.RunElevated("cmd.exe", $"/c start {RequisitesConfig.VisualStudioDownloadUrl}"); + } + } + else + { + Output.Success($"Visual Studio detected: {result.StandardOutput.Trim()}. {editorNote}"); + } + } + + // ---- helpers --------------------------------------------------------------------------- + + [SupportedOSPlatform("windows")] + private async Task InstallViaWingetAsync(string packageId, string displayName, CancellationToken ct) + { + Output.Warning($"Installing {displayName} via winget..."); + await RunAsync("winget", new[] + { + "install", "--id", packageId, "--exact", "--silent", + "--accept-package-agreements", "--accept-source-agreements", + }, ct); + SystemEnvironment.RefreshPath(); + } + + private static string VsWherePath() + { + var programFilesX86 = Environment.GetEnvironmentVariable("ProgramFiles(x86)") + ?? Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + return Path.Combine(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe"); + } + + private async Task ToolAvailableAsync(string executable, CancellationToken ct) + => (await RunAsync(executable, new[] { "--version" }, ct)).Success; + + private Task RunAsync(string executable, string[] arguments, CancellationToken ct) + => runner.RunAsync(new ProcessRequest { Executable = executable, Arguments = arguments, EchoToConsole = false }, ct); + + private static IReadOnlyList SplitLines(string text) + => text.Split('\n').Select(l => l.Trim('\r')).Where(l => l.Length > 0).ToList(); + + private static bool LogVersion(string item, string actual, string required, Func comparer) + { + if (VersionComparer.IsValidRequiredVersion(actual) && comparer(actual, required)) + { + Output.Success($"The actual version of the {item} ({actual}) is equal or higher than required ({required})."); + return true; + } + + Output.Error($"The actual version of the {item} ({actual}) is lower than required ({required})."); + return false; + } + + private static async Task IsHttpReachableAsync(string url, CancellationToken ct) + { + try + { + using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) }; + using var response = await client.GetAsync(url, ct); + return (int)response.StatusCode == 200; + } + catch + { + return false; + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Requisites/VersionComparer.cs b/src/axopen.dev/AXOpen.Dev/Requisites/VersionComparer.cs new file mode 100644 index 000000000..9020061e7 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Requisites/VersionComparer.cs @@ -0,0 +1,48 @@ +using System.Text.RegularExpressions; + +namespace AXOpen.Dev.Requisites; + +/// +/// Faithful port of the version-comparison helpers in scripts/check_requisites.ps1. +/// PowerShell's [version] cast maps to , where unspecified +/// components are -1 (so 8.0 < 8.0.0). The comparisons preserve that semantics. +/// +public static partial class VersionComparer +{ + [GeneratedRegex(@"^\d+\.\d+(\.\d+){0,2}$")] + private static partial Regex RequiredVersionPattern(); + + /// Mirrors the guard each Verify* function applies to its RequiredVersion parameter. + public static bool IsValidRequiredVersion(string requiredVersion) + => !string.IsNullOrEmpty(requiredVersion) && RequiredVersionPattern().IsMatch(requiredVersion); + + /// MajorMinorBuildRevisionEqual — actual version equals the required version exactly. + public static bool Equal(string actualVersion, string requiredVersion) + => Version.Parse(actualVersion) == Version.Parse(requiredVersion); + + /// MajorMinorBuildRevisionEqualOrHigher — actual version is greater than or equal to required. + public static bool EqualOrHigher(string actualVersion, string requiredVersion) + => Version.Parse(actualVersion) >= Version.Parse(requiredVersion); + + /// MajorMinorEqualBuildRevisionEqualOrHigher — major+minor equal, build+revision >= required. + public static bool MajorMinorEqualBuildRevisionEqualOrHigher(string actualVersion, string requiredVersion) + { + var actual = Version.Parse(actualVersion); + var required = Version.Parse(requiredVersion); + return actual.Major == required.Major + && actual.Minor == required.Minor + && actual.Build >= required.Build + && actual.Revision >= required.Revision; + } + + /// MajorMinorBuildEqualRevisionEqualOrHigher — major+minor+build equal, revision >= required. + public static bool MajorMinorBuildEqualRevisionEqualOrHigher(string actualVersion, string requiredVersion) + { + var actual = Version.Parse(actualVersion); + var required = Version.Parse(requiredVersion); + return actual.Major == required.Major + && actual.Minor == required.Minor + && actual.Build == required.Build + && actual.Revision >= required.Revision; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Scaffolding/ComponentScaffolder.cs b/src/axopen.dev/AXOpen.Dev/Scaffolding/ComponentScaffolder.cs new file mode 100644 index 000000000..14d2cd3e3 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Scaffolding/ComponentScaffolder.cs @@ -0,0 +1,123 @@ +using System.Text; + +namespace AXOpen.Dev.Scaffolding; + +/// +/// Copies a component template tree to a destination, renaming directories/files that contain +/// the template name and replacing the template name + namespace inside text files. +/// Port of the copy/rename/sed steps in create_complex_component.sh. +/// +public static class ComponentScaffolder +{ + public static string Scaffold( + string sourceDirectory, + string destinationParent, + string componentName, + string componentNamespace, + string templateName = "TemplateComponent", + string templateNamespace = "Template.Axolibrary") + { + if (!Directory.Exists(sourceDirectory)) + { + throw new DirectoryNotFoundException($"Source directory '{sourceDirectory}' does not exist."); + } + + var finalDirectory = Path.Combine(destinationParent, componentName); + if (Directory.Exists(finalDirectory) || File.Exists(finalDirectory)) + { + throw new IOException($"Destination directory '{finalDirectory}' already exists."); + } + + Directory.CreateDirectory(destinationParent); + var copied = Path.Combine(destinationParent, Path.GetFileName(sourceDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + CopyTree(sourceDirectory, copied); + + RenameDirectories(copied, templateName, componentName); + // The copied root itself may carry the template name. + var renamedRoot = RenameLeaf(copied, templateName, componentName); + + RenameFiles(renamedRoot, templateName, componentName); + ReplaceInTextFiles(renamedRoot, templateName, componentName, templateNamespace, componentNamespace); + + return renamedRoot; + } + + private static void CopyTree(string source, string destination) + { + Directory.CreateDirectory(destination); + foreach (var dir in Directory.EnumerateDirectories(source, "*", SearchOption.AllDirectories)) + { + Directory.CreateDirectory(dir.Replace(source, destination)); + } + + foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)) + { + File.Copy(file, file.Replace(source, destination), overwrite: false); + } + } + + private static void RenameDirectories(string root, string templateName, string componentName) + { + // Deepest first so parent renames don't invalidate child paths. + var directories = Directory + .EnumerateDirectories(root, "*", SearchOption.AllDirectories) + .OrderByDescending(d => d.Length) + .ToList(); + + foreach (var dir in directories) + { + if (Path.GetFileName(dir).Contains(templateName, StringComparison.Ordinal)) + { + RenameLeaf(dir, templateName, componentName); + } + } + } + + private static void RenameFiles(string root, string templateName, string componentName) + { + foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories).ToList()) + { + var name = Path.GetFileName(file); + if (name.Contains(templateName, StringComparison.Ordinal)) + { + var target = Path.Combine(Path.GetDirectoryName(file)!, name.Replace(templateName, componentName)); + File.Move(file, target); + } + } + } + + private static string RenameLeaf(string path, string templateName, string componentName) + { + var name = Path.GetFileName(path); + if (!name.Contains(templateName, StringComparison.Ordinal)) + { + return path; + } + + var target = Path.Combine(Path.GetDirectoryName(path)!, name.Replace(templateName, componentName)); + Directory.Move(path, target); + return target; + } + + private static void ReplaceInTextFiles(string root, string templateName, string componentName, string templateNamespace, string componentNamespace) + { + foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) + { + var bytes = File.ReadAllBytes(file); + if (Array.IndexOf(bytes, (byte)0) >= 0) + { + continue; // binary heuristic — skip (mirrors the bash `file | grep text` guard) + } + + var text = Encoding.UTF8.GetString(bytes); + var replaced = text + .Replace(templateName, componentName) + .Replace(templateNamespace, componentNamespace); + + if (!ReferenceEquals(text, replaced) && text != replaced) + { + File.WriteAllText(file, replaced, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + } + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Scaffolding/NamespaceConverter.cs b/src/axopen.dev/AXOpen.Dev/Scaffolding/NamespaceConverter.cs new file mode 100644 index 000000000..bfdf400b5 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Scaffolding/NamespaceConverter.cs @@ -0,0 +1,48 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace AXOpen.Dev.Scaffolding; + +/// +/// Converts an apax-style namespace into the PascalCase component namespace. +/// Port of the bash logic in create_complex_component.sh: drop everything up to and +/// including the first '/', split on '.', capitalize each segment's first letter (preserving +/// the rest), with a special case mapping any-case "axopen" to "AXOpen". +/// +public static class NamespaceConverter +{ + public static string ToComponentNamespace(string @namespace) + { + var afterSlash = @namespace.Contains('/') + ? @namespace[(@namespace.IndexOf('/') + 1)..] + : @namespace; + + var builder = new StringBuilder(); + foreach (var part in afterSlash.Split('.')) + { + if (string.Equals(part, "axopen", StringComparison.OrdinalIgnoreCase)) + { + builder.Append("AXOpen."); + } + else if (part.Length > 0) + { + builder.Append(char.ToUpperInvariant(part[0])).Append(part[1..]).Append('.'); + } + else + { + builder.Append('.'); + } + } + + return builder.ToString().TrimEnd('.'); + } +} + +/// Component name validation: must start upper-case, then letters/digits/underscore. +public static partial class ComponentName +{ + [GeneratedRegex(@"^[A-Z][a-zA-Z0-9_]*$")] + private static partial Regex Pattern(); + + public static bool IsValid(string? name) => !string.IsNullOrEmpty(name) && Pattern().IsMatch(name); +} diff --git a/src/axopen.dev/AXOpen.Dev/Tools/DotnetClient.cs b/src/axopen.dev/AXOpen.Dev/Tools/DotnetClient.cs new file mode 100644 index 000000000..c32177eb3 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Tools/DotnetClient.cs @@ -0,0 +1,21 @@ +using AXOpen.Dev.Process; + +namespace AXOpen.Dev.Tools; + +/// Thin wrapper over the dotnet CLI for the AX builder tools. +public sealed class DotnetClient(IProcessRunner runner) +{ + public const string Executable = "dotnet"; + + /// dotnet ixc — runs the AX# compiler. + public Task IxcAsync(CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest { Executable = Executable, Arguments = new[] { "ixc" } }, ct); + + /// dotnet run --project PROJECT -- ARGS + public Task RunProjectAsync(string projectPath, IReadOnlyList args, CancellationToken ct = default) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = new[] { "run", "--project", projectPath, "--" }.Concat(args).ToArray(), + }, ct); +} diff --git a/src/axopen.dev/AXOpen.Dev/Tools/OpensslClient.cs b/src/axopen.dev/AXOpen.Dev/Tools/OpensslClient.cs new file mode 100644 index 000000000..657686b79 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Tools/OpensslClient.cs @@ -0,0 +1,38 @@ +using AXOpen.Dev.Process; + +namespace AXOpen.Dev.Tools; + +/// +/// Wraps the external openssl CLI for the certificate-generation steps of +/// setup_secure_communication.sh. openssl is kept external (cross-platform) rather than +/// porting to .NET crypto so the produced artifacts match the original exactly. +/// All operations run with as the CWD (the per-PLC certs dir). +/// +public sealed class OpensslClient(IProcessRunner runner) +{ + public const string Executable = "openssl"; + + private Task Run(string workingDirectory, string[] args, CancellationToken ct) + => runner.RunAsync(new ProcessRequest + { + Executable = Executable, + Arguments = args, + WorkingDirectory = workingDirectory, + }, ct); + + /// openssl genrsa -out KEY BITS + public Task GenerateRsaKeyAsync(string workingDirectory, string keyFile, int bits = 2048, CancellationToken ct = default) + => Run(workingDirectory, new[] { "genrsa", "-out", keyFile, bits.ToString() }, ct); + + /// openssl req -new -x509 -days 3650 -key KEY -out CERT -config CFG -extensions v3_req + public Task SelfSignAsync(string workingDirectory, string keyFile, string certFile, string configFile, CancellationToken ct = default) + => Run(workingDirectory, new[] { "req", "-new", "-x509", "-days", "3650", "-key", keyFile, "-out", certFile, "-config", configFile, "-extensions", "v3_req" }, ct); + + /// openssl pkcs12 -export -in CERT -inkey KEY -out P12 -passout pass:PWD + public Task ExportPkcs12Async(string workingDirectory, string certFile, string keyFile, string p12File, string password, CancellationToken ct = default) + => Run(workingDirectory, new[] { "pkcs12", "-export", "-in", certFile, "-inkey", keyFile, "-out", p12File, "-passout", $"pass:{password}" }, ct); + + /// openssl pkcs12 -in P12 -out CRT -nokeys -passin pass:PWD + public Task ExportCertificateOnlyAsync(string workingDirectory, string p12File, string crtFile, string password, CancellationToken ct = default) + => Run(workingDirectory, new[] { "pkcs12", "-in", p12File, "-out", crtFile, "-nokeys", "-passin", $"pass:{password}" }, ct); +} diff --git a/src/axopen.dev/AXOpen.Dev/Utils/CtrlFolderCopier.cs b/src/axopen.dev/AXOpen.Dev/Utils/CtrlFolderCopier.cs new file mode 100644 index 000000000..35323483e --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Utils/CtrlFolderCopier.cs @@ -0,0 +1,52 @@ +namespace AXOpen.Dev.Utils; + +/// A planned directory copy (source absolute path → destination absolute path). +public sealed record DirectoryCopyOperation(string Source, string Destination); + +/// +/// Recursively finds directories named ctrl (case-insensitive) under a source root and copies +/// each into a destination root, preserving the relative folder hierarchy. Pure port of +/// scripts/copy-ctrl-folders.ps1. The plan is sorted by source path for deterministic behavior. +/// +public static class CtrlFolderCopier +{ + public const string CtrlDirectoryName = "ctrl"; + + public static IReadOnlyList Plan(string sourceRoot, string destinationRoot) + { + if (!Directory.Exists(sourceRoot)) + { + return Array.Empty(); + } + + return Directory + .EnumerateDirectories(sourceRoot, "*", SearchOption.AllDirectories) + .Where(d => string.Equals(Path.GetFileName(d), CtrlDirectoryName, StringComparison.OrdinalIgnoreCase)) + .OrderBy(d => d, StringComparer.Ordinal) + .Select(d => new DirectoryCopyOperation(d, Path.Combine(destinationRoot, Path.GetRelativePath(sourceRoot, d)))) + .ToList(); + } + + /// Plans then executes the copies. Returns the number of ctrl directories copied. + public static int Copy(string sourceRoot, string destinationRoot) + { + var operations = Plan(sourceRoot, destinationRoot); + foreach (var op in operations) + { + CopyDirectory(op.Source, op.Destination); + } + + return operations.Count; + } + + private static void CopyDirectory(string source, string destination) + { + Directory.CreateDirectory(destination); + foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)) + { + var target = Path.Combine(destination, Path.GetRelativePath(source, file)); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + File.Copy(file, target, overwrite: true); + } + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Validation/ArgumentGuards.cs b/src/axopen.dev/AXOpen.Dev/Validation/ArgumentGuards.cs new file mode 100644 index 000000000..6873389ba --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Validation/ArgumentGuards.cs @@ -0,0 +1,34 @@ +namespace AXOpen.Dev.Validation; + +/// Thrown when a workflow argument fails validation. +public sealed class ArgumentValidationException(string message) : Exception(message); + +/// +/// Input guards ported from the bash scripts (e.g. all.sh). The source scripts only +/// require non-empty values (no special-character password filtering exists), validate +/// USE_PLC_SIM_ADVANCED as a case-insensitive true/false, and treat FORCE as a +/// case-sensitive exact "true". +/// +public static class ArgumentGuards +{ + public static void EnsureNotEmpty(string name, string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentValidationException($"The {name} could not be an empty string."); + } + } + + public static bool ParsePlcSim(string? value) + { + return (value?.Trim().ToLowerInvariant()) switch + { + "true" => true, + "false" => false, + _ => throw new ArgumentValidationException( + $"USE_PLC_SIM_ADVANCED has an invalid or undefined value: '{value}'."), + }; + } + + public static bool ParseForce(string? value) => value == "true"; +} diff --git a/src/axopen.dev/AXOpen.Dev/Validation/IpValidator.cs b/src/axopen.dev/AXOpen.Dev/Validation/IpValidator.cs new file mode 100644 index 000000000..e622b4eb6 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Validation/IpValidator.cs @@ -0,0 +1,50 @@ +using System.Text.RegularExpressions; + +namespace AXOpen.Dev.Validation; + +/// +/// IPv4 / CIDR validation. Ports validate_ip.sh and validate_ip_cidr.sh: +/// four dot-separated 1-3 digit octets each in 0..255, with an optional /0..32 prefix for CIDR. +/// +public static partial class IpValidator +{ + [GeneratedRegex(@"^([0-9]{1,3}\.){3}[0-9]{1,3}$")] + private static partial Regex IpShape(); + + [GeneratedRegex(@"^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[12][0-9]|3[0-2])$")] + private static partial Regex CidrShape(); + + public static bool IsValidIp(string? value) + { + if (string.IsNullOrEmpty(value) || !IpShape().IsMatch(value)) + { + return false; + } + + return OctetsInRange(value); + } + + public static bool IsValidCidr(string? value) + { + if (string.IsNullOrEmpty(value) || !CidrShape().IsMatch(value)) + { + return false; + } + + var ip = value[..value.IndexOf('/')]; + return OctetsInRange(ip); + } + + private static bool OctetsInRange(string ip) + { + foreach (var octet in ip.Split('.')) + { + if (!int.TryParse(octet, out var n) || n < 0 || n > 255) + { + return false; + } + } + + return true; + } +} diff --git a/src/axopen.dev/AXOpen.Dev/Validation/MacValidator.cs b/src/axopen.dev/AXOpen.Dev/Validation/MacValidator.cs new file mode 100644 index 000000000..1c8def0c4 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Validation/MacValidator.cs @@ -0,0 +1,12 @@ +using System.Text.RegularExpressions; + +namespace AXOpen.Dev.Validation; + +/// MAC address validation. Port of the regex in dcp_utility_discover.sh. +public static partial class MacValidator +{ + [GeneratedRegex(@"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")] + private static partial Regex Pattern(); + + public static bool IsValid(string? mac) => !string.IsNullOrEmpty(mac) && Pattern().IsMatch(mac); +} diff --git a/src/axopen.dev/AXOpen.Dev/Validation/PasswordValidator.cs b/src/axopen.dev/AXOpen.Dev/Validation/PasswordValidator.cs new file mode 100644 index 000000000..2d7910ec4 --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Validation/PasswordValidator.cs @@ -0,0 +1,29 @@ +namespace AXOpen.Dev.Validation; + +/// +/// Rejects passwords containing shell-problematic characters or whitespace. Port of +/// validate_password_safe_chars in all_first.sh (kept for parity; less necessary in +/// C# since arguments are not passed through a shell, but the first-setup flow enforces it). +/// +public static class PasswordValidator +{ + private const string Problematic = "$`\\\"'&|;<>()*?[]{}"; + + public static bool IsSafe(string? password) + { + if (string.IsNullOrEmpty(password)) + { + return false; + } + + foreach (var c in password) + { + if (char.IsWhiteSpace(c) || Problematic.IndexOf(c) >= 0) + { + return false; + } + } + + return true; + } +} diff --git a/src/scripts/Directory.Packages.props b/src/scripts/Directory.Packages.props new file mode 100644 index 000000000..733448d59 --- /dev/null +++ b/src/scripts/Directory.Packages.props @@ -0,0 +1,12 @@ + + + + false + + diff --git a/src/scripts/dev.cs b/src/scripts/dev.cs new file mode 100644 index 000000000..1cabec840 --- /dev/null +++ b/src/scripts/dev.cs @@ -0,0 +1,9 @@ +#:project ../axopen.dev/AXOpen.Dev.Tool/AXOpen.Dev.Tool.csproj + +// In-repo file-based dispatcher for the axdev developer CLI. +// apax aliases call `dotnet run ..\..\scripts\dev.cs -- `; downstream apps use the +// packed `dotnet axdev ` tool. Both share the same CommandApp (AxdevApp.Build). +// Named dev.cs (not axdev.cs) to avoid an assembly-name clash with the tool (AssemblyName=axdev). +using AXOpen.Dev.Tool; + +return AxdevApp.Build().Run(args); diff --git a/src/showcase/app/apax.yml b/src/showcase/app/apax.yml index 62da9cb6b..0d771d8fc 100644 --- a/src/showcase/app/apax.yml +++ b/src/showcase/app/apax.yml @@ -60,11 +60,11 @@ apaxVersion: 3.5.0 scripts: plcsim: | # start the PlcSimAdvanced if installed and if $USE_PLC_SIM_ADVANCED = true, register instance name according to name of the project, set its IpAddress to the value of $AXTARGET START=$(date +%s) - ..\\..\\scripts\\plcsimadvanced.sh $APAX_YML_NAME $PLC_NAME $AXTARGET + dotnet run ../../scripts/dev.cs -- plcsim -x $APAX_YML_NAME -n $PLC_NAME -t $AXTARGET echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax plcsim' Finished in :" $(expr $(date +%s) - $START) "s" r: | # restart PLC using certificates (apax plc-info set-mode STOP [using cert file] & apax plc-info set-mode RUN [using cert file]) START=$(date +%s) - ..\\..\\scripts\\restart_PLC.sh $AXTARGET $PLC_NAME $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- r -t $AXTARGET -n $PLC_NAME -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax r' Finished in :" $(expr $(date +%s) - $START) "s" ixc: | # run ix builder START=$(date +%s) @@ -73,15 +73,15 @@ scripts: echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax ixc' Finished in :" $(expr $(date +%s) - $START) "s" dcpli: | # list all interfaces, used to discover MAC address of the adapter connected to PLC (apax dcp-utility list-interfaces ) START=$(date +%s) - ..\\..\\scripts\\dcp_utility_list_interfaces.sh + dotnet run ../../scripts/dev.cs -- dcpli echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax dcpli' Finished in :" $(expr $(date +%s) - $START) "s" dcpd: | # discover all accesible devices connected to adapter with MAC address equal to entered MAC, used to discover MAC-addresses of the slaves (apax dcp-utility discover) START=$(date +%s) - ..\\..\\scripts\\dcp_utility_discover.sh $PNIO_MAC + dotnet run ../../scripts/dev.cs -- dcpd $PNIO_MAC echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax dcpd' Finished in :" $(expr $(date +%s) - $START) "s" hdl: | #List configured harware and its state (apax hw-diag list [using cert file]) START=$(date +%s) - ..\\..\\scripts\\hw_diag_list.sh $AXTARGET $PLC_NAME $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- hdl -t $AXTARGET -n $PLC_NAME -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hdl' Finished in :" $(expr $(date +%s) - $START) "s" ci: | #clean and install dependencies START=$(date +%s) @@ -90,96 +90,94 @@ scripts: echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax ci' Finished in :" $(expr $(date +%s) - $START) "s" reset_plc: | #total reset of the PLC including IP and name (apax hwld --reset-plc All) START=$(date +%s) - ..\\..\\scripts\\reset_plc.sh $AXTARGET $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- reset_plc -t $AXTARGET -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax reset_plc' Finished in :" $(expr $(date +%s) - $START) "s" clean_plc: | #total reset of the PLC excluding IP and name (apax hwld --reset-plc KeepOnlyIP) START=$(date +%s) - ..\\..\\scripts\\clean_plc.sh $AXTARGET $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- clean_plc -t $AXTARGET -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax clean_plc' Finished in :" $(expr $(date +%s) - $START) "s" gsd: | # copy and install all gsdml files from libraries (copy gsdml files from all assets dir & apax hwc install-gsd) START=$(date +%s) - ..\\..\\scripts\\copy_and_install_gsd.sh + dotnet run ../../scripts/dev.cs -- gsd echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax gsd' Finished in :" $(expr $(date +%s) - $START) "s" hwl: | # copy all templates from libraries START=$(date +%s) - ..\\..\\scripts\\copy_hwl_templates.sh + dotnet run ../../scripts/dev.cs -- hwl echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hwl' Finished in :" $(expr $(date +%s) - $START) "s" ssc: | # setup secure communication, create and import certificates, setup password for AX_USERNAME (create pkcs12ForCertificateImport.p12 & apax hwc setup-secure-communication & apax hwc import-certificate [TLS+Webserver] & apax hwc manage-users set-password) START=$(date +%s) - ..\\..\\scripts\\setup_secure_communication.sh $PLC_NAME $AX_USERNAME $AX_TARGET_PWD $AXTARGET + dotnet run ../../scripts/dev.cs -- ssc -n $PLC_NAME -u $AX_USERNAME -t $AXTARGET echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax ssc' Finished in :" $(expr $(date +%s) - $START) "s" hwcc: | # compile hardware configuration (apax hwc compile) START=$(date +%s) - ..\\..\\scripts\\hw_compile.sh + dotnet run ../../scripts/dev.cs -- hwcc echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hwcc' Finished in :" $(expr $(date +%s) - $START) "s" hwid: | # copy the generated HwIds from global constants into the type definition, matching the format as the TIA2AX tool creates START=$(date +%s) - ..\\..\\scripts\\copy_hardware_ids.sh $DEFAULT_NAMESPACE $PLC_NAME + dotnet run ../../scripts/dev.cs -- hwid $DEFAULT_NAMESPACE $PLC_NAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hwid' Finished in :" $(expr $(date +%s) - $START) "s" hwadr: | # copy the generated IoAddresses START=$(date +%s) - ..\\..\\scripts\\copy_io_addresses.sh $DEFAULT_NAMESPACE $PLC_NAME + dotnet run ../../scripts/dev.cs -- hwadr $DEFAULT_NAMESPACE $PLC_NAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hwadr' Finished in :" $(expr $(date +%s) - $START) "s" # the following command must be triggered only once hwfd: | # copy and install gsd, copy templates,compile, copy the HwIds, copy the IoAddresses, first download HW using password and upload certificate (apax gsd & apax hwl & apax hwcc & apax hwid & apax hwadr & apax hwld [using password] & apax plc-cert) START=$(date +%s) - ..\\..\\scripts\\hw_first_download.sh $DEFAULT_NAMESPACE $PLC_NAME $AXTARGET $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- hwfd --namespace $DEFAULT_NAMESPACE -n $PLC_NAME -t $AXTARGET -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hwfd' Finished in :" $(expr $(date +%s) - $START) "s" hwu: | # copy and install gsd, copy templates, compile, copy the HwIds, copy the IoAddresses, download HW using certificate (apax gsd & apax hwl & apax hwcc & apax hwid & apax hwadr & apax hwld [using cert file]) START=$(date +%s) - ..\\..\\scripts\\hw_update.sh $DEFAULT_NAMESPACE $PLC_NAME $AXTARGET $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- hwu --namespace $DEFAULT_NAMESPACE -n $PLC_NAME -t $AXTARGET -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hwu' Finished in :" $(expr $(date +%s) - $START) "s" hwfdo: | # first download HW using password and upload certificate (apax hwld [using password] & apax plc-cert) START=$(date +%s) - ..\\..\\scripts\\hw_first_download_only.sh $PLC_NAME $AXTARGET $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- hwfdo -n $PLC_NAME -t $AXTARGET echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hwfdo' Finished in :" $(expr $(date +%s) - $START) "s" hwdo: | # download HW only using certificate (apax hwld [using cert file]) START=$(date +%s) - ..\\..\\scripts\\hw_download_only.sh $PLC_NAME $AXTARGET $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- hwdo -n $PLC_NAME -t $AXTARGET -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax hwdo' Finished in :" $(expr $(date +%s) - $START) "s" swfd: | # software build and full download (apax build & dotnet ixc & apax sld load [using cert file]) START=$(date +%s) - ..\\..\\scripts\\sw_build_and_download_full.sh $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- swfd -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax swfd' Finished in :" $(expr $(date +%s) - $START) "s" swfdo: | # software full download only (apax sld load [using cert file]) START=$(date +%s) - ..\\..\\scripts\\sw_download_full.sh $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- swfdo -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax swfdo' Finished in :" $(expr $(date +%s) - $START) "s" swdd: | # software build and delta download (apax build & dotnet ixc & apax sld load --mode delta [using cert file]) START=$(date +%s) - ..\\..\\scripts\\sw_build_and_download_delta.sh $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- swdd -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax swdd' Finished in :" $(expr $(date +%s) - $START) "s" swddo: | # software delta download only (apax sld load --mode delta [using cert file]) START=$(date +%s) - ..\\..\\scripts\\sw_download_delta.sh $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- swddo -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax swddo' Finished in :" $(expr $(date +%s) - $START) "s" alf: | #clear plc except ip and name and provide all actions for install all, build and initial download hw so as sw (apax plcsim & apax clean & apax install & apax clean_plc & apax ssc & apax hwfd & apax swfd) START=$(date +%s) - if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then - ..\\..\\scripts\\all_first.sh $DEFAULT_NAMESPACE $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD $USE_PLC_SIM_ADVANCED true - else - ..\\..\\scripts\\all_first.sh $DEFAULT_NAMESPACE $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD $USE_PLC_SIM_ADVANCED false - fi + FORCE_FLAG=""; if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then FORCE_FLAG="--force"; fi + SIM_FLAG=""; if [ "$USE_PLC_SIM_ADVANCED" = "true" ]; then SIM_FLAG="--use-plc-sim"; fi + dotnet run ../../scripts/dev.cs -- alf --namespace $DEFAULT_NAMESPACE -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME $SIM_FLAG $FORCE_FLAG echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax alf' Finished in :" $(expr $(date +%s) - $START) "s" all: | #build and download hardware and software using cert file. If cert file does not exists or its hash is different calls apax alf (apax plcsim & apax clean & apax install & apax hwu & apax swfd) START=$(date +%s) - if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then - ..\\..\\scripts\\all.sh $DEFAULT_NAMESPACE $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD $USE_PLC_SIM_ADVANCED true - else - ..\\..\\scripts\\all.sh $DEFAULT_NAMESPACE $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD $USE_PLC_SIM_ADVANCED false - fi + FORCE_FLAG=""; if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then FORCE_FLAG="--force"; fi + SIM_FLAG=""; if [ "$USE_PLC_SIM_ADVANCED" = "true" ]; then SIM_FLAG="--use-plc-sim"; fi + dotnet run ../../scripts/dev.cs -- a --namespace $DEFAULT_NAMESPACE -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME $SIM_FLAG $FORCE_FLAG echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax all' Finished in :" $(expr $(date +%s) - $START) "s" cla: | #compile all START=$(date +%s) - ..\\..\\scripts\\compile_all.sh $DEFAULT_NAMESPACE $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD $USE_PLC_SIM_ADVANCED + SIM_FLAG=""; if [ "$USE_PLC_SIM_ADVANCED" = "true" ]; then SIM_FLAG="--use-plc-sim"; fi + dotnet run ../../scripts/dev.cs -- cla --namespace $DEFAULT_NAMESPACE -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME $SIM_FLAG echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax cla' Finished in :" $(expr $(date +%s) - $START) "s" cpa: | #compare all START=$(date +%s) - ..\\..\\scripts\\compare_all.sh $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD + dotnet run ../../scripts/dev.cs -- cpa -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax cpa' Finished in :" $(expr $(date +%s) - $START) "s" cca: | #compile all compare all START=$(date +%s) - ..\\..\\scripts\\compile_all_compare_all.sh $DEFAULT_NAMESPACE $PLC_NAME $AXTARGET $AXTARGETPLATFORMINPUT $AX_USERNAME $AX_TARGET_PWD $USE_PLC_SIM_ADVANCED + SIM_FLAG=""; if [ "$USE_PLC_SIM_ADVANCED" = "true" ]; then SIM_FLAG="--use-plc-sim"; fi + dotnet run ../../scripts/dev.cs -- cca --namespace $DEFAULT_NAMESPACE -n $PLC_NAME -t $AXTARGET --platform $AXTARGETPLATFORMINPUT -u $AX_USERNAME $SIM_FLAG echo $(date +%D)"-"$(date +%H)":"$(date +%M)":"$(date +%S) " - 'apax cca' Finished in :" $(expr $(date +%s) - $START) "s" ib: | START=$(date +%s) From 3adec4402e2a86820aa06718c433df13ddf84bc0 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sat, 30 May 2026 18:37:53 +0200 Subject: [PATCH 15/23] build(cake): pack + publish the axdev tool to the inxton feed Add AXOpen.Dev.Tool to AXOpen-packable-only.proj so the existing CI PackNuGets + PushPackages flow builds and pushes the GitVersion-stamped 'axdev' dotnet tool to nuget.pkg.github.com/inxton. AXOpen.proj already globs the axopen.dev projects, so restore/build is covered (NoRestore pack is safe); the test projects are not matched by the packable globs, so only the tool is packed. --- src/AXOpen-packable-only.proj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/AXOpen-packable-only.proj b/src/AXOpen-packable-only.proj index 08aa38985..685a8abb4 100644 --- a/src/AXOpen-packable-only.proj +++ b/src/AXOpen-packable-only.proj @@ -1,6 +1,8 @@  - - - + + + + + \ No newline at end of file From cd2ad89cc080f803f5c925f3d9662ee4f58c0100 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sat, 30 May 2026 21:52:38 +0200 Subject: [PATCH 16/23] fix(deps): pin Spectre.Console* to 0.47.0 to keep Cake build working Repo-wide CentralPackageTransitivePinningEnabled meant bumping Spectre.Console.Cli to 0.50.0 (for the axdev tool) also forced Cake.Cli 4.0.0's transitive Spectre to 0.50.0, which dropped IConfigurator.AddExample(string[]) that Cake.Frosting calls -> cake aborted at startup with MissingMethodException. Pin both Spectre packages to 0.47.0 (the version Cake.Cli 4.0.0 declares); the axdev tool only uses APIs present in 0.47.0. Verified: cake runs the full task graph, 168 unit tests pass, the tool runs. --- Directory.Packages.props | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d8217ca64..a0aabcb24 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -66,8 +66,10 @@ - - + + + From 7381d0da774fa4765be57a0ba1c4d32bed7bc5aa Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sun, 31 May 2026 07:45:11 +0200 Subject: [PATCH 17/23] feat(cake): run showcase offline build + Blazor at test level 2 Test level 2 now builds the showcase app the way `apax alf` does, minus every step that needs PLC/PLCSIM access (plcsim, clean_plc, ssc, hardware/software download), then starts the Blazor server with a dummy connector and probes it over HTTPS. Gives a full offline UI smoke test on runners without hardware or a simulator. - Entry.cs: select connector via AXOPEN_USE_DUMMY_CONNECTOR env var; load the PLC certificate lazily so the dummy path needs no cert --- cake/AppsRunTaskHelpers.cs | 84 +++++++++++++++++++++++++++++++++++- cake/Program.cs | 4 ++ src/showcase/app/ix/Entry.cs | 18 +++++--- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/cake/AppsRunTaskHelpers.cs b/cake/AppsRunTaskHelpers.cs index cb9a0ae81..677094924 100644 --- a/cake/AppsRunTaskHelpers.cs +++ b/cake/AppsRunTaskHelpers.cs @@ -90,8 +90,8 @@ public static void RunShowcaseIntegration(BuildContext context, string appYamlFi } finally { - // Always tear down the Blazor server and the simulator, even on success. - KillProcess(context, "dotnet"); + // The Blazor process tree is terminated inside DotNetRunWithHealthCheck; here we make sure + // the simulator is always torn down, even on success. KillProcess(context, "Siemens.Simatic.PlcSim.Advanced.UserInterface"); } @@ -104,6 +104,86 @@ public static void RunShowcaseIntegration(BuildContext context, string appYamlFi context.Log.Information("Showcase integration (test level 4) done."); } + /// + /// Offline portion of the showcase "apax alf" workflow, used at test level 2: performs everything + /// alf does that does NOT require PLC/PLCSIM access — clean, install, hardware-configuration compile + /// and generate, and software build. The PLC-bound steps (plcsim, clean_plc, ssc, hardware/software + /// download) are intentionally skipped. The Blazor server is then started with a DUMMY connector + /// (no PLC) and probed over HTTPS. Fails the build (non-zero exit) on any error. + /// + public static void BuildShowcaseOffline(BuildContext context, string appYamlFile) + { + bool summaryResult = true; + + if (string.IsNullOrWhiteSpace(appYamlFile) || !File.Exists(appYamlFile)) + { + context.Log.Error($"Showcase application file does not exist: {appYamlFile}"); + Environment.Exit(1); + return; + } + + string appFolder = Path.GetFullPath(Path.GetDirectoryName(appYamlFile)); + + context.Log.Information("###################################################"); + context.Log.Information("Test level 2 showcase offline build (apax alf without PLC access)"); + context.Log.Information($"Application file: {appYamlFile}"); + context.Log.Information("###################################################"); + + ApaxCmd.ApaxCommand(context, appFolder, "clean", ref summaryResult); // local clean + ApaxCmd.ApaxCommand(context, appFolder, "install", ref summaryResult); // install dependencies + ApaxCmd.ApaxCommand(context, appFolder, "gsd", ref summaryResult); // copy & install GSDML files + ApaxCmd.ApaxCommand(context, appFolder, "hwl", ref summaryResult); // copy hardware templates + ApaxCmd.ApaxCommand(context, appFolder, "hwcc", ref summaryResult); // compile hardware configuration + ApaxCmd.ApaxCommand(context, appFolder, "hwid", ref summaryResult); // copy generated HwIds + ApaxCmd.ApaxCommand(context, appFolder, "hwadr", ref summaryResult); // copy generated IO addresses + ApaxCmd.ApaxCommand(context, appFolder, "build", ref summaryResult); // compile SIMATIC AX code + DotNetCmd.DotNetIxc(context, appFolder, ref summaryResult); // generate IXC twin controller + + // Run the Blazor server with the dummy connector and probe it over HTTPS (no PLC required). + if (summaryResult) + { + var blazorFile = Directory + .GetFiles(appFolder, "*.csproj", SearchOption.AllDirectories) + .FirstOrDefault(file => file.Contains("blazor") && !File.ReadAllText(file).Contains("")); + + if (string.IsNullOrEmpty(blazorFile)) + { + context.Log.Error("No runnable Blazor project (*blazor*.csproj without ) was found."); + summaryResult = false; + } + else + { + // Select the dummy connector for the child process (see TwinConnectorSelector in Entry.cs). + Environment.SetEnvironmentVariable("AXOPEN_USE_DUMMY_CONNECTOR", "true"); + try + { + DotNetCmd.DotNetBuildWithResult(context, blazorFile, "-c Debug", ref summaryResult); + + DotNetCmd.DotNetRunWithHealthCheck( + context, + blazorFile, + "-c Debug --launch-profile https", + "https://localhost:7290", + 120, + ref summaryResult); + } + finally + { + // The spawned Blazor process tree is terminated inside DotNetRunWithHealthCheck. + Environment.SetEnvironmentVariable("AXOPEN_USE_DUMMY_CONNECTOR", null); + } + } + } + + if (!summaryResult) + { + context.Log.Error("Showcase offline build (test level 2) failed."); + Environment.Exit(1); + } + + context.Log.Information("Showcase offline build (test level 2) done."); + } + /// /// Performs the full first-download apax sequence for the showcase application onto PLCSIM Advanced: /// install -> plcsim -> gsd -> hwl -> hwcc -> hwid -> hwadr -> hwdo -> build -> dotnet ixc -> swfdo. diff --git a/cake/Program.cs b/cake/Program.cs index a069a94cd..065dec738 100644 --- a/cake/Program.cs +++ b/cake/Program.cs @@ -379,6 +379,10 @@ public override void Run(BuildContext context) { context.DotNetTest(Path.Combine(context.RootDir, "AXOpen-L2-tests.proj"), context.DotNetTestSettings); + + // Offline showcase build: everything `apax alf` does except the PLC-access steps. + var showcaseApp = Path.Combine(context.RootDir, "showcase", "app", "apax.yml"); + AppsRunTaskHelpers.BuildShowcaseOffline(context, showcaseApp); } if (context.BuildParameters.TestLevel >= 3) { diff --git a/src/showcase/app/ix/Entry.cs b/src/showcase/app/ix/Entry.cs index 90727e230..00bbe983e 100644 --- a/src/showcase/app/ix/Entry.cs +++ b/src/showcase/app/ix/Entry.cs @@ -23,18 +23,24 @@ public class TwinConnectorSelector private const bool IgnoreSslErrors = true; private static string CertificatePath = "..\\..\\certs\\plc_line\\plc_line.cer"; - static readonly X509Certificate2 Certificate = new X509Certificate2(CertificatePath); + // Loaded lazily so the dummy-connector path (used for offline runs) does not require the + // certificate file to be present. + static readonly Lazy Certificate = new(() => new X509Certificate2(CertificatePath)); private static bool CertificateValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - return certificate.Thumbprint == Certificate.Thumbprint; + return certificate.Thumbprint == Certificate.Value.Thumbprint; } - public static showcaseTwinController SecurePlc { get; } - = new(ConnectorAdapterBuilder.Build() - .CreateWebApi(TargetIp, UserName, Pass, CertificateValidation, IgnoreSslErrors)); + // Set AXOPEN_USE_DUMMY_CONNECTOR=true to run the UI without a PLC (offline / CI smoke test); + // otherwise the secure WebAPI connector to the real/simulated PLC is used. + private static bool UseDummyConnector => + string.Equals(Environment.GetEnvironmentVariable("AXOPEN_USE_DUMMY_CONNECTOR"), "true", StringComparison.OrdinalIgnoreCase); - // public static showcaseTwinController SecurePlc { get; } = new showcaseTwinController(ConnectorAdapterBuilder.Build().CreateDummy()); + public static showcaseTwinController SecurePlc { get; } = UseDummyConnector + ? new(ConnectorAdapterBuilder.Build().CreateDummy()) + : new(ConnectorAdapterBuilder.Build() + .CreateWebApi(TargetIp, UserName, Pass, CertificateValidation, IgnoreSslErrors)); } public static class Entry From fd0154f5e5543f001ea222319cc5552ebb8feaa5 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sun, 31 May 2026 08:36:54 +0200 Subject: [PATCH 18/23] fix(axopen.dev): track Certs source folder ignored by gitignore The broad `certs` HWC-secrets rule also matched the AXOpen.Dev/Certs source namespace, so CertService.cs never reached CI and the build failed with CS0234 (AXOpen.Dev.Certs not found). Add a scoped negation so the source folder is tracked while real cert stores stay ignored. --- .gitignore | 4 ++ .../AXOpen.Dev/Certs/CertService.cs | 56 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/axopen.dev/AXOpen.Dev/Certs/CertService.cs diff --git a/.gitignore b/.gitignore index cb9410427..2961d21cb 100644 --- a/.gitignore +++ b/.gitignore @@ -412,6 +412,10 @@ hwc\import-cache #AX HWC secrets certs +# ...but the AXOpen.Dev source namespace folder is code, not a secret store +!src/axopen.dev/AXOpen.Dev/Certs/ +!src/axopen.dev/AXOpen.Dev/Certs/** + #AX HWC installed gsdml import-cache diff --git a/src/axopen.dev/AXOpen.Dev/Certs/CertService.cs b/src/axopen.dev/AXOpen.Dev/Certs/CertService.cs new file mode 100644 index 000000000..813e9a14f --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Certs/CertService.cs @@ -0,0 +1,56 @@ +using System.Security.Cryptography.X509Certificates; + +namespace AXOpen.Dev.Certs; + +/// +/// Certificate SHA1 thumbprint computation and comparison. Replaces the +/// certutil -dump | grep "Cert Hash(sha1)" pipeline in is_cert_hash_sha1_equal.sh +/// with cross-platform .NET X509 APIs. +/// +public static class CertService +{ + /// SHA1 thumbprint of a certificate file, as uppercase hex without separators. + public static string ComputeSha1Thumbprint(string certificatePath) + { + if (!File.Exists(certificatePath)) + { + throw new FileNotFoundException("Certificate file does not exist.", certificatePath); + } + + using var cert = X509CertificateLoader.LoadCertificateFromFile(certificatePath); + return cert.Thumbprint; // .NET computes the SHA1 thumbprint, uppercase hex, no separators + } + + /// + /// Compares two SHA1 hashes ignoring case and any whitespace/colon separators + /// (certutil prints space-separated bytes; .NET prints contiguous hex). + /// + public static bool AreEqual(string? a, string? b) + { + var na = Normalize(a); + var nb = Normalize(b); + return na.Length > 0 && string.Equals(na, nb, StringComparison.Ordinal); + } + + private static string Normalize(string? hash) + { + if (string.IsNullOrWhiteSpace(hash)) + { + return string.Empty; + } + + Span buffer = hash.Length <= 256 ? stackalloc char[hash.Length] : new char[hash.Length]; + var count = 0; + foreach (var c in hash) + { + if (c is ' ' or ':' or '\t' or '\r' or '\n' or '-') + { + continue; + } + + buffer[count++] = char.ToUpperInvariant(c); + } + + return new string(buffer[..count]); + } +} From 6435640f596ec7b3cba1d6020b8b0f8bc88fd87d Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sun, 31 May 2026 08:39:30 +0200 Subject: [PATCH 19/23] refactor(axopen.dev): move CertService out of ignored Certs dir The `certs` HWC-secrets gitignore rule swallowed the source folder. Relocating to CertificateService/ avoids depending on a fragile gitignore negation. Namespace AXOpen.Dev.Certs is unchanged, so call sites compile as-is. --- .gitignore | 4 ---- .../AXOpen.Dev/{Certs => CertificateService}/CertService.cs | 0 2 files changed, 4 deletions(-) rename src/axopen.dev/AXOpen.Dev/{Certs => CertificateService}/CertService.cs (100%) diff --git a/.gitignore b/.gitignore index 2961d21cb..cb9410427 100644 --- a/.gitignore +++ b/.gitignore @@ -412,10 +412,6 @@ hwc\import-cache #AX HWC secrets certs -# ...but the AXOpen.Dev source namespace folder is code, not a secret store -!src/axopen.dev/AXOpen.Dev/Certs/ -!src/axopen.dev/AXOpen.Dev/Certs/** - #AX HWC installed gsdml import-cache diff --git a/src/axopen.dev/AXOpen.Dev/Certs/CertService.cs b/src/axopen.dev/AXOpen.Dev/CertificateService/CertService.cs similarity index 100% rename from src/axopen.dev/AXOpen.Dev/Certs/CertService.cs rename to src/axopen.dev/AXOpen.Dev/CertificateService/CertService.cs From 00502ba64431213d45cb274e46003e57a2fcce3f Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sun, 31 May 2026 08:40:24 +0200 Subject: [PATCH 20/23] chore(deps): bump AXSharp to 0.47.0-alpha.489 --- .config/dotnet-tools.json | 6 +++--- Directory.Packages.props | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d48523e89..9433df44b 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "AXSharp.ixc": { - "version": "0.47.0-alpha.484", + "version": "0.47.0-alpha.489", "commands": [ "ixc" ], @@ -17,14 +17,14 @@ "rollForward": false }, "AXSharp.ixd": { - "version": "0.47.0-alpha.484", + "version": "0.47.0-alpha.489", "commands": [ "ixd" ], "rollForward": false }, "AXSharp.ixr": { - "version": "0.47.0-alpha.484", + "version": "0.47.0-alpha.489", "commands": [ "ixr" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index a0aabcb24..63f610038 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,11 +11,11 @@ - - - - - + + + + + From b73e009ccb200b094837584e3cc8e235f9191a59 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sun, 31 May 2026 16:36:53 +0200 Subject: [PATCH 21/23] feat(axopen.dev): load dotnet user-secrets at axdev startup Add UserSecretsLoader that reads the twin project's user-secrets store into the process environment, so PLC verbs resolve AX_TARGET_PWD / AX_USERNAME without a prior `source load-secrets.sh`. Locates the by probing ../axpansion/twin then . (override via AX_SECRETS_PROJECT), reads secrets.json from the OS user-secrets root, flattens nested keys as key:subkey. Route both the packed tool (Program.cs) and the in-repo dispatcher (dev.cs) through a shared AxdevApp.Run(args) that loads secrets before build+run. An already-set env var wins; missing project/store is a silent no-op. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 18 ++ .../Secrets/UserSecretsLoaderTests.cs | 171 ++++++++++++++++ src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs | 14 +- src/axopen.dev/AXOpen.Dev.Tool/Program.cs | 2 +- .../AXOpen.Dev/Secrets/UserSecretsLoader.cs | 183 ++++++++++++++++++ src/scripts/dev.cs | 2 +- 6 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 src/axopen.dev/AXOpen.Dev.Tests/Secrets/UserSecretsLoaderTests.cs create mode 100644 src/axopen.dev/AXOpen.Dev/Secrets/UserSecretsLoader.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index ea863b69f..249ccbe1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +### [BUILD] `axdev` loads dotnet user-secrets at startup + +**Note:** Developer-CLI enhancement in `src/axopen.dev`. No PLC source change, no public-API removal. Branch: `feat/axdev-user-secrets-loader`. + +- feat: `AXOpen.Dev.Secrets.UserSecretsLoader` reads dotnet user-secrets into the process environment so PLC verbs resolve `AX_TARGET_PWD` / `AX_USERNAME` without a prior `source load-secrets.sh`. It locates the twin project's `` (probes `../axpansion/twin` then `.`, overridable via the `AX_SECRETS_PROJECT` environment variable), reads its `secrets.json` from the OS user-secrets root, and flattens nested keys with the standard `key:subkey` convention. +- feat: New shared entry point `AxdevApp.Run(args)` calls `UserSecretsLoader.Load()` then builds and runs the command app. Both the packed `dotnet axdev` tool (`AXOpen.Dev.Tool/Program.cs`) and the in-repo dispatcher (`src/scripts/dev.cs`) now call `AxdevApp.Run` instead of `AxdevApp.Build().Run`. +- test: `AXOpen.Dev.Tests/Secrets/UserSecretsLoaderTests.cs` covers apply-from-store, existing-env-wins precedence, missing project / missing store / malformed JSON / no-`UserSecretsId` no-ops, the `AX_SECRETS_PROJECT` override, and nested-key flattening. + +**Impact:** +- The template's credential UX (per-project `dotnet user-secrets`) is preserved while removing the bash `source load-secrets.sh` step, so apax verbs can call `axdev` directly on any platform. +- Precedence is non-surprising: an already-set environment variable (or apax variable) always wins over the secrets store; an explicit `-p/--password` still overrides everything in `PlcCommandSettings.ResolvePassword`. + +**Risks/Review:** +- Secret loading is best-effort: a missing twin project, missing store, or unreadable JSON is a silent no-op, and the per-command argument guards still report any genuinely missing credential. + +**Testing:** +- `dotnet test src/axopen.dev/AXOpen.Dev.Tests` — full suite green (176 passed), including the 8 new `UserSecretsLoaderTests`. + ### [BUILD] Dependency-maintenance tooling + AXSharp `0.47.0-alpha.484` bump **Note:** Build/CI tooling and dependency maintenance. No public-API change, no PLC source change. Branch: `deps-update`. diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Secrets/UserSecretsLoaderTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Secrets/UserSecretsLoaderTests.cs new file mode 100644 index 000000000..8739754bf --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev.Tests/Secrets/UserSecretsLoaderTests.cs @@ -0,0 +1,171 @@ +using AXOpen.Dev.Secrets; + +namespace AXOpen.Dev.Tests.Secrets; + +public sealed class UserSecretsLoaderTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "axdev-secrets-tests", Guid.NewGuid().ToString("N")); + + public void Dispose() + { + try { if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true); } catch { /* best effort */ } + } + + private string MakeProject(string dirName, string? userSecretsId) + { + var dir = Path.Combine(_root, dirName); + Directory.CreateDirectory(dir); + var idLine = userSecretsId is null ? string.Empty : $" {userSecretsId}\n"; + File.WriteAllText(Path.Combine(dir, "twin.csproj"), + $"\n \n{idLine} \n\n"); + return dir; + } + + private string MakeSecretsStore(string secretsRoot, string id, string json) + { + var dir = Path.Combine(secretsRoot, id); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, "secrets.json"); + File.WriteAllText(path, json); + return path; + } + + private static (Func get, Action set, Dictionary store) + FakeEnv(IDictionary? seed = null) + { + var store = seed is null + ? new Dictionary(StringComparer.Ordinal) + : new Dictionary(seed, StringComparer.Ordinal); + Func get = k => store.TryGetValue(k, out var v) ? v : null; + Action set = (k, v) => store[k] = v; + return (get, set, store); + } + + [Fact] + public void Applies_secrets_from_twin_project_store() + { + // app dir is the working dir; twin lives at ../axpansion/twin + var appDir = Path.Combine(_root, "app", "ax"); + Directory.CreateDirectory(appDir); + MakeProject(Path.Combine("app", "axpansion", "twin"), "id-applies"); + var secretsRoot = Path.Combine(_root, "usersecrets"); + MakeSecretsStore(secretsRoot, "id-applies", "{\"AX_TARGET_PWD\":\"Pwd123456789+\",\"AX_USERNAME\":\"admin\"}"); + + var (get, set, store) = FakeEnv(); + var applied = UserSecretsLoader.LoadInto(appDir, secretsRoot, get, set); + + Assert.Contains("AX_TARGET_PWD", applied); + Assert.Contains("AX_USERNAME", applied); + Assert.Equal("Pwd123456789+", store["AX_TARGET_PWD"]); + Assert.Equal("admin", store["AX_USERNAME"]); + } + + [Fact] + public void Existing_environment_value_wins() + { + var appDir = Path.Combine(_root, "app", "ax"); + Directory.CreateDirectory(appDir); + MakeProject(Path.Combine("app", "axpansion", "twin"), "id-precedence"); + var secretsRoot = Path.Combine(_root, "usersecrets"); + MakeSecretsStore(secretsRoot, "id-precedence", "{\"AX_TARGET_PWD\":\"from-store\"}"); + + var (get, set, store) = FakeEnv(new Dictionary { ["AX_TARGET_PWD"] = "from-env" }); + var applied = UserSecretsLoader.LoadInto(appDir, secretsRoot, get, set); + + Assert.DoesNotContain("AX_TARGET_PWD", applied); + Assert.Equal("from-env", store["AX_TARGET_PWD"]); + } + + [Fact] + public void Missing_project_is_noop() + { + var appDir = Path.Combine(_root, "lonely"); + Directory.CreateDirectory(appDir); + var secretsRoot = Path.Combine(_root, "usersecrets"); + + var (get, set, store) = FakeEnv(); + var applied = UserSecretsLoader.LoadInto(appDir, secretsRoot, get, set); + + Assert.Empty(applied); + Assert.Empty(store); + } + + [Fact] + public void Missing_store_is_noop() + { + var appDir = Path.Combine(_root, "app", "ax"); + Directory.CreateDirectory(appDir); + MakeProject(Path.Combine("app", "axpansion", "twin"), "id-no-store"); + var secretsRoot = Path.Combine(_root, "usersecrets"); // no secrets.json written + + var (get, set, store) = FakeEnv(); + var applied = UserSecretsLoader.LoadInto(appDir, secretsRoot, get, set); + + Assert.Empty(applied); + Assert.Empty(store); + } + + [Fact] + public void Malformed_json_is_noop() + { + var appDir = Path.Combine(_root, "app", "ax"); + Directory.CreateDirectory(appDir); + MakeProject(Path.Combine("app", "axpansion", "twin"), "id-bad-json"); + var secretsRoot = Path.Combine(_root, "usersecrets"); + MakeSecretsStore(secretsRoot, "id-bad-json", "{ not valid json "); + + var (get, set, store) = FakeEnv(); + var applied = UserSecretsLoader.LoadInto(appDir, secretsRoot, get, set); + + Assert.Empty(applied); + Assert.Empty(store); + } + + [Fact] + public void Project_without_user_secrets_id_is_noop() + { + var appDir = Path.Combine(_root, "app", "ax"); + Directory.CreateDirectory(appDir); + MakeProject(Path.Combine("app", "axpansion", "twin"), userSecretsId: null); + var secretsRoot = Path.Combine(_root, "usersecrets"); + + var (get, set, store) = FakeEnv(); + var applied = UserSecretsLoader.LoadInto(appDir, secretsRoot, get, set); + + Assert.Empty(applied); + Assert.Empty(store); + } + + [Fact] + public void Ax_secrets_project_override_takes_precedence() + { + var appDir = Path.Combine(_root, "app", "ax"); + Directory.CreateDirectory(appDir); + // No twin at the default probe path; the override points elsewhere. + var customDir = MakeProject("custom", "id-override"); + var secretsRoot = Path.Combine(_root, "usersecrets"); + MakeSecretsStore(secretsRoot, "id-override", "{\"AX_TARGET_PWD\":\"override-pwd\"}"); + + var (get, set, store) = FakeEnv(new Dictionary { ["AX_SECRETS_PROJECT"] = customDir }); + var applied = UserSecretsLoader.LoadInto(appDir, secretsRoot, get, set); + + Assert.Contains("AX_TARGET_PWD", applied); + Assert.Equal("override-pwd", store["AX_TARGET_PWD"]); + } + + [Fact] + public void Nested_json_flattens_with_colon() + { + var appDir = Path.Combine(_root, "app", "ax"); + Directory.CreateDirectory(appDir); + MakeProject(Path.Combine("app", "axpansion", "twin"), "id-nested"); + var secretsRoot = Path.Combine(_root, "usersecrets"); + MakeSecretsStore(secretsRoot, "id-nested", "{\"Plc\":{\"Password\":\"deep\"}}"); + + var (get, set, store) = FakeEnv(); + var applied = UserSecretsLoader.LoadInto(appDir, secretsRoot, get, set); + + Assert.Contains("Plc:Password", applied); + Assert.Equal("deep", store["Plc:Password"]); + } +} diff --git a/src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs b/src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs index 834b4d8a0..e9b2ada17 100644 --- a/src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs +++ b/src/axopen.dev/AXOpen.Dev.Tool/AxdevApp.cs @@ -1,3 +1,4 @@ +using AXOpen.Dev.Secrets; using AXOpen.Dev.Tool.Commands; using Spectre.Console.Cli; @@ -5,11 +6,22 @@ namespace AXOpen.Dev.Tool; /// /// Builds the shared used by both the packed dotnet tool -/// (axdev) and the in-repo file-based dispatcher (src/scripts/axdev.cs). +/// (axdev) and the in-repo file-based dispatcher (src/scripts/dev.cs). /// Descriptive verb names are canonical; the apax aliases are real invokable aliases. /// public static class AxdevApp { + /// + /// Shared entry point for both the packed tool (Program.cs) and the in-repo + /// dispatcher (dev.cs). Loads dotnet user-secrets into the environment (replacing + /// source load-secrets.sh) before building and running the command app. + /// + public static int Run(string[] args) + { + UserSecretsLoader.Load(); + return Build().Run(args); + } + public static CommandApp Build() { var app = new CommandApp(); diff --git a/src/axopen.dev/AXOpen.Dev.Tool/Program.cs b/src/axopen.dev/AXOpen.Dev.Tool/Program.cs index 57257019a..38d502c18 100644 --- a/src/axopen.dev/AXOpen.Dev.Tool/Program.cs +++ b/src/axopen.dev/AXOpen.Dev.Tool/Program.cs @@ -1,3 +1,3 @@ using AXOpen.Dev.Tool; -return AxdevApp.Build().Run(args); +return AxdevApp.Run(args); diff --git a/src/axopen.dev/AXOpen.Dev/Secrets/UserSecretsLoader.cs b/src/axopen.dev/AXOpen.Dev/Secrets/UserSecretsLoader.cs new file mode 100644 index 000000000..558df1b8d --- /dev/null +++ b/src/axopen.dev/AXOpen.Dev/Secrets/UserSecretsLoader.cs @@ -0,0 +1,183 @@ +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace AXOpen.Dev.Secrets; + +/// +/// Loads dotnet user-secrets into the process environment so PLC verbs can resolve +/// credentials (AX_TARGET_PWD, AX_USERNAME, …) without a prior source load-secrets.sh. +/// +/// Replaces the template's load-secrets.sh: that script ran +/// dotnet user-secrets list in the twin project and exported every key. This reads the +/// same store directly — it locates the twin project's <UserSecretsId> and parses +/// %APPDATA%/Microsoft/UserSecrets/<id>/secrets.json (Windows) or +/// ~/.microsoft/usersecrets/<id>/secrets.json (Unix). +/// +/// Precedence matches the bash flow: an already-set environment variable always wins, so an +/// explicit AX_TARGET_PWD (or apax variable) is never overwritten. Missing project / +/// missing store / unreadable JSON are silent no-ops — axdev still runs and the per-command +/// argument guards report any genuinely missing credential. +/// +public static class UserSecretsLoader +{ + /// Project locations probed (in order) for a <UserSecretsId>, relative to the + /// current directory. axdev runs in the app (ax/) dir; the secrets live in the twin. + public static readonly string[] DefaultProjectProbePaths = + { + Path.Combine("..", "axpansion", "twin"), + ".", + }; + + private static readonly Regex UserSecretsIdRegex = + new("\\s*(?[^<\\s]+)\\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Production entry point: probes from the current directory, reads the real user-secrets + /// root, and applies values to the process environment. Never throws. + /// + /// The keys that were applied (i.e. set because no env var already existed). + public static IReadOnlyList Load() + { + try + { + return LoadInto( + Directory.GetCurrentDirectory(), + DefaultUserSecretsRoot(), + Environment.GetEnvironmentVariable, + (k, v) => Environment.SetEnvironmentVariable(k, v)); + } + catch + { + // Secret loading is best-effort; never let it break a command. + return Array.Empty(); + } + } + + /// + /// Testable core. Resolves the secrets project, reads its secrets.json, and applies + /// each flattened key to unless already + /// returns a non-empty value for it. + /// + /// Directory to probe from (the app/working dir). + /// Root holding <id>/secrets.json folders. + /// Reads an environment variable (existing value wins). + /// Sets an environment variable. + /// The keys that were applied. + public static IReadOnlyList LoadInto( + string startDir, + string userSecretsRoot, + Func getEnv, + Action setEnv) + { + var id = ResolveUserSecretsId(startDir, getEnv); + if (string.IsNullOrEmpty(id)) return Array.Empty(); + + var secretsPath = Path.Combine(userSecretsRoot, id, "secrets.json"); + if (!File.Exists(secretsPath)) return Array.Empty(); + + Dictionary values; + try + { + using var doc = JsonDocument.Parse(File.ReadAllText(secretsPath)); + values = new Dictionary(StringComparer.Ordinal); + Flatten(doc.RootElement, prefix: null, values); + } + catch (JsonException) + { + return Array.Empty(); + } + + var applied = new List(); + foreach (var (key, value) in values) + { + if (!string.IsNullOrEmpty(getEnv(key))) continue; // existing env wins + setEnv(key, value); + applied.Add(key); + } + return applied; + } + + /// Resolves the twin project's UserSecretsId, honouring the AX_SECRETS_PROJECT override. + private static string? ResolveUserSecretsId(string startDir, Func getEnv) + { + // Explicit override: a .csproj file or a directory containing one. + var overridePath = getEnv("AX_SECRETS_PROJECT"); + if (!string.IsNullOrWhiteSpace(overridePath)) + { + var resolved = Path.IsPathRooted(overridePath) ? overridePath : Path.Combine(startDir, overridePath); + var id = ReadUserSecretsIdFromPath(resolved); + if (!string.IsNullOrEmpty(id)) return id; + } + + foreach (var probe in DefaultProjectProbePaths) + { + var id = ReadUserSecretsIdFromPath(Path.Combine(startDir, probe)); + if (!string.IsNullOrEmpty(id)) return id; + } + return null; + } + + /// Reads UserSecretsId from a csproj file path, or from the first csproj in a directory. + private static string? ReadUserSecretsIdFromPath(string path) + { + if (File.Exists(path)) return ReadUserSecretsIdFromCsproj(path); + if (!Directory.Exists(path)) return null; + + foreach (var csproj in Directory.EnumerateFiles(path, "*.csproj", SearchOption.TopDirectoryOnly)) + { + var id = ReadUserSecretsIdFromCsproj(csproj); + if (!string.IsNullOrEmpty(id)) return id; + } + return null; + } + + private static string? ReadUserSecretsIdFromCsproj(string csprojPath) + { + try + { + var match = UserSecretsIdRegex.Match(File.ReadAllText(csprojPath)); + return match.Success ? match.Groups["id"].Value : null; + } + catch (IOException) + { + return null; + } + } + + /// The OS-default user-secrets root, matching the .NET Secret Manager layout. + public static string DefaultUserSecretsRoot() + { + // Windows: %APPDATA%\Microsoft\UserSecrets ; Unix: ~/.microsoft/usersecrets + var appData = Environment.GetEnvironmentVariable("APPDATA"); + if (!string.IsNullOrEmpty(appData)) + return Path.Combine(appData, "Microsoft", "UserSecrets"); + + var home = Environment.GetEnvironmentVariable("HOME") ?? string.Empty; + return Path.Combine(home, ".microsoft", "usersecrets"); + } + + /// Flattens nested JSON to key:subkey form (the Secret Manager convention), + /// keeping flat keys (e.g. AX_TARGET_PWD) exact. + private static void Flatten(JsonElement element, string? prefix, IDictionary into) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var prop in element.EnumerateObject()) + { + var key = prefix is null ? prop.Name : $"{prefix}:{prop.Name}"; + Flatten(prop.Value, key, into); + } + break; + case JsonValueKind.String: + if (prefix is not null) into[prefix] = element.GetString() ?? string.Empty; + break; + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + if (prefix is not null) into[prefix] = element.GetRawText(); + break; + // Arrays / null: not used by the secrets convention here — skip. + } + } +} diff --git a/src/scripts/dev.cs b/src/scripts/dev.cs index 1cabec840..98d0cf006 100644 --- a/src/scripts/dev.cs +++ b/src/scripts/dev.cs @@ -6,4 +6,4 @@ // Named dev.cs (not axdev.cs) to avoid an assembly-name clash with the tool (AssemblyName=axdev). using AXOpen.Dev.Tool; -return AxdevApp.Build().Run(args); +return AxdevApp.Run(args); From dde024f2c1ae79c5c7ddb11cd1d5bf3f2a4fa5d3 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sun, 31 May 2026 17:21:26 +0200 Subject: [PATCH 22/23] fix(axopen.dev): align password guard with secrets complexity policy PasswordValidator rejected $ & ( ) *, but configure-secrets.sh requires a special char from the set !@#$%^&*()_+-=. A complexity-valid password was then rejected at use time by axdev alf / axdev all. Remove those five chars from the blocklist; keep only genuinely-dangerous shell metacharacters and whitespace. Safe because args reach apax/openssl via CliWrap, not a shell. Update error message and tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 10 ++++++++++ .../Validation/PasswordValidatorTests.cs | 9 +++++++-- .../AXOpen.Dev/Commands/OrchestratorCommands.cs | 2 +- .../AXOpen.Dev/Validation/PasswordValidator.cs | 9 ++++++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 249ccbe1c..1dad8197d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### [FIX] `axdev` password guard contradicted the secrets complexity policy + +**Note:** Bug fix in `src/axopen.dev`. Branch: `feat/axdev-user-secrets-loader`. + +- fix: `AXOpen.Dev.Validation.PasswordValidator` no longer rejects `$ & ( ) *`. These are endorsed by the set-time complexity policy (`configure-secrets.sh` requires a special char from `!@#$%^&*()_+-=`), so a password that satisfied the complexity rule was then rejected at use time by `axdev alf` / `axdev all` with "The PASSWORD contains problematic characters." The blocklist now keeps only genuinely-dangerous shell metacharacters (`` ` \ " ' | ; < > ? [ ] { } `` and whitespace) — safe because arguments reach apax/openssl via CliWrap (no shell). Error message and `PasswordValidatorTests` updated. + +**Impact:** `apax alf` / `apax all` accept the same passwords the secrets-setup flow accepts; no more spurious rejection of compliant passwords. + +**Testing:** `dotnet test src/axopen.dev/AXOpen.Dev.Tests` — 180 passed. + ### [BUILD] `axdev` loads dotnet user-secrets at startup **Note:** Developer-CLI enhancement in `src/axopen.dev`. No PLC source change, no public-API removal. Branch: `feat/axdev-user-secrets-loader`. diff --git a/src/axopen.dev/AXOpen.Dev.Tests/Validation/PasswordValidatorTests.cs b/src/axopen.dev/AXOpen.Dev.Tests/Validation/PasswordValidatorTests.cs index 66b8d7058..fbd1f8c7e 100644 --- a/src/axopen.dev/AXOpen.Dev.Tests/Validation/PasswordValidatorTests.cs +++ b/src/axopen.dev/AXOpen.Dev.Tests/Validation/PasswordValidatorTests.cs @@ -8,17 +8,22 @@ public class PasswordValidatorTests [InlineData("Qwerty123456+")] // the showcase password — '+' is allowed [InlineData("Abc123-_=.~")] [InlineData("PlainPassword1")] + // $ & ( ) * are endorsed by the configure-secrets complexity set, so they are allowed. + [InlineData("dollar$ign")] + [InlineData("amper&sand")] + [InlineData("paren(s)here")] + [InlineData("star*power")] public void Safe_passwords_pass(string pwd) => Assert.True(PasswordValidator.IsSafe(pwd)); [Theory] [InlineData("has space")] - [InlineData("dollar$ign")] [InlineData("back`tick")] [InlineData("pipe|d")] [InlineData("semi;colon")] [InlineData("quote\"d")] - [InlineData("star*")] [InlineData("brace{}")] + [InlineData("angle")] + [InlineData("quest?ion")] [InlineData("")] [InlineData(null)] public void Unsafe_passwords_fail(string? pwd) => Assert.False(PasswordValidator.IsSafe(pwd)); diff --git a/src/axopen.dev/AXOpen.Dev/Commands/OrchestratorCommands.cs b/src/axopen.dev/AXOpen.Dev/Commands/OrchestratorCommands.cs index f21e03939..eca7baef7 100644 --- a/src/axopen.dev/AXOpen.Dev/Commands/OrchestratorCommands.cs +++ b/src/axopen.dev/AXOpen.Dev/Commands/OrchestratorCommands.cs @@ -64,7 +64,7 @@ public async Task ExecuteAsync(string @namespace, string plcName, string ip if (!PasswordValidator.IsSafe(password)) { - Output.Error("The PASSWORD contains problematic characters. Cannot use: $ ` \\ \" ' & | ; < > ( ) * ? [ ] { } or whitespace"); + Output.Error("The PASSWORD contains problematic characters. Cannot use: ` \\ \" ' | ; < > ? [ ] { } or whitespace"); return 1; } diff --git a/src/axopen.dev/AXOpen.Dev/Validation/PasswordValidator.cs b/src/axopen.dev/AXOpen.Dev/Validation/PasswordValidator.cs index 2d7910ec4..03a92d750 100644 --- a/src/axopen.dev/AXOpen.Dev/Validation/PasswordValidator.cs +++ b/src/axopen.dev/AXOpen.Dev/Validation/PasswordValidator.cs @@ -4,10 +4,17 @@ namespace AXOpen.Dev.Validation; /// Rejects passwords containing shell-problematic characters or whitespace. Port of /// validate_password_safe_chars in all_first.sh (kept for parity; less necessary in /// C# since arguments are not passed through a shell, but the first-setup flow enforces it). +/// +/// The blocklist is aligned with the complexity policy enforced when secrets are set +/// (configure-secrets.sh: special chars !@#$%^&*()_+-= are required/allowed). +/// The characters that policy endorses — $ & ( ) * — are therefore NOT blocked here, so a +/// password that satisfies the set-time complexity rule is not rejected at use time. /// public static class PasswordValidator { - private const string Problematic = "$`\\\"'&|;<>()*?[]{}"; + // Note: $ & ( ) * are intentionally absent — they are endorsed by the configure-secrets + // complexity set. Arguments reach apax/openssl via CliWrap (no shell), so these are safe. + private const string Problematic = "`\\\"'|;<>?[]{}"; public static bool IsSafe(string? password) { From 774505a351ba9f7c139509c95c1f6b1814a38c29 Mon Sep 17 00:00:00 2001 From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com> Date: Sun, 31 May 2026 21:22:58 +0200 Subject: [PATCH 23/23] chore: regenerate artifacts for AXSharp 0.47 AXSharp 0.47 codegen now emits [SourceFileAttribute] on generated classes; refresh the committed .g.cs test artifacts to match. Showcase HMI security config (cert/PKI) and tailwind css are regeneration side effects. Refs 00502ba64 --- .../ix/.g/Onliners/DataExchange/Context.g.cs | 1 + .../ix/.g/Onliners/Distributed/Context.g.cs | 1 + .../Onliners/Distributed/ExchangesWrappedInAxoObject.g.cs | 1 + .../ix/.g/Onliners/FragmentData/Context.g.cs | 1 + .../ix/.g/Onliners/FragmentData/FragmentDataManager.g.cs | 1 + .../Onliners/PersistentData/ObjectWithPersistentMember.g.cs | 1 + .../.g/Onliners/PersistentData/PersistentDataContext.g.cs | 1 + .../ix/.g/Onliners/PersistentData/PersistentRootObject.g.cs | 1 + .../ix/.g/Onliners/Primitives/InitializedPrimitives.g.cs | 1 + .../ix/.g/Onliners/Primitives/PrimitivesDataEntity.g.cs | 1 + .../ix/.g/Onliners/Primitives/PrimitivesDataManager.g.cs | 1 + .../.g/Onliners/SharedEntityHeader/SharedEntityHeader.g.cs | 1 + .../SharedEntityHeader/SharedEntityHeaderManager.g.cs | 1 + .../ix/.g/POCO/DataExchange/Context.g.cs | 1 + .../ix/.g/POCO/Distributed/Context.g.cs | 1 + .../ix/.g/POCO/Distributed/ExchangesWrappedInAxoObject.g.cs | 1 + .../ix/.g/POCO/FragmentData/Context.g.cs | 1 + .../ix/.g/POCO/FragmentData/FragmentDataManager.g.cs | 1 + .../.g/POCO/PersistentData/ObjectWithPersistentMember.g.cs | 1 + .../ix/.g/POCO/PersistentData/PersistentDataContext.g.cs | 1 + .../ix/.g/POCO/PersistentData/PersistentRootObject.g.cs | 1 + .../ix/.g/POCO/Primitives/InitializedPrimitives.g.cs | 1 + .../ix/.g/POCO/Primitives/PrimitivesDataEntity.g.cs | 1 + .../ix/.g/POCO/Primitives/PrimitivesDataManager.g.cs | 1 + .../ix/.g/POCO/SharedEntityHeader/SharedEntityHeader.g.cs | 1 + .../POCO/SharedEntityHeader/SharedEntityHeaderManager.g.cs | 1 + .../ix/.g/POCO/StationData/StationData.g.cs | 1 + .../ix/.g/POCO/StationData/StationDataManager.g.cs | 1 + .../app/hwc/hwc.gen/plc_line.SecurityConfiguration.json | 6 +++--- src/styling/src/wwwroot/css/momentum.css | 2 +- 30 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/DataExchange/Context.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/DataExchange/Context.g.cs index 3469c5e14..4706d143c 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/DataExchange/Context.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/DataExchange/Context.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1.DataExchange { + [AXSharp.Connector.SourceFileAttribute(@"DataExchange/Context.st")] public partial class DataExchangeContext : AXOpen.Core.AxoContext { public AXOpen.Core.AxoObject _rootObject { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Distributed/Context.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Distributed/Context.g.cs index f8c32a898..f11cb91ec 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Distributed/Context.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Distributed/Context.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1.Distributed { + [AXSharp.Connector.SourceFileAttribute(@"Distributed/Context.st")] public partial class DistributedDataContext : AXOpen.Core.AxoContext { public AXOpen.Core.AxoObject _rootObject { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Distributed/ExchangesWrappedInAxoObject.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Distributed/ExchangesWrappedInAxoObject.g.cs index 93b4aa2ae..5bffe4ad7 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Distributed/ExchangesWrappedInAxoObject.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Distributed/ExchangesWrappedInAxoObject.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1.Distributed { + [AXSharp.Connector.SourceFileAttribute(@"Distributed/ExchangesWrappedInAxoObject.st")] public partial class ExchangesWrappedInAxoObject : AXOpen.Core.AxoObject { public Tests_L1.SharedEntityHeaderManager Header { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/FragmentData/Context.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/FragmentData/Context.g.cs index adb015477..49778eaf1 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/FragmentData/Context.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/FragmentData/Context.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1.FragmentData { + [AXSharp.Connector.SourceFileAttribute(@"FragmentData/Context.st")] public partial class FragmentDataContext : AXOpen.Core.AxoContext { public AXOpen.Core.AxoObject _rootObject { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/FragmentData/FragmentDataManager.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/FragmentData/FragmentDataManager.g.cs index cab74ffc3..10a501f01 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/FragmentData/FragmentDataManager.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/FragmentData/FragmentDataManager.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1.FragmentData { + [AXSharp.Connector.SourceFileAttribute(@"FragmentData/FragmentDataManager.st")] public partial class FragmentProcessDataManager : AXOpen.Data.AxoDataFragmentExchange { [AXOpen.Data.AxoDataFragmentAttribute] diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/ObjectWithPersistentMember.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/ObjectWithPersistentMember.g.cs index b505f62ff..2799a6ffb 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/ObjectWithPersistentMember.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/ObjectWithPersistentMember.g.cs @@ -10,6 +10,7 @@ namespace Tests_L1.PersistentData { + [AXSharp.Connector.SourceFileAttribute(@"PersistentData/ObjectWithPersistentMember.st")] public partial class ObjectWithPersistentMember : AXSharp.Connector.ITwinObject { public OnlinerInt NotPersistentVariable { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/PersistentDataContext.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/PersistentDataContext.g.cs index 4ddc092d0..b5829c5a7 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/PersistentDataContext.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/PersistentDataContext.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1.PersistentData { + [AXSharp.Connector.SourceFileAttribute(@"PersistentData/PersistentDataContext.st")] public partial class PersistentDataContext : AXOpen.Core.AxoContext { public AXOpen.Core.AxoObject _rootObject { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/PersistentRootObject.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/PersistentRootObject.g.cs index a4ba9e286..068c891bf 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/PersistentRootObject.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/PersistentData/PersistentRootObject.g.cs @@ -10,6 +10,7 @@ namespace Tests_L1.PersistentData { + [AXSharp.Connector.SourceFileAttribute(@"PersistentData/PersistentRootObject.st")] public partial class PersistentRootObject : AXSharp.Connector.ITwinObject { public OnlinerBool NotPersistentVariable { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/InitializedPrimitives.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/InitializedPrimitives.g.cs index b14f9c5b2..ec9e86110 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/InitializedPrimitives.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/InitializedPrimitives.g.cs @@ -10,6 +10,7 @@ namespace Tests_L1.Primitives { + [AXSharp.Connector.SourceFileAttribute(@"Primitives/InitializedPrimitives.st")] public partial class InitializedPrimitives : AXSharp.Connector.ITwinObject { public OnlinerBool v_BOOL { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/PrimitivesDataEntity.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/PrimitivesDataEntity.g.cs index 904be0996..d52f72011 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/PrimitivesDataEntity.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/PrimitivesDataEntity.g.cs @@ -10,6 +10,7 @@ namespace Tests_L1.Primitives { + [AXSharp.Connector.SourceFileAttribute(@"Primitives/PrimitivesDataEntity.st")] public partial class PrimitivesDataEntity : AXOpen.Data.AxoDataEntity { public OnlinerBool v_BOOL { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/PrimitivesDataManager.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/PrimitivesDataManager.g.cs index ea5dbb5c3..e22177e2d 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/PrimitivesDataManager.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/Primitives/PrimitivesDataManager.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1.Primitives { + [AXSharp.Connector.SourceFileAttribute(@"Primitives/PrimitivesDataManager.st")] public partial class PrimitivesDataManager : AXOpen.Data.AxoDataExchange { [AXOpen.Data.AxoDataEntityAttribute] diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/SharedEntityHeader/SharedEntityHeader.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/SharedEntityHeader/SharedEntityHeader.g.cs index 4197a250e..ce81bceb6 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/SharedEntityHeader/SharedEntityHeader.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/SharedEntityHeader/SharedEntityHeader.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1 { + [AXSharp.Connector.SourceFileAttribute(@"SharedEntityHeader/SharedEntityHeader.st")] public partial class SharedEntityHeader : AXOpen.Data.AxoDataEntity { public OnlinerInt ComesFrom { get; } diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/SharedEntityHeader/SharedEntityHeaderManager.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/SharedEntityHeader/SharedEntityHeaderManager.g.cs index a7d5747b1..d4d9c723c 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/SharedEntityHeader/SharedEntityHeaderManager.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/Onliners/SharedEntityHeader/SharedEntityHeaderManager.g.cs @@ -9,6 +9,7 @@ namespace Tests_L1 { + [AXSharp.Connector.SourceFileAttribute(@"SharedEntityHeader/SharedEntityHeaderManager.st")] public partial class SharedEntityHeaderManager : AXOpen.Data.AxoDataExchange { [AXOpen.Data.AxoDataEntityAttribute] diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/DataExchange/Context.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/DataExchange/Context.g.cs index 7120ed90b..cdd607a42 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/DataExchange/Context.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/DataExchange/Context.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1.DataExchange { + [AXSharp.Connector.SourceFileAttribute(@"DataExchange/Context.st")] public partial class DataExchangeContext : AXOpen.Core.AxoContext, AXSharp.Connector.IPlain { public DataExchangeContext() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Distributed/Context.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Distributed/Context.g.cs index 01f9af127..1d390495a 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Distributed/Context.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Distributed/Context.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1.Distributed { + [AXSharp.Connector.SourceFileAttribute(@"Distributed/Context.st")] public partial class DistributedDataContext : AXOpen.Core.AxoContext, AXSharp.Connector.IPlain { public DistributedDataContext() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Distributed/ExchangesWrappedInAxoObject.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Distributed/ExchangesWrappedInAxoObject.g.cs index 1ca17717c..3b5d8433f 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Distributed/ExchangesWrappedInAxoObject.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Distributed/ExchangesWrappedInAxoObject.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1.Distributed { + [AXSharp.Connector.SourceFileAttribute(@"Distributed/ExchangesWrappedInAxoObject.st")] public partial class ExchangesWrappedInAxoObject : AXOpen.Core.AxoObject, AXSharp.Connector.IPlain { public ExchangesWrappedInAxoObject() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/FragmentData/Context.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/FragmentData/Context.g.cs index b9e22abe5..924f12d4c 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/FragmentData/Context.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/FragmentData/Context.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1.FragmentData { + [AXSharp.Connector.SourceFileAttribute(@"FragmentData/Context.st")] public partial class FragmentDataContext : AXOpen.Core.AxoContext, AXSharp.Connector.IPlain { public FragmentDataContext() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/FragmentData/FragmentDataManager.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/FragmentData/FragmentDataManager.g.cs index b6052c64e..0bb8bc91a 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/FragmentData/FragmentDataManager.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/FragmentData/FragmentDataManager.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1.FragmentData { + [AXSharp.Connector.SourceFileAttribute(@"FragmentData/FragmentDataManager.st")] public partial class FragmentProcessDataManager : AXOpen.Data.AxoDataFragmentExchange, AXSharp.Connector.IPlain { public FragmentProcessDataManager() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/ObjectWithPersistentMember.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/ObjectWithPersistentMember.g.cs index feb713608..58ebb011c 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/ObjectWithPersistentMember.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/ObjectWithPersistentMember.g.cs @@ -9,6 +9,7 @@ namespace Pocos { namespace Tests_L1.PersistentData { + [AXSharp.Connector.SourceFileAttribute(@"PersistentData/ObjectWithPersistentMember.st")] public partial class ObjectWithPersistentMember : AXSharp.Connector.IPlain { public ObjectWithPersistentMember() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/PersistentDataContext.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/PersistentDataContext.g.cs index 1c03abf6c..c91432f90 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/PersistentDataContext.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/PersistentDataContext.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1.PersistentData { + [AXSharp.Connector.SourceFileAttribute(@"PersistentData/PersistentDataContext.st")] public partial class PersistentDataContext : AXOpen.Core.AxoContext, AXSharp.Connector.IPlain { public PersistentDataContext() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/PersistentRootObject.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/PersistentRootObject.g.cs index b63aa3111..92a7a9d90 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/PersistentRootObject.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/PersistentData/PersistentRootObject.g.cs @@ -9,6 +9,7 @@ namespace Pocos { namespace Tests_L1.PersistentData { + [AXSharp.Connector.SourceFileAttribute(@"PersistentData/PersistentRootObject.st")] public partial class PersistentRootObject : AXSharp.Connector.IPlain { public PersistentRootObject() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/InitializedPrimitives.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/InitializedPrimitives.g.cs index 89100a6f5..0f4c1a2ff 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/InitializedPrimitives.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/InitializedPrimitives.g.cs @@ -9,6 +9,7 @@ namespace Pocos { namespace Tests_L1.Primitives { + [AXSharp.Connector.SourceFileAttribute(@"Primitives/InitializedPrimitives.st")] public partial class InitializedPrimitives : AXSharp.Connector.IPlain { public InitializedPrimitives() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/PrimitivesDataEntity.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/PrimitivesDataEntity.g.cs index 0d82507cf..455a77990 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/PrimitivesDataEntity.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/PrimitivesDataEntity.g.cs @@ -9,6 +9,7 @@ namespace Pocos { namespace Tests_L1.Primitives { + [AXSharp.Connector.SourceFileAttribute(@"Primitives/PrimitivesDataEntity.st")] public partial class PrimitivesDataEntity : AXOpen.Data.AxoDataEntity, AXSharp.Connector.IPlain { public PrimitivesDataEntity() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/PrimitivesDataManager.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/PrimitivesDataManager.g.cs index e31a11593..59b0137a6 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/PrimitivesDataManager.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/Primitives/PrimitivesDataManager.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1.Primitives { + [AXSharp.Connector.SourceFileAttribute(@"Primitives/PrimitivesDataManager.st")] public partial class PrimitivesDataManager : AXOpen.Data.AxoDataExchange, AXSharp.Connector.IPlain { public PrimitivesDataManager() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/SharedEntityHeader/SharedEntityHeader.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/SharedEntityHeader/SharedEntityHeader.g.cs index 4e0f13a4e..92fc8a2dd 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/SharedEntityHeader/SharedEntityHeader.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/SharedEntityHeader/SharedEntityHeader.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1 { + [AXSharp.Connector.SourceFileAttribute(@"SharedEntityHeader/SharedEntityHeader.st")] public partial class SharedEntityHeader : AXOpen.Data.AxoDataEntity, AXSharp.Connector.IPlain { public SharedEntityHeader() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/SharedEntityHeader/SharedEntityHeaderManager.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/SharedEntityHeader/SharedEntityHeaderManager.g.cs index a60642a7b..4d0577236 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/SharedEntityHeader/SharedEntityHeaderManager.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/SharedEntityHeader/SharedEntityHeaderManager.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1 { + [AXSharp.Connector.SourceFileAttribute(@"SharedEntityHeader/SharedEntityHeaderManager.st")] public partial class SharedEntityHeaderManager : AXOpen.Data.AxoDataExchange, AXSharp.Connector.IPlain { public SharedEntityHeaderManager() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/StationData/StationData.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/StationData/StationData.g.cs index d66ce0144..e04b7a73c 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/StationData/StationData.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/StationData/StationData.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1 { + [AXSharp.Connector.SourceFileAttribute(@"StationData/StationData.st")] public partial class StationData : AXOpen.Data.AxoDataEntity, AXSharp.Connector.IPlain { public StationData() : base() diff --git a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/StationData/StationDataManager.g.cs b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/StationData/StationDataManager.g.cs index d32b69217..6a9297613 100644 --- a/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/StationData/StationDataManager.g.cs +++ b/src/data/tests/AXOpen.Data.Tests_L1/ix/.g/POCO/StationData/StationDataManager.g.cs @@ -8,6 +8,7 @@ namespace Pocos { namespace Tests_L1 { + [AXSharp.Connector.SourceFileAttribute(@"StationData/StationDataManager.st")] public partial class StationDataManager : AXOpen.Data.AxoDataExchange, AXSharp.Connector.IPlain { public StationDataManager() : base() diff --git a/src/showcase/app/hwc/hwc.gen/plc_line.SecurityConfiguration.json b/src/showcase/app/hwc/hwc.gen/plc_line.SecurityConfiguration.json index 64b2012b3..716d0466d 100644 --- a/src/showcase/app/hwc/hwc.gen/plc_line.SecurityConfiguration.json +++ b/src/showcase/app/hwc/hwc.gen/plc_line.SecurityConfiguration.json @@ -1,9 +1,9 @@ { - "PKIData": "AQAAAAAAAAAAAAAAAAAAAAEBAb4gAAACAAAAAAAAAAAAAwAAAN8ALS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUhvd0ZBWUhLb1pJemowQ0FRWUpLeVFEQXdJSUFRRUxBMklBQkhHZFcyTmgwUzE0Ry9VbnNteUM0MHBBZWI2NApNbVI2Sk9IUmhkd2ZVWXhOTVNSUXJld3dKZUIyU3FXOW9GNjAxd0JubEFQNFd2WlJjd0dPaWlPbGxkbjR5bW15CjZ0RzFpalgwVVhZbXo3eUg0NzIzZnlBSzQ1TG1NbXNqVHB2UHNBPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCnsBAQACAAAnEAAgf84+g0oNFc10YQOVrScnpXHmoJ9LQDM3GRKnAyxeVTABACAADAAQxIBGwC2bDFbSbRZNT6hqMQEAAAE2YWT+94UZtnwgo24Mn6E66BqTJ+0/F0CUe+BkqPxjo+FWGki45gIOzSM5p8CvvRnK/467oCXQhaCPB0DGTR1CLd5nsXcgchyvB2y6syd9SrmGed8z2mm5+g2knXs4Swtd5JfAJ/rDsh/cHfeVdeIIcIVHkx9yn1M4zJxPcYsBKiBxtpmyiQDBS4Y98AziyVHaJj7rZBz5kYGmuw4OH4sNkOkULXpaRDqHO9Rs0mdyKESexITeYG9LCnqyqQXAe9jMf0E28FYg8Ygdew8Swn9Y/51MK4gmgLY8kED9K52yhl2EElzHzGaaSkqdseqMa3gB30XWCyYXsiLzrJlxab71Bb7tvnexfI/HxJ9UPITHF50fwUwM+5RY4S2278WO2bX5TIKcx2dfpeD1dZXKEx2GT32qM6fUGgIACWxvY2FsaG9zdAEAAAAAAAAAAQAAAAIAAAAAAAAAFAA9dfqdiqn2hJ9G0iJJlA3vu14VZrQGAQAGsC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQ0KTUlJRXN6Q0NBNXVnQXdJQkFnSVVUc25sM2s3Vm5tK3h5M2tMcnYzTHhFUDZnVTR3RFFZSktvWklodmNOQVFFTEJRQXdlekVMTUFrRw0KQTFVRUJoTUNXRmd4RWpBUUJnTlZCQWdNQ1ZOMFlYUmxUbUZ0WlRFUk1BOEdBMVVFQnd3SVEybDBlVTVoYldVeEZEQVNCZ05WQkFvTQ0KQzBOdmJYQmhibmxPWVcxbE1Sc3dHUVlEVlFRTERCSkRiMjF3WVc1NVUyVmpkR2x2Yms1aGJXVXhFakFRQmdOVkJBTU1DV3h2WTJGcw0KYUc5emREQWVGdzB5TmpBMU1qWXdPREE0TWpGYUZ3MHpOakExTWpNd09EQTRNakZhTUhzeEN6QUpCZ05WQkFZVEFsaFlNUkl3RUFZRA0KVlFRSURBbFRkR0YwWlU1aGJXVXhFVEFQQmdOVkJBY01DRU5wZEhsT1lXMWxNUlF3RWdZRFZRUUtEQXREYjIxd1lXNTVUbUZ0WlRFYg0KTUJrR0ExVUVDd3dTUTI5dGNHRnVlVk5sWTNScGIyNU9ZVzFsTVJJd0VBWURWUVFEREFsc2IyTmhiR2h2YzNRd2dnRWlNQTBHQ1NxRw0KU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ094UnJzUEhGcEROR2krRXp5OG41dnp0QWxsQnpZYS9SRmxjTmxrbStGcWNPMg0KWTZBeTJJRUhERGVjRzhyYzlJTERqRjZnVW51WVNoa1FvekN1OUs2VUN6Zk8zMEc1MmdqRFdFQ1k0ZEJSTU96L2d4bFdtbHJsKzRNQw0KRXVONlpuL28wSitzRzIzOVV6S2ZLRVZLT2ZOaFpCendaNFJ2ZEQwV3lYcTBUV3BLc1lCLzZaN1JSb2FtZS80U3IzZzB4TzNVSWtmaQ0KeHBoMVpBOHQzWUNkeFdYT0tjaHdwaDhqSDdnaFdQTkN3U0VvNmU4alF3Mk1QdFVQYXAzUHlveW1SM1k1dEVacUZ3d0xncGtDMU1VbQ0KUFV0bGJXV0JTYU1JNDN0cHNsd2NuaVRGNWV1ZXBYdi9jUjc1WDdhOUJIOWNQWVIvdTJTOFhuNnZEZ1lDelZudHloaUZBZ01CQUFHag0KZ2dFdE1JSUJLVEFKQmdOVkhSTUVBakFBTUE0R0ExVWREd0VCL3dRRUF3SUM5REFkQmdOVkhTVUVGakFVQmdnckJnRUZCUWNEQVFZSQ0KS3dZQkJRVUhBd0l3RXdZRFZSMFJCQXd3Q29JQWh3VEFxR1FCaGdBd0hRWURWUjBPQkJZRUZLcE5zYzFIbFZldUdKVk5kZUpJbnhkTw0Kem5wMU1JRzRCZ05WSFNNRWdiQXdnYTJBRktwTnNjMUhsVmV1R0pWTmRlSklueGRPem5wMW9YK2tmVEI3TVFzd0NRWURWUVFHRXdKWQ0KV0RFU01CQUdBMVVFQ0F3SlUzUmhkR1ZPWVcxbE1SRXdEd1lEVlFRSERBaERhWFI1VG1GdFpURVVNQklHQTFVRUNnd0xRMjl0Y0dGdQ0KZVU1aGJXVXhHekFaQmdOVkJBc01Fa052YlhCaGJubFRaV04wYVc5dVRtRnRaVEVTTUJBR0ExVUVBd3dKYkc5allXeG9iM04wZ2hSTw0KeWVYZVR0V2ViN0hMZVF1dS9jdkVRL3FCVGpBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQVduUGRsYy9ud2hoaml0MzRoTzZsVHVRSA0KaCtvdkVDTzNxamQ0ZVBhaWJaUnRWS3Q5ZXBRYk1KSW1xVWNQSEd6SjRnakJGcUJ6L1RaWXRXdlgzcTUrTlAzRUFCYXN3K1FRUFhVdw0KekZPbmpsOC96YzFBak1qTTlzVHg0L0VrSHFoWGdPbFRkOGxva3pDSExBSXJjUDNMNDZydzdQRndCbjMzQTFtdWdQNnUyVStyKzhOQQ0KNUJMUlpaT285a3BhTlZNaEE3ZUF4endNMWpPWHpxVXV5Tm4rMjJFQTlKVTFwMWg5NThpY09GeTEwaTVGMVNXbGpaRDluVktIdEh2Sw0KOGw3aDVzQlBtVGRzVlJkMHhPajVFQjIyOGdsbVFzdWYwYm1wOW9DSE8zWXRkZis1LzFINHIrKyt3amY0dUdJS3hQdzJPSE5IZ0c2YQ0KdXNyaWhTSWtMWmJDWHc9PQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0KFwgBAACzAQACAHwwejAUBgcqhkjOPQIBBgkrJAMDAggBAQsDYgAEGeScPs1Vde6mVIcqPvUqpPIv0ZhED/1GCR7lXPjo3iyg3CGK3d+gedGAGMqDYKNSbQKRZxMTBCf50CzWKU5r9fqPn0G5CouDd2/0/Fv815A9dkx8BpnNs7a1zePl6XFb588jhsnERGdTapDHXWoORAAgbKqyj9pgkYGRgMPkfZfBWj4f0mt4gt702mRbUQDrjVEHXi0tLS0tQkVHSU4gRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0KTUlJRk5UQmZCZ2txaGtpRzl3MEJCUTB3VWpBeEJna3Foa2lHOXcwQkJRd3dKQVFRekNhdWd0NmdMUSs2UGY0dQpTV29lWHdJQ0NBQXdEQVlJS29aSWh2Y05BZ2tGQURBZEJnbGdoa2dCWlFNRUFTb0VFTEVIdUpZc3BqZTkxbHlBCkVLMDFYR29FZ2dUUVVud0QvejAyZDF0NG1NLzhsd2N0TFpCZnMvcUorVWFvdVYwajNYdDlHbHp2VmFUakZyeWQKaEUrc2FTK0lkd01JN2lMcWFMY2NQUm4weFRFdDJtbXFxdFZTUHMxQXg3cXMxRGYwU2Vncnd1ZFIvNVVHQURtSgpvYnFTeXBqbDJ3TmhyRTlZQzJvcjRLaVBuQXJPT0FjdEVDa0Q1OFNTYmthbStPS2VDMjR5bzR0YzJwVlFCV1M4ClRzS1V0enBMWUdwSTBySmo1UjB0U2JQMWRlSWxlTm5hanl6aGwzRkJLbHdadkxEQlJQUGFMVnZvVW95dXpjWUsKa0Flc3VVc0JtTXpycE9pbHpEaXVDL3JueEV5RE5hUHlRWmtVeWpHUEptSGFsM0llQlpJWE1HK0ZmbUFPVlpDZwpXZVFITXBEeFljeml3UzFmU3c4ZGZMVmxUdk9qTE05a3FqZGt5Q29veEhrbFQ2bmRiZWFZYkVCNEFJY1NWK0NICjRmYWd2MWVydS9pWHgxajMxbXhsWndnMDNsbmExKzVxRCsyaUprOVd1TE96Vmp2NE12LzF0SzZKSnZMcG41ZmgKWHlxeVdRWFlnTmx6bTBxbEszV0NBUlozRXcxRzVmVXpheDNWdmpKbi9EV0VIM0gwSVRFQVFLbVVSTW50R01pTwpvNmpld21EZ3EzbGVVVlVhWm9aY0lWMzcwK1RLZWtBMXBReUpLNXhNL2dDdUQ5RkVxRUpYNUJJWE1WamJiNEVqCjd5dWwrdkxEU2pocVJtc1BUMkpucDIwZW5QWTZBOVQyUk5DYVBoQVZSREpxRjljOHNTVlhvS3phSnhWN1JsSXkKRHZkV24rZGF5Q2lwRmhEQWN5SURVT2pKRkQxWGcrY1BNcWcwemhRMm05cnFIL0ozRnVFREwxNTFreVlMNjBBcgpiWE9aSis0b1pWTU9ITHREWnRFUkxZU244OUlhdGlmWk9LUUJpL3A3czBIRFBVUzRBWlUwOWlRVFQzMnFVQ0JnCnZUa21mWFAvQ2o1Z1hZMEx4eFBYZSs2ZmhIeHZDOVhUOGpqL2wwbnJyaGlic3JUbXJGMUVEWE5DakJZUmdnczEKOFJVeVkyeHFYMy9IcE5jWnNIQkJYSWhVUlZhS0pFWCtudnFzTndvVjRyMmhZc3JJVHJmU2czUDU3ZG5ZSDBXWgpaclBGcFNWL29FS3dTMDZvV3ZuQk95MkhGcFJham1JTzFZUnMvNGxFKyttMzBxNFdySWpnL0dVaWJmdTRoZEdwCmVudHdaUFgyanY5RjNNY0lIdnNZME1GZ1cyMUpYbWlvUC9SdFVKdDFabUlxdy92OGVoYUFyYk11WFdxMHlrMSsKTXVWem45b0VjRElLSkl4R2tPSGJRZEYxNklTZkprRncraWR2Y0k0Y0tSbk10c2pkUWNnUy9WMDkyWnNwWGlpego4aGhpOWZjVFhack5MdHg4cFlBMlZSazF4YzhNM0xEQ2h5UTQyL0tPUGZJUFYrWWRtUUphTFFjQ1NJTzBJaXk5Ckc0QUprVFZScG04VitaM3hZRllZUUZSdjVhOCt6eXRkUjJlZDBobC9JWTFIRFRaTDZldDVRaDlaOVpUUTY1STkKbjdUYjIvVVBTaEdBS1FrZ0lzb2dCUDZtczZaYWdlczhGYnJmWmNkQm93Y2t1c0tRcTlISEt2YnNwNCsvVjE0TwpYNUV3YzdtWGE0dk1IY2FRRkI3VitCM1lVZ3d6S084T1MzU1VTMmtJUVU5Q2xCSzdvdVZrS2hzdjhWdHlHU053Ck9aMVdCaE14YzlMaDdwMUFPV3U4RHJRaFFhcktOUVhlVVMzK0tZcXFCejkyeVhUSU1uMEdHclRUWm9Nc3ZTdTMKM0VPL25MZDA3NjNtM3ZVMXdNNU1NcHlFc1FJMGs3NjRzM1V3aTJvTFV4alFIUmJUUTFJUlNMQUVIc1RwS3RsNgplbzN1UTNNZWtpalYzaGs1dDJ0STBaRnFEaHlSQkFlUStsTEVwWmhETEhvSTFCK29xemk5NXZJQXZIK0xzNHVvCmxWZXJMSGpGeGlhdUtpLzZwYjZFSGdRSnRsNHR3VThqZUxFN24xdVBBcGxlQ1FHckJIU25FeXk2NFF3RXo3OHIKa3hLZnZaRDNnTHR0MzJoN0REMVBSLzBUSjd1WGh1bGtRNGYzQnpGNXgyWm5CWU1rc3UrbFNqZz0KLS0tLS1FTkQgRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0KJEROUzosIElQIEFkZHJlc3M6MTkyLjE2OC4xMDAuMSwgVVJJOglsb2NhbGhvc3QCAAAAAAAAAAEAAAACAAAAAAAAABQAPXX6nYqp9oSfRtIiSZQN77teFWa0BgEABrAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0NCk1JSUVzekNDQTV1Z0F3SUJBZ0lVVHNubDNrN1ZubSt4eTNrTHJ2M0x4RVA2Z1U0d0RRWUpLb1pJaHZjTkFRRUxCUUF3ZXpFTE1Ba0cNCkExVUVCaE1DV0ZneEVqQVFCZ05WQkFnTUNWTjBZWFJsVG1GdFpURVJNQThHQTFVRUJ3d0lRMmwwZVU1aGJXVXhGREFTQmdOVkJBb00NCkMwTnZiWEJoYm5sT1lXMWxNUnN3R1FZRFZRUUxEQkpEYjIxd1lXNTVVMlZqZEdsdmJrNWhiV1V4RWpBUUJnTlZCQU1NQ1d4dlkyRnMNCmFHOXpkREFlRncweU5qQTFNall3T0RBNE1qRmFGdzB6TmpBMU1qTXdPREE0TWpGYU1Ic3hDekFKQmdOVkJBWVRBbGhZTVJJd0VBWUQNClZRUUlEQWxUZEdGMFpVNWhiV1V4RVRBUEJnTlZCQWNNQ0VOcGRIbE9ZVzFsTVJRd0VnWURWUVFLREF0RGIyMXdZVzU1VG1GdFpURWINCk1Ca0dBMVVFQ3d3U1EyOXRjR0Z1ZVZObFkzUnBiMjVPWVcxbE1SSXdFQVlEVlFRRERBbHNiMk5oYkdodmMzUXdnZ0VpTUEwR0NTcUcNClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNPeFJyc1BIRnBETkdpK0V6eThuNXZ6dEFsbEJ6WWEvUkZsY05sa20rRnFjTzINClk2QXkySUVIRERlY0c4cmM5SUxEakY2Z1VudVlTaGtRb3pDdTlLNlVDemZPMzBHNTJnakRXRUNZNGRCUk1Pei9neGxXbWxybCs0TUMNCkV1TjZabi9vMEorc0cyMzlVektmS0VWS09mTmhaQnp3WjRSdmREMFd5WHEwVFdwS3NZQi82WjdSUm9hbWUvNFNyM2cweE8zVUlrZmkNCnhwaDFaQTh0M1lDZHhXWE9LY2h3cGg4akg3Z2hXUE5Dd1NFbzZlOGpRdzJNUHRVUGFwM1B5b3ltUjNZNXRFWnFGd3dMZ3BrQzFNVW0NClBVdGxiV1dCU2FNSTQzdHBzbHdjbmlURjVldWVwWHYvY1I3NVg3YTlCSDljUFlSL3UyUzhYbjZ2RGdZQ3pWbnR5aGlGQWdNQkFBR2oNCmdnRXRNSUlCS1RBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lDOURBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUkNCkt3WUJCUVVIQXdJd0V3WURWUjBSQkF3d0NvSUFod1RBcUdRQmhnQXdIUVlEVlIwT0JCWUVGS3BOc2MxSGxWZXVHSlZOZGVKSW54ZE8NCnpucDFNSUc0QmdOVkhTTUVnYkF3Z2EyQUZLcE5zYzFIbFZldUdKVk5kZUpJbnhkT3pucDFvWCtrZlRCN01Rc3dDUVlEVlFRR0V3SlkNCldERVNNQkFHQTFVRUNBd0pVM1JoZEdWT1lXMWxNUkV3RHdZRFZRUUhEQWhEYVhSNVRtRnRaVEVVTUJJR0ExVUVDZ3dMUTI5dGNHRnUNCmVVNWhiV1V4R3pBWkJnTlZCQXNNRWtOdmJYQmhibmxUWldOMGFXOXVUbUZ0WlRFU01CQUdBMVVFQXd3SmJHOWpZV3hvYjNOMGdoUk8NCnllWGVUdFdlYjdITGVRdXUvY3ZFUS9xQlRqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFXblBkbGMvbndoaGppdDM0aE82bFR1UUgNCmgrb3ZFQ08zcWpkNGVQYWliWlJ0Vkt0OWVwUWJNSkltcVVjUEhHeko0Z2pCRnFCei9UWll0V3ZYM3E1K05QM0VBQmFzdytRUVBYVXcNCnpGT25qbDgvemMxQWpNak05c1R4NC9Fa0hxaFhnT2xUZDhsb2t6Q0hMQUlyY1AzTDQ2cnc3UEZ3Qm4zM0ExbXVnUDZ1MlUrcis4TkENCjVCTFJaWk9vOWtwYU5WTWhBN2VBeHp3TTFqT1h6cVV1eU5uKzIyRUE5SlUxcDFoOTU4aWNPRnkxMGk1RjFTV2xqWkQ5blZLSHRIdksNCjhsN2g1c0JQbVRkc1ZSZDB4T2o1RUIyMjhnbG1Rc3VmMGJtcDlvQ0hPM1l0ZGYrNS8xSDRyKysrd2pmNHVHSUt4UHcyT0hOSGdHNmENCnVzcmloU0lrTFpiQ1h3PT0NCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0NChcIAQAAswEAAgB8MHowFAYHKoZIzj0CAQYJKyQDAwIIAQELA2IABDme1/iPCoyXWRwsYoZ5BaJNLe9N1N3XIa4gGdGoelDzFFhTQk8f0HQ1U5HMCe/aMg0SWr25siBeGm1HtaGZjrEIEGg7T4cYjnzJQrc6+CI9uITmhwvPkv2wEkhRjKQKG93Rg6rqgiGpakZi0wXWAQAAICR1XeOYQ21LppgR0GnsKPD8qA/4oxq3hywLIzaK7uiLB14tLS0tLUJFR0lOIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCk1JSUZOVEJmQmdrcWhraUc5dzBCQlEwd1VqQXhCZ2txaGtpRzl3MEJCUXd3SkFRUXlQN05jNU0wZURVc2ZrWEEKMnVxV1pRSUNDQUF3REFZSUtvWklodmNOQWdrRkFEQWRCZ2xnaGtnQlpRTUVBU29FRVB6a0E1WlhHU3VMU0ZLWApOcHFNbHMwRWdnVFFoSFZRZElRNm5PUWxNUkxBdUlSV1p1R2hLSUFxamRyajNBNXA0dlFRaGxCdzNLZktLQ2tTClR3VFFBbnR0ZE5sOC9MeDF1TVVYeDlHRUY5cG8wVUZ0OGtZdHo4aS8wZXAvM0NGTldleHpDQUMyRlZhTlN4dHQKMXVHeElTd3JkVWxEcnYzWlhkQUNGd09TWUQ5L3VTbmFrdXMrbXBGaUF0MUlKY1l5U0xad2ZNRkhZZmRzVW8xcApEaXVqYWt6MkJwRnc3UVRvK1pBbW1JYndwN015d3hvcW8wczZhOFhqaHpIQ1N6ZkVaZlRIV1k5U1ZTWnlQNXZqCjYyaThlNFlVclhxWFN6S1FwdHE4S1hmczJOZWd1Skt3aGFaRkdJYXFJOUY0RkdIaDlBVFJod0pSYVJDc3AxdDEKY2dSQyttTjBNdlgrZ05RUmVlSzlvN2RuaGdOaWY0SngyTmRjWGJ4QnVjSU9Qa291eHhYL1B4MlIzeUhLakVHMQpDK2RZTU5naFZhUWJBNTNvaTJDMHh6aXA3ZHkrRldFZ1g2aFU1aERBM2tlenJFalBMSGR6Y0N1Uk1GeVBNMlRVClhjcjA0NEhQK3dKbmF1Tnc4RDFhMTBFbTllcCtkU0xRWm5tblV5blV4d0FlbFlXRVF4QVpFb1dVZXJyMlNkalMKbzh3VldMTTl1Y0owOTdRbWRMMFp3bnhaUVhlREtTeFdNNGVKV2xOM3VseTIyTlVHVm5YR24wc01Xb3owbXNuSApSV05qeEx6aGt1RkNCbkFTMi9rR3VsM00vZTBxTnJXRTZxa3FWZXdsZWZMbUZBSDJXdWFsNlhNZngvMzduWC90CmVmZDlQTStlVFFKZlFmZVJyaFRJUG03Z1VtczU1UnphZU1JMFFKVVhVb0ZLZGRMazJQUU9KRzV2YnZkZm9SeGcKeWxPSThCbGM2SWM1MDNDSEVKanZWa0hERzZ5VDdkais2Y3dqeHNkTDBkMlgvaThVS0cyWG5HWUlNaklQejV6ego2QnlTaVZoZkZQcHBYQXBUdUFpVFZ6SnowUnN5Y0ROdURFdTVlK2xXUU8zekY2Z3dUVG43NHZqNzJrbnNUWkhsCnA2dUJjZ0IxbDRBSFNZazMyaUI5Nzl5bUNNeldOWnRzZkkrUlExVUFaeHY0NlJLTEJlTzBnaXZxYU1JWlRMQVMKdkN4NTVHZ2M5ak1qQW0rVi9hSEtuSEF0ZE1YT3A3a0ZrN0hmV1EzOERyeEhHclZwYlprU3o1WEdKTEc4MHhBUgpWY3NGcXg3RlBNN0JUTUJxOUhjV0JUclhrbW0ycXNlYzJiZ1VPYisybitWejdiT09JVkZVNGNRSFVUQ00rdCtGCkZaVk5mUHlGaFo1bjJ4MTNwcVFQdGNaVThSMitiOW83ZlhUcXUvOGtsVnRzK1hqSEdUWTdITjdsVDVZbTJYM0UKVmZKLzJ1TUJrSDlPSmVXempPaDJNQmhLN3pScEtaanBQUXJCNkRJZ09KYWlXakdiZkZEcXU0YkE5TGVEalBUeQpBU3V6VnBFRFFNWmhVR3VoWXVlaVdqMm1oK2VRNUN2RVMzVEswdmJQeXhSQk9OM2l3Y00vMUViamNkUUpDd2duCnQySk9ySTcvaFlUeXN5U3VQZjdGUDFkb2pWRy96S2xOaU8xQVdCWE5hTlVzY2JYUlBrMjZjMjhlSTBHcXQrYkkKc2VWRnE2eWFwaHhLbWJISnd6MFl2SktCY2s2TzN0NmhzWlpNZVJTSkZCdkVjdDJoUllJVStxTzlHRllKcXFTdgpKOXBoL2MyT1pacFFJelI4M1doMjY5dTRlS1NIZ2ZXNGJwcDROa0FmYXVHV21YRkZSMVRzYS9jK1JlVEpJY2dCCkJwL2FGNnVCR1VkL1JTNXBNSFJtYmd0ZVZBTFRiSlcwUUpqQ2RXc2xMa2IvNG42dEVhbG84WEdzZ1kvUEhnZFgKMFJFRnc0K0VYNGVTejdqMUZSK2ZDaVU2d3lDcGJaUDI4TEEwaUE2WmxvQU5TVTdmbVJDL0QvbEo2aVQ1T2s2ZgpqVHhRc2U5eEFGMTl0aEJ4ZDBTUFNMR3ZqYTBqSE1KWkt2eG9ybGVkdzk4a1h0UEFiRE54ejlnakVlNGZ6TExlCnVzUkYzRVNwUGh6enMzbjVZTXgzZnJzS2o3VzdQOXcyWHlkTW93N1E4NWtXWldmVmNVYVNCbUE9Ci0tLS0tRU5EIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCiRETlM6LCBJUCBBZGRyZXNzOjE5Mi4xNjguMTAwLjEsIFVSSTo=", - "UserData": "AgAAAAAAAAAAAAAAAAAAAAEAAAAFAGFkbWludAAAAAEAAAEBAAEFAAAnEAAAACAu47mAJL9PzsbeDyG4BxzAHl8CDuFwLkopOyeWTpd0LQAAAEDgMlcY11zfEsA1nQ6p21Jr0YIWv7iDKb0vlFHQWq1UjzIYiiL3gmEFF+V3iYM88J1BpKLIR+7ISPzBVzz9mr9/AA==", + "PKIData": "AQAAAAAAAAAAAAAAAAAAAAEBAb4gAAACAAAAAAAAAAAAAwAAAN8ALS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUhvd0ZBWUhLb1pJemowQ0FRWUpLeVFEQXdJSUFRRUxBMklBQkdsYW1ZSHdRUzRpMjVlU2kvdHE1TzlIb0k1YgpQb1l3MFJiaDF2bTlVRFY4SHVVVXkrSiswM1c4RklrVmhhRG1IbjVMQ0Q5T01TZEs4VTNMQUp6OE1PcDhZMTAwCk91T3lCYnhmam5zT1RpNVZlMERCUlp6MWM1b0xsRjJkK3hhR2NBPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCnsBAQACAAAnEAAgnsAjq+7cjygk1ZekVCSLVoaYwBE5U7VZFHO517+umjEBACAADAAQV3bwXMC9ZEyD26iryWx8WwEAAAE2JMWPXDSGhOCk43nnssaQua6rtSc1GAyveGPNqx1x6RkGSJBTDkSBa9yVkHesruve/lR6hGgnfN+fpT39LIPg7eiKZeZv+GsTwuRNSK/YZOSRigZhc1Ngtp8EcI6j/sjWzPmnErcmnypwXuZIAR4p4iERHkRtUdgGPeGNs19DxBKQcD2BTrcOE596UuiLGjDu7qvnv6I1SAnm7bqu4EQ1ABk7JglqbU7YjLE1Y/b47ebihx+J89thRQh5xInQUJuxd8kcB+pjVEsvgC99J4GaLqLpXJPlBwVnrVPRpNFkkfmAgVSxmZEt+I87aGnHTQg5hclrXdnpipuGS6lOOnq9aMF9OXf1QfplxrSGRaubcJEBSTrBbPOzEFWeO9X0fz1c/gBfGs8897uT8g4UzNPfMdlll/AdQQIACWxvY2FsaG9zdAEAAAAAAAAAAQAAAAIAAAAAAAAAFADfZfmK8KmL0EF8M7zqbCWJPcaRM7QGAQAGsC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQ0KTUlJRXN6Q0NBNXVnQXdJQkFnSVVDUGdQeTR6RFdUb2VBZVl1T0tWRzYrMUdtaTh3RFFZSktvWklodmNOQVFFTEJRQXdlekVMTUFrRw0KQTFVRUJoTUNXRmd4RWpBUUJnTlZCQWdNQ1ZOMFlYUmxUbUZ0WlRFUk1BOEdBMVVFQnd3SVEybDBlVTVoYldVeEZEQVNCZ05WQkFvTQ0KQzBOdmJYQmhibmxPWVcxbE1Sc3dHUVlEVlFRTERCSkRiMjF3WVc1NVUyVmpkR2x2Yms1aGJXVXhFakFRQmdOVkJBTU1DV3h2WTJGcw0KYUc5emREQWVGdzB5TmpBMU16QXhPVEF6TXpSYUZ3MHpOakExTWpjeE9UQXpNelJhTUhzeEN6QUpCZ05WQkFZVEFsaFlNUkl3RUFZRA0KVlFRSURBbFRkR0YwWlU1aGJXVXhFVEFQQmdOVkJBY01DRU5wZEhsT1lXMWxNUlF3RWdZRFZRUUtEQXREYjIxd1lXNTVUbUZ0WlRFYg0KTUJrR0ExVUVDd3dTUTI5dGNHRnVlVk5sWTNScGIyNU9ZVzFsTVJJd0VBWURWUVFEREFsc2IyTmhiR2h2YzNRd2dnRWlNQTBHQ1NxRw0KU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRG9IY2tOZzhtcUszcXdsei9velhUS2RUTkVvbC93YkMxTVJLNFc5N1Y4a1JENQ0KeW04S1ljOUs0dHBTQXh6ck1QcUkvaG9SMXJpTjhmRlhvNU1HeHNVeDdpRTZrRXNjZ1RhL1p4NWZDMWhzRFEzL3NMVnhtblJLb29SSA0KdktWbVZtS1g1UDE4NVBLTUUyaUtyWDNNY0xxUkJ1WS9EbE5weVgxQmlndFFISFZhaFJSSlhDWElZYjlOZ0ZSVnlOTzNnL29qTFdabg0KWVcyWGhGMS9xaGNzOFRZS0MzS2dXNlRyc2FLcS9YZ3dtRDRBN3JtMksvNllUYXdLTy9tVEd2YjJGcCtWcUVhY0UzZ3YxMFdGaTVycQ0KalNkcXBOZWFySUJtSVZCalRwdjVKVEs3cHlzTzRDNmJTTlFHb1lnWmZDblJJSEpmK2cxVUlZYkZ6R29iZ0JHMVpXS3pBZ01CQUFHag0KZ2dFdE1JSUJLVEFKQmdOVkhSTUVBakFBTUE0R0ExVWREd0VCL3dRRUF3SUM5REFkQmdOVkhTVUVGakFVQmdnckJnRUZCUWNEQVFZSQ0KS3dZQkJRVUhBd0l3RXdZRFZSMFJCQXd3Q29JQWh3VEFxR1FCaGdBd0hRWURWUjBPQkJZRUZBOTh4WGFXVEJNUDloL0FmZWxwT2R2Kw0KVXZYME1JRzRCZ05WSFNNRWdiQXdnYTJBRkE5OHhYYVdUQk1QOWgvQWZlbHBPZHYrVXZYMG9YK2tmVEI3TVFzd0NRWURWUVFHRXdKWQ0KV0RFU01CQUdBMVVFQ0F3SlUzUmhkR1ZPWVcxbE1SRXdEd1lEVlFRSERBaERhWFI1VG1GdFpURVVNQklHQTFVRUNnd0xRMjl0Y0dGdQ0KZVU1aGJXVXhHekFaQmdOVkJBc01Fa052YlhCaGJubFRaV04wYVc5dVRtRnRaVEVTTUJBR0ExVUVBd3dKYkc5allXeG9iM04wZ2hRSQ0KK0EvTGpNTlpPaDRCNWk0NHBVYnI3VWFhTHpBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQUZGd05BWGd2OGt1OW1jZmFOcE1ydWRPdg0KTHpleHB0QnpzUUlvQUd5K09raEQzbTVOekpvc2tUWFIyb3FUL1NJeGp1OGVyczg4NjNKQ2lRMVdwSnZnMUZoVVNXaUI4dDQvbFAxbQ0KcGdaOVBoQVJHY0c5czlNSUVqaUNjbWxiOFpoMDNHNGVZN09ydmNZZ1ZPWjR5MGx2dThrNzFyN1pzVTB2eXBzaHd5NEg3MisyQ1A1dg0KcVZlUEdiQ3pVcEowT2JYVHd0bXpxMHVIVE9GVFl1N3A0bnhMVzhPcklaaDBGMFk1R0hTcEJ3VDVFb3BVY3BmbnhadnFuS0l1em5WWg0KOVlEdzNxMUt6VFh6UTdtL3c3QnlWMkk4MldReGVCSmhZTXBmOEFsYXZ3UHFMdFNyQWI5VHl5UkJCSWNYaGFNa1N1RTM4SHVpYjR0VQ0KSGZkQWxMZzVOWHlGdEE9PQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0KFwgBAACzAQACAHwwejAUBgcqhkjOPQIBBgkrJAMDAggBAQsDYgAEesms4Jr2FTqrIcOap2DlOx8AHNBX/zq4jx+k9SCw95moNI7LUxmf89RsDA3EOh3dRuUftkZZ2E1WsLqh0TLflgDK3x66P9OoakIplJ5saZLamHsrp1J7RsBz/vc4FHOrGc8R/d8aP8yqCh2C5jJP4QAgORTMYx+3he7iDh5VU3YKo7e6HyFD9FkD06n4vFjRCaEHXi0tLS0tQkVHSU4gRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0KTUlJRk5UQmZCZ2txaGtpRzl3MEJCUTB3VWpBeEJna3Foa2lHOXcwQkJRd3dKQVFRYkNCbUZTZGx3dHFCZ2RpUQpyL2Jub3dJQ0NBQXdEQVlJS29aSWh2Y05BZ2tGQURBZEJnbGdoa2dCWlFNRUFTb0VFSFRvMTNrcHRuVGFQZFVkCitSakR4N0VFZ2dUUURyT1pvM0VadGY2RVd3YnQwY1JSSk8yWHdPUUd4c2xvd1JXZ3dIc1c4bU9xL2ZhNnV4dGsKbmw2T1p6a2VGU1ltTTlSWFdoME5Fd3c4ZE01RkhiZG1qU0pkREVKOFdnWFVnZDJJMjJHWDFROFVqNFlHZ0FqWgpHM2VUTlVadmFjRDNUamZwVHEySE9SS0g4MjJkV3R3dkJlRDQ3WHpzOXVUN21sQzVSQ0lzQ0MzUW03V25PRmNNCm1YMjBMRWFPSk9tVFRDYmtlcFBWQVhXV2hGWlRnK1BxSElmOHNOczBjOE5YcEg2b2NGaTE3elY1dDR6Z2dNL0cKRkgyb2lJaFEyMkxxMjhLa2I5YTlBaThhMWZvd3pFUFhKTGhoQUtMV25zUVJ1bTcveHhlZDJzSmF1dDJpbUpyNApXMHBZeW5zOW85Q3hjaXlySEd5MWs4dVNxTFh3ajc3ZE5ISVNZcVdvRUFKMkJXMkF5c0tlMXlTdTdNZ3dxd0ZICnM0OGV5ZGNnTFJmY003TGo1a1ZkSUZhUDlHM054RW1DeWZLaEJ6WGhUS2tKQk1leXArZXRJTkE3SDVJNnRiem8KTWdSbWRVdUFJZ3l3VitIamFMbWp3dmFyL3gzYk52YkhRRjNBQjRCbHZmQVpaYjNmbitRSWVmdk9WekJKeUhTMgpVMjN4eGh3Q0RnY2xmU0VGemxFNGVWL1ZSMndrcDRBWWVUN0QzOVhFeUV3SFVRREExV3FJSVI1UG1QblBlVU9tCm41VCtEL24xQmVrTE9WVFpnb0NhSExuZ21mcmNHUnBydFd6bGRqc1V4ZWpCZ1dBNTNIQjZnMUlEWVdKY1UwaEwKTzBTNlBuTGVZYjZ3K1ovcUI1Z0RpZG55TFRTVDVUbzVGcGRtblFUOGNuSFA2bHhNRW10TzFsY0dzVjRoWlRWdwpDNTVuQytzeHZwaXJVY2EvTHFzdzBUSFhjSTJmd2RVN1lQamlKcTV2WTlDRlFUKzAybVVBMlJJZjIvcmh1VnBrCnFCZlpPYVZTa0FiNTBvRnU5blFtRkY3UC80Z2RudWxFT0xldm43NmJyaVE2R1BhK0J0S2tINDlRcnZFNWZnY0oKUlRVZ0FoZ0tqSjJ1NFo1MWw0blVxWHR0VVQxa2J6OCtSVXRLSGFsc1lscERubTVpc2pBWS91ZVFwcGhlKzR5RAp0ejNJNkRJaTd2dTRjTktLTmlUeC9hbytkWTRTbGt3cjVQNkRiNzNHRk5EczhSNHliaDJYYlBJZCtRckRWR0FnCnkwSDVpRVZEUGhZcERCS05GWGdUdTNOVS9FOS8zS3lQZkcvM3pTZ3dJNlZmV2dCd1Era2JvajVXU0FBcWVaSDMKOU5XZitPcVgwb1FFVWhENExONWxiYmhtVEJqcjlrL0dMUmtKdm9kZ0RiV0pqUFE3VUhSQUhjOVI5MjhBZHJndgpaT0g1WFl1Z2xYaktzeldyNUZQc2szVjF2QXBBOTh5K3ptVm5kU0MrZmZMdmoySXphY0pXSkRsQVByZHhzZUcyClJqYlJUaG5BT05zc2JYSnoyTVFxRkpYcXFPQzQ3c3BCemlHM0Y2T3dHQW9Qc3VaUXQ4OGl3YlR1SWpLSE9jYXkKUGlQaG96cm9VY0JsTHZhVllhdU5xNFdkNDFnRUROWnVtSkptVXR4NkExQVFQSk1kTWluN2NDSDhWYVZBMUNYSgo3SU05S09zYXRqYUdJNkFUUWpEb0UyaHNHL0YvZjBNNnU2dTZkY1JQYWFxKzRpQWlDV24yNTljMGVEQzdRdWlhCjVQZjRiZ2Y0T2UrTDBQOGNiWmxXR0JpYjZLSVFoKzJteXRKYnZ1ZkpGWTE3OWh3Ukx2bEJMUzRRSHoxNmJoNVYKY09MOW5WTmpVWjQ1bG9WMjdtdGVNdmZITkVadGYvQzdSUFViaG5wcGJlVHgrVHNsOUdoTWs5bm0rd2hvOTVrRQpEVENiamFvdlgycmtQSEMyRTA4UGZPanc2NHBBRXk1U0JQNXdtOGR0UTJYeFdKN2llOHFUR1phTkZsbTVrRlNQCkIzTVFPZzZEaFlqaWw0ZW5DL2R3eVJaK3lJQUxIVUVEdkE1LzM5MzRMc2hOejYzaVorTnhwd1FkSytEZjgxWlAKeGJwdWtINUVLdmdvYmt2R3pwLzk3TmhBNzA2SHhaVlQ5NWczTFlheU5XWkJuVm1XM0xMdWJOUT0KLS0tLS1FTkQgRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0KJEROUzosIElQIEFkZHJlc3M6MTkyLjE2OC4xMDAuMSwgVVJJOglsb2NhbGhvc3QCAAAAAAAAAAEAAAACAAAAAAAAABQA32X5ivCpi9BBfDO86mwliT3GkTO0BgEABrAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0NCk1JSUVzekNDQTV1Z0F3SUJBZ0lVQ1BnUHk0ekRXVG9lQWVZdU9LVkc2KzFHbWk4d0RRWUpLb1pJaHZjTkFRRUxCUUF3ZXpFTE1Ba0cNCkExVUVCaE1DV0ZneEVqQVFCZ05WQkFnTUNWTjBZWFJsVG1GdFpURVJNQThHQTFVRUJ3d0lRMmwwZVU1aGJXVXhGREFTQmdOVkJBb00NCkMwTnZiWEJoYm5sT1lXMWxNUnN3R1FZRFZRUUxEQkpEYjIxd1lXNTVVMlZqZEdsdmJrNWhiV1V4RWpBUUJnTlZCQU1NQ1d4dlkyRnMNCmFHOXpkREFlRncweU5qQTFNekF4T1RBek16UmFGdzB6TmpBMU1qY3hPVEF6TXpSYU1Ic3hDekFKQmdOVkJBWVRBbGhZTVJJd0VBWUQNClZRUUlEQWxUZEdGMFpVNWhiV1V4RVRBUEJnTlZCQWNNQ0VOcGRIbE9ZVzFsTVJRd0VnWURWUVFLREF0RGIyMXdZVzU1VG1GdFpURWINCk1Ca0dBMVVFQ3d3U1EyOXRjR0Z1ZVZObFkzUnBiMjVPWVcxbE1SSXdFQVlEVlFRRERBbHNiMk5oYkdodmMzUXdnZ0VpTUEwR0NTcUcNClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURvSGNrTmc4bXFLM3F3bHovb3pYVEtkVE5Fb2wvd2JDMU1SSzRXOTdWOGtSRDUNCnltOEtZYzlLNHRwU0F4enJNUHFJL2hvUjFyaU44ZkZYbzVNR3hzVXg3aUU2a0VzY2dUYS9aeDVmQzFoc0RRMy9zTFZ4bW5SS29vUkgNCnZLVm1WbUtYNVAxODVQS01FMmlLclgzTWNMcVJCdVkvRGxOcHlYMUJpZ3RRSEhWYWhSUkpYQ1hJWWI5TmdGUlZ5Tk8zZy9vakxXWm4NCllXMlhoRjEvcWhjczhUWUtDM0tnVzZUcnNhS3EvWGd3bUQ0QTdybTJLLzZZVGF3S08vbVRHdmIyRnArVnFFYWNFM2d2MTBXRmk1cnENCmpTZHFwTmVhcklCbUlWQmpUcHY1SlRLN3B5c080QzZiU05RR29ZZ1pmQ25SSUhKZitnMVVJWWJGekdvYmdCRzFaV0t6QWdNQkFBR2oNCmdnRXRNSUlCS1RBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lDOURBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUkNCkt3WUJCUVVIQXdJd0V3WURWUjBSQkF3d0NvSUFod1RBcUdRQmhnQXdIUVlEVlIwT0JCWUVGQTk4eFhhV1RCTVA5aC9BZmVscE9kdisNClV2WDBNSUc0QmdOVkhTTUVnYkF3Z2EyQUZBOTh4WGFXVEJNUDloL0FmZWxwT2R2K1V2WDBvWCtrZlRCN01Rc3dDUVlEVlFRR0V3SlkNCldERVNNQkFHQTFVRUNBd0pVM1JoZEdWT1lXMWxNUkV3RHdZRFZRUUhEQWhEYVhSNVRtRnRaVEVVTUJJR0ExVUVDZ3dMUTI5dGNHRnUNCmVVNWhiV1V4R3pBWkJnTlZCQXNNRWtOdmJYQmhibmxUWldOMGFXOXVUbUZ0WlRFU01CQUdBMVVFQXd3SmJHOWpZV3hvYjNOMGdoUUkNCitBL0xqTU5aT2g0QjVpNDRwVWJyN1VhYUx6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFGRndOQVhndjhrdTltY2ZhTnBNcnVkT3YNCkx6ZXhwdEJ6c1FJb0FHeStPa2hEM201TnpKb3NrVFhSMm9xVC9TSXhqdThlcnM4ODYzSkNpUTFXcEp2ZzFGaFVTV2lCOHQ0L2xQMW0NCnBnWjlQaEFSR2NHOXM5TUlFamlDY21sYjhaaDAzRzRlWTdPcnZjWWdWT1o0eTBsdnU4azcxcjdac1Uwdnlwc2h3eTRINzIrMkNQNXYNCnFWZVBHYkN6VXBKME9iWFR3dG16cTB1SFRPRlRZdTdwNG54TFc4T3JJWmgwRjBZNUdIU3BCd1Q1RW9wVWNwZm54WnZxbktJdXpuVloNCjlZRHczcTFLelRYelE3bS93N0J5VjJJODJXUXhlQkpoWU1wZjhBbGF2d1BxTHRTckFiOVR5eVJCQkljWGhhTWtTdUUzOEh1aWI0dFUNCkhmZEFsTGc1Tlh5RnRBPT0NCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0NChcIAQAAswEAAgB8MHowFAYHKoZIzj0CAQYJKyQDAwIIAQELA2IABFxKLs7Le4khH9IYkEsz9kcDDGDF8nvEVwgn+eeo0AtGH8Grj+KWKY5rS+d89rmWV2QhEsLD6MCq1wF3RvCRE3/4SboaY6x3+Sg5/roZ+lZ4KHRPJ1k59MfQ+CAUeYZIKIUNNk0LoDNbtoVKaMPRJ0QAINkAgqWvmvNdaLoz5A4cw06qvAvDvPM0TQcAvh10fZSQB14tLS0tLUJFR0lOIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCk1JSUZOVEJmQmdrcWhraUc5dzBCQlEwd1VqQXhCZ2txaGtpRzl3MEJCUXd3SkFRUXQvOHkzTXNtbUo1L25YdmYKbllUdWh3SUNDQUF3REFZSUtvWklodmNOQWdrRkFEQWRCZ2xnaGtnQlpRTUVBU29FRUh3ZVZURDBKUVltYmxmSQpITmNuRkgwRWdnVFFtRy9NanZabTFobUpHWHJZeUJxMU9iNjU5Mmx0V1pndlE5VmR0RzBYTHpocW1VS1JZbFVqCktXaTE3K1NiNi94VldHUHZMRU1HNDR6NVZPYWlXR1FSODA4cWY3cVZJY3NkNVdBNlk2UDJmR2JOeFBxYzNCWVUKK0xhUlJVdTRiSkZMSUNpM2htY1FFRFpzQkh4Ti8vekM4Wm9Balp2ajZoajlPcnoyMXhSK0hoTkRUUEtlelIzaApTTnExU2hSL0Yrb0haUXN2ZklwckxXQ1ppMEVEU0NRY2E0N2dVc1FHK2JyRkFlTHh1NlZ0azl0OGthVG5JTUk0CkJtc1gyRWYrem4yUmRmamhPVEh1SlZjcjRwRG1mQTVqT1E4NzNtOWdGM0NtWXlsTjgwK1huaFlGRm9WM3hqdjMKL0wxM01xU2x2Yldpa2VMUjhEcW1qZWhJaUlnVnZVazlzU3BEbnBQZjVVbEpwa1owb3VuMlVKTGpRN2ZVVDNWMApQZFBBN0o4TzVpdnJlMEFINkFKamVDdy9rZzlubWhmZDluYVZVazdOMjdPWHBOSEhLRDNjSFpkbDI3NDhPY2ZVCnl6K3hpRDlIY0FBdVRiSTNXdmQ2c0toVXhzbWZaQzk1dWhqWlNyR3FqTkdUVHRsbm1Od1JseURtc0tTZDZVaVkKclMxalR4ZFkzSWFwckxManRHZFlSeFFPaEcrcjZWakRoay9WT1V4UTQ1TklvWno0QXFNZm5XcnRYQktsd25nWgp6VS9BUEN5dGhEUWxGeHZOMGNadnM2bnp2RTVRMVpaN0wwaStEc01vbTR3MEZTWFhua1hWZzZzdVhNSmVBR0k4CmlwNC9NanFFM2x2Z2NqRE11cXRwZytzQzlyU0RqMzR5ZUtabTU1R0o3dmhSd2c0dlJ4TUVsbkNnR2ZjS2NQSFoKS0FqNlZaU3Y1cnE2Y0dEeEhYRTJYbTBEajlTd01qeFJqcnJ0QThEY3pycWdOZ2pYNzV4S3d3TkY5SlY5U3VEdAp2enlMYktZcFpHMUpIRThPT2ZWK3VVdFdCeXZwemJmLzEwWjZ2RHhTUkdJbFZmSzA4L0NJRmFzeEd3RG10VEsrCnJDNHdWS29aWk5vWWg4OHJiMGhVdGlCbnRDdlpqN1VoWFlHL1lOdjNmQ2NPSUVUb2hrcDIvQ3JFR1RJVjVTZFMKU1ltRDhVYlJJbzFNTExQSFhPMld4SC8rYU9aVmhubkY3N0tiTzlYdU1Md0VNVWh2V0FrM05jN1Vhdy9SQXY1UQo2b0NvOUMvZXdtTlFaU2NwRzN0eFN2QUh4bENKTG5tb05ISDNMOUtYZHhiNHU2MUpzN2JlamI4WnBVUFg0bGlRCnNwWGQ5ZXpWQ2Z4bkJ4LzNoSzJtNzZXMkRLNTVYOGQ5Vis0WjZzVFZwNnpWQ3g5cW9ldnNGTUk4Z0wrL1htVjkKUEtXeGREQW5SSXlXbGFkVm5BZ25ZUWJFSU1vZnF5Q0d3OE9Hb1BZeHl6eEtFeU1XM2RqaWlObG1HajllOFovVgp3NE5xNnV5SERyNlBER1ZNaFRWSXgrL3dieStCcDVhTXlpNEh2dElPcTQxZEhFVmhMb2pkMVRVRFFaYW51RFpJCmxia1ZtaFZBTjFld0trclY1QzNPTE9leUt0ZklITUliZVM1Q2lmRjJjRDh1YUIybG1jSDljMGtLblBQa2R3bTcKQUpSRktYZTV1REtTMG05WE9lMWVhdVVBMGpUWGxhRXV5dnZCNHh1Nmpzay96ZDM5V1lKQ0x3RUhXR2NuaWR1QgppSGcwOFliVTNJT2l0NVM5K2FZaU8zLzJpTGw2VEFtZnFEVldmdlpKSDdsY2Y4aTczbHpNaVNLM0haam5EeXgyCm0yUkJ0NVVlREZmUGcxelFmdFpCQTJQOGt6UnZYQ1Z2ZlZ0b25JY0ppdEgxSlRTb2U3UHpRQnYxS3RNV0ZzblUKbnZBb3VEVkkrcHVGTE1DelN0QUZBVXk2SDFrWGZsL1FYUFRtWE9ZK0NJakN1ZDZUVElMdUNyVC8wQ1ZncmN5cgpMSVFRWkRqUTVDNUdkWWg0RHRha3MyOEU4emV5S0M1MmZEZ0RCRkowcE11UEdGanJlRTUvLzhQZGtWZDl0VkpiCkdEdVhuODlQTWdhV0czYUxtY1ZtM1F4UkZKcHJCcEF5T3VnZzFKbEtQcDRtY1lLM3J6SGdFWFU9Ci0tLS0tRU5EIEVOQ1JZUFRFRCBQUklWQVRFIEtFWS0tLS0tCiRETlM6LCBJUCBBZGRyZXNzOjE5Mi4xNjguMTAwLjEsIFVSSTo=", + "UserData": "AgAAAAAAAAAAAAAAAAAAAAEAAAAFAGFkbWludAAAAAEAAAEBAAEFAAAnEAAAACAKxabqh8/HqSSmiUjmPTle47rkpbXX5RLQz3bEWqEJBQAAAECno5ExkpVRGcsAI9wjyUgLDqKgCbjXLR+ywfJt1ccUqxR9KuiOEwmq+UTuKxk1pLjkm2qOmARp03EtMTgAE1tBAA==", "CertificateAssignments": { "TLS": 1, "WebServer": 2 }, - "AccessProtectionData": "AQAAAAAAAAAAAAAAAAAAAAABAABQAAAAAAEBAAAAAgDQBwAAIAAgHErm84EDDLgmxTxOIptkMMaHJMN92TptPOoLOKU7xSAABsJ4RI+V0LFtEww+4/qefufcKMpIYV9PBueoHLuQKs1QAAAAAAEBAAAAAgDQBwAAIAAgHErm84EDDLgmxTxOIptkMMaHJMN92TptPOoLOKU7xSAAxQUdljYvE8ydcEkY/NqMM0zsedV9a01PJRR415bQXvtQAAAAAAEBAAAAAgDQBwAAIAAgHErm84EDDLgmxTxOIptkMMaHJMN92TptPOoLOKU7xSAABsJ4RI+V0LFtEww+4/qefufcKMpIYV9PBueoHLuQKs1QAAAAAAEBAAAAAgDQBwAAIAAgHErm84EDDLgmxTxOIptkMMaHJMN92TptPOoLOKU7xSAABsJ4RI+V0LFtEww+4/qefufcKMpIYV9PBueoHLuQKs0gAAAAIBxK5vOBAwy4JsU8TiKbZDDGhyTDfdk6bTzqCzilO8U=" + "AccessProtectionData": "AQAAAAAAAAAAAAAAAAAAAAABAABQAAAAAAEBAAAAAgDQBwAAIAANpF3looMeZYCQW82Ez/m7ygNZ4GOQh/SOZVs+tSWiISAAaEJNxqgEdFxXPR2gDNGn9FjPUzRFBg6WoJ3btPYSa/dQAAAAAAEBAAAAAgDQBwAAIAANpF3looMeZYCQW82Ez/m7ygNZ4GOQh/SOZVs+tSWiISAAGkVXUjiOBK8kPCZRHAArnvGe9nDngtos9GOsXUHRt41QAAAAAAEBAAAAAgDQBwAAIAANpF3looMeZYCQW82Ez/m7ygNZ4GOQh/SOZVs+tSWiISAAaEJNxqgEdFxXPR2gDNGn9FjPUzRFBg6WoJ3btPYSa/dQAAAAAAEBAAAAAgDQBwAAIAANpF3looMeZYCQW82Ez/m7ygNZ4GOQh/SOZVs+tSWiISAAaEJNxqgEdFxXPR2gDNGn9FjPUzRFBg6WoJ3btPYSa/cgAAAADaRd5aKDHmWAkFvNhM/5u8oDWeBjkIf0jmVbPrUloiE=" } \ No newline at end of file diff --git a/src/styling/src/wwwroot/css/momentum.css b/src/styling/src/wwwroot/css/momentum.css index 4ac1215be..049a6f306 100644 --- a/src/styling/src/wwwroot/css/momentum.css +++ b/src/styling/src/wwwroot/css/momentum.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-orange-50:oklch(98% .016 73.684);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-lime-400:oklch(84.1% .238 128.85);--color-green-500:oklch(72.3% .219 149.579);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-900:oklch(39.8% .07 227.392);--color-sky-400:oklch(74.6% .16 232.661);--color-blue-500:oklch(62.3% .214 259.815);--color-purple-500:oklch(62.7% .265 303.9);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--container-xl:36rem;--container-3xl:48rem;--container-4xl:56rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base,components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.top-2{top:calc(var(--spacing) * 2)}.right-2{right:calc(var(--spacing) * 2)}.bottom-0{bottom:calc(var(--spacing) * 0)}.-z-1{z-index:calc(1 * -1)}.z-10{z-index:10}.z-\[600\]{z-index:600}.col-span-2{grid-column:span 2/span 2}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.\!m-0{margin:calc(var(--spacing) * 0)!important}.m-0{margin:calc(var(--spacing) * 0)}.m-0\!{margin:calc(var(--spacing) * 0)!important}.m-1{margin:calc(var(--spacing) * 1)}.m-2{margin:calc(var(--spacing) * 2)}.m-4{margin:calc(var(--spacing) * 4)}.-mx-2{margin-inline:calc(var(--spacing) * -2)}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing) * 1)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.my-auto{margin-block:auto}.ms-1{margin-inline-start:calc(var(--spacing) * 1)}.ms-2{margin-inline-start:calc(var(--spacing) * 2)}.ms-4{margin-inline-start:calc(var(--spacing) * 4)}.ms-auto{margin-inline-start:auto}.me-1{margin-inline-end:calc(var(--spacing) * 1)}.me-2{margin-inline-end:calc(var(--spacing) * 2)}.me-4{margin-inline-end:calc(var(--spacing) * 4)}.me-6{margin-inline-end:calc(var(--spacing) * 6)}.me-auto{margin-inline-end:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-\[15vh\]{margin-top:15vh}.mt-auto{margin-top:auto}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-5{margin-left:calc(var(--spacing) * 5)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.flex\!{display:flex!important}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-15{height:calc(var(--spacing) * 15)}.h-auto{height:auto}.h-full{height:100%}.max-h-\[50vh\]{max-height:50vh}.max-h-\[70vh\]{max-height:70vh}.min-h-40{min-height:calc(var(--spacing) * 40)}.w-1\/3{width:33.3333%}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-15{width:calc(var(--spacing) * 15)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-50{width:calc(var(--spacing) * 50)}.w-75{width:calc(var(--spacing) * 75)}.w-100{width:calc(var(--spacing) * 100)}.w-125{width:calc(var(--spacing) * 125)}.w-\[1px\]{width:1px}.w-auto{width:auto}.w-full{width:100%}.w-md{width:var(--container-md)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-200{max-width:calc(var(--spacing) * 200)}.max-w-none{max-width:none}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-20{min-width:calc(var(--spacing) * 20)}.min-w-32{min-width:calc(var(--spacing) * 32)}.min-w-\[10rem\]{min-width:10rem}.min-w-\[12rem\]{min-width:12rem}.flex-1{flex:1}.flex-\[2\]{flex:2}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow-1,.grow,.grow-1{flex-grow:1}.basis-1\/3{flex-basis:33.3333%}.basis-2\/3{flex-basis:66.6667%}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-move{cursor:move}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-row\!{flex-direction:row!important}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-center\!{align-items:center!important}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0{gap:calc(var(--spacing) * 0)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-1{row-gap:calc(var(--spacing) * 1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}.self-center{align-self:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-visible{overflow-y:visible}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-b,.border-b-1{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-solid{--tw-border-style:solid;border-style:solid}.border-blue-500\/50{border-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.border-blue-500\/50{border-color:color-mix(in oklab, var(--color-blue-500) 50%, transparent)}}.border-current{border-color:currentColor}.border-cyan-200{border-color:var(--color-cyan-200)}.border-cyan-400{border-color:var(--color-cyan-400)}.border-green-500\/50{border-color:#00c75880}@supports (color:color-mix(in lab, red, red)){.border-green-500\/50{border-color:color-mix(in oklab, var(--color-green-500) 50%, transparent)}}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-400\/50{border-color:#ff8b1a80}@supports (color:color-mix(in lab, red, red)){.border-orange-400\/50{border-color:color-mix(in oklab, var(--color-orange-400) 50%, transparent)}}.border-red-500\/50{border-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.border-red-500\/50{border-color:color-mix(in oklab, var(--color-red-500) 50%, transparent)}}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-400\/35{border-color:#90a1b959}@supports (color:color-mix(in lab, red, red)){.border-slate-400\/35{border-color:color-mix(in oklab, var(--color-slate-400) 35%, transparent)}}.border-slate-500\/40{border-color:#62748e66}@supports (color:color-mix(in lab, red, red)){.border-slate-500\/40{border-color:color-mix(in oklab, var(--color-slate-500) 40%, transparent)}}.border-yellow-500\/50{border-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/50{border-color:color-mix(in oklab, var(--color-yellow-500) 50%, transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-current{background-color:currentColor}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-cyan-100\/40{background-color:#cefafe66}@supports (color:color-mix(in lab, red, red)){.bg-cyan-100\/40{background-color:color-mix(in oklab, var(--color-cyan-100) 40%, transparent)}}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-green-500{background-color:var(--color-green-500)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-red-500{background-color:var(--color-red-500)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-400\/20{background-color:#90a1b933}@supports (color:color-mix(in lab, red, red)){.bg-slate-400\/20{background-color:color-mix(in oklab, var(--color-slate-400) 20%, transparent)}}.bg-slate-500{background-color:var(--color-slate-500)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-slate-700\/40{background-color:#31415866}@supports (color:color-mix(in lab, red, red)){.bg-slate-700\/40{background-color:color-mix(in oklab, var(--color-slate-700) 40%, transparent)}}.bg-slate-800\/60{background-color:#1d293d99}@supports (color:color-mix(in lab, red, red)){.bg-slate-800\/60{background-color:color-mix(in oklab, var(--color-slate-800) 60%, transparent)}}.bg-slate-900\/35{background-color:#0f172b59}@supports (color:color-mix(in lab, red, red)){.bg-slate-900\/35{background-color:color-mix(in oklab, var(--color-slate-900) 35%, transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab, var(--color-white) 60%, transparent)}}.bg-white\/80{background-color:#fffc}@supports (color:color-mix(in lab, red, red)){.bg-white\/80{background-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-linear-to-br{--tw-gradient-position:to bottom right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-linear-to-br{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-cyan-50{--tw-gradient-from:var(--color-cyan-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-emerald-50{--tw-gradient-from:var(--color-emerald-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-500{--tw-gradient-from:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-500{--tw-gradient-from:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-400{--tw-gradient-from:var(--color-yellow-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-0\%{--tw-gradient-from-position:0%}.via-amber-400{--tw-gradient-via:var(--color-amber-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-cyan-500{--tw-gradient-via:var(--color-cyan-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-lime-400{--tw-gradient-via:var(--color-lime-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-purple-500{--tw-gradient-via:var(--color-purple-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-sky-400{--tw-gradient-via:var(--color-sky-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-yellow-400{--tw-gradient-via:var(--color-yellow-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-blue-500{--tw-gradient-to:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-green-500{--tw-gradient-to:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-50\%{--tw-gradient-to-position:50%}.\!p-0{padding:calc(var(--spacing) * 0)!important}.p-0{padding:calc(var(--spacing) * 0)}.p-0\!{padding:calc(var(--spacing) * 0)!important}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\!{padding-inline:calc(var(--spacing) * 1)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\!{padding-inline:calc(var(--spacing) * 2)!important}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\!{padding-block:calc(var(--spacing) * 1)!important}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\!{padding-block:calc(var(--spacing) * 2)!important}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.ps-3{padding-inline-start:calc(var(--spacing) * 3)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.85rem\]{font-size:.85rem}.text-\[0\.95rem\]{font-size:.95rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-balance{text-wrap:balance}.text-nowrap{text-wrap:nowrap}.wrap-anywhere{overflow-wrap:anywhere}.whitespace-nowrap{white-space:nowrap}.text-amber-400{color:var(--color-amber-400)}.text-amber-600\/80{color:#dd7400cc}@supports (color:color-mix(in lab, red, red)){.text-amber-600\/80{color:color-mix(in oklab, var(--color-amber-600) 80%, transparent)}}.text-amber-700{color:var(--color-amber-700)}.text-blue-500{color:var(--color-blue-500)}.text-cyan-700{color:var(--color-cyan-700)}.text-cyan-900{color:var(--color-cyan-900)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-700{color:var(--color-emerald-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-inherit{color:inherit}.text-inherit\!{color:inherit!important}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-slate-100\/95{color:#f1f5f9f2}@supports (color:color-mix(in lab, red, red)){.text-slate-100\/95{color:color-mix(in oklab, var(--color-slate-100) 95%, transparent)}}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-400\/90{color:#90a1b9e6}@supports (color:color-mix(in lab, red, red)){.text-slate-400\/90{color:color-mix(in oklab, var(--color-slate-400) 90%, transparent)}}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-900\/70{color:#0f172bb3}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/70{color:color-mix(in oklab, var(--color-slate-900) 70%, transparent)}}.text-slate-900\/85{color:#0f172bd9}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/85{color:color-mix(in oklab, var(--color-slate-900) 85%, transparent)}}.text-slate-900\/90{color:#0f172be6}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/90{color:color-mix(in oklab, var(--color-slate-900) 90%, transparent)}}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.uppercase{text-transform:uppercase}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.placeholder-slate-400::placeholder{color:var(--color-slate-400)}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_8px_rgba\(34\,197\,94\,0\.6\)\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#22c55e99);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-blue-500\/50{--tw-shadow-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.shadow-blue-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-blue-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-green-500\/50{--tw-shadow-color:#00c75880}@supports (color:color-mix(in lab, red, red)){.shadow-green-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-green-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-orange-400\/40{--tw-shadow-color:#ff8b1a66}@supports (color:color-mix(in lab, red, red)){.shadow-orange-400\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-orange-400) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-red-500\/50{--tw-shadow-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.shadow-red-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-red-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-yellow-500\/50{--tw-shadow-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.shadow-yellow-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-yellow-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-400{--tw-duration:.4s;transition-duration:.4s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[assembly\:InternalsVisibleTo\(\"axopen\.inspectors_tests\"\)\]{assembly:InternalsVisibleTo("axopen.inspectors tests")}.\[assembly\:InternalsVisibleTo\(\"axopen_core_tests\"\)\]{assembly:InternalsVisibleTo("axopen core tests")}.\[assembly\:InternalsVisibleTo\(\"axopen_core_tests_L1\"\)\]{assembly:InternalsVisibleTo("axopen core tests L1")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsabbrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsabbrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsballuffidentification_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsballuffidentification tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentscognexvision_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentscognexvision tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsdesouttertightening_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsdesouttertightening tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsdrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsdrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsfestodrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsfestodrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentskeyencevision_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentskeyencevision tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentskukarobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentskukarobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsmitsubishirobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsmitsubishirobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrexrothdrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrexrothdrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrexrothpress_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrexrothpress tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentssiemidentification_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentssiemidentification tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsurrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsurrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopenio_tests\"\)\]{assembly:InternalsVisibleTo("axopenio tests")}.\[assembly\:InternalsVisibleTo\(\"components\.dukane\.welders_tests\"\)\]{assembly:InternalsVisibleTo("components.dukane.welders tests")}.\[assembly\:InternalsVisibleTo\(\"components\.rexroth\.tightening_tests\"\)\]{assembly:InternalsVisibleTo("components.rexroth.tightening tests")}.\[assembly\:InternalsVisibleTo\(\"components\.siem\.communication_tests\"\)\]{assembly:InternalsVisibleTo("components.siem.communication tests")}.\[assembly\:InternalsVisibleTo\(\"components\.zebra\.vision_tests\"\)\]{assembly:InternalsVisibleTo("components.zebra.vision tests")}.\[assembly\:InternalsVisibleTo\(\"elementscomponents_tests\"\)\]{assembly:InternalsVisibleTo("elementscomponents tests")}.\[assembly\:InternalsVisibleTo\(\"librarytemplate_tests\"\)\]{assembly:InternalsVisibleTo("librarytemplate tests")}.\[assembly\:InternalsVisibleTo\(\"pneumaticcomponents_tests\"\)\]{assembly:InternalsVisibleTo("pneumaticcomponents tests")}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-slate-300:hover{border-color:var(--color-slate-300)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.hover\:text-slate-700:hover{color:var(--color-slate-700)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-cyan-500:focus{border-color:var(--color-cyan-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-cyan-200:focus{--tw-ring-color:var(--color-cyan-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-between{justify-content:space-between}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}@media (min-width:80rem){.xl\:col-span-1{grid-column:span 1/span 1}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-orange-50:oklch(98% .016 73.684);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-lime-400:oklch(84.1% .238 128.85);--color-green-500:oklch(72.3% .219 149.579);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-900:oklch(39.8% .07 227.392);--color-sky-400:oklch(74.6% .16 232.661);--color-blue-500:oklch(62.3% .214 259.815);--color-purple-500:oklch(62.7% .265 303.9);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--container-xl:36rem;--container-3xl:48rem;--container-4xl:56rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base,components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.top-2{top:calc(var(--spacing) * 2)}.right-2{right:calc(var(--spacing) * 2)}.bottom-0{bottom:calc(var(--spacing) * 0)}.-z-1{z-index:calc(1 * -1)}.z-10{z-index:10}.z-\[600\]{z-index:600}.col-span-2{grid-column:span 2/span 2}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.\!m-0{margin:calc(var(--spacing) * 0)!important}.m-0{margin:calc(var(--spacing) * 0)}.m-0\!{margin:calc(var(--spacing) * 0)!important}.m-1{margin:calc(var(--spacing) * 1)}.m-2{margin:calc(var(--spacing) * 2)}.m-4{margin:calc(var(--spacing) * 4)}.-mx-2{margin-inline:calc(var(--spacing) * -2)}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing) * 1)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.my-auto{margin-block:auto}.ms-1{margin-inline-start:calc(var(--spacing) * 1)}.ms-2{margin-inline-start:calc(var(--spacing) * 2)}.ms-4{margin-inline-start:calc(var(--spacing) * 4)}.ms-auto{margin-inline-start:auto}.me-1{margin-inline-end:calc(var(--spacing) * 1)}.me-2{margin-inline-end:calc(var(--spacing) * 2)}.me-4{margin-inline-end:calc(var(--spacing) * 4)}.me-6{margin-inline-end:calc(var(--spacing) * 6)}.me-auto{margin-inline-end:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-\[15vh\]{margin-top:15vh}.mt-auto{margin-top:auto}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-5{margin-left:calc(var(--spacing) * 5)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.flex\!{display:flex!important}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-15{height:calc(var(--spacing) * 15)}.h-auto{height:auto}.h-full{height:100%}.max-h-\[50vh\]{max-height:50vh}.max-h-\[70vh\]{max-height:70vh}.min-h-40{min-height:calc(var(--spacing) * 40)}.w-1\/3{width:33.3333%}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-15{width:calc(var(--spacing) * 15)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-50{width:calc(var(--spacing) * 50)}.w-75{width:calc(var(--spacing) * 75)}.w-100{width:calc(var(--spacing) * 100)}.w-125{width:calc(var(--spacing) * 125)}.w-\[1px\]{width:1px}.w-auto{width:auto}.w-full{width:100%}.w-md{width:var(--container-md)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-200{max-width:calc(var(--spacing) * 200)}.max-w-none{max-width:none}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-6{min-width:calc(var(--spacing) * 6)}.min-w-20{min-width:calc(var(--spacing) * 20)}.min-w-32{min-width:calc(var(--spacing) * 32)}.min-w-\[10rem\]{min-width:10rem}.min-w-\[12rem\]{min-width:12rem}.flex-1{flex:1}.flex-\[2\]{flex:2}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow-1,.grow,.grow-1{flex-grow:1}.basis-1\/3{flex-basis:33.3333%}.basis-2\/3{flex-basis:66.6667%}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-move{cursor:move}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-row\!{flex-direction:row!important}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-center\!{align-items:center!important}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0{gap:calc(var(--spacing) * 0)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-1{row-gap:calc(var(--spacing) * 1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}.self-center{align-self:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-visible{overflow-y:visible}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-b,.border-b-1{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-solid{--tw-border-style:solid;border-style:solid}.border-blue-500\/50{border-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.border-blue-500\/50{border-color:color-mix(in oklab, var(--color-blue-500) 50%, transparent)}}.border-current{border-color:currentColor}.border-cyan-200{border-color:var(--color-cyan-200)}.border-cyan-400{border-color:var(--color-cyan-400)}.border-green-500\/50{border-color:#00c75880}@supports (color:color-mix(in lab, red, red)){.border-green-500\/50{border-color:color-mix(in oklab, var(--color-green-500) 50%, transparent)}}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-400\/50{border-color:#ff8b1a80}@supports (color:color-mix(in lab, red, red)){.border-orange-400\/50{border-color:color-mix(in oklab, var(--color-orange-400) 50%, transparent)}}.border-red-500\/50{border-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.border-red-500\/50{border-color:color-mix(in oklab, var(--color-red-500) 50%, transparent)}}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-400\/35{border-color:#90a1b959}@supports (color:color-mix(in lab, red, red)){.border-slate-400\/35{border-color:color-mix(in oklab, var(--color-slate-400) 35%, transparent)}}.border-slate-500\/40{border-color:#62748e66}@supports (color:color-mix(in lab, red, red)){.border-slate-500\/40{border-color:color-mix(in oklab, var(--color-slate-500) 40%, transparent)}}.border-yellow-500\/50{border-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/50{border-color:color-mix(in oklab, var(--color-yellow-500) 50%, transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-current{background-color:currentColor}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-cyan-100\/40{background-color:#cefafe66}@supports (color:color-mix(in lab, red, red)){.bg-cyan-100\/40{background-color:color-mix(in oklab, var(--color-cyan-100) 40%, transparent)}}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-green-500{background-color:var(--color-green-500)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-red-500{background-color:var(--color-red-500)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-400\/20{background-color:#90a1b933}@supports (color:color-mix(in lab, red, red)){.bg-slate-400\/20{background-color:color-mix(in oklab, var(--color-slate-400) 20%, transparent)}}.bg-slate-500{background-color:var(--color-slate-500)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-slate-700\/40{background-color:#31415866}@supports (color:color-mix(in lab, red, red)){.bg-slate-700\/40{background-color:color-mix(in oklab, var(--color-slate-700) 40%, transparent)}}.bg-slate-800\/60{background-color:#1d293d99}@supports (color:color-mix(in lab, red, red)){.bg-slate-800\/60{background-color:color-mix(in oklab, var(--color-slate-800) 60%, transparent)}}.bg-slate-900\/35{background-color:#0f172b59}@supports (color:color-mix(in lab, red, red)){.bg-slate-900\/35{background-color:color-mix(in oklab, var(--color-slate-900) 35%, transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab, red, red)){.bg-white\/60{background-color:color-mix(in oklab, var(--color-white) 60%, transparent)}}.bg-white\/80{background-color:#fffc}@supports (color:color-mix(in lab, red, red)){.bg-white\/80{background-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-linear-to-br{--tw-gradient-position:to bottom right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-linear-to-br{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-cyan-50{--tw-gradient-from:var(--color-cyan-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-emerald-50{--tw-gradient-from:var(--color-emerald-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-500{--tw-gradient-from:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-500{--tw-gradient-from:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-400{--tw-gradient-from:var(--color-yellow-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-0\%{--tw-gradient-from-position:0%}.via-amber-400{--tw-gradient-via:var(--color-amber-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-cyan-500{--tw-gradient-via:var(--color-cyan-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-lime-400{--tw-gradient-via:var(--color-lime-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-purple-500{--tw-gradient-via:var(--color-purple-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-sky-400{--tw-gradient-via:var(--color-sky-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-yellow-400{--tw-gradient-via:var(--color-yellow-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-blue-500{--tw-gradient-to:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-green-500{--tw-gradient-to:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-50\%{--tw-gradient-to-position:50%}.\!p-0{padding:calc(var(--spacing) * 0)!important}.p-0{padding:calc(var(--spacing) * 0)}.p-0\!{padding:calc(var(--spacing) * 0)!important}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\!{padding-inline:calc(var(--spacing) * 1)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\!{padding-inline:calc(var(--spacing) * 2)!important}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\!{padding-block:calc(var(--spacing) * 1)!important}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\!{padding-block:calc(var(--spacing) * 2)!important}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.ps-3{padding-inline-start:calc(var(--spacing) * 3)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.85rem\]{font-size:.85rem}.text-\[0\.95rem\]{font-size:.95rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-balance{text-wrap:balance}.text-nowrap{text-wrap:nowrap}.wrap-anywhere{overflow-wrap:anywhere}.whitespace-nowrap{white-space:nowrap}.text-amber-400{color:var(--color-amber-400)}.text-amber-600\/80{color:#dd7400cc}@supports (color:color-mix(in lab, red, red)){.text-amber-600\/80{color:color-mix(in oklab, var(--color-amber-600) 80%, transparent)}}.text-amber-700{color:var(--color-amber-700)}.text-blue-500{color:var(--color-blue-500)}.text-cyan-700{color:var(--color-cyan-700)}.text-cyan-900{color:var(--color-cyan-900)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-700{color:var(--color-emerald-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-inherit{color:inherit}.text-inherit\!{color:inherit!important}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-slate-100\/95{color:#f1f5f9f2}@supports (color:color-mix(in lab, red, red)){.text-slate-100\/95{color:color-mix(in oklab, var(--color-slate-100) 95%, transparent)}}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-400\/90{color:#90a1b9e6}@supports (color:color-mix(in lab, red, red)){.text-slate-400\/90{color:color-mix(in oklab, var(--color-slate-400) 90%, transparent)}}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-900\/70{color:#0f172bb3}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/70{color:color-mix(in oklab, var(--color-slate-900) 70%, transparent)}}.text-slate-900\/85{color:#0f172bd9}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/85{color:color-mix(in oklab, var(--color-slate-900) 85%, transparent)}}.text-slate-900\/90{color:#0f172be6}@supports (color:color-mix(in lab, red, red)){.text-slate-900\/90{color:color-mix(in oklab, var(--color-slate-900) 90%, transparent)}}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.placeholder-slate-400::placeholder{color:var(--color-slate-400)}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_8px_rgba\(34\,197\,94\,0\.6\)\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#22c55e99);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-blue-500\/50{--tw-shadow-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.shadow-blue-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-blue-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-green-500\/50{--tw-shadow-color:#00c75880}@supports (color:color-mix(in lab, red, red)){.shadow-green-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-green-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-orange-400\/40{--tw-shadow-color:#ff8b1a66}@supports (color:color-mix(in lab, red, red)){.shadow-orange-400\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-orange-400) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-red-500\/50{--tw-shadow-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.shadow-red-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-red-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-yellow-500\/50{--tw-shadow-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.shadow-yellow-500\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-yellow-500) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-400{--tw-duration:.4s;transition-duration:.4s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[assembly\:InternalsVisibleTo\(\"axopen\.inspectors_tests\"\)\]{assembly:InternalsVisibleTo("axopen.inspectors tests")}.\[assembly\:InternalsVisibleTo\(\"axopen_core_tests\"\)\]{assembly:InternalsVisibleTo("axopen core tests")}.\[assembly\:InternalsVisibleTo\(\"axopen_core_tests_L1\"\)\]{assembly:InternalsVisibleTo("axopen core tests L1")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsabbrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsabbrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsballuffidentification_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsballuffidentification tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentscognexvision_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentscognexvision tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsdesouttertightening_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsdesouttertightening tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsdrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsdrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsfestodrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsfestodrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentskeyencevision_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentskeyencevision tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentskukarobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentskukarobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsmitsubishirobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsmitsubishirobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrexrothdrives_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrexrothdrives tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrexrothpress_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrexrothpress tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentssiemidentification_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentssiemidentification tests")}.\[assembly\:InternalsVisibleTo\(\"axopencomponentsurrobotics_tests\"\)\]{assembly:InternalsVisibleTo("axopencomponentsurrobotics tests")}.\[assembly\:InternalsVisibleTo\(\"axopenio_tests\"\)\]{assembly:InternalsVisibleTo("axopenio tests")}.\[assembly\:InternalsVisibleTo\(\"components\.dukane\.welders_tests\"\)\]{assembly:InternalsVisibleTo("components.dukane.welders tests")}.\[assembly\:InternalsVisibleTo\(\"components\.rexroth\.tightening_tests\"\)\]{assembly:InternalsVisibleTo("components.rexroth.tightening tests")}.\[assembly\:InternalsVisibleTo\(\"components\.siem\.communication_tests\"\)\]{assembly:InternalsVisibleTo("components.siem.communication tests")}.\[assembly\:InternalsVisibleTo\(\"components\.zebra\.vision_tests\"\)\]{assembly:InternalsVisibleTo("components.zebra.vision tests")}.\[assembly\:InternalsVisibleTo\(\"elementscomponents_tests\"\)\]{assembly:InternalsVisibleTo("elementscomponents tests")}.\[assembly\:InternalsVisibleTo\(\"librarytemplate_tests\"\)\]{assembly:InternalsVisibleTo("librarytemplate tests")}.\[assembly\:InternalsVisibleTo\(\"pneumaticcomponents_tests\"\)\]{assembly:InternalsVisibleTo("pneumaticcomponents tests")}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-slate-300:hover{border-color:var(--color-slate-300)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.hover\:text-slate-700:hover{color:var(--color-slate-700)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-cyan-500:focus{border-color:var(--color-cyan-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-cyan-200:focus{--tw-ring-color:var(--color-cyan-200)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-between{justify-content:space-between}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}@media (min-width:80rem){.xl\:col-span-1{grid-column:span 1/span 1}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} \ No newline at end of file