fix: PXE boot debugging — bisect root cause, syslog logging, serial console #3

Merged
michal merged 31 commits from wip/ks-debugging into main 2026-03-29 00:50:05 +00:00
22 changed files with 1885 additions and 75 deletions
Showing only changes of commit ed1df8a77c - Show all commits

247
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,247 @@
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
GITEA_REGISTRY: 10.0.0.194:3012
GITEA_PUBLIC_URL: https://mysources.co.uk
GITEA_OWNER: michal
# ============================================================
# Required Gitea secrets:
# PACKAGES_TOKEN -- Gitea API token (packages + registry)
# ============================================================
jobs:
# -- CI checks (run in parallel on every push/PR) ----------
lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: bastion
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint || echo "::warning::Lint has errors -- not blocking CI yet"
typecheck:
runs-on: ubuntu-latest
defaults:
run:
working-directory: bastion
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm typecheck
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: bastion
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: pnpm install --frozen-lockfile
- name: Build (needed by completions check)
run: pnpm build
- name: Run tests
run: pnpm test:run
# -- Build & package ---------------------------------------
build:
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
defaults:
run:
working-directory: bastion
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build all packages
run: pnpm build
- name: Generate shell completions
run: pnpm completions:generate
- uses: oven-sh/setup-bun@v2
- name: Install nfpm
run: |
curl -sL -o /tmp/nfpm.tar.gz "https://github.com/goreleaser/nfpm/releases/download/v2.45.0/nfpm_2.45.0_Linux_x86_64.tar.gz"
tar xzf /tmp/nfpm.tar.gz -C /usr/local/bin nfpm
- name: Bundle standalone binary
run: |
mkdir -p dist
bun build src/cli/src/index.ts --compile --outfile dist/lab
- name: Package RPM
run: nfpm pkg --packager rpm --target dist/
- name: Package DEB
run: nfpm pkg --packager deb --target dist/
- name: Upload RPM artifact
uses: actions/upload-artifact@v3
with:
name: rpm-package
path: bastion/dist/lab-*.rpm
retention-days: 7
- name: Upload DEB artifact
uses: actions/upload-artifact@v3
with:
name: deb-package
path: bastion/dist/lab*.deb
retention-days: 7
# -- Release pipeline (main branch push only) --------------
publish-rpm:
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
defaults:
run:
working-directory: bastion
steps:
- uses: actions/checkout@v4
- name: Download RPM artifact
uses: actions/download-artifact@v3
with:
name: rpm-package
path: bastion/dist/
- name: Install rpm tools
run: sudo apt-get update && sudo apt-get install -y rpm
- name: Publish RPM to Gitea
env:
GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
GITEA_URL: http://${{ env.GITEA_REGISTRY }}
GITEA_OWNER: ${{ env.GITEA_OWNER }}
GITEA_REPO: lab
run: |
RPM_FILE=$(ls dist/lab-*.rpm | head -1)
RPM_VERSION=$(rpm -qp --queryformat '%{VERSION}-%{RELEASE}' "$RPM_FILE")
echo "Publishing $RPM_FILE (version $RPM_VERSION)..."
# Delete existing version if present
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/rpm/lab/${RPM_VERSION}")
if [ "$HTTP_CODE" = "200" ]; then
echo "Version exists, replacing..."
curl -s -o /dev/null -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/rpm/lab/${RPM_VERSION}"
fi
# Upload
curl --fail -X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
--upload-file "$RPM_FILE" \
"${GITEA_URL}/api/packages/${GITEA_OWNER}/rpm/upload"
echo "Published successfully!"
# Link package to repo
source scripts/link-package.sh
link_package "rpm" "lab"
publish-deb:
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
defaults:
run:
working-directory: bastion
steps:
- uses: actions/checkout@v4
- name: Download DEB artifact
uses: actions/download-artifact@v3
with:
name: deb-package
path: bastion/dist/
- name: Publish DEB to Gitea
env:
GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
GITEA_URL: http://${{ env.GITEA_REGISTRY }}
GITEA_OWNER: ${{ env.GITEA_OWNER }}
GITEA_REPO: lab
run: |
DEB_FILE=$(ls dist/lab*.deb | head -1)
DEB_VERSION=$(dpkg-deb --field "$DEB_FILE" Version)
echo "Publishing $DEB_FILE (version $DEB_VERSION)..."
# Publish to each supported distribution
DISTRIBUTIONS="trixie forky noble plucky"
for DIST in $DISTRIBUTIONS; do
echo " -> $DIST..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
--upload-file "$DEB_FILE" \
"${GITEA_URL}/api/packages/${GITEA_OWNER}/debian/pool/${DIST}/main/upload")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo " Published to $DIST"
elif [ "$HTTP_CODE" = "409" ]; then
echo " Already exists in $DIST (skipping)"
else
echo " WARNING: Upload to $DIST returned HTTP $HTTP_CODE"
fi
done
echo "Published successfully!"
# Link package to repo
source scripts/link-package.sh
link_package "debian" "lab"

View File

@@ -0,0 +1,67 @@
# lab bash completions -- auto-generated by scripts/generate-completions.ts
# DO NOT EDIT MANUALLY -- run: pnpm completions:generate
_lab() {
local cur prev words cword
_init_completion || return
local top_commands="init provision"
# Extract the subcommand chain (skip options and their values)
local -a subcmd_chain=()
local i skip_next=false
for ((i=1; i < cword; i++)); do
if $skip_next; then skip_next=false; continue; fi
case "${words[i]}" in
-*) ;; # skip options
*) subcmd_chain+=("${words[i]}") ;;
esac
done
local chain_len=${#subcmd_chain[@]}
local chain_str="${subcmd_chain[*]}"
case "$chain_str" in
"init bastion standalone start")
COMPREPLY=($(compgen -W "--port --dir --domain --dhcp-mode --fedora --arch --timezone --locale --skip-dnsmasq --skip-artifacts -h --help" -- "$cur"))
return ;;
"init bastion standalone stop")
COMPREPLY=($(compgen -W "--dir -h --help" -- "$cur"))
return ;;
"init bastion standalone status")
COMPREPLY=($(compgen -W "--dir --port -h --help" -- "$cur"))
return ;;
"init bastion standalone")
COMPREPLY=($(compgen -W "start stop status -h --help" -- "$cur"))
return ;;
"init bastion")
COMPREPLY=($(compgen -W "standalone -h --help" -- "$cur"))
return ;;
"provision list")
COMPREPLY=($(compgen -W "--port -h --help" -- "$cur"))
return ;;
"provision install")
COMPREPLY=($(compgen -W "--role --disk --port -h --help" -- "$cur"))
return ;;
"provision reprovision")
COMPREPLY=($(compgen -W "--role --disk --port -h --help" -- "$cur"))
return ;;
"provision forget")
COMPREPLY=($(compgen -W "--port -h --help" -- "$cur"))
return ;;
"init")
COMPREPLY=($(compgen -W "bastion -h --help" -- "$cur"))
return ;;
"provision")
COMPREPLY=($(compgen -W "list install reprovision forget -h --help" -- "$cur"))
return ;;
"")
COMPREPLY=($(compgen -W "$top_commands -h --help -v --version" -- "$cur"))
return ;;
*)
COMPREPLY=($(compgen -W "-h --help" -- "$cur"))
return ;;
esac
}
complete -F _lab lab

View File

