diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..14170b78 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +*.db* +/stacks +/node_modules +*.md +/docker +*.dot +*.svg +*.mmd +*.lock +src/tests +.github +.local-tests +.gitai.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..7eeacf33 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +*.ts info@itsnik.de \ No newline at end of file diff --git a/.github/DockStat.png b/.github/DockStat.png new file mode 100644 index 00000000..d375bd49 Binary files /dev/null and b/.github/DockStat.png differ diff --git a/.github/scripts/dep-graph.sh b/.github/scripts/dep-graph.sh new file mode 100644 index 00000000..be7d8731 --- /dev/null +++ b/.github/scripts/dep-graph.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +mermaidContent="$(cat dependency-graph.mmd)" + +echo "--- +config: + flowchart: + defaultRenderer: elk +--- + +$mermaidContent +" > dependency-graph.mmd + diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml deleted file mode 100644 index 053123d0..00000000 --- a/.github/workflows/anchore.yml +++ /dev/null @@ -1,48 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow checks out code, builds an image, performs a container image -# vulnerability scan with Anchore's Grype tool, and integrates the results with GitHub Advanced Security -# code scanning feature. For more information on the Anchore scan action usage -# and parameters, see https://github.com/anchore/scan-action. For more -# information on Anchore's container image scanning tool Grype, see -# https://github.com/anchore/grype -name: Anchore Grype vulnerability scan - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '30 9 * * 1' - -permissions: - contents: read - -jobs: - Anchore-Build-Scan: - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - runs-on: ubuntu-latest - steps: - - name: Check out the code - uses: actions/checkout@v4 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - name: Run the Anchore Grype scan action - uses: anchore/scan-action@d5aa5b6cb9414b0c7771438046ff5bcfa2854ed7 - id: scan - with: - image: "localbuild/testimage:latest" - fail-build: true - severity-cutoff: critical - - name: Upload vulnerability report - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml deleted file mode 100644 index 72a370e7..00000000 --- a/.github/workflows/build-dev.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Docker Image CI (dev) - -on: - push: - branches: [ "dev" ] - -permissions: - packages: write - contents: read - -jobs: - build-main: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - name: Checkout repository - - - uses: pmorelli92/github-container-registry-build-push@2.2.1 - name: Build and Publish latest service image - with: - github-push-secret: ${{secrets.GITHUB_TOKEN}} - docker-image-name: dockstatapi - docker-image-tag: dev # optional - dockerfile-path: Dockerfile # optional - build-context: . # optional - build-only: false # optional diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml deleted file mode 100644 index 2836ac9a..00000000 --- a/.github/workflows/build-image.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Docker Image CI - -on: - push: - branches: [ "main" ] - -permissions: - packages: write - contents: read - -jobs: - build-main: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - name: Checkout repository - - - uses: pmorelli92/github-container-registry-build-push@2.2.1 - name: Build and Publish latest service image - with: - github-push-secret: ${{secrets.GITHUB_TOKEN}} - docker-image-name: dockstatapi - docker-image-tag: latest # optional - dockerfile-path: Dockerfile # optional - build-context: . # optional - build-only: false # optional diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..e7061106 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,63 @@ +name: "Continuous Delivery" + +on: + release: + types: [published, prereleased] + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Publish Container Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine tags + id: tags + uses: docker/metadata-action@v5 + with: + images: ghcr.io/its4nik/dockstatapi + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.tags.outputs.tags }} + + sbom: + name: Generate SBOM + runs-on: ubuntu-latest + needs: publish + steps: + - name: Generate SBOM + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/its4nik/dockstatapi:${{ github.event.release.tag_name }} + format: spdx-json + output: sbom.json + + - name: Upload SBOM + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: sbom.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..87552540 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: "Continuous Integration" + +on: + push: + pull_request: + +jobs: + unit-test: + name: Unit Testing + runs-on: ubuntu-latest + permissions: + contents: write + checks: write + security-events: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run unit tests + run: | + export PAD_NEW_LINES=false + docker compose -f docker/docker-compose.unit-test.yaml up -d + bun test + + - name: Log unit test files + run: | + ls -lah reports/markdown + + - name: Publish Test Report + if: always() + run: | + SUMMARY="" + for element in $(ls reports/markdown); do + SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown/${element}")")" + done + echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY + + build-scan: + name: Build and Security Scan + runs-on: ubuntu-latest + needs: unit-test + permissions: + contents: read + checks: write + security-events: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + tags: dockstatapi:ci-${{ github.sha }} + load: true + + - name: Start and test container + run: | + docker run --name test-container -d dockstatapi:ci-${{ github.sha }} + sleep 10 + docker ps | grep test-container + docker logs test-container + docker stop test-container + + - name: Trivy vulnerability scan + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "dockstatapi:ci-${{ github.sha }}" + format: "sarif" + output: "trivy-results.sarif" + severity: "HIGH,CRITICAL" + + - name: Upload security results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: "trivy-results.sarif" diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 00000000..9edece27 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,21 @@ +name: Update CONTRIBUTORS file +on: + schedule: + - cron: "0 0 1 * *" + workflow_dispatch: +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: minicli/action-contributors@v3.3 + name: "Update a projects CONTRIBUTORS file" + env: + CONTRIB_REPOSITORY: "Its4Nik/DockStatAPI" + CONTRIB_OUTPUT_FILE: "CONTRIBUTORS.md" + + - name: Commit changes + uses: test-room-7/action-update-file@v1 + with: + file-path: "CONTRIBUTORS.md" + commit-msg: Update Contributors + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml new file mode 100644 index 00000000..9e178ce7 --- /dev/null +++ b/.github/workflows/dependency-graph.yml @@ -0,0 +1,44 @@ +name: "Generate Dependency Graph" + +on: + push: + +permissions: write-all + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: oven-sh/setup-bun@v2 + + - name: Install dependency-cruiser and graphviz + run: | + bun add dependency-cruiser + sudo apt-get install -y graphviz + + - name: Generate Mermaid Dependency Graph + run: | + bun run dependency-cruiser --output-type mermaid src/index.ts --output-to dependency-graph.mmd --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + echo "Mermaid graph generated at dependency-graph.mmd" + + - name: Convert to ELK Layout + run: | + bash ./.github/scripts/dep-graph.sh + + - name: Generate Dependency Graph (SVG) + run: | + bun run dependency-cruiser --output-type dot src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + dot -T svg -Gsplines=ortho dependency-graph.dot -o dependency-graph.svg + echo "SVG graph generated at dependency-graph.svg" + + - name: Commit and Push Changes + uses: EndBug/add-and-commit@v9 + with: + add: '["dependency-graph.svg", "dependency-graph.mmd"]' + message: "Update dependency graphs" + committer_name: "GitHub Action" + committer_email: "action@github.com" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..5fe920a7 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,55 @@ +name: "Lint" + +on: + push: + pull_request: + +jobs: + lint-test: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: write + checks: write + security-events: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Knip check + if: ${{ github.event_name == 'pull_request' }} + uses: codex-/knip-reporter@v2 + + - name: Run linter + run: | + bun biome format --fix + bun biome lint --fix + bun biome check --fix + bun biome ci + + - name: Add linted files + run: git add src/ + + - name: Check for changes + id: check-changes + run: | + git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT + + - name: Commit and push lint changes + if: | + steps.check-changes.outputs.changes_detected == 'true' && + github.event_name == 'push' + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git commit -m "CQL: Apply lint fixes [skip ci]" + git push diff --git a/.gitignore b/.gitignore index 2e7f14aa..d75865c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ -dockstat.log -node_modules -.dockerignore -apprise_config.yml \ No newline at end of file +*.db* +/stacks +/node_modules +.test +build +*.xml +dependency-*.{mmd,dot,svg} +Knip-Report.md +reports/** +.gitai.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..71322072 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/typings"] + path = src/typings + url = git@github.com:Its4Nik/dockstat-types.git diff --git a/.knip.json b/.knip.json new file mode 100644 index 00000000..e786d748 --- /dev/null +++ b/.knip.json @@ -0,0 +1,5 @@ +{ + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"], + "ignore": ["src/plugins/*.plugin.ts", "src/tests/*.ts"] +} diff --git a/.local-tests/test-container-changes.sh b/.local-tests/test-container-changes.sh new file mode 100644 index 00000000..5df50759 --- /dev/null +++ b/.local-tests/test-container-changes.sh @@ -0,0 +1,15 @@ +commands=("kill" "start" "restart" "start" "pause" "unpause") +container="SQLite-web" + +press(){ + echo "Press enter to continue" + read -r -p ">" +} + +for command in "${commands[@]}"; do + press + echo "Running $command for $container" + docker "$command" "$container" +done + +docker start "$container" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..28db443c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +## Changelog + +### Added + +* **Database Stats Routes and More** - (Its4Nik, 2025-06-11) - (ef74b6a64d91d0683a9962e977c6aa4db145f26a) +* **Stack Validation** - (Its4Nik, 2025-06-02) - (871ab4f92dee49569d9028490442b2537d265430) +* **Unknown Changes** - (Its4Nik, 2025-06-02) - (3a9a3dc8d37c0eea1b7af67b18176b8209989dcb) - *Note: Description indicates significant but unspecified changes.* + +### Changed + +* **Dependency Graphs Updated** - (Its4Nik, 2025-06-07) - (ffab878a9d536af7aa14dd7b785643f50ac973ec) +* **Stacks Controller Refactor** - (Its4Nik, 2025-06-06) - (e0ed2557ecfdc546786fb4969e8dec602a35708b) + +### Fixed + +* **Linter Fixes** - (Its4Nik, 2025-06-02) - (ec0c79abe3f2f55fe80b220305eb96f19f03c173) +* **Minor Refactor and Bug Fixes** - (Its4Nik, 2025-06-07) - (1177e7e5d90d0f675719b60c669f05f2871024ca) +* **UT: Fix Wrong Body Selection** - (Its4Nik, 2025-05-16) - (4afe4ca05c999da6d70cea95b7d4ea871dcf0723) +* **UT: Fix Wrong Body Selection** - (Its4Nik, 2025-05-16) - (2e8528ab8a500a925f3411582a21fbeccc683262) +* **CQL: Apply Lint Fixes** - (GitHub Actions, 2025-05-16) - (7bb328b0f4a8770130e71eddc419bd926b7af90f) \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 00000000..e69de29b diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8c70ae68..00000000 --- a/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# Stage 1: Build stage -FROM node:latest AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="1.0" -LABEL description="API for DockStat: Docker container statistics." -LABEL license="MIT" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" - -WORKDIR /api - -COPY package*.json ./ - -RUN npm install --production - -COPY . . - -# Stage 2: Production stage -FROM node:alpine - -WORKDIR /api - -COPY --from=builder /api . - -RUN apk add --no-cache bash curl - -RUN bash /api/scripts/install_apprise.sh - -EXPOSE 7070 - -HEALTHCHECK CMD curl --fail http://localhost:7070/ || exit 1 - -ENTRYPOINT [ "bash", "entrypoint.sh" ] diff --git a/LICENSE b/LICENSE index 0a731244..428e5953 100644 --- a/LICENSE +++ b/LICENSE @@ -1,28 +1,407 @@ -BSD 3-Clause License - -Copyright (c) 2024, ItsNik - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Attribution-NonCommercial 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + j. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the "Licensor." The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/README.md b/README.md index 0735e1af..36384590 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,39 @@ -# DockstatAPI +![Logo](.github/DockStat.png) +![CC BY-NC 4.0 License](https://img.shields.io/badge/License-CC_BY--NC_4.0-lightgrey.svg) -## This tool relies on the [DockerSocket Proxy](https://docs.linuxserver.io/images/docker-socket-proxy/), please see it's documentation for more information. - -This is the DockStatAPI used in [DockStat](https://github.com/its4nik/dockstat). - -It features an easy way to configure using a yaml file. - -You can specify multiple hosts, using a Docker Socket Proxy like this: - -## Installation: - -docker-compose.yaml -```yaml -services: - dockstatapi: - image: ghcr.io/its4nik/dockstatapi:latest - container_name: dockstatapi - environment: - - SECRET=CHANGEME # This is required in the header 'Authorization': 'CHANGEME' - - ALLOW_LOCALHOST="False" # Defaults to False - ports: - - "7070:7070" - volumes: - - ./dockstatapi:/api/config # Place your hosts.yaml file here - restart: always -``` - -Example docker-socket onfiguration: +--- -```yaml -socket-proxy: - image: lscr.io/linuxserver/socket-proxy:latest - container_name: socket-proxy - environment: - - CONTAINERS=1 # Needed for the api to worrk - - INFO=1 # Needed for the api to work - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - restart: unless-stopped - read_only: true - tmpfs: - - /run - ports: - - 2375:2375 -``` +# DockStatAPI -Configuration inside the mounted folder, as hosts.yaml -```yaml -mintimeout: 10000 # The minimum time to wait before querying the same server again, defaults to 5000 Ms +Docker monitoring API with real-time statistics, stack management, and plugin support. -log: - logsize: 10 # Specify the Size of the log files in MB, default is 1MB - LogCount: 1 # How many log files should be kept in rotation. Default is 5 +## Features -hosts: - YourHost1: - url: hetzner - port: 2375 +- Real-time container metrics via WebSocket +- Multi-host Docker environment monitoring +- Compose stack deployment/management +- Plugin system for custom logic/notifications +- Historical stats storage (SQLite) +- Swagger API documentation +- Web dashboard ([DockStat](https://github.com/its4nik/DockStat)) -# This is used for DockStat -# Please see the dockstat documentation for more information -tags: - raspberry: red-200 - private: violet-400 +## Tech Stack -container: - dozzle: # Container name - link: https://github.com -``` +- **Runtime**: [Bun.sh](https://bun.sh) +- **Framework**: [Elysia.js](https://elysiajs.com/) +- **Database**: SQLite (WAL mode) +- **Docker**: dockerode + compose +- **Monitoring**: Custom metrics collection +- **Auth**: [Authentication](https://outline.itsnik.de/s/dockstat/doc/authentication-VSGhxqjtXf) -Please see the documentation for more information on what endpoints will be provieded. +## Documentation and Wiki -[Documentation](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) +Please see [DockStatAPI](https://dockstatapi.itsnik.de) ---- +## Project Graph -This Api uses a "queuing" mechanism to communicate to the servers, so that we dont ask the same server multiple times without getting an answer. +### SVG: -Feel free to use this API in any of your projects :D +![Dependency Graph](./dependency-graph.svg) -The `/stats` endpoint server all information that are gethered from the server in a json format. +Click [here](./dependency-graph.mmd) for the mermaid version. diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..f9d82247 --- /dev/null +++ b/biome.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "files": { + "ignore": ["./src/tests/junit-exporter.ts"] + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..f9588ab6 --- /dev/null +++ b/bun.lock @@ -0,0 +1,515 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "dockstatapi", + "dependencies": { + "chalk": "^5.4.1", + "date-fns": "^4.1.0", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.7", + "js-yaml": "^4.1.0", + "knip": "latest", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.8.0", + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@its_4_nik/gitai": "^1.1.14", + "@types/bun": "latest", + "@types/dockerode": "^3.3.42", + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.16.0", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0", + }, + }, + }, + "trustedDependencies": [ + "protobufjs", + ], + "packages": { + "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + + "@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@4.1.9", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DBJBkzI5Wx4jFaYm221LHvAhpKYkhVS0k9plqHwaHhofGNxvYB7J3Bz8w+bFJ05zaMb0sZNHo4KdmENQFlNTuQ=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.13", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw=="], + + "@inquirer/core": ["@inquirer/core@10.1.14", "", { "dependencies": { "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A=="], + + "@inquirer/editor": ["@inquirer/editor@4.2.14", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7", "external-editor": "^3.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA=="], + + "@inquirer/expand": ["@inquirer/expand@4.0.16", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-oiDqafWzMtofeJyyGkb1CTPaxUkjIcSxePHHQCfif8t3HV9pHcw1Kgdw3/uGpDvaFfeTluwQtWiqzPVjAqS3zA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.12", "", {}, "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ=="], + + "@inquirer/input": ["@inquirer/input@4.2.0", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-opqpHPB1NjAmDISi3uvZOTrjEEU5CWVu/HBkDby8t93+6UxYX0Z7Ps0Ltjm5sZiEbWenjubwUkivAEYQmy9xHw=="], + + "@inquirer/number": ["@inquirer/number@3.0.16", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kMrXAaKGavBEoBYUCgualbwA9jWUx2TjMA46ek+pEKy38+LFpL9QHlTd8PO2kWPUgI/KB+qi02o4y2rwXbzr3Q=="], + + "@inquirer/password": ["@inquirer/password@4.0.16", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-g8BVNBj5Zeb5/Y3cSN+hDUL7CsIFDIuVxb9EPty3lkxBaYpjL5BNRKSYOF9yOLe+JOcKFd+TSVeADQ4iSY7rbg=="], + + "@inquirer/prompts": ["@inquirer/prompts@7.6.0", "", { "dependencies": { "@inquirer/checkbox": "^4.1.9", "@inquirer/confirm": "^5.1.13", "@inquirer/editor": "^4.2.14", "@inquirer/expand": "^4.0.16", "@inquirer/input": "^4.2.0", "@inquirer/number": "^3.0.16", "@inquirer/password": "^4.0.16", "@inquirer/rawlist": "^4.1.4", "@inquirer/search": "^3.0.16", "@inquirer/select": "^4.2.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jAhL7tyMxB3Gfwn4HIJ0yuJ5pvcB5maYUcouGcgd/ub79f9MqZ+aVnBtuFf+VC2GTkCBF+R+eo7Vi63w5VZlzw=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.4", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5GGvxVpXXMmfZNtvWw4IsHpR7RzqAR624xtkPd1NxxlV5M+pShMqzL4oRddRkg8rVEOK9fKdJp1jjVML2Lr7TQ=="], + + "@inquirer/search": ["@inquirer/search@3.0.16", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-POCmXo+j97kTGU6aeRjsPyuCpQQfKcMXdeTMw708ZMtWrj5aykZvlUxH4Qgz3+Y1L/cAVZsSpA+UgZCu2GMOMg=="], + + "@inquirer/select": ["@inquirer/select@4.2.4", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-unTppUcTjmnbl/q+h8XeQDhAqIOmwWYWNyiiP2e3orXrg6tOaa5DHXja9PChCSbChOsktyKgOieRZFnajzxoBg=="], + + "@inquirer/type": ["@inquirer/type@3.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA=="], + + "@its_4_nik/gitai": ["@its_4_nik/gitai@1.1.14", "", { "dependencies": { "@google/generative-ai": "^0.24.1", "commander": "^14.0.0", "ignore": "^7.0.5", "inquirer": "^12.6.3", "ollama": "^0.5.16" }, "peerDependencies": { "typescript": "^5.8.3" }, "bin": { "gitai": "dist/gitai.js" } }, "sha512-vpZnCWtgMcfqPNpkjOpEG3+dEr+t87C0wlH+FOiHDiLVw2ebZir9QJiw7yOl75hhkxHqXVDnluj6U0e3yAfzqA=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.5.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IQZZP6xjGvVNbXVPEwZeCDTkG7iajFsVZSaq7QwxuiJqkcE/GKd0GxGQMs6jjE72nrgSGVHQD/yws1PNzP9j5w=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.5.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-nY15IBY5NjOPKIDRJ2sSLr0GThFXz4J4lgIo4fmnXanJjeeXaM5aCOL3oIxT7RbONqyMki0lzMkbX7PWqW3/lw=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.5.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WQibNtsWiJZ36Q2QKYSedN6c4xoZtLhU7UOFPGTMaw/J8eb+WYh5pfzTtZR9WGZQRoS3kj0E/9683Wuskz5mMQ=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.5.0", "", { "os": "linux", "cpu": "arm" }, "sha512-oZj20OTnjGn1qnBGYTjRXEMyd0inlw127s+DTC+Y0kdxoz5BUMqUhq5M9mZ1BH4c1qPlRto6shOFVrK4hNkhhA=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zxFuO4Btd1BSFjuaO0mnIA9XRWP4FX3bTbVO9KjKvO8MX6Ig2+ZDNHpzzK2zkOunHGc4sJQm5oDTcMvww+hyag=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mmDNrt2yyEnsPrmq3wzRsqEYM+cpVuv8itgYU++BNJrfzdJpK+OpvR3rPToTZSOZQt3iYLfqQ2hauIIraJnJGw=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.5.0", "", { "os": "linux", "cpu": "none" }, "sha512-CxW3/uVUlSpIEJ3sLi5Q+lk7SVgQoxUKBTsMwpY2nFiCmtzHBOuwMMKES1Hk+w/Eirz09gDjoIrxkzg3ETDSGQ=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.5.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-RxfVqJnmO7uEGzpEgEzVb5Sxjy8NAYpQj+7JZZunxIyJiDK1KgOJqVJ0NZnRC1UAe/yyEpO82wQIOInaLqFBgA=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Ri36HuV91PVXFw1BpTisJOZ2x9dkfgsvrjVa3lPX+QS6QRvvcdogGjPTTqgg8WkzCh6RTzd7Lx9mCZQdw06HTQ=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xskd2J4Jnfuze2jYKiZx4J+PY4hJ5Z0MuVh8JPNvu/FY1+SAdRei9S95dhc399Nw6eINre7xOrsugr11td3k4Q=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.5.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-ZAHTs0MzHUlHAqKffvutprVhO7OlENWisu1fW/bVY6r+TPxsl25Q0lzbOUhrxTIJ9f0Sl5meCI2fkPeovZA7bQ=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.5.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-4/3RJnkrKo7EbBdWAYsSHZEjgZ8TYYAt/HrHDo5yy/5dUvxvPoetNtAudCiYKNgJOlFLzmzIXyn713MljEy6RA=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.5.0", "", { "os": "win32", "cpu": "x64" }, "sha512-poXrxQLJA770Xy3gAS9mrC/dp6GatYdvNlwCWwjL6lzBNToEK66kx3tgqIaOYIqtjJDKYR58P3jWgmwJyJxEAQ=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-m3wyqBh1TOHjpr/dXeIZY7OoX+MQazb+bMHQdDtwUvefrafUx+5YHRvulYh1sZSQ449nQ3nk3qj5qj535vZRjg=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-75fJfF/9xNypr7cnOYoZBhfmG1yP7ex3pUOeYGakmtZRffO9z1i1quLYhjZsmaDXsAIZ3drMhenYHMmFKS3SRg=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YhXGf0FXa72bEt4F7eTVKx5X3zWpbAOPnaA/dZ6/g8tGhw1m9IFjrabVHFjzcx3dQny4MgA59EhyElkDvpUe8A=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-T3JDhx8mjGjvh5INsPZJrlKHmZsecgDYvtvussKRdkc1Nnn7WC+jH9sh5qlmYvwzvmetlPVNezAoNvmGO9vtMg=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dx7ghtAl8aXBdqofJpi338At6lkeCtTfoinTYQXd9/TEJx+f+zCGNlQO6nJz3ydJBX48FDuOFKkNC+lUlWrd8w=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7KvMGdWmAZtAtg6IjoEJHKxTXdAcrHnUnqfgs0JpXst7trquV2mxBeRZusQXwxpu4HCSomKMvJfsp1qKaqSFDg=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSGC9RwX+dl7o5KFr5aH7Gq3nFbkq/3Gda6mxNPMvNkWrgXdIyiINxpyD8hJu566M+QSv1wEAu934BZotFDyoQ=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jOj3L/gfLc0IwgOTkZMiZ5c673i/hbAmidlaylT0gE6H18hln9HxPgp5GCf4E4y6mwEJlW8QC5hQi221+9otdA=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + + "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], + + "@types/dockerode": ["@types/dockerode@3.3.42", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-U1jqHMShibMEWHdxYhj3rCMNCiLx5f35i4e3CEUuW+JSSszc/tVqc6WCAPdhwBymG5R/vgbcceagK0St7Cq6Eg=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="], + + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + + "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], + + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], + + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], + + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], + + "dockerode": ["dockerode@4.0.7", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA=="], + + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "formatly": ["formatly@0.2.4", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "inquirer": ["inquirer@12.7.0", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/prompts": "^7.6.0", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", "run-async": "^4.0.4", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "knip": ["knip@5.61.3", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.2.4", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^11.1.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.3.4", "strip-json-comments": "5.0.2", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-8iSz8i8ufIjuUwUKzEwye7ROAW0RzCze7T770bUiz0PKL+SSwbs4RS32fjMztLwcOzSsNPlXdUAeqmkdzXxJ1Q=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + + "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], + + "ollama": ["ollama@0.5.16", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-OEbxxOIUZtdZgOaTPAULo051F5y+Z1vosxEYOoABPnQKeW7i4O8tJNlxCB+xioyoorVqgjkdj+TA1f1Hy2ug/w=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "oxc-resolver": ["oxc-resolver@11.5.0", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "11.5.0", "@oxc-resolver/binding-darwin-x64": "11.5.0", "@oxc-resolver/binding-freebsd-x64": "11.5.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.5.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.5.0", "@oxc-resolver/binding-linux-arm64-musl": "11.5.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.5.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.5.0", "@oxc-resolver/binding-linux-x64-gnu": "11.5.0", "@oxc-resolver/binding-linux-x64-musl": "11.5.0", "@oxc-resolver/binding-wasm32-wasi": "11.5.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.5.0", "@oxc-resolver/binding-win32-x64-msvc": "11.5.0" } }, "sha512-lG/AiquYQP/4OOXaKmlPvLeCOxtlZ535489H3yk4euimwnJXIViQus2Y9Mc4c45wFQ0UYM1rFduiJ8+RGjUtTQ=="], + + "oxlint": ["oxlint@1.6.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.6.0", "@oxlint/darwin-x64": "1.6.0", "@oxlint/linux-arm64-gnu": "1.6.0", "@oxlint/linux-arm64-musl": "1.6.0", "@oxlint/linux-x64-gnu": "1.6.0", "@oxlint/linux-x64-musl": "1.6.0", "@oxlint/win32-arm64": "1.6.0", "@oxlint/win32-x64": "1.6.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-jtaD65PqzIa1udvSxxscTKBxYKuZoFXyKGLiU1Qjo1ulq3uv/fQDtoV1yey1FrQZrQjACGPi1Widsy1TucC7Jg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "protobufjs": ["protobufjs@7.5.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-async": ["run-async@4.0.4", "", { "dependencies": { "oxlint": "^1.2.0", "prettier": "^3.5.3" } }, "sha512-2cgeRHnV11lSXBEhq7sN7a5UVjTKm9JTb9x8ApIT//16D7QL96AgnNeWSGoB4gIHc0iYw/Ha0Z+waBaCYZVNhg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "smol-toml": ["smol-toml@1.4.1", "", {}, "sha512-CxdwHXyYTONGHThDbq5XdwbFsuY4wlClRGejfE2NtwUtiHYsP1QtNsHb/hnj31jKYSchztJsaA8pSQoVzkfCFg=="], + + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="], + + "tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + + "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], + + "zod": ["zod@3.25.75", "", {}, "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg=="], + + "zod-validation-error": ["zod-validation-error@3.5.2", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw=="], + + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "@types/ssh2/@types/node": ["@types/node@18.19.115", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@inquirer/core/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@inquirer/core/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + } +} diff --git a/config/apprise_config_example.yml b/config/apprise_config_example.yml deleted file mode 100644 index 88e33870..00000000 --- a/config/apprise_config_example.yml +++ /dev/null @@ -1,5 +0,0 @@ -# Please see the apprise documentation -urls: - - tgram://bottoken/ChatID - - rocket://user:password@hostname/RoomID/Channel - - ntfy://topic/ diff --git a/config/hosts.yaml b/config/hosts.yaml deleted file mode 100644 index d40e6697..00000000 --- a/config/hosts.yaml +++ /dev/null @@ -1,24 +0,0 @@ -mintimeout: 10000 # The minimum time to wait before querying the same server again, defaults to 5000 Ms - -log: - logsize: 10 # Specify the Size of the log files in MB, default is 1MB - LogCount: 1 # How many log files should be kept in rotation. Default is 5 - -tags: - raspberry: red-200 - private: violet-400 - -hosts: - YourHost1: - url: hetzner - port: 2375 - - YourHost2: - url: 100.78.180.21 - port: 2375 - -container: - dozzle: # Container name - link: https://github.com - icon: minecraft.png - tags: private,raspberry diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 00000000..60821b11 --- /dev/null +++ b/dependency-graph.mmd @@ -0,0 +1,247 @@ +--- +config: + flowchart: + defaultRenderer: elk +--- + +flowchart LR + +subgraph 0["src"] +1["index.ts"] +subgraph 2["handlers"] +3["index.ts"] +4["config.ts"] +subgraph P["modules"] +Q["logs-socket.ts"] +1I["starter.ts"] +1J["docker-socket.ts"] +end +1F["database.ts"] +1G["docker.ts"] +1H["logs.ts"] +1L["stacks.ts"] +1U["store.ts"] +1V["themes.ts"] +1W["utils.ts"] +end +subgraph B["core"] +subgraph C["database"] +D["index.ts"] +E["backup.ts"] +G["_dbState.ts"] +H["database.ts"] +M["helper.ts"] +S["config.ts"] +T["containerStats.ts"] +U["dockerHosts.ts"] +V["hostStats.ts"] +W["logs.ts"] +X["stacks.ts"] +Z["stores.ts"] +10["themes.ts"] +end +subgraph N["utils"] +O["logger.ts"] +Y["helpers.ts"] +15["calculations.ts"] +1C["change-me-checker.ts"] +1D["package-json.ts"] +end +subgraph 11["docker"] +12["scheduler.ts"] +13["store-container-stats.ts"] +14["client.ts"] +16["store-host-stats.ts"] +end +subgraph 18["plugins"] +19["plugin-manager.ts"] +1B["loader.ts"] +end +subgraph 1M["stacks"] +1N["controller.ts"] +1P["checker.ts"] +subgraph 1Q["operations"] +1R["runStackCommand.ts"] +1S["stackHelpers.ts"] +1T["stackStatus.ts"] +end +end +end +end +subgraph 5["~"] +subgraph 6["typings"] +7["database"] +8["docker"] +9["plugin"] +F["misc"] +17["dockerode"] +1K["websocket"] +1O["docker-compose"] +end +end +subgraph A["fs"] +J["promises"] +end +I["bun:sqlite"] +K["os"] +L["path"] +R["stream"] +1A["events"] +1E["package.json"] +1-->3 +3-->4 +3-->1F +3-->1G +3-->1H +3-->1I +3-->1L +3-->1U +3-->1V +3-->1W +4-->D +4-->E +4-->12 +4-->19 +4-->O +4-->1D +4-->7 +4-->8 +4-->9 +4-->A +D-->E +D-->S +D-->T +D-->H +D-->U +D-->V +D-->W +D-->X +D-->Z +D-->10 +E-->G +E-->H +E-->M +E-->O +E-->F +E-->A +H-->I +H-->A +H-->J +H-->K +H-->L +M-->G +M-->O +O-->Q +O-->G +O-->D +O-->7 +O-->L +Q-->O +Q-->7 +Q-->R +S-->H +S-->M +T-->H +T-->M +T-->7 +U-->H +U-->M +U-->8 +V-->H +V-->M +V-->8 +W-->H +W-->M +W-->7 +X-->Y +X-->H +X-->M +X-->7 +Y-->O +Z-->H +Z-->M +10-->O +10-->H +10-->M +10-->7 +12-->D +12-->13 +12-->16 +12-->O +12-->7 +13-->O +13-->D +13-->14 +13-->15 +13-->7 +14-->O +14-->8 +16-->D +16-->14 +16-->O +16-->8 +16-->17 +19-->O +19-->1B +19-->8 +19-->9 +19-->1A +1B-->1C +1B-->O +1B-->19 +1B-->A +1B-->L +1C-->O +1C-->J +1D-->1E +1F-->D +1G-->D +1G-->14 +1G-->O +1G-->8 +1G-->17 +1H-->D +1H-->O +1I-->1J +1I-->12 +1I-->19 +1J-->Q +1J-->D +1J-->14 +1J-->15 +1J-->O +1J-->7 +1J-->8 +1J-->1K +1L-->D +1L-->1N +1L-->O +1L-->7 +1N-->1J +1N-->1P +1N-->1R +1N-->1S +1N-->1T +1N-->D +1N-->O +1N-->7 +1N-->1O +1N-->J +1P-->D +1P-->O +1R-->1J +1R-->1S +1R-->O +1R-->1O +1S-->D +1S-->Y +1S-->O +1S-->1O +1T-->1R +1T-->D +1T-->O +1U-->Z +1V-->D +1V-->7 +1W-->O + diff --git a/dependency-graph.svg b/dependency-graph.svg new file mode 100644 index 00000000..495b4a21 --- /dev/null +++ b/dependency-graph.svg @@ -0,0 +1,1581 @@ + + + + + + +dependency-cruiser output + + +cluster_fs + +fs + + +cluster_src + +src + + +cluster_src/core + +core + + +cluster_src/core/database + +database + + +cluster_src/core/docker + +docker + + +cluster_src/core/plugins + +plugins + + +cluster_src/core/stacks + +stacks + + +cluster_src/core/stacks/operations + +operations + + +cluster_src/core/utils + +utils + + +cluster_src/handlers + +handlers + + +cluster_src/handlers/modules + +modules + + +cluster_~ + +~ + + +cluster_~/typings + +typings + + + +bun:sqlite + + +bun:sqlite + + + + + +events + + +events + + + + + +fs + + +fs + + + + + +fs/promises + + +promises + + + + + +os + + +os + + + + + +package.json + + +package.json + + + + + +path + + +path + + + + + +src/core/database/_dbState.ts + + +_dbState.ts + + + + + +src/core/database/backup.ts + + +backup.ts + + + + + +src/core/database/backup.ts->fs + + + + + +src/core/database/backup.ts->src/core/database/_dbState.ts + + + + + +src/core/database/database.ts + + +database.ts + + + + + +src/core/database/backup.ts->src/core/database/database.ts + + + + + +src/core/database/helper.ts + + +helper.ts + + + + + +src/core/database/backup.ts->src/core/database/helper.ts + + + + + + + +src/core/utils/logger.ts + + +logger.ts + + + + + +src/core/database/backup.ts->src/core/utils/logger.ts + + + + + + + +~/typings/misc + + +misc + + + + + +src/core/database/backup.ts->~/typings/misc + + + + + +src/core/database/database.ts->bun:sqlite + + + + + +src/core/database/database.ts->fs + + + + + +src/core/database/database.ts->fs/promises + + + + + +src/core/database/database.ts->os + + + + + +src/core/database/database.ts->path + + + + + +src/core/database/helper.ts->src/core/database/_dbState.ts + + + + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + + + + +src/core/utils/logger.ts->path + + + + + +src/core/utils/logger.ts->src/core/database/_dbState.ts + + + + + +~/typings/database + + +database + + + + + +src/core/utils/logger.ts->~/typings/database + + + + + +src/core/database/index.ts + + +index.ts + + + + + +src/core/utils/logger.ts->src/core/database/index.ts + + + + + + + +src/handlers/modules/logs-socket.ts + + +logs-socket.ts + + + + + +src/core/utils/logger.ts->src/handlers/modules/logs-socket.ts + + + + + + + +src/core/database/config.ts + + +config.ts + + + + + +src/core/database/config.ts->src/core/database/database.ts + + + + + +src/core/database/config.ts->src/core/database/helper.ts + + + + + + + +src/core/database/containerStats.ts + + +containerStats.ts + + + + + +src/core/database/containerStats.ts->src/core/database/database.ts + + + + + +src/core/database/containerStats.ts->src/core/database/helper.ts + + + + + + + +src/core/database/containerStats.ts->~/typings/database + + + + + +src/core/database/dockerHosts.ts + + +dockerHosts.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/database.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/helper.ts + + + + + + + +~/typings/docker + + +docker + + + + + +src/core/database/dockerHosts.ts->~/typings/docker + + + + + +src/core/database/hostStats.ts + + +hostStats.ts + + + + + +src/core/database/hostStats.ts->src/core/database/database.ts + + + + + +src/core/database/hostStats.ts->src/core/database/helper.ts + + + + + + + +src/core/database/hostStats.ts->~/typings/docker + + + + + +src/core/database/index.ts->src/core/database/backup.ts + + + + + + + +src/core/database/index.ts->src/core/database/database.ts + + + + + +src/core/database/index.ts->src/core/database/config.ts + + + + + + + +src/core/database/index.ts->src/core/database/containerStats.ts + + + + + + + +src/core/database/index.ts->src/core/database/dockerHosts.ts + + + + + + + +src/core/database/index.ts->src/core/database/hostStats.ts + + + + + + + +src/core/database/logs.ts + + +logs.ts + + + + + +src/core/database/index.ts->src/core/database/logs.ts + + + + + + + +src/core/database/stacks.ts + + +stacks.ts + + + + + +src/core/database/index.ts->src/core/database/stacks.ts + + + + + + + +src/core/database/stores.ts + + +stores.ts + + + + + +src/core/database/index.ts->src/core/database/stores.ts + + + + + + + +src/core/database/themes.ts + + +themes.ts + + + + + +src/core/database/index.ts->src/core/database/themes.ts + + + + + + + +src/core/database/logs.ts->src/core/database/database.ts + + + + + +src/core/database/logs.ts->src/core/database/helper.ts + + + + + + + +src/core/database/logs.ts->~/typings/database + + + + + +src/core/database/stacks.ts->src/core/database/database.ts + + + + + +src/core/database/stacks.ts->src/core/database/helper.ts + + + + + + + +src/core/database/stacks.ts->~/typings/database + + + + + +src/core/utils/helpers.ts + + +helpers.ts + + + + + +src/core/database/stacks.ts->src/core/utils/helpers.ts + + + + + + + +src/core/database/stores.ts->src/core/database/database.ts + + + + + +src/core/database/stores.ts->src/core/database/helper.ts + + + + + + + +src/core/database/themes.ts->src/core/database/database.ts + + + + + +src/core/database/themes.ts->src/core/database/helper.ts + + + + + + + +src/core/database/themes.ts->src/core/utils/logger.ts + + + + + + + +src/core/database/themes.ts->~/typings/database + + + + + +src/core/utils/helpers.ts->src/core/utils/logger.ts + + + + + +src/core/docker/client.ts + + +client.ts + + + + + +src/core/docker/client.ts->src/core/utils/logger.ts + + + + + +src/core/docker/client.ts->~/typings/docker + + + + + +src/core/docker/scheduler.ts + + +scheduler.ts + + + + + +src/core/docker/scheduler.ts->src/core/utils/logger.ts + + + + + +src/core/docker/scheduler.ts->~/typings/database + + + + + +src/core/docker/scheduler.ts->src/core/database/index.ts + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts + + + + + +src/core/docker/store-host-stats.ts + + +store-host-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/utils/logger.ts + + + + + +src/core/docker/store-container-stats.ts->~/typings/database + + + + + +src/core/docker/store-container-stats.ts->src/core/database/index.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/docker/client.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/utils/logger.ts + + + + + +src/core/docker/store-host-stats.ts->~/typings/docker + + + + + +src/core/docker/store-host-stats.ts->src/core/database/index.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/docker/client.ts + + + + + +~/typings/dockerode + + +dockerode + + + + + +src/core/docker/store-host-stats.ts->~/typings/dockerode + + + + + +src/core/plugins/loader.ts + + +loader.ts + + + + + +src/core/plugins/loader.ts->fs + + + + + +src/core/plugins/loader.ts->path + + + + + +src/core/plugins/loader.ts->src/core/utils/logger.ts + + + + + +src/core/utils/change-me-checker.ts + + +change-me-checker.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + + + + + + +src/core/utils/change-me-checker.ts->fs/promises + + + + + +src/core/utils/change-me-checker.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->events + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->~/typings/docker + + + + + +src/core/plugins/plugin-manager.ts->src/core/plugins/loader.ts + + + + + + + +~/typings/plugin + + +plugin + + + + + +src/core/plugins/plugin-manager.ts->~/typings/plugin + + + + + +src/core/stacks/checker.ts + + +checker.ts + + + + + +src/core/stacks/checker.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/checker.ts->src/core/database/index.ts + + + + + +src/core/stacks/controller.ts + + +controller.ts + + + + + +src/core/stacks/controller.ts->fs/promises + + + + + +src/core/stacks/controller.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/controller.ts->~/typings/database + + + + + +src/core/stacks/controller.ts->src/core/database/index.ts + + + + + +src/core/stacks/controller.ts->src/core/stacks/checker.ts + + + + + +src/handlers/modules/docker-socket.ts + + +docker-socket.ts + + + + + +src/core/stacks/controller.ts->src/handlers/modules/docker-socket.ts + + + + + +src/core/stacks/operations/runStackCommand.ts + + +runStackCommand.ts + + + + + +src/core/stacks/controller.ts->src/core/stacks/operations/runStackCommand.ts + + + + + +src/core/stacks/operations/stackHelpers.ts + + +stackHelpers.ts + + + + + +src/core/stacks/controller.ts->src/core/stacks/operations/stackHelpers.ts + + + + + +src/core/stacks/operations/stackStatus.ts + + +stackStatus.ts + + + + + +src/core/stacks/controller.ts->src/core/stacks/operations/stackStatus.ts + + + + + +~/typings/docker-compose + + +docker-compose + + + + + +src/core/stacks/controller.ts->~/typings/docker-compose + + + + + +src/handlers/modules/docker-socket.ts->src/core/utils/logger.ts + + + + + +src/handlers/modules/docker-socket.ts->~/typings/database + + + + + +src/handlers/modules/docker-socket.ts->~/typings/docker + + + + + +src/handlers/modules/docker-socket.ts->src/core/database/index.ts + + + + + +src/handlers/modules/docker-socket.ts->src/core/docker/client.ts + + + + + +src/handlers/modules/docker-socket.ts->src/core/utils/calculations.ts + + + + + +src/handlers/modules/docker-socket.ts->src/handlers/modules/logs-socket.ts + + + + + +~/typings/websocket + + +websocket + + + + + +src/handlers/modules/docker-socket.ts->~/typings/websocket + + + + + +src/core/stacks/operations/runStackCommand.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/operations/runStackCommand.ts->src/handlers/modules/docker-socket.ts + + + + + +src/core/stacks/operations/runStackCommand.ts->src/core/stacks/operations/stackHelpers.ts + + + + + +src/core/stacks/operations/runStackCommand.ts->~/typings/docker-compose + + + + + +src/core/stacks/operations/stackHelpers.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/operations/stackHelpers.ts->src/core/database/index.ts + + + + + +src/core/stacks/operations/stackHelpers.ts->src/core/utils/helpers.ts + + + + + +src/core/stacks/operations/stackHelpers.ts->~/typings/docker-compose + + + + + +src/core/stacks/operations/stackStatus.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/operations/stackStatus.ts->src/core/database/index.ts + + + + + +src/core/stacks/operations/stackStatus.ts->src/core/stacks/operations/runStackCommand.ts + + + + + +src/handlers/modules/logs-socket.ts->src/core/utils/logger.ts + + + + + + + +src/handlers/modules/logs-socket.ts->~/typings/database + + + + + +stream + + +stream + + + + + +src/handlers/modules/logs-socket.ts->stream + + + + + +src/core/utils/package-json.ts + + +package-json.ts + + + + + +src/core/utils/package-json.ts->package.json + + + + + +src/handlers/config.ts + + +config.ts + + + + + +src/handlers/config.ts->fs + + + + + +src/handlers/config.ts->src/core/database/backup.ts + + + + + +src/handlers/config.ts->src/core/utils/logger.ts + + + + + +src/handlers/config.ts->~/typings/database + + + + + +src/handlers/config.ts->~/typings/docker + + + + + +src/handlers/config.ts->src/core/database/index.ts + + + + + +src/handlers/config.ts->src/core/docker/scheduler.ts + + + + + +src/handlers/config.ts->src/core/plugins/plugin-manager.ts + + + + + +src/handlers/config.ts->~/typings/plugin + + + + + +src/handlers/config.ts->src/core/utils/package-json.ts + + + + + +src/handlers/database.ts + + +database.ts + + + + + +src/handlers/database.ts->src/core/database/index.ts + + + + + +src/handlers/docker.ts + + +docker.ts + + + + + +src/handlers/docker.ts->src/core/utils/logger.ts + + + + + +src/handlers/docker.ts->~/typings/docker + + + + + +src/handlers/docker.ts->src/core/database/index.ts + + + + + +src/handlers/docker.ts->src/core/docker/client.ts + + + + + +src/handlers/docker.ts->~/typings/dockerode + + + + + +src/handlers/index.ts + + +index.ts + + + + + +src/handlers/index.ts->src/handlers/config.ts + + + + + +src/handlers/index.ts->src/handlers/database.ts + + + + + +src/handlers/index.ts->src/handlers/docker.ts + + + + + +src/handlers/logs.ts + + +logs.ts + + + + + +src/handlers/index.ts->src/handlers/logs.ts + + + + + +src/handlers/modules/starter.ts + + +starter.ts + + + + + +src/handlers/index.ts->src/handlers/modules/starter.ts + + + + + +src/handlers/stacks.ts + + +stacks.ts + + + + + +src/handlers/index.ts->src/handlers/stacks.ts + + + + + +src/handlers/store.ts + + +store.ts + + + + + +src/handlers/index.ts->src/handlers/store.ts + + + + + +src/handlers/themes.ts + + +themes.ts + + + + + +src/handlers/index.ts->src/handlers/themes.ts + + + + + +src/handlers/utils.ts + + +utils.ts + + + + + +src/handlers/index.ts->src/handlers/utils.ts + + + + + +src/handlers/logs.ts->src/core/utils/logger.ts + + + + + +src/handlers/logs.ts->src/core/database/index.ts + + + + + +src/handlers/modules/starter.ts->src/core/docker/scheduler.ts + + + + + +src/handlers/modules/starter.ts->src/core/plugins/plugin-manager.ts + + + + + +src/handlers/modules/starter.ts->src/handlers/modules/docker-socket.ts + + + + + +src/handlers/stacks.ts->src/core/utils/logger.ts + + + + + +src/handlers/stacks.ts->~/typings/database + + + + + +src/handlers/stacks.ts->src/core/database/index.ts + + + + + +src/handlers/stacks.ts->src/core/stacks/controller.ts + + + + + +src/handlers/store.ts->src/core/database/stores.ts + + + + + +src/handlers/themes.ts->~/typings/database + + + + + +src/handlers/themes.ts->src/core/database/index.ts + + + + + +src/handlers/utils.ts->src/core/utils/logger.ts + + + + + +src/index.ts + + +index.ts + + + + + +src/index.ts->src/handlers/index.ts + + + + + diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..bc09e1bf --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,41 @@ +ARG BUILD_DATE +ARG VCS_REF + +FROM oven/bun:alpine AS base +WORKDIR /base + +COPY package.json ./ +RUN bun install -p + +COPY . . + +FROM oven/bun:alpine AS production +WORKDIR /DockStatAPI + +LABEL org.opencontainers.image.title="DockStatAPI" \ + org.opencontainers.image.description="A Dockerized DockStatAPI built with Bun on Alpine Linux." \ + org.opencontainers.image.version="3.0.0" \ + org.opencontainers.image.authors="info@itsnik.de" \ + org.opencontainers.image.vendor="Its4Nik" \ + org.opencontainers.image.licenses="CC BY-NC 4.0" \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.revision=$VCS_REF + +RUN apk add --no-cache curl + +HEALTHCHECK --timeout=10s --start-period=2s --retries=3 \ + CMD curl --fail http://localhost:3000/health || exit 1 + +VOLUME [ "/DockStatAPI/src/plugins" ] + +ENV NODE_ENV=production +ENV LOG_LEVEL=info + +EXPOSE 3000 + +COPY --from=base /base /DockStatAPI + +RUN adduser -D DockStatAPI && chown -R DockStatAPI:DockStatAPI /DockStatAPI +USER DockStatAPI + +ENTRYPOINT [ "bun", "run", "src/index.ts" ] diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml new file mode 100644 index 00000000..7d4e6ca8 --- /dev/null +++ b/docker/docker-compose.dev.yaml @@ -0,0 +1,53 @@ +name: "dockstatapi-dev" +services: + socket-proxy: + container_name: socket-proxy + image: lscr.io/linuxserver/socket-proxy:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: never + read_only: true + tmpfs: + - /run + ports: + - 2375:2375 + environment: + - ALLOW_START=1 + - ALLOW_STOP=1 + - ALLOW_RESTARTS=1 + - AUTH=1 + - BUILD=1 + - COMMIT=1 + - CONFIGS=1 + - CONTAINERS=1 + - DISABLE_IPV6=1 + - DISTRIBUTION=1 + - EVENTS=1 + - EXEC=1 + - IMAGES=1 + - INFO=1 + - NETWORKS=1 + - NODES=1 + - PING=1 + - PLUGINS=1 + - POST=1 + - PROXY_READ_TIMEOUT=240 + - SECRETS=1 + - SERVICES=1 + - SESSION=1 + - SWARM=1 + - SYSTEM=1 + - TASKS=1 + - VERSION=1 + - VOLUMES=1 + + sqlite-web: + container_name: sqlite-web + image: ghcr.io/coleifer/sqlite-web:latest + restart: never + ports: + - 8080:8080 + volumes: + - /home/nik/Documents/Code-local/dockstat-project/DockStat/data:/data:ro + environment: + - SQLITE_DATABASE=dockstatapi.db diff --git a/docker/docker-compose.unit-test.yaml b/docker/docker-compose.unit-test.yaml new file mode 100644 index 00000000..0dc445e3 --- /dev/null +++ b/docker/docker-compose.unit-test.yaml @@ -0,0 +1,42 @@ +name: "dockstatapi-unit-test" +services: + socket-proxy: + container_name: socket-proxy + image: lscr.io/linuxserver/socket-proxy:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + read_only: true + tmpfs: + - /run + ports: + - 2375:2375 + environment: + - ALLOW_START=1 + - ALLOW_STOP=1 + - ALLOW_RESTARTS=1 + - AUTH=1 + - BUILD=1 + - COMMIT=1 + - CONFIGS=1 + - CONTAINERS=1 + - DISABLE_IPV6=1 + - DISTRIBUTION=1 + - EVENTS=1 + - EXEC=1 + - IMAGES=1 + - INFO=1 + - NETWORKS=1 + - NODES=1 + - PING=1 + - PLUGINS=1 + - POST=1 + - PROXY_READ_TIMEOUT=240 + - SECRETS=1 + - SERVICES=1 + - SESSION=1 + - SWARM=1 + - SYSTEM=1 + - TASKS=1 + - VERSION=1 + - VOLUMES=1 diff --git a/dockstatapi.js b/dockstatapi.js deleted file mode 100644 index d06fb1e7..00000000 --- a/dockstatapi.js +++ /dev/null @@ -1,379 +0,0 @@ -const express = require('express'); -const path = require('path'); -const yaml = require('yamljs'); -const Docker = require('dockerode'); -const cors = require('cors'); -const fs = require('fs'); -const { exec } = require('child_process'); -const logger = require('./logger'); -const updateAvailable = require('./modules/updateAvailable') -const app = express(); -const port = 7070; -const key = process.env.SECRET || 'CHANGE-ME'; -const skipAuth = process.env.SKIP_AUTH || 'True' -const cupUrl = process.env.CUP_URL || 'null' - -let config = yaml.load('./config/hosts.yaml'); -let hosts = config.hosts; -let containerConfigs = config.container || {}; -let maxlogsize = config.log.logsize || 1; -let LogAmount = config.log.LogCount || 5; -let queryInterval = config.mintimeout || 5000; -let latestStats = {}; -let hostQueues = {}; -let previousNetworkStats = {}; -let generalStats = {}; -let previousContainerStates = {}; -let previousRunningContainers = {}; - - -app.use(cors()); -app.use(express.json()); - -const authenticateHeader = (req, res, next) => { - const authHeader = req.headers['authorization']; - - if (skipAuth === 'True') { - next(); - } else { - if (!authHeader || authHeader !== key) { - logger.error(`${authHeader} != ${key}`); - return res.status(401).json({ error: "Unauthorized" }); - } - else { - next(); - } - } -}; - -function createDockerClient(hostConfig) { - return new Docker({ - host: hostConfig.url, - port: hostConfig.port, - }); -} - -function getTagColor(tag) { - const tagsConfig = config.tags || {}; - return tagsConfig[tag] || ''; -} - -async function getContainerStats(docker, containerId) { - const container = docker.getContainer(containerId); - return new Promise((resolve, reject) => { - container.stats({ stream: false }, (err, stats) => { - if (err) return reject(err); - resolve(stats); - }); - }); -} - -async function handleContainerStateChanges(hostName, currentContainers) { - const currentRunningContainers = currentContainers - .filter(container => container.state === 'running') - .reduce((map, container) => { - map[container.id] = container; - return map; - }, {}); - - const previousHostContainers = previousRunningContainers[hostName] || {}; - - // Check for containers that have been removed or exited - for (const containerId of Object.keys(previousHostContainers)) { - const container = previousHostContainers[containerId]; - if (!currentRunningContainers[containerId]) { - if (container.state === 'running') { - // Container removed - exec(`bash ./scripts/notify.sh REMOVE ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing REMOVE notify.sh: ${error.message}`); - } else { - logger.info(`Container removed: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - else if (container.state === 'exited') { - // Container exited - exec(`bash ./scripts/notify.sh EXIT ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing EXIT notify.sh: ${error.message}`); - } else { - logger.info(`Container exited: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - } - } - - // Check for new containers or state changes - for (const containerId of Object.keys(currentRunningContainers)) { - const container = currentRunningContainers[containerId]; - const previousContainer = previousHostContainers[containerId]; - - if (!previousContainer) { - // New container added - exec(`bash ./scripts/notify.sh ADD ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing ADD notify.sh: ${error.message}`); - } else { - logger.info(`Container added: ${container.name} (${containerId}) to host ${hostName}`); - logger.info(stdout); - } - }); - } else if (previousContainer.state !== container.state) { - // Container state has changed - const newState = container.state; - if (newState === 'exited') { - exec(`bash ./scripts/notify.sh EXIT ${containerId} ${container.name} ${hostName} ${newState}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing EXIT notify.sh: ${error.message}`); - } else { - logger.info(`Container exited: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } else { - // Any other state change - exec(`bash ./scripts/notify.sh ANY ${containerId} ${container.name} ${hostName} ${newState}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing ANY notify.sh: ${error.message}`); - } else { - logger.info(`Container state changed to ${newState}: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - } - } - - // Update the previous state for the next comparison - previousRunningContainers[hostName] = currentRunningContainers; -} - -async function queryHostStats(hostName, hostConfig) { - logger.debug(`Querying Docker stats for host: ${hostName} (${hostConfig.url}:${hostConfig.port})`); - - const docker = createDockerClient(hostConfig); - - try { - const info = await docker.info(); - const totalMemory = info.MemTotal; - const totalCPUs = info.NCPU; - const containers = await docker.listContainers({ all: true }); - - const statsPromises = containers.map(async (container) => { - try { - const containerName = container.Names[0].replace('/', ''); - const containerState = container.State; - const updateAvailableFlag = await updateAvailable(container.Image, cupUrl); - let networkMode = container.HostConfig.NetworkMode; - - // Check if network mode is in the format "container:IDXXXXXXXX" - if (networkMode.startsWith("container:")) { - const linkedContainerId = networkMode.split(":")[1]; - const linkedContainer = await docker.getContainer(linkedContainerId).inspect(); - const linkedContainerName = linkedContainer.Name.replace('/', ''); // Remove leading slash - - networkMode = `Container: ${linkedContainerName}`; // Format the network mode - } - - if (containerState !== 'running') { - previousContainerStates[container.Id] = containerState; - return { - name: containerName, - id: container.Id, - hostName: hostName, - state: containerState, - image: container.Image, - update_available: updateAvailableFlag || false, - cpu_usage: 0, - mem_usage: 0, - mem_limit: 0, - net_rx: 0, - net_tx: 0, - current_net_rx: 0, - current_net_tx: 0, - networkMode: networkMode, - link: containerConfigs[containerName]?.link || '', - icon: containerConfigs[containerName]?.icon || '', - tags: getTagColor(containerConfigs[containerName]?.tags || ''), - }; - } - - // Fetch container stats for running containers - const containerStats = await getContainerStats(docker, container.Id); - const containerCpuUsage = containerStats.cpu_stats.cpu_usage.total_usage; - const containerMemoryUsage = containerStats.memory_stats.usage; - - let netRx = 0, netTx = 0, currentNetRx = 0, currentNetTx = 0; - - if (networkMode !== 'host' && containerStats.networks?.eth0) { - const previousStats = previousNetworkStats[container.Id] || { rx_bytes: 0, tx_bytes: 0 }; - currentNetRx = containerStats.networks.eth0.rx_bytes - previousStats.rx_bytes; - currentNetTx = containerStats.networks.eth0.tx_bytes - previousStats.tx_bytes; - - previousNetworkStats[container.Id] = { - rx_bytes: containerStats.networks.eth0.rx_bytes, - tx_bytes: containerStats.networks.eth0.tx_bytes, - }; - - netRx = containerStats.networks.eth0.rx_bytes; - netTx = containerStats.networks.eth0.tx_bytes; - } - - previousContainerStates[container.Id] = containerState; - const config = containerConfigs[containerName] || {}; - - const tagArray = (config.tags || '') - .split(',') - .map(tag => { - const color = getTagColor(tag); - return color ? `${tag}:${color}` : tag; - }) - .join(','); - - return { - name: containerName, - id: container.Id, - hostName: hostName, - image: container.Image, - update_available: updateAvailableFlag || false, - state: containerState, - cpu_usage: containerCpuUsage, - mem_usage: containerMemoryUsage, - mem_limit: containerStats.memory_stats.limit, - net_rx: netRx, - net_tx: netTx, - current_net_rx: currentNetRx, - current_net_tx: currentNetTx, - networkMode: networkMode, - link: config.link || '', - icon: config.icon || '', - tags: tagArray, - }; - } catch (err) { - logger.error(`Failed to fetch stats for container ${container.Names[0]} (${container.Id}): ${err.message}`); - return null; - } - }); - - const hostStats = await Promise.all(statsPromises); - const validStats = hostStats.filter(stat => stat !== null); - - const totalCpuUsage = validStats.reduce((acc, container) => acc + parseFloat(container.cpu_usage), 0); - const totalMemoryUsage = validStats.reduce((acc, container) => acc + container.mem_usage, 0); - const memoryUsagePercent = ((totalMemoryUsage / totalMemory) * 100).toFixed(2); - - generalStats[hostName] = { - containerCount: validStats.length, - totalCPUs: totalCPUs, - totalMemory: totalMemory, - cpuUsage: totalCpuUsage, - memoryUsage: memoryUsagePercent, - }; - - latestStats[hostName] = validStats; - - logger.debug(`Fetched stats for ${validStats.length} containers from ${hostName}`); - - // Handle container state changes - await handleContainerStateChanges(hostName, validStats); - } catch (err) { - logger.error(`Failed to fetch containers from ${hostName}: ${err.message}`); - } -} - - -async function handleHostQueue(hostName, hostConfig) { - while (true) { - await queryHostStats(hostName, hostConfig); - await new Promise(resolve => setTimeout(resolve, queryInterval)); - } -} - -// Initialize the host queues -function initializeHostQueues() { - for (const [hostName, hostConfig] of Object.entries(hosts)) { - hostQueues[hostName] = handleHostQueue(hostName, hostConfig); - } -} - -// Dynamically reloads the yaml file -function reloadConfig() { - for (const hostName in hostQueues) { - hostQueues[hostName] = null; - } - try { - config = yaml.load('./config/hosts.yaml'); - hosts = config.hosts; - containerConfigs = config.container || {}; - maxlogsize = config.log.logsize || 1; - LogAmount = config.log.LogCount || 5; - queryInterval = config.mintimeout || 5000; - - logger.info('Configuration reloaded successfully.'); - - initializeHostQueues(); - } catch (err) { - logger.error(`Failed to reload configuration: ${err.message}`); - } -} - -// Watch the YAML file for changes and reload the config -fs.watchFile('./config/hosts.yaml', (curr, prev) => { - if (curr.mtime !== prev.mtime) { - logger.info('Detected change in configuration file. Reloading...'); - reloadConfig(); - } -}); - -// Endpoint to get stats -app.get('/stats', authenticateHeader, (req, res) => { - res.json(latestStats); -}); - -// Endpoint for general Host based statistics -app.get('/hosts', authenticateHeader, (req, res) => { - res.json(generalStats); -}); - -// Read Only config endpoint -app.get('/config', authenticateHeader, (req, res) => { - const filePath = path.join(__dirname, './config/hosts.yaml'); - res.set('Content-Type', 'text/plain'); // Keep as plain text - fs.readFile(filePath, 'utf8', (err, data) => { - logger.debug('Requested config file: ' + filePath); - if (err) { - logger.error(err); - res.status(500).send('Error reading file'); - } else { - res.send(data); - } - }); -}); - -app.get('/', (req, res) => { - res.redirect(301, '/stats'); -}); - -app.get('/status', (req, res) => { - logger.info("Healthcheck requested"); - return res.status(200).send('UP'); -}); - -// Start the server and log the startup message -app.listen(port, () => { - logger.info('=============================== DockStat ===============================') - logger.info(`DockStatAPI is running on http://localhost:${port}/stats`); - logger.info(`Minimum timeout between stats queries is: ${queryInterval} milliseconds`); - logger.info(`The max size for Log files is: ${maxlogsize}MB`) - logger.info(`The amount of log files to keep is: ${LogAmount}`); - logger.info(`Secret Key: ${key}`) - logger.info(`Cup URL: ${cupUrl}`) - logger.info("Press Ctrl+C to stop the server."); - logger.info('========================================================================') -}); - -initializeHostQueues(); diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index df95b988..00000000 --- a/entrypoint.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -SECRET="${SECRET//\"}" - -export SECRET - -exec npm run start \ No newline at end of file diff --git a/logger.js b/logger.js deleted file mode 100644 index ebaacc38..00000000 --- a/logger.js +++ /dev/null @@ -1,24 +0,0 @@ -const winston = require('winston'); -const yaml = require('yamljs'); -const config = yaml.load('./config/hosts.yaml'); - -const maxlogsize = config.log.logsize || 1; -const LogAmount = config.log.LogCount || 5; - -const logger = winston.createLogger({ - level: 'debug', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json() - ), - transports: [ - new winston.transports.Console(), - new winston.transports.File({ - filename: './logs/dockstat.log', - maxsize: 1024 * 1024 * maxlogsize, - maxFiles: LogAmount - }) - ] -}); - -module.exports = logger; \ No newline at end of file diff --git a/modules/updateAvailable.js b/modules/updateAvailable.js deleted file mode 100644 index 1a25ce3e..00000000 --- a/modules/updateAvailable.js +++ /dev/null @@ -1,32 +0,0 @@ -const logger = require('../logger'); - -async function getData(target, url) { - - if (url === 'null') { - return false; - } - else { - try { - const response = await fetch(`${url}/json`, { - method: "GET" - }); - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); - } - - const json = await response.json(); - - const images = json.images; - - for (const image in images) { - if (target === image) { - return images.hasOwnProperty(target); - } - } - } catch (error) { - logger.error(error.message); - } - } -} - -module.exports = getData; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 37c8cf27..00000000 --- a/package-lock.json +++ /dev/null @@ -1,1548 +0,0 @@ -{ - "name": "dockstatapi", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "dockstatapi", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "child_process": "^1.0.2", - "cors": "^2.8.5", - "dockerode": "^4.0.2", - "express": "^4.21.0", - "node-fetch": "^3.3.2", - "winston": "^3.14.2", - "yamljs": "^0.3.0" - } - }, - "node_modules/@balena/dockerignore": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", - "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "license": "Apache-2.0" - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/child_process": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", - "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==", - "license": "ISC" - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cpu-features": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", - "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "buildcheck": "~0.0.6", - "nan": "^2.19.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/docker-modem": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", - "integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.1", - "readable-stream": "^3.5.0", - "split-ca": "^1.0.1", - "ssh2": "^1.15.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dockerode": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz", - "integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==", - "license": "Apache-2.0", - "dependencies": { - "@balena/dockerignore": "^1.0.2", - "docker-modem": "^5.0.3", - "tar-fs": "~2.0.1" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/logform": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", - "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", - "license": "MIT", - "optional": true - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", - "license": "ISC" - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/ssh2": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", - "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==", - "hasInstallScript": true, - "dependencies": { - "asn1": "^0.2.6", - "bcrypt-pbkdf": "^1.0.2" - }, - "engines": { - "node": ">=10.16.0" - }, - "optionalDependencies": { - "cpu-features": "~0.0.9", - "nan": "^2.18.0" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/winston": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", - "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.6.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.7.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", - "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", - "license": "MIT", - "dependencies": { - "logform": "^2.6.1", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" - } - } - } -} \ No newline at end of file diff --git a/package.json b/package.json index 372caad0..94eec8d0 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,57 @@ { "name": "dockstatapi", - "version": "1.0.0", - "description": "API for docker hosts using dockerode", - "main": "dockerstatsapi.js", + "author": { + "email": "info@itsnik.de", + "name": "ItsNik", + "url": "https://github.com/Its4Nik" + }, + "license": "CC BY-NC 4.0", + "contributors": [], + "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", + "version": "3.0.0", "scripts": { - "start": "node dockstatapi.js", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", + "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", + "build": "bun build --target bun src/index.ts --outdir ./dist", + "build:prod": "NODE_ENV=production bun build --no-native --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts", + "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", + "clean": "bun run clean:win || bun run clean:lin", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q stacks/* && cmd /c del /Q reports/markdown/*.md && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && sudo rm -rf stacks/* && rm -f reports/markdown/*.md && echo 'success'", + "knip": "knip", + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, - "keywords": [], - "author": "Its4Nik", - "license": "ISC", "dependencies": { - "child_process": "^1.0.2", - "cors": "^2.8.5", - "dockerode": "^4.0.2", - "express": "^4.21.0", - "node-fetch": "^3.3.2", - "winston": "^3.14.2", - "yamljs": "^0.3.0" - } -} \ No newline at end of file + "chalk": "^5.4.1", + "date-fns": "^4.1.0", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.7", + "js-yaml": "^4.1.0", + "knip": "latest", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.8.0" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@its_4_nik/gitai": "^1.1.14", + "@types/bun": "latest", + "@types/dockerode": "^3.3.42", + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.16.0", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0" + }, + "module": "src/index.js", + "trustedDependencies": [ + "protobufjs" + ], + "type": "module", + "private": true +} diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..39b107d4 --- /dev/null +++ b/public/404.html @@ -0,0 +1,99 @@ + + + + + + + 404 - Page Not Found + + + + +
+ +
404
+
+ Oops! The page you're looking for doesn't exist. +
+
+ + +
+
+ + + \ No newline at end of file diff --git a/public/DockStat.png b/public/DockStat.png new file mode 100644 index 00000000..d375bd49 Binary files /dev/null and b/public/DockStat.png differ diff --git a/scripts/install_apprise.sh b/scripts/install_apprise.sh deleted file mode 100644 index 7506d0e8..00000000 --- a/scripts/install_apprise.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -VENV_DIR="/api" - -apk update -apk add python3 py3-pip py3-virtualenv - -python3 -m venv "$VENV_DIR" - -. "$VENV_DIR/bin/activate" - -pip install apprise - -deactivate - -echo "Apprise has been successfully installed in the virtual environment." diff --git a/scripts/notify.sh b/scripts/notify.sh deleted file mode 100755 index 54dc2262..00000000 --- a/scripts/notify.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -NOTIFY_TYPE=$1 # ADD, REMOVE, EXIT, ANY -CONTAINER_ID=$2 # Container ID -CONTAINER_NAME=$3 # Container Name -HOST=$4 # Host Name -STATE=$5 # Current State - -ADD_MESSAGE="${ADD_MESSAGE:-🆕 Container Added: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -REMOVE_MESSAGE="${REMOVE_MESSAGE:-🚫 Container Removed: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -EXIT_MESSAGE="${EXIT_MESSAGE:-❌ Container Exited: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -ANY_MESSAGE="${ANY_MESSAGE:-⚠️ Container State Changed: $CONTAINER_NAME ($CONTAINER_ID) on $HOST - New State: $STATE}" - -case "$NOTIFY_TYPE" in - ADD) - MESSAGE="$ADD_MESSAGE" - ;; - REMOVE) - MESSAGE="$REMOVE_MESSAGE" - ;; - EXIT) - MESSAGE="$EXIT_MESSAGE" - ;; - ANY) - MESSAGE="$ANY_MESSAGE" - ;; - *) - MESSAGE="Unknown action for $CONTAINER_NAME ($CONTAINER_ID) on $HOST" - ;; -esac - -if [[ ! -f ./config/apprise_config.yml ]]; then - echo -n "No Apprise configuration found, aborting." - exit 1 -fi - -# Send notification via Apprise - -### PYTHON ENVIRONMENT: ### -. /api/bin/activate - -apprise -b "$MESSAGE" --config ./config/apprise_config.yml - -deactivate -########################### - -exit 0 diff --git a/src/core/database/_dbState.ts b/src/core/database/_dbState.ts new file mode 100644 index 00000000..e159ca05 --- /dev/null +++ b/src/core/database/_dbState.ts @@ -0,0 +1,5 @@ +export let backupInProgress = false; + +export function setBackupInProgress(val: boolean) { + backupInProgress = val; +} diff --git a/src/core/database/backup.ts b/src/core/database/backup.ts new file mode 100644 index 00000000..df6a744a --- /dev/null +++ b/src/core/database/backup.ts @@ -0,0 +1,163 @@ +import { copyFileSync, existsSync, readdirSync } from "node:fs"; +import { logger } from "~/core/utils/logger"; +import type { BackupInfo } from "~/typings/misc"; +import { backupInProgress, setBackupInProgress } from "./_dbState"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +export const backupDir = "data/"; + +export async function backupDatabase(): Promise { + if (backupInProgress) { + logger.error("Backup attempt blocked: Another backup already in progress"); + throw new Error("Backup already in progress"); + } + + logger.debug("Starting database backup process..."); + setBackupInProgress(true); + + try { + logger.debug("Executing WAL checkpoint..."); + db.exec("PRAGMA wal_checkpoint(FULL);"); + logger.debug("WAL checkpoint completed successfully"); + + const now = new Date(); + const day = String(now.getDate()).padStart(2, "0"); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const year = now.getFullYear(); + const dateStr = `${day}-${month}-${year}`; + logger.debug(`Using date string for backup: ${dateStr}`); + + logger.debug(`Scanning backup directory: ${backupDir}`); + const files = readdirSync(backupDir); + logger.debug(`Found ${files.length} files in backup directory`); + + const regex = new RegExp( + `^dockstatapi-${day}-${month}-${year}-(\\d+)\\.db\\.bak$`, + ); + let maxBackupNum = 0; + + for (const file of files) { + const match = file.match(regex); + if (match?.[1]) { + const num = Number.parseInt(match[1], 10); + logger.debug(`Found existing backup file: ${file} with number ${num}`); + if (num > maxBackupNum) { + maxBackupNum = num; + } + } else { + logger.debug(`Skipping non-matching file: ${file}`); + } + } + + logger.debug(`Maximum backup number found: ${maxBackupNum}`); + const backupNumber = maxBackupNum + 1; + const backupFilename = `${backupDir}dockstatapi-${dateStr}-${backupNumber}.db.bak`; + logger.debug(`Generated backup filename: ${backupFilename}`); + + logger.debug(`Attempting to copy database to ${backupFilename}`); + try { + copyFileSync(`${backupDir}dockstatapi.db`, backupFilename); + logger.info(`Backup created successfully: ${backupFilename}`); + logger.debug("File copy operation completed without errors"); + } catch (error) { + logger.error(`Failed to create backup file: ${(error as Error).message}`); + throw new Error(error as string); + } + + return backupFilename; + } finally { + setBackupInProgress(false); + logger.debug("Backup process completed, in progress flag reset"); + } +} + +export function restoreDatabase(backupFilename: string): void { + const backupFile = `${backupDir}${backupFilename}`; + + if (backupInProgress) { + logger.error("Restore attempt blocked: Backup in progress"); + throw new Error("Backup in progress. Cannot restore."); + } + + logger.debug(`Starting database restore from ${backupFile}`); + + if (!existsSync(backupFile)) { + logger.error(`Backup file not found: ${backupFile}`); + throw new Error(`Backup file ${backupFile} does not exist.`); + } + + setBackupInProgress(true); + try { + executeDbOperation( + "restore", + () => { + logger.debug(`Attempting to restore database from ${backupFile}`); + try { + copyFileSync(backupFile, `${backupDir}dockstatapi.db`); + logger.info(`Database restored successfully from: ${backupFilename}`); + logger.debug("Database file replacement completed"); + } catch (error) { + logger.error(`Restore failed: ${(error as Error).message}`); + throw new Error(error as string); + } + }, + () => { + if (backupInProgress) { + logger.error("Database operation attempted during restore"); + throw new Error("Cannot perform database operations during restore"); + } + }, + ); + } finally { + setBackupInProgress(false); + logger.debug("Restore process completed, in progress flag reset"); + } +} + +export const findLatestBackup = (): string => { + logger.debug(`Searching for latest backup in directory: ${backupDir}`); + + const files = readdirSync(backupDir); + logger.debug(`Found ${files.length} files to process`); + + const backups = files + .map((file): BackupInfo | null => { + const match = file.match( + /^dockstatapi-(\d{2})-(\d{2})-(\d{4})-(\d+)\.db\.bak$/, + ); + if (!match) { + logger.debug(`Skipping non-backup file: ${file}`); + return null; + } + + const date = new Date( + Number(match[3]), + Number(match[2]) - 1, + Number(match[1]), + ); + logger.debug( + `Found backup file: ${file} with date ${date.toISOString()}`, + ); + + return { + filename: file, + date, + backupNum: Number(match[4]), + }; + }) + .filter((backup): backup is BackupInfo => backup !== null) + .sort((a, b) => { + const dateDiff = b.date.getTime() - a.date.getTime(); + return dateDiff !== 0 ? dateDiff : b.backupNum - a.backupNum; + }); + + if (!backups.length) { + logger.error("No valid backup files found"); + throw new Error("No backups available"); + } + + const latestBackup = backups[0].filename; + logger.debug(`Determined latest backup file: ${latestBackup}`); + return latestBackup; +}; diff --git a/src/core/database/config.ts b/src/core/database/config.ts new file mode 100644 index 00000000..0fa66da7 --- /dev/null +++ b/src/core/database/config.ts @@ -0,0 +1,49 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + update: db.prepare( + "UPDATE config SET fetching_interval = ?, keep_data_for = ?", + ), + select: db.prepare("SELECT keep_data_for, fetching_interval FROM config"), + deleteOld: db.prepare( + `DELETE FROM container_stats WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), + deleteOldLogs: db.prepare( + `DELETE FROM backend_log_entries WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), +}; + +export function updateConfig(fetching_interval: number, keep_data_for: number) { + return executeDbOperation( + "Update Config", + () => stmt.update.run(fetching_interval, keep_data_for), + () => { + if ( + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + throw new TypeError("Invalid config parameters"); + } + }, + ); +} + +export function getConfig() { + return executeDbOperation("Get Config", () => stmt.select.all()); +} + +export function deleteOldData(days: number) { + return executeDbOperation( + "Delete Old Data", + () => { + db.transaction(() => { + stmt.deleteOld.run(days); + stmt.deleteOldLogs.run(days); + })(); + }, + () => { + if (typeof days !== "number") throw new TypeError("Invalid days type"); + }, + ); +} diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts new file mode 100644 index 00000000..a8466701 --- /dev/null +++ b/src/core/database/containerStats.ts @@ -0,0 +1,43 @@ +import type { container_stats } from "~/typings/database"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const insert = db.prepare(` + INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) +`); + +const get = db.prepare("SELECT * FROM container_stats"); + +export function addContainerStats(stats: container_stats) { + return executeDbOperation( + "Add Container Stats", + () => + insert.run( + stats.id, + stats.hostId, + stats.name, + stats.image, + stats.status, + stats.state, + stats.cpu_usage, + stats.memory_usage, + ), + () => { + if ( + typeof stats.id !== "string" || + typeof stats.hostId !== "number" || + typeof stats.cpu_usage !== "number" || + typeof stats.memory_usage !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + }, + ); +} + +export function getContainerStats(): container_stats[] { + return executeDbOperation("Get Container Stats", () => + get.all(), + ) as container_stats[]; +} diff --git a/src/core/database/database.ts b/src/core/database/database.ts new file mode 100644 index 00000000..f5949b41 --- /dev/null +++ b/src/core/database/database.ts @@ -0,0 +1,172 @@ +import { Database } from "bun:sqlite"; +import { existsSync } from "node:fs"; +import { mkdir } from "node:fs/promises"; +import { userInfo } from "node:os"; +import path from "node:path"; + +const dataFolder = path.join(process.cwd(), "data"); + +const username = userInfo().username; +const gid = userInfo().gid; +const uid = userInfo().uid; + +export let db: Database; + +try { + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); + + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } + + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); + + db.exec("PRAGMA journal_mode = WAL;"); +} catch (error) { + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); +} + +export function init() { + db.exec(` + CREATE TABLE IF NOT EXISTS backend_log_entries ( + timestamp STRING NOT NULL, + level TEXT NOT NULL, + message TEXT NOT NULL, + file TEXT NOT NULL, + line NUMBER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS stacks_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + version INTEGER NOT NULL, + custom BOOLEAN NOT NULL, + source TEXT NOT NULL, + compose_spec TEXT NOT NULL, + status TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS docker_hosts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + hostAddress TEXT NOT NULL, + secure BOOLEAN NOT NULL + ); + + CREATE TABLE IF NOT EXISTS host_stats ( + hostId INTEGER NOT NULL, + hostName TEXT NOT NULL, + dockerVersion TEXT NOT NULL, + apiVersion TEXT NOT NULL, + os TEXT NOT NULL, + architecture TEXT NOT NULL, + totalMemory INTEGER NOT NULL, + totalCPU INTEGER NOT NULL, + labels TEXT NOT NULL, + containers INTEGER NOT NULL, + containersRunning INTEGER NOT NULL, + containersStopped INTEGER NOT NULL, + containersPaused INTEGER NOT NULL, + images INTEGER NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS container_stats ( + id TEXT NOT NULL, + hostId TEXT NOT NULL, + name TEXT NOT NULL, + image TEXT NOT NULL, + status TEXT NOT NULL, + state TEXT NOT NULL, + cpu_usage FLOAT NOT NULL, + memory_usage, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS config ( + keep_data_for NUMBER NOT NULL, + fetching_interval NUMBER NOT NULL ); + + CREATE TABLE IF NOT EXISTS store_repos ( + slug TEXT NOT NULL, + base TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS themes ( + name TEXT PRIMARY KEY, + creator TEXT NOT NULL, + vars TEXT NOT NULL, + tags TEXT NOT NULL + ) + `); + + const themeRows = db + .prepare("SELECT COUNT(*) AS count FROM themes") + .get() as { count: number }; + + const defaultCss = ` + .root, + #root, + #docs-root { + --accent: #818cf9; + --muted-bg: #0f172a; + --gradient-from: #1e293b; + --gradient-to: #334155; + --border: #334155; + --border-accent: rgba(129, 140, 249, 0.3); + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --text-tertiary: #64748b; + --state-success: #4ade80; + --state-warning: #facc15; + --state-error: #f87171; + --state-info: #38bdf8; + --shadow-glow: 0 0 15px rgba(129, 140, 249, 0.5); + --background-gradient: linear-gradient(145deg, #0f172a 0%, #1e293b 100%); + } + `; + + if (themeRows.count === 0) { + db.prepare( + "INSERT INTO themes (name, creator, vars, tags) VALUES (?,?,?,?)", + ).run("default", "Its4Nik", defaultCss, "[default]"); + } + + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } + + const storeRow = db + .prepare("SELECT COUNT(*) AS count FROM store_repos") + .get() as { count: number }; + + if (storeRow.count === 0) { + db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( + "DockStacks", + "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", + ); + } +} + +init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts new file mode 100644 index 00000000..18180c54 --- /dev/null +++ b/src/core/database/dockerHosts.ts @@ -0,0 +1,62 @@ +import type { DockerHost } from "~/typings/docker"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), +}; + +export function addDockerHost(host: DockerHost) { + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + }, + ); +} + +export function getDockerHosts(): DockerHost[] { + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); +} +1; +export function updateDockerHost(host: DockerHost) { + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + }, + ); +} + +export function deleteDockerHost(id: number) { + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + }, + ); +} diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts new file mode 100644 index 00000000..1f1cabd9 --- /dev/null +++ b/src/core/database/helper.ts @@ -0,0 +1,28 @@ +import { logger } from "~/core/utils/logger"; +import { backupInProgress } from "./_dbState"; + +export function executeDbOperation( + label: string, + operation: () => T, + validate?: () => void, + dontLog?: boolean, +): T { + if (backupInProgress && label !== "backup" && label !== "restore") { + throw new Error( + `backup in progress Database operation not allowed: ${label}`, + ); + } + const startTime = Date.now(); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ⏳`); + } + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + } + return result; +} diff --git a/src/core/database/hostStats.ts b/src/core/database/hostStats.ts new file mode 100644 index 00000000..49e6ce93 --- /dev/null +++ b/src/core/database/hostStats.ts @@ -0,0 +1,65 @@ +import type { HostStats } from "~/typings/docker"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const insert = db.prepare(` + INSERT INTO host_stats ( + hostId, hostName, dockerVersion, apiVersion, os, architecture, + totalMemory, totalCPU, labels, containers, containersRunning, + containersStopped, containersPaused, images + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +`); + +const selectStmt = db.prepare(` + SELECT * + FROM host_stats +`); + +export function addHostStats(stats: HostStats) { + return executeDbOperation( + "Update Host Stats", + () => + insert.run( + stats.hostId, + stats.hostName, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + JSON.stringify(stats.labels), + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images, + ), + () => { + if ( + typeof stats.hostId !== "number" || + typeof stats.hostName !== "string" || + typeof stats.dockerVersion !== "string" || + typeof stats.apiVersion !== "string" || + typeof stats.os !== "string" || + typeof stats.architecture !== "string" || + typeof stats.totalMemory !== "number" || + typeof stats.totalCPU !== "number" || + typeof JSON.stringify(stats.labels) !== "string" || + typeof stats.containers !== "number" || + typeof stats.containersRunning !== "number" || + typeof stats.containersStopped !== "number" || + typeof stats.containersPaused !== "number" || + typeof stats.images !== "number" + ) { + throw new TypeError(`Invalid Host Stats! - ${stats}`); + } + }, + ); +} + +export function getHostStats(): HostStats[] { + return executeDbOperation("Get Host Stats", () => + selectStmt.all(), + ) as HostStats[]; +} diff --git a/src/core/database/index.ts b/src/core/database/index.ts new file mode 100644 index 00000000..7bc61473 --- /dev/null +++ b/src/core/database/index.ts @@ -0,0 +1,27 @@ +import { init } from "~/core/database/database"; + +init(); + +import * as backup from "~/core/database/backup"; +import * as config from "~/core/database/config"; +import * as containerStats from "~/core/database/containerStats"; +import * as dockerHosts from "~/core/database/dockerHosts"; +import * as hostStats from "~/core/database/hostStats"; +import * as logs from "~/core/database/logs"; +import * as stacks from "~/core/database/stacks"; +import * as stores from "~/core/database/stores"; +import * as themes from "~/core/database/themes"; + +export const dbFunctions = { + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, + ...stores, + ...themes, +}; + +export type dbFunctions = typeof dbFunctions; diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts new file mode 100644 index 00000000..e6c30d1a --- /dev/null +++ b/src/core/database/logs.ts @@ -0,0 +1,79 @@ +import type { log_message } from "~/typings/database"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + insert: db.prepare( + "INSERT INTO backend_log_entries (level, timestamp, message, file, line) VALUES (?, ?, ?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT level, timestamp, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", + ), + selectByLevel: db.prepare( + "SELECT level, timestamp, message, file, line FROM backend_log_entries WHERE level = ?", + ), + deleteAll: db.prepare("DELETE FROM backend_log_entries"), + deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), +}; + +function convertToLogMessage(row: log_message): log_message { + return { + level: row.level, + timestamp: row.timestamp, + message: row.message, + file: row.file, + line: row.line, + }; +} + +export function addLogEntry(data: log_message) { + return executeDbOperation( + "Add Log Entry", + () => + stmt.insert.run( + data.level, + data.timestamp, + data.message, + data.file, + data.line, + ), + () => { + if ( + typeof data.level !== "string" || + typeof data.timestamp !== "string" || + typeof data.message !== "string" || + typeof data.file !== "string" || + typeof data.line !== "number" + ) { + throw new TypeError( + `Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}`, + ); + } + }, + true, + ); +} + +export function getAllLogs(): log_message[] { + return executeDbOperation("Get All Logs", () => + stmt.selectAll.all().map((row) => convertToLogMessage(row as log_message)), + ); +} + +export function getLogsByLevel(level: string): log_message[] { + return executeDbOperation("Get Logs By Level", () => + stmt.selectByLevel + .all(level) + .map((row) => convertToLogMessage(row as log_message)), + ); +} + +export function clearAllLogs() { + return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); +} + +export function clearLogsByLevel(level: string) { + return executeDbOperation("Clear Logs By Level", () => + stmt.deleteByLevel.run(level), + ); +} diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts new file mode 100644 index 00000000..c160a982 --- /dev/null +++ b/src/core/database/stacks.ts @@ -0,0 +1,85 @@ +import type { stacks_config } from "~/typings/database"; +import { findObjectByKey } from "../utils/helpers"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + insert: db.prepare(` + INSERT INTO stacks_config ( + name, version, custom, source, compose_spec, status + ) VALUES (?, ?, ?, ?, ?, ?) + `), + selectAll: db.prepare(` + SELECT id, name, version, custom, source, compose_spec, status + FROM stacks_config + ORDER BY id DESC + `), + update: db.prepare(` + UPDATE stacks_config + SET name = ?, custom = ?, source = ?, compose_spec = ? + WHERE id = ? + `), + setStatus: db.prepare(` + UPDATE stacks_config + SET status = ? + WHERE id = ? + `), + delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), +}; + +export function addStack(stack: stacks_config) { + executeDbOperation("Add Stack", () => + stmt.insert.run( + stack.name, + stack.version, + stack.custom, + stack.source, + stack.compose_spec, + "active", + ), + ); + + return findObjectByKey(getStacks(), "name", stack.name)?.id; +} + +export function getStacks() { + return executeDbOperation("Get Stacks", () => + stmt.selectAll.all(), + ) as stacks_config[]; +} + +export function deleteStack(id: number) { + return executeDbOperation( + "Delete Stack", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid stack ID"); + }, + ); +} + +export function updateStack(stack: stacks_config) { + return executeDbOperation("Update Stack", () => { + if (!stack.id) { + throw new Error("Stack ID needed"); + } + stmt.update.run( + stack.id, + stack.version, + stack.custom, + stack.source, + stack.name, + stack.compose_spec, + ); + }); +} + +export function setStackStatus( + stack: stacks_config, + status: "active" | "error" = "active", +) { + if (!stack.id) { + throw new Error("Stack ID needed"); + } + stmt.setStatus.run(status, stack.id); +} diff --git a/src/core/database/stores.ts b/src/core/database/stores.ts new file mode 100644 index 00000000..c8a330cb --- /dev/null +++ b/src/core/database/stores.ts @@ -0,0 +1,31 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + insert: db.prepare(` + INSERT INTO store_repos (slug, base) VALUES (?, ?) + `), + selectAll: db.prepare(` + SELECT slug, base FROM store_repos + `), + delete: db.prepare(` + DELETE FROM store_repos WHERE slug = ? + `), +}; + +export function getStoreRepos() { + return executeDbOperation("Get Store Repos", () => stmt.selectAll.all()) as { + slug: string; + base: string; + }[]; +} + +export function addStoreRepo(slug: string, base: string) { + return executeDbOperation("Add Store Repo", () => + stmt.insert.run(slug, base), + ); +} + +export function deleteStoreRepo(slug: string) { + return executeDbOperation("Delete Store Repo", () => stmt.delete.run(slug)); +} diff --git a/src/core/database/themes.ts b/src/core/database/themes.ts new file mode 100644 index 00000000..08f245dd --- /dev/null +++ b/src/core/database/themes.ts @@ -0,0 +1,34 @@ +import type { Theme } from "~/typings/database"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; +import { logger } from "../utils/logger"; + +const stmt = { + insert: db.prepare(` + INSERT INTO themes (name, creator, vars, tags) VALUES (?, ?, ?, ?) + `), + remove: db.prepare("DELETE FROM themes WHERE name = ?"), + read: db.prepare("SELECT * FROM themes WHERE name = ?"), + readAll: db.prepare("SELECT * FROM themes"), +}; + +export function getThemes() { + return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; +} + +export function addTheme({ name, creator, vars, tags }: Theme) { + return executeDbOperation("Save Theme", () => + stmt.insert.run(name, creator, vars, tags.toString()), + ); +} +export function getSpecificTheme(name: string): Theme { + return executeDbOperation( + "Getting specific Theme", + () => stmt.read.get(name) as Theme, + ); +} + +export function deleteTheme(name: string) { + logger.debug(`Removing ${name} from themes `); + return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); +} diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts new file mode 100644 index 00000000..788a910c --- /dev/null +++ b/src/core/docker/client.ts @@ -0,0 +1,35 @@ +import Docker from "dockerode"; +import { logger } from "~/core/utils/logger"; +import type { DockerHost } from "~/typings/docker"; + +export const getDockerClient = (host: DockerHost): Docker => { + try { + logger.info(`Setting up host: ${JSON.stringify(host)}`); + + const inputUrl = host.hostAddress.includes("://") + ? host.hostAddress + : `${host.secure ? "https" : "http"}://${host.hostAddress}`; + const parsedUrl = new URL(inputUrl); + const hostAddress = parsedUrl.hostname; + const port = parsedUrl.port + ? Number.parseInt(parsedUrl.port) + : host.secure + ? 2376 + : 2375; + + if (Number.isNaN(port) || port < 1 || port > 65535) { + throw new Error("Invalid port number in Docker host URL"); + } + + return new Docker({ + protocol: host.secure ? "https" : "http", + host: hostAddress, + port, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration:", error); + throw new Error("Invalid Docker host configuration"); + } +}; diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts new file mode 100644 index 00000000..e4a2510c --- /dev/null +++ b/src/core/docker/monitor.ts @@ -0,0 +1,142 @@ +import { sleep } from "bun"; +import Docker from "dockerode"; +import { dbFunctions } from "~/core/database"; +import { getDockerClient } from "~/core/docker/client"; +import { logger } from "~/core/utils/logger"; +import type { DockerHost } from "~/typings/docker"; +import type { ContainerInfo } from "~/typings/docker"; +import { pluginManager } from "../plugins/plugin-manager"; + +export async function monitorDockerEvents() { + let hosts: DockerHost[]; + + try { + hosts = dbFunctions.getDockerHosts(); + logger.debug( + `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, + ); + } catch (error: unknown) { + logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); + return; + } + + for (const host of hosts) { + await startFor(host); + } +} + +async function startFor(host: DockerHost) { + const docker = getDockerClient(host); + try { + await docker.ping(); + pluginManager.handleHostReachableAgain(host.name); + } catch (err) { + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + pluginManager.handleHostUnreachable(host.name, String(err)); + await sleep(10000); + startFor(host); + } + + try { + const eventsStream = await docker.getEvents(); + logger.debug(`Started events stream for host: ${host.name}`); + + let buffer = ""; + + eventsStream.on("data", (chunk: Buffer) => { + buffer += chunk.toString("utf8"); + const lines = buffer.split(/\r?\n/); + + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.trim() === "") { + continue; + } + + //biome-ignore lint/suspicious/noExplicitAny: Unsure what data we are receiving here + let event: any; + try { + event = JSON.parse(line); + } catch (parseErr) { + logger.error( + `Failed to parse event from host ${host.name}: ${String(parseErr)}`, + ); + continue; + } + + if (event.Type === "container") { + const containerInfo: ContainerInfo = { + id: event.Actor?.ID || event.id || "", + hostId: host.id, + name: event.Actor?.Attributes?.name || "", + image: event.Actor?.Attributes?.image || event.from || "", + status: event.status || event.Actor?.Attributes?.status || "", + state: event.Actor?.Attributes?.state || event.Action || "", + cpuUsage: 0, + memoryUsage: 0, + }; + + const action = event.Action; + logger.debug(`Triggering Action [${action}]`); + switch (action) { + case "stop": + pluginManager.handleContainerStop(containerInfo); + break; + case "start": + pluginManager.handleContainerStart(containerInfo); + break; + case "die": + pluginManager.handleContainerDie(containerInfo); + break; + case "kill": + pluginManager.handleContainerKill(containerInfo); + break; + case "create": + pluginManager.handleContainerCreate(containerInfo); + break; + case "destroy": + pluginManager.handleContainerDestroy(containerInfo); + break; + case "pause": + pluginManager.handleContainerPause(containerInfo); + break; + case "unpause": + pluginManager.handleContainerUnpause(containerInfo); + break; + case "restart": + pluginManager.handleContainerRestart(containerInfo); + break; + case "update": + pluginManager.handleContainerUpdate(containerInfo); + break; + case "health_status": + pluginManager.handleContainerHealthStatus(containerInfo); + break; + default: + logger.debug( + `Unhandled container event "${action}" on host ${host.name}`, + ); + } + } + } + }); + + eventsStream.on("error", async (err: Error) => { + logger.error(`Events stream error for host ${host.name}: ${err.message}`); + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + await sleep(10000); + startFor(host); + }); + + eventsStream.on("end", () => { + logger.info(`Events stream ended for host ${host.name}`); + }); + } catch (streamErr) { + logger.error( + `Failed to start events stream for host ${host.name}: ${String( + streamErr, + )}`, + ); + } +} diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts new file mode 100644 index 00000000..fa6da95c --- /dev/null +++ b/src/core/docker/scheduler.ts @@ -0,0 +1,150 @@ +import { dbFunctions } from "~/core/database"; +import storeContainerData from "~/core/docker/store-container-stats"; +import storeHostData from "~/core/docker/store-host-stats"; +import { logger } from "~/core/utils/logger"; +import type { config } from "~/typings/database"; + +function convertFromMinToMs(minutes: number): number { + return minutes * 60 * 1000; +} + +async function initialRun( + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, +) { + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } +} + +type CancelFn = () => void; +let cancelFunctions: CancelFn[] = []; + +async function reloadSchedules() { + logger.info("Reloading schedules..."); + + cancelFunctions.forEach((cancel) => cancel()); + cancelFunctions = []; + + await setSchedules(); +} + +function scheduledJob( + name: string, + jobFn: () => Promise, + intervalMs: number, +): CancelFn { + let stopped = false; + + async function run() { + if (stopped) return; + const start = Date.now(); + logger.info(`Task Start: ${name}`); + try { + await jobFn(); + logger.info(`Task End: ${name} succeeded.`); + } catch (e) { + logger.error(`Task End: ${name} failed:`, e); + } + const elapsed = Date.now() - start; + const delay = Math.max(0, intervalMs - elapsed); + setTimeout(run, delay); + } + + run(); + + return () => { + stopped = true; + }; +} + +async function setSchedules() { + logger.info("Starting DockStatAPI"); + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + cancelFunctions.push( + scheduledJob( + "storeContainerData", + storeContainerData, + convertFromMinToMs(fetching_interval), + ), + ); + + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + cancelFunctions.push( + scheduledJob( + "storeHostData", + storeHostData, + convertFromMinToMs(fetching_interval), + ), + ); + + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); + cancelFunctions.push( + scheduledJob( + "cleanupOldData", + () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), + convertFromMinToMs(60), + ), + ); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw new Error(error as string); + } +} + +export { setSchedules, reloadSchedules }; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts new file mode 100644 index 00000000..a2778777 --- /dev/null +++ b/src/core/docker/store-container-stats.ts @@ -0,0 +1,101 @@ +import type Docker from "dockerode"; +import { dbFunctions } from "~/core/database"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; +import type { container_stats } from "~/typings/database"; +import { logger } from "../utils/logger"; + +async function storeContainerData() { + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts for storing container data"); + + // Process each host concurrently and wait for them all to finish + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); + + // Test the connection with a ping + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } + + let containers: Docker.ContainerInfo[] = []; + try { + containers = await docker.listContainers({ all: true }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to list containers on host "${host.name}": ${errMsg}`, + ); + } + + // Process each container concurrently + await Promise.all( + containers.map(async (containerInfo) => { + const containerName = containerInfo.Names[0].replace(/^\//, ""); + try { + const container = docker.getContainer(containerInfo.Id); + + const stats: Docker.ContainerStats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + return reject( + new Error( + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ), + ); + } + if (!stats) { + return reject( + new Error( + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, + ), + ); + } + resolve(stats); + }); + }, + ); + + const parsed: container_stats = { + cpu_usage: calculateCpuPercent(stats), + hostId: host.id, + id: containerInfo.Id, + image: containerInfo.Image, + memory_usage: calculateMemoryUsage(stats), + name: containerName, + state: containerInfo.State, + status: containerInfo.Status, + }; + + dbFunctions.addContainerStats(parsed); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error( + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ); + } + }), + ); + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to store container data: ${errMsg}`); + } +} + +export default storeContainerData; diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts new file mode 100644 index 00000000..815b9db5 --- /dev/null +++ b/src/core/docker/store-host-stats.ts @@ -0,0 +1,68 @@ +import { dbFunctions } from "~/core/database"; +import { getDockerClient } from "~/core/docker/client"; +import { logger } from "~/core/utils/logger"; +import type { HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; + +async function storeHostData() { + try { + const hosts = dbFunctions.getDockerHosts(); + + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); + + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } + + let hostStats: DockerInfo; + try { + hostStats = await docker.info(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to fetch stats for host "${host.name}": ${errMsg}`, + ); + } + + try { + const stats: HostStats = { + hostId: host.id, + hostName: host.name, + dockerVersion: hostStats.ServerVersion, + apiVersion: hostStats.Driver, + os: hostStats.OperatingSystem, + architecture: hostStats.Architecture, + totalMemory: hostStats.MemTotal, + totalCPU: hostStats.NCPU, + labels: hostStats.Labels, + images: hostStats.Images, + containers: hostStats.Containers, + containersPaused: hostStats.ContainersPaused, + containersRunning: hostStats.ContainersRunning, + containersStopped: hostStats.ContainersStopped, + }; + + dbFunctions.addHostStats(stats); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to store stats for host "${host.name}": ${errMsg}`, + ); + } + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`storeHostData failed: ${errMsg}`); + throw new Error(`Failed to store host data: ${errMsg}`); + } +} + +export default storeHostData; diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts new file mode 100644 index 00000000..c6da8764 --- /dev/null +++ b/src/core/plugins/loader.ts @@ -0,0 +1,54 @@ +import fs from "node:fs"; +import path from "node:path"; +import { checkFileForChangeMe } from "../utils/change-me-checker"; +import { logger } from "../utils/logger"; +import { pluginManager } from "./plugin-manager"; + +export async function loadPlugins(pluginDir: string) { + const pluginPath = path.join(process.cwd(), pluginDir); + + logger.debug(`Loading plugins (${pluginPath})`); + + if (!fs.existsSync(pluginPath)) { + throw new Error("Failed to check plugin directory"); + } + logger.debug("Plugin directory exists"); + + let pluginCount = 0; + let files: string[]; + try { + files = fs.readdirSync(pluginPath); + logger.debug(`Found ${files.length} files in plugin directory`); + } catch (error) { + throw new Error(`Failed to read plugin-directory: ${error}`); + } + + if (!files) { + logger.info(`No plugins found in ${pluginPath}`); + return; + } + + for (const file of files) { + if (!file.endsWith(".plugin.ts")) { + logger.debug(`Skipping non-plugin file: ${file}`); + continue; + } + + const absolutePath = path.join(pluginPath, file); + logger.info(`Loading plugin: ${absolutePath}`); + try { + await checkFileForChangeMe(absolutePath); + const module = await import(/* @vite-ignore */ absolutePath); + const plugin = module.default; + pluginManager.register(plugin); + pluginCount++; + } catch (error) { + pluginManager.fail({ name: file, version: "0.0.0" }); + logger.error( + `Error while registering plugin ${absolutePath}: ${error as string}`, + ); + } + } + + logger.info(`Registered ${pluginCount} plugin(s)`); +} diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts new file mode 100644 index 00000000..f68b80d8 --- /dev/null +++ b/src/core/plugins/plugin-manager.ts @@ -0,0 +1,185 @@ +import { EventEmitter } from "node:events"; +import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin, PluginInfo } from "~/typings/plugin"; +import { logger } from "../utils/logger"; +import { loadPlugins } from "./loader"; + +function getHooks(plugin: Plugin) { + return { + onContainerStart: !!plugin.onContainerStart, + onContainerStop: !!plugin.onContainerStop, + onContainerExit: !!plugin.onContainerExit, + onContainerCreate: !!plugin.onContainerCreate, + onContainerKill: !!plugin.onContainerKill, + handleContainerDie: !!plugin.handleContainerDie, + onContainerDestroy: !!plugin.onContainerDestroy, + onContainerPause: !!plugin.onContainerPause, + onContainerUnpause: !!plugin.onContainerUnpause, + onContainerRestart: !!plugin.onContainerRestart, + onContainerUpdate: !!plugin.onContainerUpdate, + onContainerRename: !!plugin.onContainerRename, + onContainerHealthStatus: !!plugin.onContainerHealthStatus, + onHostUnreachable: !!plugin.onHostUnreachable, + onHostReachableAgain: !!plugin.onHostReachableAgain, + }; +} + +class PluginManager extends EventEmitter { + private plugins: Map = new Map(); + private failedPlugins: Map = new Map(); + + async start() { + try { + await loadPlugins("./server/src/plugins"); + return; + } catch (error) { + logger.error(`Failed to init plugin manager: ${error}`); + return; + } + } + + fail(plugin: Plugin) { + try { + this.failedPlugins.set(plugin.name, plugin); + logger.debug(`Set status to failed for plugin: ${plugin.name}`); + } catch (error) { + logger.error(`Adding failed plugin to list failed: ${error as string}`); + } + } + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getPlugins(): PluginInfo[] { + const plugins: PluginInfo[] = []; + + for (const plugin of this.plugins.values()) { + logger.debug(`Loaded plugin: ${JSON.stringify(plugin)}`); + const hooks = getHooks(plugin); + plugins.push({ + name: plugin.name, + version: plugin.version, + status: "active", + usedHooks: hooks, + }); + } + + for (const plugin of this.failedPlugins.values()) { + logger.debug(`Loaded plugin: ${JSON.stringify(plugin)}`); + const hooks = getHooks(plugin); + plugins.push({ + name: plugin.name, + version: plugin.version, + status: "inactive", + usedHooks: hooks, + }); + } + + return plugins; + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } +} + +export const pluginManager = new PluginManager(); diff --git a/src/core/stacks/checker.ts b/src/core/stacks/checker.ts new file mode 100644 index 00000000..9913cafd --- /dev/null +++ b/src/core/stacks/checker.ts @@ -0,0 +1,50 @@ +import yaml from "js-yaml"; +import { dbFunctions } from "../database"; +import { logger } from "../utils/logger"; + +const stacks = dbFunctions.getStacks(); + +export async function checkStacks() { + logger.debug(`Checking ${stacks.length} stack(s)`); + for (const stack of stacks) { + try { + const composeFilePath = + `stacks/${stack.id}-${stack.name}/docker-compose.yaml`.replaceAll( + " ", + "_", + ); + const composeFile = Bun.file(composeFilePath); + logger.debug(`Checking ${stack.id} - ${composeFilePath}`); + + if (!(await composeFile.exists())) { + logger.error(`Stack (${stack.id} - ${stack.name}) has no compose file`); + dbFunctions.setStackStatus(stack, "error"); + continue; + } + + if ( + stack.compose_spec !== + JSON.stringify(yaml.load(await composeFile.text())) + ) { + logger.error( + `Stack (${stack.id} - ${stack.name}) does not match the saved compose file`, + ); + logger.debug(`Database config: ${stack.compose_spec}`); + logger.debug( + `Compose config: ${JSON.stringify( + yaml.load(await composeFile.text()), + )}`, + ); + dbFunctions.setStackStatus(stack, "error"); + continue; + } + + dbFunctions.setStackStatus(stack, "active"); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + } + } + + logger.info("Checked stacks"); +} diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts new file mode 100644 index 00000000..0b0b5174 --- /dev/null +++ b/src/core/stacks/controller.ts @@ -0,0 +1,275 @@ +import { rm } from "node:fs/promises"; +import DockerCompose from "docker-compose"; +import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; +import type { stacks_config } from "~/typings/database"; +import type { Stack } from "~/typings/docker-compose"; +import type { ComposeSpec } from "~/typings/docker-compose"; +import { broadcast } from "../../handlers/modules/docker-socket"; +import { checkStacks } from "./checker"; +import { runStackCommand } from "./operations/runStackCommand"; +import { wrapProgressCallback } from "./operations/runStackCommand"; +import { + createStackYAML, + getStackName, + getStackPath, +} from "./operations/stackHelpers"; + +export async function deployStack(stack_config: stacks_config): Promise { + let stackId: number | null = null; + let stackPath = ""; + + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + if (!stack_config.name) throw new Error("Stack name needed"); + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + stackId = dbFunctions.addStack(jsonStringStack) || null; + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + // Broadcast pending status + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, + }; + + await createStackYAML(stackYaml); + stackPath = await getStackPath(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); + + // Broadcast deployed status + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }, + }); + + await checkStacks(); + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logger.error(errorMsg); + + if (stackId !== null) { + // Attempt to remove any containers created during failed deployment + if (stackPath) { + try { + await DockerCompose.down({ + cwd: stackPath, + log: false, // No need for progress logging during cleanup + }); + } catch (downError) { + const downErrorMsg = + downError instanceof Error + ? downError.message + : JSON.stringify(downError); + logger.error(`Failed to cleanup containers: ${downErrorMsg}`); + } + } + + // Proceed with existing cleanup (DB and filesystem) + dbFunctions.deleteStack(stackId); + if (stackPath) { + try { + await rm(stackPath, { recursive: true }); + } catch (cleanupError) { + logger.error(`Error cleaning up stack path: ${cleanupError}`); + } + } + } + + // Broadcast deployment error + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-error", + data: { + stack_id: stackId ?? 0, + action: "deploying", + message: errorMsg, + }, + }, + }); + throw new Error(errorMsg); + } +} + +export async function stopStack(stack_id: number): Promise { + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); +} + +export async function startStack(stack_id: number): Promise { + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); +} + +export async function pullStackImages(stack_id: number): Promise { + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); +} + +export async function restartStack(stack_id: number): Promise { + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); +} + +export async function removeStack(stack_id: number): Promise { + try { + if (!stack_id) { + throw new Error("Stack ID needed"); + } + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + // Add 'volumes' flag to remove named volumes + commandOptions: ["--volumes", "--remove-orphans"], + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { + recursive: true, + force: true, + maxRetries: 3, + retryDelay: 300, + }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + // Broadcast removal error + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-error", + data: { + stack_id, + action: "removing", + message: `Directory removal failed: ${errorMsg}`, + }, + }, + }); + throw new Error(errorMsg); + } + + dbFunctions.deleteStack(stack_id); + + // Broadcast successful removal + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + // Broadcast removal error + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + }, + }, + }); + throw new Error(errorMsg); + } +} + +export { getStackStatus, getAllStacksStatus } from "./operations/stackStatus"; diff --git a/src/core/stacks/operations/runStackCommand.ts b/src/core/stacks/operations/runStackCommand.ts new file mode 100644 index 00000000..d613a7c7 --- /dev/null +++ b/src/core/stacks/operations/runStackCommand.ts @@ -0,0 +1,114 @@ +import { logger } from "~/core/utils/logger"; +import type { Stack } from "~/typings/docker-compose"; +import { broadcast } from "../../../handlers/modules/docker-socket"; +import { getStackName, getStackPath } from "./stackHelpers"; + +export function wrapProgressCallback(progressCallback?: (log: string) => void) { + return progressCallback + ? (chunk: Buffer) => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; +} + +export async function runStackCommand( + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, +): Promise { + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, + ); + + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, + ); + + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}`, + ); + + if (message.includes("Error response from daemon")) { + const extracted = message.match(/Error response from daemon: (.+)/); + if (extracted) { + logger.error(`Error response from daemon: ${extracted[1]}`); + } + } + + // Broadcast progress + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-progress", + data: { + stack_id, + message, + action, + }, + }, + }); + }; + + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"`, + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"`, + ); + + // Optionally broadcast status on completion + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-status", + data: { + stack_id, + status: "completed", + message: `Completed ${action}`, + action, + }, + }, + }); + + return result; + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}`, + ); + + // Broadcast error + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-error", + data: { + stack_id, + action, + message: errorMsg, + }, + }, + }); + + throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); + } +} diff --git a/src/core/stacks/operations/stackHelpers.ts b/src/core/stacks/operations/stackHelpers.ts new file mode 100644 index 00000000..ab01b69d --- /dev/null +++ b/src/core/stacks/operations/stackHelpers.ts @@ -0,0 +1,35 @@ +import YAML from "yaml"; +import { dbFunctions } from "~/core/database"; +import { findObjectByKey } from "~/core/utils/helpers"; +import { logger } from "~/core/utils/logger"; +import type { Stack } from "~/typings/docker-compose"; + +export async function getStackName(stack_id: number): Promise { + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; +} + +export async function getStackPath(stack: Stack): Promise { + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; + + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } + + return `stacks/${stackId}-${stackName}`; +} + +export async function createStackYAML(compose_spec: Stack): Promise { + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); +} diff --git a/src/core/stacks/operations/stackStatus.ts b/src/core/stacks/operations/stackStatus.ts new file mode 100644 index 00000000..b29e6e34 --- /dev/null +++ b/src/core/stacks/operations/stackStatus.ts @@ -0,0 +1,89 @@ +import DockerCompose from "docker-compose"; +import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; +import { runStackCommand } from "./runStackCommand"; + +interface DockerServiceStatus { + status: string; + ports: string[]; +} + +interface StackStatus { + services: Record; + healthy: number; + unhealthy: number; + total: number; +} + +type StacksStatus = Record; + +export async function getStackStatus( + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: +): Promise> { + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; +} + +export async function getAllStacksStatus(): Promise { + try { + const stacks = dbFunctions.getStacks(); + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}`, + ), + }; + return acc; + }, + {}, + ); + + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up"), + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up"), + ).length, + total: statusValues.length, + }; + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); + + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts new file mode 100644 index 00000000..fbb7a422 --- /dev/null +++ b/src/core/utils/calculations.ts @@ -0,0 +1,37 @@ +import type Docker from "dockerode"; + +const calculateCpuPercent = (stats: Docker.ContainerStats): number => { + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + + if (cpuDelta <= 0) { + return 0.0000001; + } + + if (systemDelta <= 0) { + return 0.0000001; + } + + const data = (cpuDelta / systemDelta) * 100; + + if (data === null) { + return 0.0000001; + } + + return data * 10; +}; + +const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { + if (stats.memory_stats.usage === null) { + return 0.0000001; + } + + const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + + return data; +}; + +export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/core/utils/change-me-checker.ts b/src/core/utils/change-me-checker.ts new file mode 100644 index 00000000..d5aefb4b --- /dev/null +++ b/src/core/utils/change-me-checker.ts @@ -0,0 +1,18 @@ +import { readFile } from "node:fs/promises"; +import { logger } from "~/core/utils/logger"; + +export async function checkFileForChangeMe(filePath: string) { + const regex = /change[\W_]*me/i; + let content = ""; + try { + content = await readFile(filePath, "utf-8"); + } catch (error) { + logger.error("Error reading file:", error); + } + + if (regex.test(content)) { + throw new Error( + `The file contains ${regex.exec(content)}. Please update it.`, + ); + } +} diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts new file mode 100644 index 00000000..989fb993 --- /dev/null +++ b/src/core/utils/helpers.ts @@ -0,0 +1,22 @@ +import { logger } from "./logger"; + +/** + * Finds and returns the first object in an array where the specified key matches the given value. + * + * @template T - The type of the objects in the array. + * @param {T[]} array - The array of objects to search through. + * @param {keyof T} key - The key of the object to match against. + * @param {T[keyof T]} value - The value to match the key against. + * @returns {T | undefined} The first matching object, or undefined if no match is found. + */ +export function findObjectByKey( + array: T[], + key: keyof T, + value: T[keyof T], +): T | undefined { + logger.debug( + `Searching for key: ${String(key)} with value: ${String(value)}`, + ); + const data = array.find((item) => item[key] === value); + return data; +} diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts new file mode 100644 index 00000000..483d73cf --- /dev/null +++ b/src/core/utils/logger.ts @@ -0,0 +1,203 @@ +import path from "node:path"; +import chalk from "chalk"; +import type { ChalkInstance } from "chalk"; +import type { TransformableInfo } from "logform"; +import { createLogger, format, transports } from "winston"; +import wrapAnsi from "wrap-ansi"; + +import { dbFunctions } from "~/core/database"; + +import { logToClients } from "../../handlers/modules/logs-socket"; + +import type { log_message } from "~/typings/database"; + +import { backupInProgress } from "../database/_dbState"; + +const padNewlines = true; //process.env.PAD_NEW_LINES !== "false"; + +type LogLevel = + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; + +// biome-ignore lint/suspicious/noControlCharactersInRegex: +const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; + +const formatTerminalMessage = (message: string, prefix: string): string => { + try { + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); + + if (!padNewlines) return message; + + const wrapped = wrapAnsi(message, wrapWidth, { + trim: true, + hard: true, + wordWrap: true, + }); + + return wrapped + .split("\n") + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) + .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } +}; + +const levelColors: Record = { + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), +}; + +const parseTimestamp = (timestamp: string): string => { + const [datePart, timePart] = timestamp.split(" "); + const [day, month] = datePart.split("/"); + const [hours, minutes, seconds] = timePart.split(":"); + const year = new Date().getFullYear(); + const date = new Date( + year, + Number.parseInt(month) - 1, + Number.parseInt(day), + Number.parseInt(hours), + Number.parseInt(minutes), + Number.parseInt(seconds), + ); + return date.toISOString(); +}; + +const handleWebSocketLog = (log: log_message) => { + try { + logToClients({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } +}; + +const handleDatabaseLog = (log: log_message): void => { + if (backupInProgress) { + return; + } + try { + dbFunctions.addLogEntry({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `Database logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } +}; + +export const logger = createLogger({ + level: process.env.LOG_LEVEL || "debug", + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(import.meta.url)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = Number.parseInt(matches[2], 10); + break; + } + } + } + } + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as TransformableInfo & log_message; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; + } + + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; + } + + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel, + ); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredTimestamp = chalk.yellow(timestamp); + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + + const formattedMessage = padNewlines + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + handleWebSocketLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }), + ), + transports: [new transports.Console()], +}); diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts new file mode 100644 index 00000000..86f9287f --- /dev/null +++ b/src/core/utils/package-json.ts @@ -0,0 +1,25 @@ +import packageJson from "../../../package.json"; + +const { version, description, license, dependencies, devDependencies } = + packageJson; +let { contributors } = packageJson; + +const authorName = packageJson.author.name; +const authorEmail = packageJson.author.email; +const authorWebsite = packageJson.author.url; + +if (contributors.length === 0) { + contributors = [":(" as never]; +} + +export { + version, + description, + authorName, + authorEmail, + authorWebsite, + license, + contributors, + dependencies, + devDependencies, +}; diff --git a/src/handlers/config.ts b/src/handlers/config.ts new file mode 100644 index 00000000..5f713f04 --- /dev/null +++ b/src/handlers/config.ts @@ -0,0 +1,201 @@ +import { existsSync, readdirSync, unlinkSync } from "node:fs"; +import { dbFunctions } from "~/core/database"; +import { backupDir } from "~/core/database/backup"; +import { reloadSchedules } from "~/core/docker/scheduler"; +import { pluginManager } from "~/core/plugins/plugin-manager"; +import { logger } from "~/core/utils/logger"; +import { + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, +} from "~/core/utils/package-json"; +import type { config } from "~/typings/database"; +import type { DockerHost } from "~/typings/docker"; +import type { PluginInfo } from "~/typings/plugin"; + +class apiHandler { + getConfig(): config { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + logger.debug( + `Updated config: fetching_interval: ${fetching_interval} - keep_data_for: ${keep_data_for}`, + ); + dbFunctions.updateConfig(fetching_interval, keep_data_for); + await reloadSchedules(); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPlugins(): PluginInfo[] { + try { + logger.debug("Gathering plugins"); + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPackage() { + try { + logger.debug("Fetching package.json"); + const data: { + version: string; + description: string; + license: string; + authorName: string; + authorEmail: string; + authorWebsite: string; + contributors: string[]; + dependencies: Record; + devDependencies: Record; + } = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup(): Promise { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: File) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } +} + +export const ApiHandler = new apiHandler(); diff --git a/src/handlers/database.ts b/src/handlers/database.ts new file mode 100644 index 00000000..d6bd0c49 --- /dev/null +++ b/src/handlers/database.ts @@ -0,0 +1,13 @@ +import { dbFunctions } from "~/core/database"; + +class databaseHandler { + async getContainers() { + return dbFunctions.getContainerStats(); + } + + async getHosts() { + return dbFunctions.getHostStats(); + } +} + +export const DatabaseHandler = new databaseHandler(); diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts new file mode 100644 index 00000000..6e6a5411 --- /dev/null +++ b/src/handlers/docker.ts @@ -0,0 +1,155 @@ +import type Docker from "dockerode"; +import { dbFunctions } from "~/core/database"; +import { getDockerClient } from "~/core/docker/client"; +import { logger } from "~/core/utils/logger"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; + +class basicDockerHandler { + async getContainers(): Promise { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + throw new Error(pingError as string); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + throw new Error(error as string); + } + if (!stats) { + throw new Error("No stats available"); + } + resolve(stats); + }); + }, + ); + + containers.push({ + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: stats.cpu_stats.system_cpu_usage, + memoryUsage: stats.memory_stats.usage, + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }), + ); + + logger.debug("Fetched all containers across all hosts"); + return containers; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getHostStats() { + //if (true) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); + } + + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + throw new Error(error as string); + } + //} + + //try { + // const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + // + // const host = findObjectByKey(hosts, "id", Number(id)); + // if (!host) { + // throw new Error(`Host (${id}) not found`); + // } + // + // const docker = getDockerClient(host); + // const info: DockerInfo = await docker.info(); + // + // const config: HostStats = { + // hostId: host.id as number, + // hostName: host.name, + // dockerVersion: info.ServerVersion, + // apiVersion: info.Driver, + // os: info.OperatingSystem, + // architecture: info.Architecture, + // totalMemory: info.MemTotal, + // totalCPU: info.NCPU, + // labels: info.Labels, + // images: info.Images, + // containers: info.Containers, + // containersPaused: info.ContainersPaused, + // containersRunning: info.ContainersRunning, + // containersStopped: info.ContainersStopped, + // }; + // + // logger.debug(`Fetched config for ${host.name}`); + // return config; + //} catch (error) { + // throw new Error(`Failed to retrieve host config: ${error}`); + //} + } +} + +export const BasicDockerHandler = new basicDockerHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts new file mode 100644 index 00000000..982f142b --- /dev/null +++ b/src/handlers/index.ts @@ -0,0 +1,23 @@ +import { ApiHandler } from "./config"; +import { DatabaseHandler } from "./database"; +import { BasicDockerHandler } from "./docker"; +import { LogHandler } from "./logs"; +import { Starter } from "./modules/starter"; +import { StackHandler } from "./stacks"; +import { StoreHandler } from "./store"; +import { ThemeHandler } from "./themes"; +import { CheckHealth } from "./utils"; + +export const handlers = { + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, + CheckHealth, + Socket: "ws://localhost:4837/ws", + StoreHandler, + ThemeHandler, +}; + +Starter.startAll(); diff --git a/src/handlers/logs.ts b/src/handlers/logs.ts new file mode 100644 index 00000000..766e60d9 --- /dev/null +++ b/src/handlers/logs.ts @@ -0,0 +1,50 @@ +import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; + +class logHandler { + async getLogs(level?: string) { + if (!level) { + try { + const logs = dbFunctions.getAllLogs(); + logger.debug("Retrieved all logs"); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs,", error); + throw new Error("Failed to retrieve logs"); + } + } + try { + const logs = dbFunctions.getLogsByLevel(level); + + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + logger.error(`Failed to retrieve logs: ${error}`); + throw new Error(`Failed to retrieve logs: ${error}`); + } + } + + async deleteLogs(level?: string) { + if (!level) { + try { + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + logger.error("Could not delete all logs,", error); + throw new Error("Could not delete all logs"); + } + } + + try { + dbFunctions.clearLogsByLevel(level); + + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + logger.error("Could not clear logs with level", level, ",", error); + throw new Error("Failed to retrieve logs"); + } + } +} + +export const LogHandler = new logHandler(); diff --git a/src/handlers/modules/docker-socket.ts b/src/handlers/modules/docker-socket.ts new file mode 100644 index 00000000..1a20c9ce --- /dev/null +++ b/src/handlers/modules/docker-socket.ts @@ -0,0 +1,175 @@ +import { serve } from "bun"; +import split2 from "split2"; +import { dbFunctions } from "~/core/database"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; +import { logger } from "~/core/utils/logger"; +import type { log_message } from "~/typings/database"; +import type { DockerHost } from "~/typings/docker"; +import type { WSMessage } from "~/typings/websocket"; +import { createLogStream } from "./logs-socket"; + +// Unified WebSocket message with topic for client-side routing +const clients = new Set>(); + +/** + * Broadcasts a WSMessage to all connected clients. + */ +export function broadcast(wsMsg: WSMessage) { + const payload = JSON.stringify(wsMsg); + for (const ws of clients) { + if (ws.readyState === 1) { + ws.send(payload); + } + } +} + +/** + * Streams Docker stats for all hosts and broadcasts events. + */ +export async function startDockerStatsBroadcast() { + logger.debug("Starting Docker stats broadcast..."); + + try { + const hosts: DockerHost[] = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} Docker host(s)`); + + for (const host of hosts) { + try { + const docker = getDockerClient(host); + await docker.ping(); + + const containers = await docker.listContainers({ all: true }); + logger.debug( + `Host ${host.name} contains ${containers.length} containers`, + ); + + for (const info of containers) { + (async () => { + try { + const statsStream = await docker + .getContainer(info.Id) + .stats({ stream: true }); + const splitter = split2(); + statsStream.pipe(splitter); + + for await (const line of splitter) { + if (!line) continue; + try { + const stats = JSON.parse(line); + const msg: WSMessage = { + topic: "stats", + data: { + id: info.Id, + hostId: host.id, + name: info.Names[0].replace(/^\//, ""), + image: info.Image, + status: info.Status, + state: stats.state || info.State, + cpuUsage: calculateCpuPercent(stats) ?? 0, + memoryUsage: calculateMemoryUsage(stats) ?? 0, + }, + }; + broadcast(msg); + } catch (err) { + const errorMsg = (err as Error).message; + const msg: WSMessage = { + topic: "error", + data: { + hostId: host.id, + containerId: info.Id, + error: `Parse error: ${errorMsg}`, + }, + }; + broadcast(msg); + } + } + } catch (err) { + const errorMsg = (err as Error).message; + const msg: WSMessage = { + topic: "error", + data: { + hostId: host.id, + containerId: info.Id, + error: `Stats stream error: ${errorMsg}`, + }, + }; + broadcast(msg); + } + })(); + } + } catch (err) { + const errorMsg = (err as Error).message; + const msg: WSMessage = { + topic: "error", + data: { + hostId: host.id, + error: `Host connection error: ${errorMsg}`, + }, + }; + broadcast(msg); + } + } + } catch (err) { + const errorMsg = (err as Error).message; + const msg: WSMessage = { + topic: "error", + data: { + hostId: 0, + error: `Initialization error: ${errorMsg}`, + }, + }; + broadcast(msg); + } +} + +/** + * Sets up a log stream to forward application logs over WebSocket. + */ +function startLogBroadcast() { + const logStream = createLogStream(); + logStream.on("data", (chunk: log_message) => { + const msg: WSMessage = { + topic: "logs", + data: chunk, + }; + broadcast(msg); + }); +} + +/** + * WebSocket server serving multiple topics over one socket. + */ +export const WSServer = serve({ + port: 4837, + reusePort: true, + fetch(req, server) { + //if (req.url.endsWith("/ws")) { + if (server.upgrade(req)) { + logger.debug("Upgraded!"); + return; + } + //} + return new Response("Expected WebSocket upgrade", { status: 426 }); + }, + websocket: { + open(ws) { + logger.debug("Client connected via WebSocket"); + clients.add(ws); + }, + message() {}, + close(ws, code, reason) { + logger.debug(`Client disconnected (${code}): ${reason}`); + clients.delete(ws); + }, + }, +}); + +// Initialize broadcasts +startDockerStatsBroadcast().catch((err) => { + logger.error("Failed to start Docker stats broadcast:", err); +}); +startLogBroadcast(); diff --git a/src/handlers/modules/live-stacks.ts b/src/handlers/modules/live-stacks.ts new file mode 100644 index 00000000..ab26ccfd --- /dev/null +++ b/src/handlers/modules/live-stacks.ts @@ -0,0 +1,31 @@ +import { PassThrough, type Readable } from "node:stream"; +import { logger } from "~/core/utils/logger"; + +const activeStreams = new Set(); + +export function createStackStream(): Readable { + const stream = new PassThrough({ objectMode: true }); + + activeStreams.add(stream); + logger.info( + `New Stack stream created. Active streams: ${activeStreams.size}`, + ); + + const removeStream = () => { + if (activeStreams.delete(stream)) { + logger.info(`Stack stream closed. Active streams: ${activeStreams.size}`); + if (!stream.destroyed) { + stream.destroy(); + } + } + }; + + stream.on("close", removeStream); + stream.on("end", removeStream); + stream.on("error", (error) => { + logger.error(`Stream error: ${error.message}`); + removeStream(); + }); + + return stream; +} diff --git a/src/handlers/modules/logs-socket.ts b/src/handlers/modules/logs-socket.ts new file mode 100644 index 00000000..77a730e0 --- /dev/null +++ b/src/handlers/modules/logs-socket.ts @@ -0,0 +1,53 @@ +import { PassThrough, type Readable } from "node:stream"; +import { logger } from "~/core/utils/logger"; +import type { log_message } from "~/typings/database"; + +const activeStreams = new Set(); + +export function createLogStream(): Readable { + const stream = new PassThrough({ objectMode: true }); + + activeStreams.add(stream); + logger.info(`New Logs stream created. Active streams: ${activeStreams.size}`); + + const removeStream = () => { + if (activeStreams.delete(stream)) { + logger.info(`Logs stream closed. Active streams: ${activeStreams.size}`); + if (!stream.destroyed) { + stream.destroy(); + } + } + }; + + stream.on("close", removeStream); + stream.on("end", removeStream); + stream.on("error", (error) => { + logger.error(`Stream error: ${error.message}`); + removeStream(); + }); + + return stream; +} + +export function logToClients(data: log_message): void { + for (const stream of activeStreams) { + try { + if (stream.writable && !stream.destroyed) { + const success = stream.write(data); + if (!success) { + logger.warn("Log stream buffer full, data may be delayed"); + } + } + } catch (error) { + logger.error( + `Failed to write to log stream: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + activeStreams.delete(stream); + if (!stream.destroyed) { + stream.destroy(); + } + } + } +} diff --git a/src/handlers/modules/starter.ts b/src/handlers/modules/starter.ts new file mode 100644 index 00000000..a9f47131 --- /dev/null +++ b/src/handlers/modules/starter.ts @@ -0,0 +1,35 @@ +import { setSchedules } from "~/core/docker/scheduler"; +import { pluginManager } from "~/core/plugins/plugin-manager"; +import { startDockerStatsBroadcast } from "./docker-socket"; + +function banner(msg: string) { + const fenced = `= ${msg} =`; + const lines = msg.length; + console.info("=".repeat(fenced.length)); + console.info(fenced); + console.info("=".repeat(fenced.length)); +} + +class starter { + public started = false; + async startAll() { + try { + if (!this.started) { + banner("Setting schedules"); + await setSchedules(); + banner("Importing plugins"); + await startDockerStatsBroadcast(); + banner("Started DockStatAPI succesfully"); + await pluginManager.start(); + banner("Starting WebSocket server"); + this.started = true; + return; + } + console.info("Already started"); + } catch (error) { + throw new Error(`Could not start DockStatAPI: ${error}`); + } + } +} + +export const Starter = new starter(); diff --git a/src/handlers/sockets.ts b/src/handlers/sockets.ts new file mode 100644 index 00000000..ff463c6c --- /dev/null +++ b/src/handlers/sockets.ts @@ -0,0 +1,9 @@ +import { WSServer } from "./modules/docker-socket"; +import { createStackStream } from "./modules/live-stacks"; +import { createLogStream } from "./modules/logs-socket"; + +export const Sockets = { + dockerStatsStream: `${WSServer.hostname}${WSServer.port}/ws`, + createLogStream, + createStackStream, +}; diff --git a/src/handlers/stacks.ts b/src/handlers/stacks.ts new file mode 100644 index 00000000..cabf5836 --- /dev/null +++ b/src/handlers/stacks.ts @@ -0,0 +1,186 @@ +import { dbFunctions } from "~/core/database"; +import { + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, +} from "~/core/stacks/controller"; +import { logger } from "~/core/utils/logger"; +import type { stacks_config } from "~/typings/database"; + +class stackHandler { + /** + * Deploys a Stack on the DockStatAPI + * + * @example + * ```ts + * deploy({ + * id: 0, + * name: "example", + * vesion: 1, + * custom: false, + * source: "https://github.com/Its4Nik/DockStacks" + * compose_spec: "{services: {web: {image: "nginx:latest",ports: ["80:80"]}}" + * }) + * ``` + * @param config + * @returns "Stack ${config.name} deployed successfully" + */ + async deploy(config: stacks_config) { + try { + await deployStack(config); + logger.info(`Deployed Stack (${config.name})`); + return `Stack ${config.name} deployed successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error deploying stack, please check the server logs for more information`; + } + } + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" up -d` + * @param stackId + * @returns `Started Stack (${stackId})` + */ + async start(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack ID needed"); + } + await startStack(stackId); + logger.info(`Started Stack (${stackId})`); + return `Stack ${stackId} started successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error starting stack`; + } + } + + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" down` + * @param stackId + * @returns `Stack ${stackId} stopped successfully` + */ + async stop(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack needed"); + } + await stopStack(stackId); + logger.info(`Stopped Stack (${stackId})`); + return `Stack ${stackId} stopped successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error stopping stack`; + } + } + + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" restart` + * @param stackId + * @returns `Stack ${stackId} restarted successfully` + */ + async restart(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await restartStack(stackId); + logger.info(`Restarted Stack (${stackId})`); + return `Stack ${stackId} restarted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error restarting stack`; + } + } + + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" pull` + * @param stackId + * @returns `Images for stack ${stackId} pulled successfully` + */ + async pullImages(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await pullStackImages(stackId); + logger.info(`Pulled Stack images (${stackId})`); + return `Images for stack ${stackId} pulled successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error pulling images`; + } + } + + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" ps` with custom formatting + * @param stackId + * @returns Idfk + */ + async getStatus(stackId?: number) { + if (stackId) { + const status = await getStackStatus(stackId); + logger.debug( + `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}`, + ); + return status; + } + + logger.debug("Fetching status for all stacks"); + const status = await getAllStacksStatus(); + logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); + + return status; + } + + /** + * @example + * ```json + * [{ + * id: 1; + * name: "example"; + * version: 1; + * custom: false; + * source: "https://github.com/Its4Nik/DockStacks"; + * compose_spec: "{services: {web: {image: "nginx:latest",ports: ["80:80"]}}" + * }] + * ``` + */ + listStacks(): stacks_config[] { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + throw new Error(`${errorMsg}, Error getting stacks`); + } + } + + /** + * Deletes a whole Stack and it's local folder, this action is irreversible + * @param stackId + * @returns `Stack ${stackId} deleted successfully` + */ + async deleteStack(stackId: number) { + try { + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return `Stack ${stackId} deleted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return `${errorMsg}, Error deleting stack`; + } + } +} + +export const StackHandler = new stackHandler(); diff --git a/src/handlers/store.ts b/src/handlers/store.ts new file mode 100644 index 00000000..4cb83c45 --- /dev/null +++ b/src/handlers/store.ts @@ -0,0 +1,51 @@ +import { + addStoreRepo, + deleteStoreRepo, + getStoreRepos, +} from "~/core/database/stores"; + +class store { + /** + * + * @returns an Array of all Repos added to the Database + * @example + * ```json + * [ + * { + * slug: "DockStacks", + * base: "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json" + * } + * ] + * ``` + */ + getRepos(): { + slug: string; + base: string; + }[] { + return getStoreRepos(); + } + + /** + * + * @param slug - "Nickname" for this repo + * @param base - The raw URL of where the [ROOT].json is located + * @example + * ```ts + * addRepo("DockStacks", "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json") + * ``` + */ + addRepo(slug: string, base: string) { + return addStoreRepo(slug, base); + } + + /** + * Deletes a Repo from the Database + * @param slug + * @returns Changes + */ + deleteRepo(slug: string) { + return deleteStoreRepo(slug); + } +} + +export const StoreHandler = new store(); diff --git a/src/handlers/themes.ts b/src/handlers/themes.ts new file mode 100644 index 00000000..8bc1f98d --- /dev/null +++ b/src/handlers/themes.ts @@ -0,0 +1,42 @@ +import { dbFunctions } from "~/core/database"; +import type { Theme } from "~/typings/database"; + +class themeHandler { + getThemes(): Theme[] { + return dbFunctions.getThemes(); + } + addTheme(theme: Theme) { + try { + const rawVars = + typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; + + const cssVars = Object.entries(rawVars) + .map(([key, value]) => `--${key}: ${value};`) + .join(" "); + + const varsString = `.root, #root, #docs-root { ${cssVars} }`; + + return dbFunctions.addTheme({ + ...theme, + vars: varsString, + }); + } catch (error) { + throw new Error( + `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, + ); + } + } + deleteTheme(name: string) { + try { + dbFunctions.deleteTheme(name); + return "Deleted theme"; + } catch (error) { + throw new Error(`Could not save theme ${name}, error: ${error}`); + } + } + getTheme(name: string): Theme { + return dbFunctions.getSpecificTheme(name); + } +} + +export const ThemeHandler = new themeHandler(); diff --git a/src/handlers/utils.ts b/src/handlers/utils.ts new file mode 100644 index 00000000..80b32d0b --- /dev/null +++ b/src/handlers/utils.ts @@ -0,0 +1,6 @@ +import { logger } from "~/core/utils/logger"; + +export async function CheckHealth(): Promise<"healthy"> { + logger.info("Checking health"); + return "healthy"; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..ade06411 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +import { handlers } from "./handlers"; + +export default handlers; diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts new file mode 100644 index 00000000..178ea705 --- /dev/null +++ b/src/plugins/example.plugin.ts @@ -0,0 +1,99 @@ +import { logger } from "~/core/utils/logger"; + +import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; + +// See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info + +const ExamplePlugin: Plugin = { + name: "Example Plugin", + version: "1.0.0", + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + ); + }, +} satisfies Plugin; + +export default ExamplePlugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts new file mode 100644 index 00000000..7e43f1a5 --- /dev/null +++ b/src/plugins/telegram.plugin.ts @@ -0,0 +1,37 @@ +import { logger } from "~/core/utils/logger"; + +import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; + +const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token +const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID + +const TelegramNotificationPlugin: Plugin = { + name: "Telegram Notification Plugin", + version: "1.0.0", + + async onContainerStart(containerInfo: ContainerInfo) { + const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; + try { + const response = await fetch( + `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: TELEGRAM_CHAT_ID, + text: message, + }), + }, + ); + if (!response.ok) { + logger.error(`HTTP error ${response.status}`); + } + logger.info("Telegram notification sent."); + } catch (error) { + logger.error("Failed to send Telegram notification", error as string); + } + }, +} satisfies Plugin; + +export default TelegramNotificationPlugin; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b0ce6926 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + "jsx": "react", + "jsxFactory": "Html.createElement", + "jsxFragmentFactory": "Html.Fragment", + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + "outDir": "build/", + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, + "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, + "paths": { + "~/*": ["./src/*"], + "~/typings/*": ["./typings/*"] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "bun-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + "inlineSourceMap": true /* Include sourcemap files inside the emitted JavaScript. */, + "inlineSources": true /* Include source code in the sourcemaps inside the emitted JavaScript. */, + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/typings b/typings new file mode 160000 index 00000000..9d5500fc --- /dev/null +++ b/typings @@ -0,0 +1 @@ +Subproject commit 9d5500fcbcb1d217b898ba85a929ebb26c42f898