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 : $"