@@ -0,0 +1,91 @@
# lab fish completions -- auto-generated by scripts/generate-completions.ts
# DO NOT EDIT MANUALLY -- run: pnpm completions:generate
complete -c lab -e
complete -c lab -f
# Global options
complete -c lab -s v -l version -d 'Show version'
complete -c lab -s h -l help -d 'Show help'
# Helper: test if a subcommand chain is active
function __lab_using_cmd
set -l tokens (commandline -opc)
set -l expected $argv
set -l depth (count $expected)
set -l found 0
set -l i 1
for tok in $tokens[2..]
if string match -q -- "-*" $tok
continue
end
set i (math $i + 1)
set -l idx (math $i - 1)
if test $idx -le $depth
if test "$tok" != "$expected[$idx]"
return 1
end
set found (math $found + 1)
else
return 1
end
end
test $found -eq $depth
end
# Top-level commands
complete -c lab -n "not __fish_seen_subcommand_from init provision" -a init -d 'Initialise infrastructure components'
complete -c lab -n "not __fish_seen_subcommand_from init provision" -a provision -d 'Machine provisioning operations'
# init subcommands
complete -c lab -n "__lab_using_cmd init" -a bastion -d 'Bastion PXE server management'
# init bastion subcommands
complete -c lab -n "__lab_using_cmd init bastion" -a standalone -d 'Standalone bastion server lifecycle'
# init bastion standalone subcommands
complete -c lab -n "__lab_using_cmd init bastion standalone" -a start -d 'Start the bastion server (HTTP + dnsmasq PXE)'
complete -c lab -n "__lab_using_cmd init bastion standalone" -a stop -d 'Stop a running bastion server'
complete -c lab -n "__lab_using_cmd init bastion standalone" -a status -d 'Show bastion server status'
# init bastion standalone start options
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l port -d 'HTTP port' -x
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l dir -d 'Bastion data directory' -x
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l domain -d 'Internal domain for hostnames' -x
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l dhcp-mode -d 'DHCP mode: proxy or full' -x
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l fedora -d 'Fedora version' -x
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l arch -d 'Architecture' -x
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l timezone -d 'Timezone' -x
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l locale -d 'Locale' -x
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l skip-dnsmasq -d 'Skip starting dnsmasq (for testing)'
complete -c lab -n "__lab_using_cmd init bastion standalone start" -l skip-artifacts -d 'Skip downloading boot artifacts (for testing)'
# init bastion standalone stop options
complete -c lab -n "__lab_using_cmd init bastion standalone stop" -l dir -d 'Bastion data directory' -x
# init bastion standalone status options
complete -c lab -n "__lab_using_cmd init bastion standalone status" -l dir -d 'Bastion data directory' -x
complete -c lab -n "__lab_using_cmd init bastion standalone status" -l port -d 'Bastion HTTP port' -x
# provision subcommands
complete -c lab -n "__lab_using_cmd provision" -a list -d 'List all known machines'
complete -c lab -n "__lab_using_cmd provision" -a install -d 'Queue a discovered machine for Fedora installation'
complete -c lab -n "__lab_using_cmd provision" -a reprovision -d 'Queue install + SSH reboot into PXE for reprovision'
complete -c lab -n "__lab_using_cmd provision" -a forget -d 'Remove a machine from bastion state'
# provision list options
complete -c lab -n "__lab_using_cmd provision list" -l port -d 'Bastion HTTP port' -x
# provision install options
complete -c lab -n "__lab_using_cmd provision install" -l role -d 'Machine role: worker or infra' -x
complete -c lab -n "__lab_using_cmd provision install" -l disk -d 'Target disk device (auto-detect if omitted)' -x
complete -c lab -n "__lab_using_cmd provision install" -l port -d 'Bastion HTTP port' -x
# provision reprovision options
complete -c lab -n "__lab_using_cmd provision reprovision" -l role -d 'Machine role: worker or infra' -x
complete -c lab -n "__lab_using_cmd provision reprovision" -l disk -d 'Target disk device (auto-detect if omitted)' -x
complete -c lab -n "__lab_using_cmd provision reprovision" -l port -d 'Bastion HTTP port' -x
# provision forget options
complete -c lab -n "__lab_using_cmd provision forget" -l port -d 'Bastion HTTP port' -x

26
bastion/eslint.config.js Normal file
View File

@@ -0,0 +1,26 @@
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
export default [
{
files: ['src/*/src/**/*.ts'],
languageOptions: {
parser: tsparser,
parserOptions: {
project: ['./src/*/tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
},
},
plugins: { '@typescript-eslint': tseslint },
rules: {
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
{
ignores: ['**/dist/**', '**/node_modules/**', '**/*.config.*'],
},
];

20
bastion/nfpm.yaml Normal file
View File

@@ -0,0 +1,20 @@
name: lab
arch: amd64
version: 0.1.0
release: "1"
maintainer: michal
description: Lab infrastructure CLI for bare-metal provisioning
license: MIT
contents:
- src: ./dist/lab
dst: /usr/bin/lab
file_info:
mode: 0755
- src: ./completions/lab.bash
dst: /usr/share/bash-completion/completions/lab
file_info:
mode: 0644
- src: ./completions/lab.fish
dst: /usr/share/fish/vendor_completions.d/lab.fish
file_info:
mode: 0644

View File

@@ -10,7 +10,10 @@
"test:run": "vitest run", "test:run": "vitest run",
"typecheck": "tsc --build", "typecheck": "tsc --build",
"clean": "pnpm -r run clean && rimraf node_modules", "clean": "pnpm -r run clean && rimraf node_modules",
"lint": "eslint 'src/*/src/**/*.ts'" "lint": "eslint 'src/*/src/**/*.ts'",
"lint:fix": "eslint 'src/*/src/**/*.ts' --fix",
"completions:generate": "tsx scripts/generate-completions.ts --write",
"completions:check": "tsx scripts/generate-completions.ts --check"
}, },
"engines": { "engines": {
"node": ">=20.0.0", "node": ">=20.0.0",
@@ -19,6 +22,10 @@
"packageManager": "pnpm@9.15.0", "packageManager": "pnpm@9.15.0",
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/parser": "^8.57.1",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"rimraf": "^6.0.0", "rimraf": "^6.0.0",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.7.0", "typescript": "^5.7.0",

621
bastion/pnpm-lock.yaml generated
View File

@@ -11,6 +11,18 @@ importers:
'@types/node': '@types/node':
specifier: ^22.10.0 specifier: ^22.10.0
version: 22.19.15 version: 22.19.15
'@typescript-eslint/eslint-plugin':
specifier: ^8.57.1
version: 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/parser':
specifier: ^8.57.1
version: 8.57.1(eslint@10.0.3)(typescript@5.9.3)
eslint:
specifier: ^10.0.3
version: 10.0.3
eslint-config-prettier:
specifier: ^10.1.8
version: 10.1.8(eslint@10.0.3)
rimraf: rimraf:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.1.3 version: 6.1.3
@@ -229,6 +241,36 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.23.3':
resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/config-helpers@0.5.3':
resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/core@1.1.1':
resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/object-schema@3.0.3':
resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/plugin-kit@0.6.1':
resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@fastify/accept-negotiator@2.0.1': '@fastify/accept-negotiator@2.0.1':
resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==}
@@ -256,6 +298,22 @@ packages:
'@fastify/static@8.3.0': '@fastify/static@8.3.0':
resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
'@humanfs/node@0.16.7':
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
engines: {node: '>=18.18.0'}
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
'@humanwhocodes/retry@0.4.3':
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@isaacs/cliui@9.0.0': '@isaacs/cliui@9.0.0':
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -411,15 +469,80 @@ packages:
'@types/deep-eql@4.0.2': '@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/esrecurse@4.3.1':
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@22.19.15': '@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
'@types/triple-beam@1.3.5': '@types/triple-beam@1.3.5':
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
'@typescript-eslint/eslint-plugin@8.57.1':
resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.57.1
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.57.1':
resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.57.1':
resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.57.1':
resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.57.1':
resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.57.1':
resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/types@8.57.1':
resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.57.1':
resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.57.1':
resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.57.1':
resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/expect@3.2.4': '@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
@@ -452,6 +575,16 @@ packages:
abstract-logging@2.0.1: abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
ajv-formats@3.0.1: ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies: peerDependencies:
@@ -460,6 +593,9 @@ packages:
ajv: ajv:
optional: true optional: true
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ajv@8.18.0: ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
@@ -542,6 +678,9 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
depd@2.0.0: depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -564,9 +703,61 @@ packages:
escape-html@1.0.3: escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
eslint-config-prettier@10.1.8:
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
eslint-scope@9.1.2:
resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@5.0.1:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.0.3:
resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
jiti: '*'
peerDependenciesMeta:
jiti:
optional: true
espree@11.2.0:
resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
esquery@1.7.0:
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
engines: {node: '>=0.10'}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
engines: {node: '>=4.0'}
estraverse@5.3.0:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
estree-walker@3.0.3: estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
execa@9.6.1: execa@9.6.1:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0} engines: {node: ^18.19.0 || >=20.5.0}
@@ -581,9 +772,15 @@ packages:
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-json-stringify@6.3.0: fast-json-stringify@6.3.0:
resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==}
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-querystring@1.1.2: fast-querystring@1.1.2:
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
@@ -615,10 +812,25 @@ packages:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'} engines: {node: '>=18'}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
find-my-way@9.5.0: find-my-way@9.5.0:
resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
fn.name@1.1.0: fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
@@ -638,6 +850,10 @@ packages:
get-tsconfig@4.13.6: get-tsconfig@4.13.6:
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
glob@11.1.0: glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -656,6 +872,18 @@ packages:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
inherits@2.0.4: inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -663,6 +891,14 @@ packages:
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-plain-obj@4.1.0: is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -689,18 +925,38 @@ packages:
js-tokens@9.0.1: js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
json-schema-ref-resolver@3.0.0: json-schema-ref-resolver@3.0.0:
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema-traverse@1.0.0: json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kuler@2.0.0: kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
light-my-request@6.6.0: light-my-request@6.6.0:
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
logform@2.7.0: logform@2.7.0:
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
@@ -736,6 +992,9 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
npm-run-path@6.0.0: npm-run-path@6.0.0:
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -747,6 +1006,18 @@ packages:
one-time@1.0.0: one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@@ -754,6 +1025,10 @@ packages:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'} engines: {node: '>=18'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
path-key@3.1.1: path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -794,6 +1069,10 @@ packages:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
pretty-ms@9.3.0: pretty-ms@9.3.0:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -804,6 +1083,10 @@ packages:
process-warning@5.0.0: process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
quick-format-unescaped@4.0.4: quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
@@ -958,11 +1241,21 @@ packages:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'} engines: {node: '>= 14.0.0'}
ts-api-utils@2.4.0:
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
tsx@4.21.0: tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
typescript@5.9.3: typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@@ -975,6 +1268,9 @@ packages:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'} engines: {node: '>=18'}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -1069,6 +1365,14 @@ packages:
resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
yoctocolors@2.1.2: yoctocolors@2.1.2:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1161,6 +1465,36 @@ snapshots:
'@esbuild/win32-x64@0.27.4': '@esbuild/win32-x64@0.27.4':
optional: true optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)':
dependencies:
eslint: 10.0.3
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.23.3':
dependencies:
'@eslint/object-schema': 3.0.3
debug: 4.4.3
minimatch: 10.2.4
transitivePeerDependencies:
- supports-color
'@eslint/config-helpers@0.5.3':
dependencies:
'@eslint/core': 1.1.1
'@eslint/core@1.1.1':
dependencies:
'@types/json-schema': 7.0.15
'@eslint/object-schema@3.0.3': {}
'@eslint/plugin-kit@0.6.1':
dependencies:
'@eslint/core': 1.1.1
levn: 0.4.1
'@fastify/accept-negotiator@2.0.1': {} '@fastify/accept-negotiator@2.0.1': {}
'@fastify/ajv-compiler@4.0.5': '@fastify/ajv-compiler@4.0.5':
@@ -1203,6 +1537,17 @@ snapshots:
fastq: 1.20.1 fastq: 1.20.1
glob: 11.1.0 glob: 11.1.0
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
dependencies:
'@humanfs/core': 0.19.1
'@humanwhocodes/retry': 0.4.3
'@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/retry@0.4.3': {}
'@isaacs/cliui@9.0.0': {} '@isaacs/cliui@9.0.0': {}
'@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/sourcemap-codec@1.5.5': {}
@@ -1302,14 +1647,109 @@ snapshots:
'@types/deep-eql@4.0.2': {} '@types/deep-eql@4.0.2': {}
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
'@types/node@22.19.15': '@types/node@22.19.15':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/triple-beam@1.3.5': {} '@types/triple-beam@1.3.5': {}
'@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.57.1(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/type-utils': 8.57.1(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/utils': 8.57.1(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.57.1
eslint: 10.0.3
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.57.1(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.57.1
debug: 4.4.3
eslint: 10.0.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.57.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3)
'@typescript-eslint/types': 8.57.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.57.1':
dependencies:
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/visitor-keys': 8.57.1
'@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.57.1(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.57.1(eslint@10.0.3)(typescript@5.9.3)
debug: 4.4.3
eslint: 10.0.3
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.57.1': {}
'@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.57.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3)
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/visitor-keys': 8.57.1
debug: 4.4.3
minimatch: 10.2.4
semver: 7.7.4
tinyglobby: 0.2.15
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.57.1(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3)
'@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
eslint: 10.0.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.57.1':
dependencies:
'@typescript-eslint/types': 8.57.1
eslint-visitor-keys: 5.0.1
'@vitest/expect@3.2.4': '@vitest/expect@3.2.4':
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
@@ -1354,10 +1794,23 @@ snapshots:
abstract-logging@2.0.1: {} abstract-logging@2.0.1: {}
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
acorn@8.16.0: {}
ajv-formats@3.0.1(ajv@8.18.0): ajv-formats@3.0.1(ajv@8.18.0):
optionalDependencies: optionalDependencies:
ajv: 8.18.0 ajv: 8.18.0
ajv@6.14.0:
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.18.0: ajv@8.18.0:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -1429,6 +1882,8 @@ snapshots:
deep-eql@5.0.2: {} deep-eql@5.0.2: {}
deep-is@0.1.4: {}
depd@2.0.0: {} depd@2.0.0: {}
dequal@2.0.3: {} dequal@2.0.3: {}
@@ -1468,10 +1923,80 @@ snapshots:
escape-html@1.0.3: {} escape-html@1.0.3: {}
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@10.0.3):
dependencies:
eslint: 10.0.3
eslint-scope@9.1.2:
dependencies:
'@types/esrecurse': 4.3.1
'@types/estree': 1.0.8
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@5.0.1: {}
eslint@10.0.3:
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.3
'@eslint/config-helpers': 0.5.3
'@eslint/core': 1.1.1
'@eslint/plugin-kit': 0.6.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.14.0
cross-spawn: 7.0.6
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
espree: 11.2.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
minimatch: 10.2.4
natural-compare: 1.4.0
optionator: 0.9.4
transitivePeerDependencies:
- supports-color
espree@11.2.0:
dependencies:
acorn: 8.16.0
acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 5.0.1
esquery@1.7.0:
dependencies:
estraverse: 5.3.0
esrecurse@4.3.0:
dependencies:
estraverse: 5.3.0
estraverse@5.3.0: {}
estree-walker@3.0.3: estree-walker@3.0.3:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
esutils@2.0.3: {}
execa@9.6.1: execa@9.6.1:
dependencies: dependencies:
'@sindresorhus/merge-streams': 4.0.0 '@sindresorhus/merge-streams': 4.0.0
@@ -1493,6 +2018,8 @@ snapshots:
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-json-stable-stringify@2.1.0: {}
fast-json-stringify@6.3.0: fast-json-stringify@6.3.0:
dependencies: dependencies:
'@fastify/merge-json-schemas': 0.2.1 '@fastify/merge-json-schemas': 0.2.1
@@ -1502,6 +2029,8 @@ snapshots:
json-schema-ref-resolver: 3.0.0 json-schema-ref-resolver: 3.0.0
rfdc: 1.4.1 rfdc: 1.4.1
fast-levenshtein@2.0.6: {}
fast-querystring@1.1.2: fast-querystring@1.1.2:
dependencies: dependencies:
fast-decode-uri-component: 1.0.1 fast-decode-uri-component: 1.0.1
@@ -1542,12 +2071,28 @@ snapshots:
dependencies: dependencies:
is-unicode-supported: 2.1.0 is-unicode-supported: 2.1.0
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
find-my-way@9.5.0: find-my-way@9.5.0:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
fast-querystring: 1.1.2 fast-querystring: 1.1.2
safe-regex2: 5.1.0 safe-regex2: 5.1.0
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
path-exists: 4.0.0
flat-cache@4.0.1:
dependencies:
flatted: 3.4.2
keyv: 4.5.4
flatted@3.4.2: {}
fn.name@1.1.0: {} fn.name@1.1.0: {}
foreground-child@3.3.1: foreground-child@3.3.1:
@@ -1567,6 +2112,10 @@ snapshots:
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 resolve-pkg-maps: 1.0.0
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
glob@11.1.0: glob@11.1.0:
dependencies: dependencies:
foreground-child: 3.3.1 foreground-child: 3.3.1
@@ -1592,10 +2141,22 @@ snapshots:
human-signals@8.0.1: {} human-signals@8.0.1: {}
ignore@5.3.2: {}
ignore@7.0.5: {}
imurmurhash@0.1.4: {}
inherits@2.0.4: {} inherits@2.0.4: {}
ipaddr.js@2.3.0: {} ipaddr.js@2.3.0: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-plain-obj@4.1.0: {} is-plain-obj@4.1.0: {}
is-stream@2.0.1: {} is-stream@2.0.1: {}
@@ -1612,20 +2173,39 @@ snapshots:
js-tokens@9.0.1: {} js-tokens@9.0.1: {}
json-buffer@3.0.1: {}
json-schema-ref-resolver@3.0.0: json-schema-ref-resolver@3.0.0:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {} json-schema-traverse@1.0.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
kuler@2.0.0: {} kuler@2.0.0: {}
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
type-check: 0.4.0
light-my-request@6.6.0: light-my-request@6.6.0:
dependencies: dependencies:
cookie: 1.1.1 cookie: 1.1.1
process-warning: 4.0.1 process-warning: 4.0.1
set-cookie-parser: 2.7.2 set-cookie-parser: 2.7.2
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
logform@2.7.0: logform@2.7.0:
dependencies: dependencies:
'@colors/colors': 1.6.0 '@colors/colors': 1.6.0
@@ -1655,6 +2235,8 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
natural-compare@1.4.0: {}
npm-run-path@6.0.0: npm-run-path@6.0.0:
dependencies: dependencies:
path-key: 4.0.0 path-key: 4.0.0
@@ -1666,10 +2248,29 @@ snapshots:
dependencies: dependencies:
fn.name: 1.1.0 fn.name: 1.1.0
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
fast-levenshtein: 2.0.6
levn: 0.4.1
prelude-ls: 1.2.1
type-check: 0.4.0
word-wrap: 1.2.5
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
parse-ms@4.0.0: {} parse-ms@4.0.0: {}
path-exists@4.0.0: {}
path-key@3.1.1: {} path-key@3.1.1: {}
path-key@4.0.0: {} path-key@4.0.0: {}
@@ -1713,6 +2314,8 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
prelude-ls@1.2.1: {}
pretty-ms@9.3.0: pretty-ms@9.3.0:
dependencies: dependencies:
parse-ms: 4.0.0 parse-ms: 4.0.0
@@ -1721,6 +2324,8 @@ snapshots:
process-warning@5.0.0: {} process-warning@5.0.0: {}
punycode@2.3.1: {}
quick-format-unescaped@4.0.4: {} quick-format-unescaped@4.0.4: {}
readable-stream@3.6.2: readable-stream@3.6.2:
@@ -1856,6 +2461,10 @@ snapshots:
triple-beam@1.4.1: {} triple-beam@1.4.1: {}
ts-api-utils@2.4.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
tsx@4.21.0: tsx@4.21.0:
dependencies: dependencies:
esbuild: 0.27.4 esbuild: 0.27.4
@@ -1863,12 +2472,20 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
typescript@5.9.3: {} typescript@5.9.3: {}
undici-types@6.21.0: {} undici-types@6.21.0: {}
unicorn-magic@0.3.0: {} unicorn-magic@0.3.0: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
vite-node@3.2.4(@types/node@22.19.15)(tsx@4.21.0): vite-node@3.2.4(@types/node@22.19.15)(tsx@4.21.0):
@@ -1975,4 +2592,8 @@ snapshots:
triple-beam: 1.4.1 triple-beam: 1.4.1
winston-transport: 4.9.0 winston-transport: 4.9.0
word-wrap@1.2.5: {}
yocto-queue@0.1.0: {}
yoctocolors@2.1.2: {} yoctocolors@2.1.2: {}

View File

@@ -0,0 +1,43 @@
#!/bin/bash
# Build bastion container image and push to Gitea container registry
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
# Load .env for GITEA_TOKEN
if [ -f .env ]; then
set -a; source .env; set +a
fi
# Registry defaults to internal address (external proxy has body size limit)
REGISTRY="${GITEA_REGISTRY:-mysources.co.uk}"
IMAGE="lab-bastion"
VERSION=$(node -p "require('./package.json').version")
TAG="${1:-$VERSION}"
echo "==> Building bastion image (tag: $TAG)..."
podman build -t "$IMAGE:$TAG" -f stack/Dockerfile .
echo "==> Tagging as $REGISTRY/michal/$IMAGE:$TAG..."
podman tag "$IMAGE:$TAG" "$REGISTRY/michal/$IMAGE:$TAG"
if [ -n "$GITEA_TOKEN" ]; then
echo "==> Logging in to $REGISTRY..."
podman login --tls-verify=false -u michal -p "$GITEA_TOKEN" "$REGISTRY"
echo "==> Pushing to $REGISTRY/michal/$IMAGE:$TAG..."
podman push --tls-verify=false "$REGISTRY/michal/$IMAGE:$TAG"
# Ensure package is linked to the repository
if [ -f "$SCRIPT_DIR/link-package.sh" ]; then
source "$SCRIPT_DIR/link-package.sh"
link_package "container" "$IMAGE"
fi
else
echo "==> GITEA_TOKEN not set, skipping push."
fi
echo "==> Done!"
echo " Image: $REGISTRY/michal/$IMAGE:$TAG"

47
bastion/scripts/build-rpm.sh Executable file
View File

@@ -0,0 +1,47 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
# Load .env if present
if [ -f .env ]; then
set -a; source .env; set +a
fi
# Ensure tools are on PATH
export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$HOME/.local/bin:$PATH"
echo "==> Running unit tests..."
pnpm test:run
echo ""
echo "==> Building TypeScript..."
pnpm build
echo "==> Generating shell completions..."
pnpm completions:generate
echo "==> Bundling standalone binary..."
mkdir -p dist
rm -f dist/lab dist/lab-*.rpm dist/lab*.deb
bun build src/cli/src/index.ts --compile --outfile dist/lab
echo "==> Packaging RPM..."
nfpm pkg --packager rpm --target dist/
RPM_FILE=$(ls dist/lab-*.rpm 2>/dev/null | head -1)
echo "==> Built: $RPM_FILE"
echo " Size: $(du -h "$RPM_FILE" | cut -f1)"
rpm -qpi "$RPM_FILE"
echo ""
echo "==> Packaging DEB..."
rm -f dist/lab*.deb
nfpm pkg --packager deb --target dist/
DEB_FILE=$(ls dist/lab*.deb 2>/dev/null | head -1)
echo "==> Built: $DEB_FILE"
echo " Size: $(du -h "$DEB_FILE" | cut -f1)"

View File

@@ -0,0 +1,385 @@
#!/usr/bin/env tsx
/**
* generate-completions.ts -- auto-generates shell completions from the commander.js command tree.
*
* Usage:
* tsx scripts/generate-completions.ts # print generated files to stdout
* tsx scripts/generate-completions.ts --write # write completions/ files
* tsx scripts/generate-completions.ts --check # exit 0 if files match, 1 if stale
*
* Requires `pnpm build` to have run first (workspace packages must be compiled).
*/
import { Command, type Option, type Argument } from 'commander';
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
// ============================================================
// Command tree extraction
// ============================================================
interface CmdInfo {
name: string;
description: string;
hidden: boolean;
options: OptInfo[];
args: ArgInfo[];
subcommands: CmdInfo[];
}
interface OptInfo {
short?: string;
long: string;
description: string;
takesValue: boolean;
choices?: string[];
negate: boolean;
}
interface ArgInfo {
name: string;
description: string;
required: boolean;
variadic: boolean;
choices?: string[];
}
function extractOption(opt: Option): OptInfo {
return {
short: (opt as unknown as Record<string, string>).short || undefined,
long: (opt as unknown as Record<string, string>).long,
description: opt.description,
takesValue: (opt as unknown as Record<string, boolean>).required || (opt as unknown as Record<string, boolean>).optional || false,
choices: (opt as unknown as Record<string, string[] | undefined>).argChoices || undefined,
negate: (opt as unknown as Record<string, boolean>).negate || false,
};
}
function extractArgument(arg: Argument): ArgInfo {
return {
name: (arg as unknown as Record<string, string>)._name ?? arg.name(),
description: arg.description,
required: (arg as unknown as Record<string, boolean>).required,
variadic: (arg as unknown as Record<string, boolean>).variadic,
choices: (arg as unknown as Record<string, string[] | undefined>)._choices || undefined,
};
}
function extractCommand(cmd: Command): CmdInfo {
const options = (cmd.options as Option[])
.filter((o) => {
const long = (o as unknown as Record<string, string>).long;
return long !== '--help' && long !== '--version';
})
.map(extractOption);
const args = ((cmd as unknown as Record<string, Argument[]>).registeredArguments ?? [])
.map(extractArgument);
const subcommands = (cmd.commands as Command[])
.filter((sub) => sub.name() !== 'help')
.map(extractCommand);
if ((cmd.commands as Command[]).some((sub) => sub.name() === 'help')) {
subcommands.push({
name: 'help',
description: 'display help for command',
hidden: false,
options: [],
args: [],
subcommands: [],
});
}
return {
name: cmd.name(),
description: cmd.description(),
hidden: (cmd as unknown as Record<string, boolean>)._hidden ?? false,
options,
args,
subcommands,
};
}
async function extractTree(): Promise<CmdInfo> {
const { createProgram } = await import('../src/cli/src/index.js') as { createProgram: () => Command };
const program = createProgram();
return extractCommand(program);
}
// ============================================================
// Utilities
// ============================================================
function esc(s: string): string {
return s.replace(/'/g, "\\'");
}
/** Collect all commands recursively with their full path. */
function collectCommands(cmd: CmdInfo, prefix: string[] = []): { path: string[]; cmd: CmdInfo }[] {
const result: { path: string[]; cmd: CmdInfo }[] = [];
for (const sub of cmd.subcommands) {
const fullPath = [...prefix, sub.name];
result.push({ path: fullPath, cmd: sub });
result.push(...collectCommands(sub, fullPath));
}
return result;
}
// ============================================================
// Fish completion generator
// ============================================================
function generateFish(root: CmdInfo): string {
const lines: string[] = [];
const emit = (s: string): void => { lines.push(s); };
const BIN = root.name;
emit(`# ${BIN} fish completions -- auto-generated by scripts/generate-completions.ts`);
emit('# DO NOT EDIT MANUALLY -- run: pnpm completions:generate');
emit('');
emit(`complete -c ${BIN} -e`);
emit(`complete -c ${BIN} -f`);
emit('');
// Global options
emit('# Global options');
emit(`complete -c ${BIN} -s v -l version -d 'Show version'`);
emit(`complete -c ${BIN} -s h -l help -d 'Show help'`);
emit('');
const allCmds = collectCommands(root);
// Helper function for fish: test if exactly the given subcommand chain is present
emit('# Helper: test if a subcommand chain is active');
emit(`function __${BIN}_using_cmd`);
emit(' set -l tokens (commandline -opc)');
emit(' set -l expected $argv');
emit(' set -l depth (count $expected)');
emit(' set -l found 0');
emit(' set -l i 1');
emit(' for tok in $tokens[2..]');
emit(' if string match -q -- "-*" $tok');
emit(' continue');
emit(' end');
emit(' set i (math $i + 1)');
emit(' set -l idx (math $i - 1)');
emit(' if test $idx -le $depth');
emit(' if test "$tok" != "$expected[$idx]"');
emit(' return 1');
emit(' end');
emit(' set found (math $found + 1)');
emit(' else');
emit(' return 1');
emit(' end');
emit(' end');
emit(' test $found -eq $depth');
emit('end');
emit('');
// Top-level commands
const topCmds = root.subcommands.filter((c) => !c.hidden);
emit('# Top-level commands');
for (const cmd of topCmds) {
emit(`complete -c ${BIN} -n "not __fish_seen_subcommand_from ${topCmds.map((c) => c.name).join(' ')}" -a ${cmd.name} -d '${esc(cmd.description)}'`);
}
emit('');
// Subcommands and options at each level
for (const { path, cmd } of allCmds) {
if (cmd.hidden) continue;
// If this command has subcommands, offer them
const visibleSubs = cmd.subcommands.filter((s) => !s.hidden);
if (visibleSubs.length > 0) {
const parentCondition = `__${BIN}_using_cmd ${path.join(' ')}`;
emit(`# ${path.join(' ')} subcommands`);
for (const sub of visibleSubs) {
emit(`complete -c ${BIN} -n "${parentCondition}" -a ${sub.name} -d '${esc(sub.description)}'`);
}
emit('');
}
// Options for this command
if (cmd.options.length > 0) {
const condition = `__${BIN}_using_cmd ${path.join(' ')}`;
emit(`# ${path.join(' ')} options`);
for (const opt of cmd.options) {
const parts = [`complete -c ${BIN} -n "${condition}"`];
if (opt.short) parts.push(`-s ${opt.short.replace('-', '')}`);
parts.push(`-l ${opt.long.replace(/^--/, '')}`);
parts.push(`-d '${esc(opt.description)}'`);
if (opt.takesValue) {
if (opt.choices) {
parts.push(`-xa '${opt.choices.join(' ')}'`);
} else {
parts.push('-x');
}
}
emit(parts.join(' '));
}
emit('');
}
}
return lines.join('\n') + '\n';
}
// ============================================================
// Bash completion generator
// ============================================================
function generateBash(root: CmdInfo): string {
const lines: string[] = [];
const emit = (s: string): void => { lines.push(s); };
const BIN = root.name;
emit(`# ${BIN} bash completions -- auto-generated by scripts/generate-completions.ts`);
emit('# DO NOT EDIT MANUALLY -- run: pnpm completions:generate');
emit('');
const allCmds = collectCommands(root);
const topCmds = root.subcommands.filter((c) => !c.hidden).map((c) => c.name);
emit(`_${BIN}() {`);
emit(' local cur prev words cword');
emit(' _init_completion || return');
emit('');
emit(` local top_commands="${topCmds.join(' ')}"`);
emit('');
// Build chain of subcommands from command line
emit(' # Extract the subcommand chain (skip options and their values)');
emit(' local -a subcmd_chain=()');
emit(' local i skip_next=false');
emit(' for ((i=1; i < cword; i++)); do');
emit(' if $skip_next; then skip_next=false; continue; fi');
emit(' case "${words[i]}" in');
emit(' -*) ;; # skip options');
emit(' *) subcmd_chain+=("${words[i]}") ;;');
emit(' esac');
emit(' done');
emit('');
emit(' local chain_len=${#subcmd_chain[@]}');
emit(' local chain_str="${subcmd_chain[*]}"');
emit('');
// Build case statement for each command path
emit(' case "$chain_str" in');
// Start with the deepest paths first to match longest
const sortedCmds = [...allCmds].sort((a, b) => b.path.length - a.path.length);
for (const { path, cmd } of sortedCmds) {
if (cmd.hidden) continue;
const pathStr = path.join(' ');
const visibleSubs = cmd.subcommands.filter((s) => !s.hidden).map((s) => s.name);
const optFlags: string[] = [];
for (const opt of cmd.options) {
if (opt.short) optFlags.push(opt.short);
optFlags.push(opt.long);
}
optFlags.push('-h', '--help');
const completions = [...visibleSubs, ...optFlags].join(' ');
emit(` "${pathStr}")`);
emit(` COMPREPLY=($(compgen -W "${completions}" -- "$cur"))`);
emit(' return ;;');
}
// Top-level (no subcommand yet)
emit(' "")');
emit(` COMPREPLY=($(compgen -W "$top_commands -h --help -v --version" -- "$cur"))`);
emit(' return ;;');
// Default
emit(' *)');
emit(' COMPREPLY=($(compgen -W "-h --help" -- "$cur"))');
emit(' return ;;');
emit(' esac');
emit('}');
emit('');
emit(`complete -F _${BIN} ${BIN}`);
return lines.join('\n') + '\n';
}
// ============================================================
// Main
// ============================================================
async function main(): Promise<void> {
const mode = process.argv[2] ?? '';
let tree: CmdInfo;
try {
tree = await extractTree();
} catch (err) {
console.error('Failed to extract command tree from createProgram().');
console.error('Make sure workspace packages are built: pnpm build');
console.error(err);
process.exit(1);
}
const fishContent = generateFish(tree);
const bashContent = generateBash(tree);
const completionsDir = join(ROOT, 'completions');
const fishPath = join(completionsDir, 'lab.fish');
const bashPath = join(completionsDir, 'lab.bash');
if (mode === '--check') {
let stale = false;
try {
const currentFish = readFileSync(fishPath, 'utf-8');
if (currentFish !== fishContent) {
console.error('completions/lab.fish is stale');
stale = true;
}
} catch {
console.error('completions/lab.fish does not exist');
stale = true;
}
try {
const currentBash = readFileSync(bashPath, 'utf-8');
if (currentBash !== bashContent) {
console.error('completions/lab.bash is stale');
stale = true;
}
} catch {
console.error('completions/lab.bash does not exist');
stale = true;
}
if (stale) {
console.error('Run: pnpm completions:generate');
process.exit(1);
}
console.log('Completions are up to date.');
process.exit(0);
}
if (mode === '--write') {
mkdirSync(completionsDir, { recursive: true });
writeFileSync(fishPath, fishContent);
writeFileSync(bashPath, bashContent);
console.log(`Wrote ${fishPath}`);
console.log(`Wrote ${bashPath}`);
process.exit(0);
}
// Default: print to stdout
console.log('=== completions/lab.fish ===');
console.log(fishContent);
console.log('=== completions/lab.bash ===');
console.log(bashContent);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

65
bastion/scripts/link-package.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Link a Gitea package to a repository.
# Works automatically on Gitea 1.24+ (uses API), warns on older versions.
#
# Usage: source scripts/link-package.sh
# link_package <type> <name>
#
# Requires: GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO
link_package() {
local PKG_TYPE="$1" # e.g. "rpm", "container"
local PKG_NAME="$2" # e.g. "lab", "lab-bastion"
if [ -z "$PKG_TYPE" ] || [ -z "$PKG_NAME" ]; then
echo "Usage: link_package <type> <name>"
return 1
fi
local GITEA_URL="${GITEA_URL:-http://10.0.0.194:3012}"
local GITEA_OWNER="${GITEA_OWNER:-michal}"
local GITEA_REPO="${GITEA_REPO:-lab}"
if [ -z "$GITEA_TOKEN" ]; then
echo "WARNING: GITEA_TOKEN not set, skipping package-repo linking."
return 0
fi
# Check if already linked (search all packages, filter by type+name client-side)
local REPO_LINK
REPO_LINK=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/packages/${GITEA_OWNER}" \
| python3 -c "
import json,sys
for p in json.load(sys.stdin):
if p['type']=='$PKG_TYPE' and p['name']=='$PKG_NAME':
r=p.get('repository')
if r: print(r['full_name'])
break
" 2>/dev/null)
if [ -n "$REPO_LINK" ]; then
echo "==> Package ${PKG_TYPE}/${PKG_NAME} already linked to ${REPO_LINK}"
return 0
fi
# Try Gitea 1.24+ link API
local HTTP_CODE
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/${PKG_TYPE}/${PKG_NAME}/-/link/${GITEA_REPO}")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo "==> Linked ${PKG_TYPE}/${PKG_NAME} to ${GITEA_OWNER}/${GITEA_REPO}"
return 0
fi
# API not available (Gitea < 1.24) -- warn with manual instructions
local PUBLIC_URL="${GITEA_PUBLIC_URL:-${GITEA_URL}}"
echo ""
echo "WARNING: Could not auto-link ${PKG_TYPE}/${PKG_NAME} to repository (Gitea < 1.24)."
echo "Link it manually in the Gitea UI:"
echo " ${PUBLIC_URL}/${GITEA_OWNER}/-/packages/${PKG_TYPE}/${PKG_NAME}/settings"
echo " -> Link to repository: ${GITEA_OWNER}/${GITEA_REPO}"
return 0
}

72
bastion/scripts/publish-deb.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
# Load .env if present
if [ -f .env ]; then
set -a; source .env; set +a
fi
GITEA_URL="${GITEA_URL:-http://10.0.0.194:3012}"
GITEA_PUBLIC_URL="${GITEA_PUBLIC_URL:-https://mysources.co.uk}"
GITEA_OWNER="${GITEA_OWNER:-michal}"
GITEA_REPO="${GITEA_REPO:-lab}"
GITEA_TOKEN="${GITEA_TOKEN:-$PACKAGES_TOKEN}"
if [ -z "$GITEA_TOKEN" ]; then
echo "Error: GITEA_TOKEN (or PACKAGES_TOKEN) not set. Add it to .env or export it."
exit 1
fi
DEB_FILE=$(ls dist/lab*.deb 2>/dev/null | head -1)
if [ -z "$DEB_FILE" ]; then
echo "Error: No DEB found in dist/. Run scripts/build-rpm.sh first."
exit 1
fi
# Extract version from the deb filename
DEB_VERSION=$(dpkg-deb --field "$DEB_FILE" Version 2>/dev/null || echo "unknown")
echo "==> Publishing $DEB_FILE (version $DEB_VERSION) to ${GITEA_URL}..."
# Gitea Debian registry: PUT /api/packages/{owner}/debian/pool/{distribution}/{component}/upload
# Publish to each supported distribution.
# Debian: trixie (13/stable), forky (14/testing)
# Ubuntu: noble (24.04 LTS), plucky (25.04)
DISTRIBUTIONS="trixie forky noble plucky"
for DIST in $DISTRIBUTIONS; do
echo " -> $DIST..."
HTTP_CODE=$(curl -s -o /tmp/deb-upload-$DIST.out -w "%{http_code}" \
-X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
--upload-file "$DEB_FILE" \
"${GITEA_URL}/api/packages/${GITEA_OWNER}/debian/pool/${DIST}/main/upload")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo " Published to $DIST"
elif [ "$HTTP_CODE" = "409" ]; then
echo " Already exists in $DIST (skipping)"
else
echo " WARNING: Upload to $DIST returned HTTP $HTTP_CODE"
cat /tmp/deb-upload-$DIST.out 2>/dev/null || true
echo ""
fi
rm -f /tmp/deb-upload-$DIST.out
done
echo ""
echo "==> Published successfully!"
# Ensure package is linked to the repository
source "$SCRIPT_DIR/link-package.sh"
link_package "debian" "lab"
echo ""
echo "Install with:"
echo " echo \"deb ${GITEA_PUBLIC_URL}/api/packages/${GITEA_OWNER}/debian trixie main\" | sudo tee /etc/apt/sources.list.d/lab.list"
echo " curl -fsSL ${GITEA_PUBLIC_URL}/api/packages/${GITEA_OWNER}/debian/repository.key | sudo gpg --dearmor -o /etc/apt/keyrings/lab.gpg"
echo " sudo apt update && sudo apt install lab"

62
bastion/scripts/publish-rpm.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
# Load .env if present
if [ -f .env ]; then
set -a; source .env; set +a
fi
GITEA_URL="${GITEA_URL:-http://10.0.0.194:3012}"
GITEA_PUBLIC_URL="${GITEA_PUBLIC_URL:-https://mysources.co.uk}"
GITEA_OWNER="${GITEA_OWNER:-michal}"
GITEA_REPO="${GITEA_REPO:-lab}"
GITEA_TOKEN="${GITEA_TOKEN:-$PACKAGES_TOKEN}"
if [ -z "$GITEA_TOKEN" ]; then
echo "Error: GITEA_TOKEN (or PACKAGES_TOKEN) not set. Add it to .env or export it."
exit 1
fi
RPM_FILE=$(ls dist/lab-*.rpm 2>/dev/null | head -1)
if [ -z "$RPM_FILE" ]; then
echo "Error: No RPM found in dist/. Run scripts/build-rpm.sh first."
exit 1
fi
# Get version string as it appears in Gitea (e.g. "0.1.0-1")
RPM_VERSION=$(rpm -qp --queryformat '%{VERSION}-%{RELEASE}' "$RPM_FILE")
echo "==> Publishing $RPM_FILE (version $RPM_VERSION) to ${GITEA_URL}..."
# Check if version already exists and delete it first
EXISTING=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/rpm/lab/${RPM_VERSION}")
if [ "$EXISTING" = "200" ]; then
echo "==> Version $RPM_VERSION already exists, replacing..."
curl -s -o /dev/null -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/rpm/lab/${RPM_VERSION}"
fi
# Upload
curl --fail -s -X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
--upload-file "$RPM_FILE" \
"${GITEA_URL}/api/packages/${GITEA_OWNER}/rpm/upload"
echo ""
echo "==> Published successfully!"
# Ensure package is linked to the repository
source "$SCRIPT_DIR/link-package.sh"
link_package "rpm" "lab"
echo ""
echo "Install with:"
echo " sudo dnf install lab # if repo already configured"

View File

@@ -116,7 +116,7 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
mkdirSync(config.httpDir, { recursive: true }); mkdirSync(config.httpDir, { recursive: true });
// Prepare boot artifacts // Prepare boot artifacts
if (!config.skipArtifacts) { if (config.skipArtifacts !== true) {
logger.info(`Preparing boot artifacts (Fedora ${config.fedoraVersion} ${config.arch})...`); logger.info(`Preparing boot artifacts (Fedora ${config.fedoraVersion} ${config.arch})...`);
copyIfMissing( copyIfMissing(
@@ -177,7 +177,7 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
generateDnsmasqConf(config); generateDnsmasqConf(config);
// Open firewall ports // Open firewall ports
if (!config.skipDnsmasq) { if (config.skipDnsmasq !== true) {
openFirewall(config); openFirewall(config);
} }
@@ -187,7 +187,7 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
logger.info(`HTTP server listening on :${config.httpPort}`); logger.info(`HTTP server listening on :${config.httpPort}`);
// Start dnsmasq (unless skipped) // Start dnsmasq (unless skipped)
if (!config.skipDnsmasq) { if (config.skipDnsmasq !== true) {
const dnsmasqProc = startDnsmasq(config); const dnsmasqProc = startDnsmasq(config);
// Monitor dnsmasq // Monitor dnsmasq
@@ -210,9 +210,9 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
printBanner(config); printBanner(config);
// Graceful shutdown // Graceful shutdown
const shutdown = async () => { const shutdown = async (): Promise<void> => {
logger.info("Shutting down..."); logger.info("Shutting down...");
if (!config.skipDnsmasq) stopDnsmasq(); if (config.skipDnsmasq !== true) stopDnsmasq();
closeFirewall(config); closeFirewall(config);
await app.close(); await app.close();
try { unlinkSync(pidFile); } catch { /* ignore */ } try { unlinkSync(pidFile); } catch { /* ignore */ }

View File

@@ -30,7 +30,7 @@ export function registerApiRoutes(
const { mac: rawMac, hostname, disk, role } = request.body ?? {}; const { mac: rawMac, hostname, disk, role } = request.body ?? {};
const mac = (rawMac ?? "").toLowerCase().replace(/-/g, ":"); const mac = (rawMac ?? "").toLowerCase().replace(/-/g, ":");
if (!mac) { if (mac === "") {
return reply.status(400).send({ error: "mac is required" }); return reply.status(400).send({ error: "mac is required" });
} }
@@ -90,7 +90,7 @@ export function registerApiRoutes(
if (queueEntry) { if (queueEntry) {
queueEntry.progress = stageName; queueEntry.progress = stageName;
queueEntry.progress_at = new Date().toISOString(); queueEntry.progress_at = new Date().toISOString();
if (detailStr) { if (detailStr !== "") {
queueEntry.progress_detail = detailStr; queueEntry.progress_detail = detailStr;
} }
@@ -111,8 +111,9 @@ export function registerApiRoutes(
}; };
s.installed[mac] = installedInfo; s.installed[mac] = installedInfo;
const admin = state.load().installed[mac]?.role ? "michal" : "root"; const installedRole = state.load().installed[mac]?.role;
console.log(`\n \x1b[0;32m\x1b[1m ssh ${admin}@${ip}\x1b[0m\n`); const admin = installedRole !== undefined && installedRole !== "" ? "michal" : "root";
console.log(`\n \x1b[0;32m\x1b[1m ssh ${admin}@${ip}\x1b[0m\n`); // eslint-disable-line no-console
} }
} }
}); });
@@ -126,21 +127,21 @@ export function registerApiRoutes(
}>("/api/machines/:mac", async (request, reply) => { }>("/api/machines/:mac", async (request, reply) => {
const mac = request.params.mac.toLowerCase().replace(/-/g, ":"); const mac = request.params.mac.toLowerCase().replace(/-/g, ":");
if (!mac) { if (mac === "") {
return reply.status(400).send({ error: "mac is required" }); return reply.status(400).send({ error: "mac is required" });
} }
let found = false; let found = false;
state.update((s) => { state.update((s) => {
if (s.discovered[mac]) { if (s.discovered[mac] !== undefined) {
delete s.discovered[mac]; delete s.discovered[mac];
found = true; found = true;
} }
if (s.install_queue[mac]) { if (s.install_queue[mac] !== undefined) {
delete s.install_queue[mac]; delete s.install_queue[mac];
found = true; found = true;
} }
if (s.installed[mac]) { if (s.installed[mac] !== undefined) {
delete s.installed[mac]; delete s.installed[mac];
found = true; found = true;
} }
@@ -171,7 +172,7 @@ export function registerApiRoutes(
}; };
}>("/api/discover", async (request, reply) => { }>("/api/discover", async (request, reply) => {
const data = request.body; const data = request.body;
if (!data) { if (data === null || data === undefined) {
return reply.status(400).send({ error: "invalid JSON" }); return reply.status(400).send({ error: "invalid JSON" });
} }

View File

@@ -10,7 +10,7 @@ import { registerDispatchRoutes } from "./routes/dispatch.js";
import { registerKickstartRoutes } from "./routes/kickstart.js"; import { registerKickstartRoutes } from "./routes/kickstart.js";
import { registerApiRoutes } from "./routes/api.js"; import { registerApiRoutes } from "./routes/api.js";
export function createApp(config: BastionConfig) { export function createApp(config: BastionConfig): { app: ReturnType<typeof Fastify>; state: StateManager } {
const app = Fastify({ const app = Fastify({
logger: false, // We use winston instead logger: false, // We use winston instead
}); });

View File

@@ -13,10 +13,11 @@ import { logger } from "./logger.js";
export function detectInterface(): string { export function detectInterface(): string {
const output = execSync("ip route", { encoding: "utf-8" }); const output = execSync("ip route", { encoding: "utf-8" });
const match = output.match(/default\s+.*\s+dev\s+(\S+)/); const match = output.match(/default\s+.*\s+dev\s+(\S+)/);
if (!match?.[1]) { const ifaceMatch = match?.[1];
if (ifaceMatch === undefined) {
throw new Error("Cannot detect default network interface"); throw new Error("Cannot detect default network interface");
} }
return match[1]; return ifaceMatch;
} }
/** /**
@@ -25,10 +26,11 @@ export function detectInterface(): string {
export function detectIp(iface: string): string { export function detectIp(iface: string): string {
const output = execSync(`ip -4 addr show ${iface}`, { encoding: "utf-8" }); const output = execSync(`ip -4 addr show ${iface}`, { encoding: "utf-8" });
const match = output.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/); const match = output.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
if (!match?.[1]) { const ipMatch = match?.[1];
if (ipMatch === undefined) {
throw new Error(`Cannot detect IP on interface ${iface}`); throw new Error(`Cannot detect IP on interface ${iface}`);
} }
return match[1]; return ipMatch;
} }
/** /**
@@ -45,10 +47,11 @@ export function deriveNetwork(ip: string): string {
export function detectGateway(): string { export function detectGateway(): string {
const output = execSync("ip route", { encoding: "utf-8" }); const output = execSync("ip route", { encoding: "utf-8" });
const match = output.match(/default\s+via\s+(\S+)/); const match = output.match(/default\s+via\s+(\S+)/);
if (!match?.[1]) { const gwMatch = match?.[1];
if (gwMatch === undefined) {
throw new Error("Cannot detect default gateway"); throw new Error("Cannot detect default gateway");
} }
return match[1]; return gwMatch;
} }
/** /**
@@ -56,11 +59,16 @@ export function detectGateway(): string {
* Sources: authorized_keys, then id_ed25519.pub, id_rsa.pub, id_ecdsa.pub (deduplicated). * Sources: authorized_keys, then id_ed25519.pub, id_rsa.pub, id_ecdsa.pub (deduplicated).
*/ */
export function collectSshKeys(bastionDir: string): { keys: string[]; source: string } { export function collectSshKeys(bastionDir: string): { keys: string[]; source: string } {
const realHome = process.env["SUDO_USER"] const sudoUser = process.env["SUDO_USER"];
? execSync(`getent passwd ${process.env["SUDO_USER"]}`, { encoding: "utf-8" }) let realHome: string;
.split(":")[5] if (sudoUser !== undefined) {
?.trim() ?? homedir() const passwdEntry = execSync(`getent passwd ${sudoUser}`, { encoding: "utf-8" })
: homedir(); .split(":")[5]
?.trim();
realHome = passwdEntry !== undefined && passwdEntry !== "" ? passwdEntry : homedir();
} else {
realHome = homedir();
}
const keys: string[] = []; const keys: string[] = [];
const fingerprints = new Set<string>(); const fingerprints = new Set<string>();
@@ -74,7 +82,7 @@ export function collectSshKeys(bastionDir: string): { keys: string[]; source: st
const trimmed = line.trim(); const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#")) { if (trimmed && !trimmed.startsWith("#")) {
const fp = trimmed.split(/\s+/)[1]; const fp = trimmed.split(/\s+/)[1];
if (fp && !fingerprints.has(fp)) { if (fp !== undefined && fp !== "" && !fingerprints.has(fp)) {
keys.push(trimmed); keys.push(trimmed);
fingerprints.add(fp); fingerprints.add(fp);
} }
@@ -90,7 +98,7 @@ export function collectSshKeys(bastionDir: string): { keys: string[]; source: st
if (existsSync(keyPath)) { if (existsSync(keyPath)) {
const keyData = readFileSync(keyPath, "utf-8").trim(); const keyData = readFileSync(keyPath, "utf-8").trim();
const fp = keyData.split(/\s+/)[1]; const fp = keyData.split(/\s+/)[1];
if (fp && !fingerprints.has(fp)) { if (fp !== undefined && fp !== "" && !fingerprints.has(fp)) {
keys.push(keyData); keys.push(keyData);
fingerprints.add(fp); fingerprints.add(fp);
source = source ? `${source} + ${keyPath}` : keyPath; source = source ? `${source} + ${keyPath}` : keyPath;
@@ -131,18 +139,18 @@ export function detectAdminUser(): string {
* Populate runtime network config fields on the config object. * Populate runtime network config fields on the config object.
*/ */
export function populateNetworkConfig(config: BastionConfig): BastionConfig { export function populateNetworkConfig(config: BastionConfig): BastionConfig {
const iface = config.iface || detectInterface(); const iface = config.iface !== "" ? config.iface : detectInterface();
const serverIp = config.serverIp || detectIp(iface); const serverIp = config.serverIp !== "" ? config.serverIp : detectIp(iface);
const network = config.network || deriveNetwork(serverIp); const network = config.network !== "" ? config.network : deriveNetwork(serverIp);
const gateway = config.gateway || detectGateway(); const gateway = config.gateway !== "" ? config.gateway : detectGateway();
const { keys: sshKeys, source: sshSource } = config.sshKeys.length > 0 const { keys: sshKeys, source: sshSource } = config.sshKeys.length > 0
? { keys: config.sshKeys, source: "config" } ? { keys: config.sshKeys, source: "config" }
: collectSshKeys(config.bastionDir); : collectSshKeys(config.bastionDir);
const adminUser = config.adminUser || detectAdminUser(); const adminUser = config.adminUser !== "" ? config.adminUser : detectAdminUser();
logger.info(`Interface: ${iface} IP: ${serverIp} Network: ${network}`); logger.info(`Interface: ${iface} IP: ${serverIp} Network: ${network}`);
logger.info(`SSH keys: ${sshKeys.length} key(s) from ${sshSource}`); logger.info(`SSH keys: ${sshKeys.length} key(s) from ${sshSource}`);
if (adminUser) { if (adminUser !== "") {
logger.info(`Admin user: ${adminUser} (will be created on installed machines)`); logger.info(`Admin user: ${adminUser} (will be created on installed machines)`);
} }

View File

@@ -21,7 +21,7 @@ export function registerInstallCommand(parent: Command): void {
hostname, hostname,
role: opts.role, role: opts.role,
}; };
if (opts.disk) { if (opts.disk !== undefined) {
payload["disk"] = opts.disk; payload["disk"] = opts.disk;
} }

View File

@@ -62,12 +62,12 @@ export function registerListCommand(parent: Command): void {
// Determine status // Determine status
let status = "discovered"; let status = "discovered";
if (queued) { if (queued !== undefined) {
status = queued.progress && queued.progress !== "waiting" status = queued.progress !== undefined && queued.progress !== "" && queued.progress !== "waiting"
? "installing" ? "installing"
: "queued"; : "queued";
} }
if (inst) status = "installed"; if (inst !== undefined) status = "installed";
const hostname = inst?.hostname ?? queued?.hostname ?? "-"; const hostname = inst?.hostname ?? queued?.hostname ?? "-";
const role = inst?.role ?? queued?.role ?? "-"; const role = inst?.role ?? queued?.role ?? "-";

View File

@@ -28,7 +28,7 @@ export function registerReprovisionCommand(parent: Command): void {
hostname, hostname,
role: opts.role, role: opts.role,
}; };
if (opts.disk) { if (opts.disk !== undefined) {
payload["disk"] = opts.disk; payload["disk"] = opts.disk;
} }
@@ -61,13 +61,14 @@ export function registerReprovisionCommand(parent: Command): void {
const adminUser = process.env["SUDO_USER"] ?? process.env["USER"] ?? ""; const adminUser = process.env["SUDO_USER"] ?? process.env["USER"] ?? "";
const effectiveUser = adminUser === "root" ? "" : adminUser; const effectiveUser = adminUser === "root" ? "" : adminUser;
if (ip && effectiveUser) { if (ip !== "" && effectiveUser !== "") {
console.log(""); console.log("");
console.log(`Attempting SSH reboot into PXE (${effectiveUser}@${ip})...`); console.log(`Attempting SSH reboot into PXE (${effectiveUser}@${ip})...`);
// Find SSH key // Find SSH key
const realHome = process.env["SUDO_USER"] const sudoUser = process.env["SUDO_USER"];
? join("/home", process.env["SUDO_USER"]) const realHome = sudoUser !== undefined
? join("/home", sudoUser)
: homedir(); : homedir();
const keyPaths = [ const keyPaths = [
join(realHome, ".ssh", "id_ed25519"), join(realHome, ".ssh", "id_ed25519"),
@@ -79,7 +80,7 @@ export function registerReprovisionCommand(parent: Command): void {
const sshArgs = [ const sshArgs = [
"-o", "StrictHostKeyChecking=no", "-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=10", "-o", "ConnectTimeout=10",
...(sshKey ? ["-i", sshKey] : []), ...(sshKey !== undefined ? ["-i", sshKey] : []),
`${effectiveUser}@${ip}`, `${effectiveUser}@${ip}`,
'PXE_ENTRY=$(sudo efibootmgr | grep -iE "pxe|network|ipv4" | head -1 | grep -oP "Boot\\K[0-9A-F]+"); if [ -n "$PXE_ENTRY" ]; then sudo efibootmgr --bootnext "$PXE_ENTRY" && echo "PXE set as next boot" && sudo reboot; else echo "No PXE boot entry found, rebooting anyway..." && sudo reboot; fi', 'PXE_ENTRY=$(sudo efibootmgr | grep -iE "pxe|network|ipv4" | head -1 | grep -oP "Boot\\K[0-9A-F]+"); if [ -n "$PXE_ENTRY" ]; then sudo efibootmgr --bootnext "$PXE_ENTRY" && echo "PXE set as next boot" && sudo reboot; else echo "No PXE boot entry found, rebooting anyway..." && sudo reboot; fi',
]; ];

View File

@@ -4,6 +4,7 @@
// init bastion standalone start/stop/status // init bastion standalone start/stop/status
// provision list/install/reprovision/forget // provision list/install/reprovision/forget
import { fileURLToPath } from "node:url";
import { Command } from "commander"; import { Command } from "commander";
import { APP_VERSION } from "@lab/shared"; import { APP_VERSION } from "@lab/shared";
import { registerStartCommand } from "./commands/serve.js"; import { registerStartCommand } from "./commands/serve.js";
@@ -14,34 +15,47 @@ import { registerListCommand } from "./commands/list.js";
import { registerReprovisionCommand } from "./commands/reprovision.js"; import { registerReprovisionCommand } from "./commands/reprovision.js";
import { registerForgetCommand } from "./commands/forget.js"; import { registerForgetCommand } from "./commands/forget.js";
const program = new Command(); export function createProgram(): Command {
const program = new Command();
program program
.name("lab") .name("lab")
.description("Lab PXE Bastion -- discover-first bare-metal provisioning") .description("Lab PXE Bastion -- discover-first bare-metal provisioning")
.version(APP_VERSION); .version(APP_VERSION);
// init bastion standalone start/stop/status // init bastion standalone start/stop/status
const initCmd = program.command("init"); const initCmd = program.command("init");
initCmd.description("Initialise infrastructure components"); initCmd.description("Initialise infrastructure components");
const bastionCmd = initCmd.command("bastion"); const bastionCmd = initCmd.command("bastion");
bastionCmd.description("Bastion PXE server management"); bastionCmd.description("Bastion PXE server management");
const standaloneCmd = bastionCmd.command("standalone"); const standaloneCmd = bastionCmd.command("standalone");
standaloneCmd.description("Standalone bastion server lifecycle"); standaloneCmd.description("Standalone bastion server lifecycle");
registerStartCommand(standaloneCmd); registerStartCommand(standaloneCmd);
registerStopCommand(standaloneCmd); registerStopCommand(standaloneCmd);
registerStatusCommand(standaloneCmd); registerStatusCommand(standaloneCmd);
// provision list/install/reprovision/forget // provision list/install/reprovision/forget
const provisionCmd = program.command("provision"); const provisionCmd = program.command("provision");
provisionCmd.description("Machine provisioning operations"); provisionCmd.description("Machine provisioning operations");
registerListCommand(provisionCmd); registerListCommand(provisionCmd);
registerInstallCommand(provisionCmd); registerInstallCommand(provisionCmd);
registerReprovisionCommand(provisionCmd); registerReprovisionCommand(provisionCmd);
registerForgetCommand(provisionCmd); registerForgetCommand(provisionCmd);
program.parse(); return program;
}
// Run CLI when executed directly (not imported)
const isDirectExecution =
process.argv[1] !== undefined &&
(process.argv[1].endsWith("/index.js") ||
process.argv[1].endsWith("/index.ts") ||
process.argv[1] === fileURLToPath(import.meta.url));
if (isDirectExecution) {
createProgram().parse();
}

View File

@@ -1,3 +1,28 @@
# Stage 1: Build TypeScript
FROM node:22-alpine AS builder
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
# Copy workspace config and package manifests
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json tsconfig.json ./
COPY src/shared/package.json src/shared/tsconfig.json src/shared/
COPY src/bastion/package.json src/bastion/tsconfig.json src/bastion/
COPY src/cli/package.json src/cli/tsconfig.json src/cli/
# Install all dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY src/shared/src/ src/shared/src/
COPY src/bastion/src/ src/bastion/src/
COPY src/cli/src/ src/cli/src/
# Build TypeScript
RUN pnpm build
# Stage 2: Production runtime
FROM fedora:43 FROM fedora:43
# Install system dependencies # Install system dependencies
@@ -7,21 +32,29 @@ RUN dnf install -y \
ipxe-bootimgs-aarch64 \ ipxe-bootimgs-aarch64 \
curl \ curl \
openssh-clients \ openssh-clients \
nodejs \
npm \
&& dnf clean all && dnf clean all
# Install Node.js 22 # Install pnpm
RUN dnf install -y nodejs npm && dnf clean all
RUN npm install -g pnpm@9 RUN npm install -g pnpm@9
# Create app directory # Create app directory
WORKDIR /app WORKDIR /app
# Copy package files and install dependencies # Copy workspace config, manifests, and lockfile
COPY package.json pnpm-lock.yaml* ./ COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install COPY src/shared/package.json src/shared/
COPY src/bastion/package.json src/bastion/
COPY src/cli/package.json src/cli/
# Copy built application # Install production dependencies
COPY dist/ ./dist/ RUN pnpm install --frozen-lockfile --prod 2>/dev/null || pnpm install --prod
# Copy built output from builder
COPY --from=builder /app/src/shared/dist/ src/shared/dist/
COPY --from=builder /app/src/bastion/dist/ src/bastion/dist/
COPY --from=builder /app/src/cli/dist/ src/cli/dist/
# Create data directories # Create data directories
RUN mkdir -p /data/state /data/tftp /data/http RUN mkdir -p /data/state /data/tftp /data/http
@@ -34,4 +67,4 @@ EXPOSE 67/udp
EXPOSE 69/udp EXPOSE 69/udp
EXPOSE 4011/udp EXPOSE 4011/udp
ENTRYPOINT ["node", "dist/cli/index.js", "serve"] ENTRYPOINT ["node", "src/cli/dist/index.js", "init", "bastion", "standalone", "start"]