diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..1adf5fe
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,17 @@
+# Should be identical to .gitignore
+.env
+node_modules
+.idea
+data
+stacks
+tmp
+/private
+
+# Docker extra
+docker
+frontend
+.editorconfig
+.eslintrc.cjs
+.git
+.gitignore
+README.md
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..47bf476
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,24 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.yaml]
+indent_size = 2
+
+[*.yml]
+indent_size = 2
+
+[*.vue]
+trim_trailing_whitespace = false
+
+[*.go]
+indent_style = tab
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..1a292dc
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,97 @@
+module.exports = {
+ root: true,
+ env: {
+ browser: true,
+ node: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:vue/vue3-recommended",
+ ],
+ parser: "vue-eslint-parser",
+ parserOptions: {
+ "parser": "@typescript-eslint/parser",
+ },
+ plugins: [
+ "@typescript-eslint",
+ "jsdoc"
+ ],
+ rules: {
+ "yoda": "error",
+ "linebreak-style": [ "error", "unix" ],
+ "camelcase": [ "warn", {
+ "properties": "never",
+ "ignoreImports": true
+ }],
+ "no-unused-vars": [ "warn", {
+ "args": "none"
+ }],
+ indent: [
+ "error",
+ 4,
+ {
+ ignoredNodes: [ "TemplateLiteral" ],
+ SwitchCase: 1,
+ },
+ ],
+ quotes: [ "error", "double" ],
+ semi: "error",
+ "vue/html-indent": [ "error", 4 ], // default: 2
+ "vue/max-attributes-per-line": "off",
+ "vue/singleline-html-element-content-newline": "off",
+ "vue/html-self-closing": "off",
+ "vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675
+ "vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
+ "vue/multi-word-component-names": "off",
+ "no-multi-spaces": [ "error", {
+ ignoreEOLComments: true,
+ }],
+ "array-bracket-spacing": [ "warn", "always", {
+ "singleValue": true,
+ "objectsInArrays": false,
+ "arraysInArrays": false
+ }],
+ "space-before-function-paren": [ "error", {
+ "anonymous": "always",
+ "named": "never",
+ "asyncArrow": "always"
+ }],
+ "curly": "error",
+ "object-curly-spacing": [ "error", "always" ],
+ "object-curly-newline": "off",
+ "object-property-newline": "error",
+ "comma-spacing": "error",
+ "brace-style": "error",
+ "no-var": "error",
+ "key-spacing": "warn",
+ "keyword-spacing": "warn",
+ "space-infix-ops": "error",
+ "arrow-spacing": "warn",
+ "no-trailing-spaces": "error",
+ "no-constant-condition": [ "error", {
+ "checkLoops": false,
+ }],
+ "space-before-blocks": "warn",
+ "no-extra-boolean-cast": "off",
+ "no-multiple-empty-lines": [ "warn", {
+ "max": 1,
+ "maxBOF": 0,
+ }],
+ "lines-between-class-members": [ "warn", "always", {
+ exceptAfterSingleLine: true,
+ }],
+ "no-unneeded-ternary": "error",
+ "array-bracket-newline": [ "error", "consistent" ],
+ "eol-last": [ "error", "always" ],
+ "comma-dangle": [ "warn", "only-multiline" ],
+ "no-empty": [ "error", {
+ "allowEmptyCatch": true
+ }],
+ "no-control-regex": "off",
+ "one-var": [ "error", "never" ],
+ "max-statements-per-line": [ "error", { "max": 1 }],
+ "@typescript-eslint/ban-ts-comment": "off",
+ "prefer-const" : "off",
+ },
+};
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..d55fbd4
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: louislam # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+#patreon: # Replace with a single Patreon username
+open_collective: uptime-kuma # Replace with a single Open Collective username
+#ko_fi: # Replace with a single Ko-fi username
+#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+#liberapay: # Replace with a single Liberapay username
+#issuehunt: # Replace with a single IssueHunt username
+#otechie: # Replace with a single Otechie username
+#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..decd817
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# Should update .dockerignore as well
+.env
+node_modules
+.idea
+data
+stacks
+tmp
+/private
+
+# Git only
+frontend-dist
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a954450
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Louis Lam
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index cd7e17a..e5b8615 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,131 @@
+
+
+
+
# Dockge
+
+A fancy, easy-to-use and reactive docker `compose.yaml` stack-oriented manager.
+
+
+
+## ⭐ Features
+
+- Manage `compose.yaml`
+- Interactive Editor for `compose.yaml`
+- Interactive Web Terminal
+- Reactive
+ - Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time
+- Easy-to-use & fancy UI
+ - If you love Uptime Kuma's UI/UX, you will love this too
+- Convert `docker run ...` commands into `compose.yaml`
+
+## 🔧 How to Install
+
+Requirements:
+- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended
+- [Docker Compose V2](https://docs.docker.com/compose/install/linux/)
+- OS:
+ - As long as you can run Docker CE, it should be fine, but:
+ - Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye
+- Arch: armv7, arm64, amd64 (a.k.a x86_64)
+
+### Basic
+
+Default stacks directory is `/opt/stacks`.
+
+```
+# Create a directory that stores your stacks and stores dockge's compose.yaml
+mkdir -p /opt/stacks /opt/dockge
+cd /opt/dockge
+
+# Download the compose.yaml
+curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --output compose.yaml
+
+# Start Server
+docker compose up -d
+
+# If you are using docker-compose V1
+# docker-compose up -d
+```
+
+### Advanced
+
+If you want to store your stacks in another directory, you can change the `DOCKGE_STACKS_DIR` environment variable and volumes.
+
+For exmaples, if you want to store your stacks in `/my-stacks`:
+
+```yaml
+version: "3.8"
+services:
+ dockge:
+ image: louislam/dockge:1
+ restart: unless-stopped
+ ports:
+ - 5001:5001
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ./data:/app/data
+
+ # Your stacks directory in the host
+ # (The paths inside container must be the same as the host)
+ - /my-stacks:/my-stacks
+ environment:
+ # Tell Dockge where is your stacks directory
+ - DOCKGE_STACKS_DIR=/my-stacks
+```
+
+## How to Update
+
+```bash
+cd /opt/stacks
+docker compose pull
+docker compose up -d
+```
+
+## Motivations
+
+- I have been using Portainer for some time, but for the stack management, I am sometimes not satisfied with it. For example, sometimes when I try to deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear.
+- Try to develop with ES Module + TypeScript (Originally, I planned to use Deno or Bun.js, but they do not support for arm64, so I stepped back to Node.js)
+
+If you love this project, please consider giving this project a ⭐.
+
+
+## FAQ
+
+#### "Dockge"?
+
+"Dockge" is a coinage word which is created by myself. I hope it sounds like `Badge` but replacing with `Dock` - `Dock-ge`.
+
+The naming idea was coming from Twitch emotes like `sadge`, `bedge` or `wokege`. They are all ending with `-ge`.
+
+If you are not comfortable with the pronunciation, you can call it `Dockage`
+
+#### Can I manage a single container without `compose.yaml`?
+
+The main objective of Dockge is that try to use docker `compose.yaml` for everything. If you want to manage a single container, you can just use Portainer or Docker CLI.
+
+#### Can I manage existing stacks?
+
+Yes, you can. However, you need to move your compose file into the stacks directory:
+
+1. Stop your stack
+2. Move your compose file into `/opt/stacks//compose.yaml`
+3. In Dockge, click the " Scan Stacks Folder" button in the top-right corner's dropdown menu
+4. Now you should see your stack in the list
+
+## More Ideas?
+
+- Stats
+- File manager
+- App store for yaml templates
+- Get app icons
+- Switch Docker context
+- Support Dockerfile and build
+- Support Docker swarm
+
+
+# Others
+
+Dockge is built on top of [Compose V2](https://docs.docker.com/compose/migrate/). `compose.yaml` is also known as `docker-compose.yml`.
+
+
diff --git a/backend/check-version.ts b/backend/check-version.ts
new file mode 100644
index 0000000..acfb5fb
--- /dev/null
+++ b/backend/check-version.ts
@@ -0,0 +1,71 @@
+import { log } from "./log";
+import compareVersions from "compare-versions";
+import packageJSON from "../package.json";
+import { Settings } from "./settings";
+
+export const obj = {
+ version: packageJSON.version,
+ latestVersion: null,
+};
+export default obj;
+
+// How much time in ms to wait between update checks
+const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
+const CHECK_URL = "https://dockge.kuma.pet/version";
+
+let interval : NodeJS.Timeout;
+
+export function startInterval() {
+ const check = async () => {
+ if (await Settings.get("checkUpdate") === false) {
+ return;
+ }
+
+ log.debug("update-checker", "Retrieving latest versions");
+
+ try {
+ const res = await fetch(CHECK_URL);
+ const data = await res.json();
+
+ // For debug
+ if (process.env.TEST_CHECK_VERSION === "1") {
+ data.slow = "1000.0.0";
+ }
+
+ const checkBeta = await Settings.get("checkBeta");
+
+ if (checkBeta && data.beta) {
+ if (compareVersions.compare(data.beta, data.slow, ">")) {
+ obj.latestVersion = data.beta;
+ return;
+ }
+ }
+
+ if (data.slow) {
+ obj.latestVersion = data.slow;
+ }
+
+ } catch (_) {
+ log.info("update-checker", "Failed to check for new versions");
+ }
+
+ };
+
+ check();
+ interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
+}
+
+/**
+ * Enable the check update feature
+ * @param value Should the check update feature be enabled?
+ * @returns
+ */
+export async function enableCheckUpdate(value : boolean) {
+ await Settings.set("checkUpdate", value);
+
+ clearInterval(interval);
+
+ if (value) {
+ startInterval();
+ }
+}
diff --git a/backend/database.ts b/backend/database.ts
new file mode 100644
index 0000000..d508af4
--- /dev/null
+++ b/backend/database.ts
@@ -0,0 +1,247 @@
+import { log } from "./log";
+import { R } from "redbean-node";
+import { DockgeServer } from "./dockge-server";
+import fs from "fs";
+import path from "path";
+import knex from "knex";
+
+import Dialect from "knex/lib/dialects/sqlite3/index.js";
+
+import sqlite from "@louislam/sqlite3";
+import { sleep } from "./util-common";
+
+interface DBConfig {
+ type?: "sqlite" | "mysql";
+}
+
+export class Database {
+ /**
+ * SQLite file path (Default: ./data/dockge.db)
+ * @type {string}
+ */
+ static sqlitePath;
+
+ static noReject = true;
+
+ static dbConfig: DBConfig = {};
+
+ static knexMigrationsPath = "./backend/migrations";
+
+ private static server : DockgeServer;
+
+ /**
+ * Use for decode the auth object
+ */
+ jwtSecret? : string;
+
+ static async init(server : DockgeServer) {
+ this.server = server;
+
+ log.debug("server", "Connecting to the database");
+ await Database.connect();
+ log.info("server", "Connected to the database");
+
+ // Patch the database
+ await Database.patch();
+ }
+
+ /**
+ * Read the database config
+ * @throws {Error} If the config is invalid
+ * @typedef {string|undefined} envString
+ * @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
+ */
+ static readDBConfig() {
+ const dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8");
+ const dbConfig = JSON.parse(dbConfigString);
+
+ if (typeof dbConfig !== "object") {
+ throw new Error("Invalid db-config.json, it must be an object");
+ }
+
+ if (typeof dbConfig.type !== "string") {
+ throw new Error("Invalid db-config.json, type must be a string");
+ }
+ return dbConfig;
+ }
+
+ /**
+ * @typedef {string|undefined} envString
+ * @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written
+ * @returns {void}
+ */
+ static writeDBConfig(dbConfig) {
+ fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
+ }
+
+ /**
+ * Connect to the database
+ * @param {boolean} autoloadModels Should models be automatically loaded?
+ * @param {boolean} noLog Should logs not be output?
+ * @returns {Promise}
+ */
+ static async connect(autoloadModels = true, noLog = false) {
+ const acquireConnectionTimeout = 120 * 1000;
+ let dbConfig;
+ try {
+ dbConfig = this.readDBConfig();
+ Database.dbConfig = dbConfig;
+ } catch (err) {
+ log.warn("db", err.message);
+ dbConfig = {
+ type: "sqlite",
+ };
+ this.writeDBConfig(dbConfig);
+ }
+
+ let config = {};
+
+ log.info("db", `Database Type: ${dbConfig.type}`);
+
+ if (dbConfig.type === "sqlite") {
+ this.sqlitePath = path.join(this.server.config.dataDir, "dockge.db");
+ Dialect.prototype._driver = () => sqlite;
+
+ config = {
+ client: Dialect,
+ connection: {
+ filename: Database.sqlitePath,
+ acquireConnectionTimeout: acquireConnectionTimeout,
+ },
+ useNullAsDefault: true,
+ pool: {
+ min: 1,
+ max: 1,
+ idleTimeoutMillis: 120 * 1000,
+ propagateCreateError: false,
+ acquireTimeoutMillis: acquireConnectionTimeout,
+ }
+ };
+ } else {
+ throw new Error("Unknown Database type: " + dbConfig.type);
+ }
+
+ const knexInstance = knex(config);
+
+ // @ts-ignore
+ R.setup(knexInstance);
+
+ if (process.env.SQL_LOG === "1") {
+ R.debug(true);
+ }
+
+ // Auto map the model to a bean object
+ R.freeze(true);
+
+ if (autoloadModels) {
+ R.autoloadModels("./backend/models", "ts");
+ }
+
+ if (dbConfig.type === "sqlite") {
+ await this.initSQLite();
+ }
+ }
+
+ /**
+ @returns {Promise}
+ */
+ static async initSQLite() {
+ await R.exec("PRAGMA foreign_keys = ON");
+ // Change to WAL
+ await R.exec("PRAGMA journal_mode = WAL");
+ await R.exec("PRAGMA cache_size = -12000");
+ await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
+
+ // This ensures that an operating system crash or power failure will not corrupt the database.
+ // FULL synchronous is very safe, but it is also slower.
+ // Read more: https://sqlite.org/pragma.html#pragma_synchronous
+ await R.exec("PRAGMA synchronous = NORMAL");
+
+ log.debug("db", "SQLite config:");
+ log.debug("db", await R.getAll("PRAGMA journal_mode"));
+ log.debug("db", await R.getAll("PRAGMA cache_size"));
+ log.debug("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
+ }
+
+ /**
+ * Patch the database
+ * @returns {void}
+ */
+ static async patch() {
+ // Using knex migrations
+ // https://knexjs.org/guide/migrations.html
+ // https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
+ try {
+ await R.knex.migrate.latest({
+ directory: Database.knexMigrationsPath,
+ });
+ } catch (e) {
+ // Allow missing patch files for downgrade or testing pr.
+ if (e.message.includes("the following files are missing:")) {
+ log.warn("db", e.message);
+ log.warn("db", "Database migration failed, you may be downgrading Dockge.");
+ } else {
+ log.error("db", "Database migration failed");
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Special handle, because tarn.js throw a promise reject that cannot be caught
+ * @returns {Promise}
+ */
+ static async close() {
+ const listener = () => {
+ Database.noReject = false;
+ };
+ process.addListener("unhandledRejection", listener);
+
+ log.info("db", "Closing the database");
+
+ // Flush WAL to main database
+ if (Database.dbConfig.type === "sqlite") {
+ await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
+ }
+
+ while (true) {
+ Database.noReject = true;
+ await R.close();
+ await sleep(2000);
+
+ if (Database.noReject) {
+ break;
+ } else {
+ log.info("db", "Waiting to close the database");
+ }
+ }
+ log.info("db", "Database closed");
+
+ process.removeListener("unhandledRejection", listener);
+ }
+
+ /**
+ * Get the size of the database (SQLite only)
+ * @returns {number} Size of database
+ */
+ static getSize() {
+ if (Database.dbConfig.type === "sqlite") {
+ log.debug("db", "Database.getSize()");
+ const stats = fs.statSync(Database.sqlitePath);
+ log.debug("db", stats);
+ return stats.size;
+ }
+ return 0;
+ }
+
+ /**
+ * Shrink the database
+ * @returns {Promise}
+ */
+ static async shrink() {
+ if (Database.dbConfig.type === "sqlite") {
+ await R.exec("VACUUM");
+ }
+ }
+
+}
diff --git a/backend/docker.ts b/backend/docker.ts
new file mode 100644
index 0000000..4083a73
--- /dev/null
+++ b/backend/docker.ts
@@ -0,0 +1,3 @@
+export class Docker {
+
+}
diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts
new file mode 100644
index 0000000..ca081b5
--- /dev/null
+++ b/backend/dockge-server.ts
@@ -0,0 +1,559 @@
+import { MainRouter } from "./routers/main-router";
+import * as fs from "node:fs";
+import { PackageJson } from "type-fest";
+import { Database } from "./database";
+import packageJSON from "../package.json";
+import { log } from "./log";
+import * as socketIO from "socket.io";
+import express, { Express } from "express";
+import { parse } from "ts-command-line-args";
+import https from "https";
+import http from "http";
+import { Router } from "./router";
+import { Socket } from "socket.io";
+import { MainSocketHandler } from "./socket-handlers/main-socket-handler";
+import { SocketHandler } from "./socket-handler";
+import { Settings } from "./settings";
+import checkVersion from "./check-version";
+import dayjs from "dayjs";
+import { R } from "redbean-node";
+import { genSecret, isDev } from "./util-common";
+import { generatePasswordHash } from "./password-hash";
+import { Bean } from "redbean-node/dist/bean";
+import { Arguments, Config, DockgeSocket } from "./util-server";
+import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler";
+import expressStaticGzip from "express-static-gzip";
+import path from "path";
+import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler";
+import { Stack } from "./stack";
+import { Cron } from "croner";
+import gracefulShutdown from "http-graceful-shutdown";
+import User from "./models/user";
+import childProcess from "child_process";
+
+export class DockgeServer {
+ app : Express;
+ httpServer : http.Server;
+ packageJSON : PackageJson;
+ io : socketIO.Server;
+ config : Config;
+ indexHTML : string = "";
+
+ /**
+ * List of express routers
+ */
+ routerList : Router[] = [
+ new MainRouter(),
+ ];
+
+ /**
+ * List of socket handlers
+ */
+ socketHandlerList : SocketHandler[] = [
+ new MainSocketHandler(),
+ new DockerSocketHandler(),
+ new TerminalSocketHandler(),
+ ];
+
+ /**
+ * Show Setup Page
+ */
+ needSetup = false;
+
+ jwtSecret? : string;
+
+ stacksDir : string = "";
+
+ /**
+ *
+ */
+ constructor() {
+ // Catch unexpected errors here
+ let unexpectedErrorHandler = (error : unknown) => {
+ console.trace(error);
+ console.error("If you keep encountering errors, please report to https://github.com/louislam/dockge");
+ };
+ process.addListener("unhandledRejection", unexpectedErrorHandler);
+ process.addListener("uncaughtException", unexpectedErrorHandler);
+
+ if (!process.env.NODE_ENV) {
+ process.env.NODE_ENV = "production";
+ }
+
+ // Log NODE ENV
+ log.info("server", "NODE_ENV: " + process.env.NODE_ENV);
+
+ // Default stacks directory
+ let defaultStacksDir;
+ if (process.platform === "win32") {
+ defaultStacksDir = "./stacks";
+ } else {
+ defaultStacksDir = "/opt/stacks";
+ }
+
+ // Define all possible arguments
+ let args = parse({
+ sslKey: {
+ type: String,
+ optional: true,
+ },
+ sslCert: {
+ type: String,
+ optional: true,
+ },
+ sslKeyPassphrase: {
+ type: String,
+ optional: true,
+ },
+ port: {
+ type: Number,
+ optional: true,
+ },
+ hostname: {
+ type: String,
+ optional: true,
+ },
+ dataDir: {
+ type: String,
+ optional: true,
+ },
+ stacksDir: {
+ type: String,
+ optional: true,
+ }
+ });
+
+ this.config = args as Config;
+
+ // Load from environment variables or default values if args are not set
+ this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
+ this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined;
+ this.config.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined;
+ this.config.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001;
+ this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
+ this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
+ this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir;
+ this.stacksDir = this.config.stacksDir;
+
+ log.debug("server", this.config);
+
+ this.packageJSON = packageJSON as PackageJson;
+
+ try {
+ this.indexHTML = fs.readFileSync("./frontend-dist/index.html").toString();
+ } catch (e) {
+ // "dist/index.html" is not necessary for development
+ if (process.env.NODE_ENV !== "development") {
+ log.error("server", "Error: Cannot find 'frontend-dist/index.html', did you install correctly?");
+ process.exit(1);
+ }
+ }
+
+ // Create all the necessary directories
+ this.initDataDir();
+
+ // Create express
+ this.app = express();
+
+ // Create HTTP server
+ if (this.config.sslKey && this.config.sslCert) {
+ log.info("server", "Server Type: HTTPS");
+ this.httpServer = https.createServer({
+ key: fs.readFileSync(this.config.sslKey),
+ cert: fs.readFileSync(this.config.sslCert),
+ passphrase: this.config.sslKeyPassphrase,
+ }, this.app);
+ } else {
+ log.info("server", "Server Type: HTTP");
+ this.httpServer = http.createServer(this.app);
+ }
+
+ // Binding Routers
+ for (const router of this.routerList) {
+ this.app.use(router.create(this.app, this));
+ }
+
+ // Static files
+ this.app.use("/", expressStaticGzip("frontend-dist", {
+ enableBrotli: true,
+ }));
+
+ // Universal Route Handler, must be at the end of all express routes.
+ this.app.get("*", async (_request, response) => {
+ response.send(this.indexHTML);
+ });
+
+ // Allow all CORS origins in development
+ let cors = undefined;
+ if (isDev) {
+ cors = {
+ origin: "*",
+ };
+ }
+
+ // Create Socket.io
+ this.io = new socketIO.Server(this.httpServer, {
+ cors,
+ });
+
+ this.io.on("connection", async (socket: Socket) => {
+ log.info("server", "Socket connected!");
+
+ this.sendInfo(socket, true);
+
+ if (this.needSetup) {
+ log.info("server", "Redirect to setup page");
+ socket.emit("setup");
+ }
+
+ // Create socket handlers
+ for (const socketHandler of this.socketHandlerList) {
+ socketHandler.create(socket as DockgeSocket, this);
+ }
+
+ // ***************************
+ // Better do anything after added all socket handlers here
+ // ***************************
+
+ log.debug("auth", "check auto login");
+ if (await Settings.get("disableAuth")) {
+ log.info("auth", "Disabled Auth: auto login to admin");
+ this.afterLogin(socket as DockgeSocket, await R.findOne("user"));
+ socket.emit("autoLogin");
+ } else {
+ log.debug("auth", "need auth");
+ }
+
+ });
+
+ this.io.on("disconnect", () => {
+
+ });
+
+ }
+
+ async afterLogin(socket : DockgeSocket, user : User) {
+ socket.userID = user.id;
+ socket.join(user.id.toString());
+
+ this.sendInfo(socket);
+
+ try {
+ this.sendStackList();
+ } catch (e) {
+ log.error("server", e);
+ }
+ }
+
+ /**
+ *
+ */
+ async serve() {
+ // Connect to database
+ try {
+ await Database.init(this);
+ } catch (e) {
+ log.error("server", "Failed to prepare your database: " + e.message);
+ process.exit(1);
+ }
+
+ // First time setup if needed
+ let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
+ "jwtSecret",
+ ]);
+
+ if (! jwtSecretBean) {
+ log.info("server", "JWT secret is not found, generate one.");
+ jwtSecretBean = await this.initJWTSecret();
+ log.info("server", "Stored JWT secret into database");
+ } else {
+ log.debug("server", "Load JWT secret from database.");
+ }
+
+ this.jwtSecret = jwtSecretBean.value;
+
+ const userCount = (await R.knex("user").count("id as count").first()).count;
+
+ log.debug("server", "User count: " + userCount);
+
+ // If there is no record in user table, it is a new Dockge instance, need to setup
+ if (userCount == 0) {
+ log.info("server", "No user, need setup");
+ this.needSetup = true;
+ }
+
+ // Listen
+ this.httpServer.listen(5001, this.config.hostname, () => {
+ if (this.config.hostname) {
+ log.info( "server", `Listening on ${this.config.hostname}:${this.config.port}`);
+ } else {
+ log.info("server", `Listening on ${this.config.port}`);
+ }
+
+ // Run every 5 seconds
+ const job = Cron("*/2 * * * * *", {
+ protect: true, // Enabled over-run protection.
+ }, () => {
+ log.debug("server", "Cron job running");
+ this.sendStackList(true);
+ });
+
+ });
+
+ gracefulShutdown(this.httpServer, {
+ signals: "SIGINT SIGTERM",
+ timeout: 30000, // timeout: 30 secs
+ development: false, // not in dev mode
+ forceExit: true, // triggers process.exit() at the end of shutdown process
+ onShutdown: this.shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
+ finally: this.finalFunction, // finally function (sync) - e.g. for logging
+ });
+
+ }
+
+ /**
+ * Emits the version information to the client.
+ * @param socket Socket.io socket instance
+ * @param hideVersion Should we hide the version information in the response?
+ * @returns
+ */
+ async sendInfo(socket : Socket, hideVersion = false) {
+ let versionProperty;
+ let latestVersionProperty;
+ let isContainer;
+
+ if (!hideVersion) {
+ versionProperty = packageJSON.version;
+ latestVersionProperty = checkVersion.latestVersion;
+ isContainer = (process.env.DOCKGE_IS_CONTAINER === "1");
+ }
+
+ socket.emit("info", {
+ version: versionProperty,
+ latestVersion: latestVersionProperty,
+ isContainer,
+ primaryHostname: await Settings.get("primaryHostname"),
+ //serverTimezone: await this.getTimezone(),
+ //serverTimezoneOffset: this.getTimezoneOffset(),
+ });
+ }
+
+ /**
+ * Get the IP of the client connected to the socket
+ * @param {Socket} socket Socket to query
+ * @returns IP of client
+ */
+ async getClientIP(socket : Socket) : Promise {
+ let clientIP = socket.client.conn.remoteAddress;
+
+ if (clientIP === undefined) {
+ clientIP = "";
+ }
+
+ if (await Settings.get("trustProxy")) {
+ const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
+
+ if (typeof forwardedFor === "string") {
+ return forwardedFor.split(",")[0].trim();
+ } else if (typeof socket.client.conn.request.headers["x-real-ip"] === "string") {
+ return socket.client.conn.request.headers["x-real-ip"];
+ }
+ }
+ return clientIP.replace(/^::ffff:/, "");
+ }
+
+ /**
+ * Attempt to get the current server timezone
+ * If this fails, fall back to environment variables and then make a
+ * guess.
+ * @returns {Promise} Current timezone
+ */
+ async getTimezone() {
+ // From process.env.TZ
+ try {
+ if (process.env.TZ) {
+ this.checkTimezone(process.env.TZ);
+ return process.env.TZ;
+ }
+ } catch (e) {
+ log.warn("timezone", e.message + " in process.env.TZ");
+ }
+
+ const timezone = await Settings.get("serverTimezone");
+
+ // From Settings
+ try {
+ log.debug("timezone", "Using timezone from settings: " + timezone);
+ if (timezone) {
+ this.checkTimezone(timezone);
+ return timezone;
+ }
+ } catch (e) {
+ log.warn("timezone", e.message + " in settings");
+ }
+
+ // Guess
+ try {
+ const guess = dayjs.tz.guess();
+ log.debug("timezone", "Guessing timezone: " + guess);
+ if (guess) {
+ this.checkTimezone(guess);
+ return guess;
+ } else {
+ return "UTC";
+ }
+ } catch (e) {
+ // Guess failed, fall back to UTC
+ log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback");
+ return "UTC";
+ }
+ }
+
+ /**
+ * Get the current offset
+ * @returns {string} Time offset
+ */
+ getTimezoneOffset() {
+ return dayjs().format("Z");
+ }
+
+ /**
+ * Throw an error if the timezone is invalid
+ * @param {string} timezone Timezone to test
+ * @returns {void}
+ * @throws The timezone is invalid
+ */
+ checkTimezone(timezone : string) {
+ try {
+ dayjs.utc("2013-11-18 11:55").tz(timezone).format();
+ } catch (e) {
+ throw new Error("Invalid timezone:" + timezone);
+ }
+ }
+
+ /**
+ * Initialize the data directory
+ */
+ initDataDir() {
+ if (! fs.existsSync(this.config.dataDir)) {
+ fs.mkdirSync(this.config.dataDir, { recursive: true });
+ }
+
+ // Check if a directory
+ if (!fs.lstatSync(this.config.dataDir).isDirectory()) {
+ throw new Error(`Fatal error: ${this.config.dataDir} is not a directory`);
+ }
+
+ // Create data/stacks directory
+ if (!fs.existsSync(this.stacksDir)) {
+ fs.mkdirSync(this.stacksDir, { recursive: true });
+ }
+
+ log.info("server", `Data Dir: ${this.config.dataDir}`);
+ }
+
+ /**
+ * Init or reset JWT secret
+ * @returns JWT secret
+ */
+ async initJWTSecret() : Promise {
+ let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
+ "jwtSecret",
+ ]);
+
+ if (!jwtSecretBean) {
+ jwtSecretBean = R.dispense("setting");
+ jwtSecretBean.key = "jwtSecret";
+ }
+
+ jwtSecretBean.value = generatePasswordHash(genSecret());
+ await R.store(jwtSecretBean);
+ return jwtSecretBean;
+ }
+
+ sendStackList(useCache = false) {
+ let roomList = this.io.sockets.adapter.rooms.keys();
+ let map : Map | undefined;
+
+ for (let room of roomList) {
+ // Check if the room is a number (user id)
+ if (Number(room)) {
+
+ // Get the list only if there is a room
+ if (!map) {
+ map = new Map();
+ let stackList = Stack.getStackList(this, useCache);
+
+ for (let [ stackName, stack ] of stackList) {
+ map.set(stackName, stack.toSimpleJSON());
+ }
+ }
+
+ log.debug("server", "Send stack list to room " + room);
+ this.io.to(room).emit("stackList", {
+ ok: true,
+ stackList: Object.fromEntries(map),
+ });
+ }
+ }
+ }
+
+ sendStackStatusList() {
+ let statusList = Stack.getStatusList();
+
+ let roomList = this.io.sockets.adapter.rooms.keys();
+
+ for (let room of roomList) {
+ // Check if the room is a number (user id)
+ if (Number(room)) {
+ log.debug("server", "Send stack status list to room " + room);
+ this.io.to(room).emit("stackStatusList", {
+ ok: true,
+ stackStatusList: Object.fromEntries(statusList),
+ });
+ } else {
+ log.debug("server", "Skip sending stack status list to room " + room);
+ }
+ }
+ }
+
+ getDockerNetworkList() : string[] {
+ let res = childProcess.spawnSync("docker", [ "network", "ls", "--format", "{{.Name}}" ]);
+ let list = res.stdout.toString().split("\n");
+
+ // Remove empty string item
+ list = list.filter((item) => {
+ return item !== "";
+ }).sort((a, b) => {
+ return a.localeCompare(b);
+ });
+
+ return list;
+ }
+
+ get stackDirFullPath() {
+ return path.resolve(this.stacksDir);
+ }
+
+ /**
+ * Shutdown the application
+ * Stops all monitors and closes the database connection.
+ * @param signal The signal that triggered this function to be called.
+ */
+ async shutdownFunction(signal : string | undefined) {
+ log.info("server", "Shutdown requested");
+ log.info("server", "Called signal: " + signal);
+
+ // TODO: Close all terminals?
+
+ await Database.close();
+ Settings.stopCacheCleaner();
+ }
+
+ /**
+ * Final function called before application exits
+ */
+ finalFunction() {
+ log.info("server", "Graceful shutdown successful!");
+ }
+}
diff --git a/backend/index.ts b/backend/index.ts
new file mode 100644
index 0000000..d0b53f2
--- /dev/null
+++ b/backend/index.ts
@@ -0,0 +1,6 @@
+import { DockgeServer } from "./dockge-server";
+import { log } from "./log";
+
+log.info("server", "Welcome to dockge!");
+const server = new DockgeServer();
+await server.serve();
diff --git a/backend/log.ts b/backend/log.ts
new file mode 100644
index 0000000..37f2d4a
--- /dev/null
+++ b/backend/log.ts
@@ -0,0 +1,208 @@
+// Console colors
+// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
+import { intHash, isDev } from "./util-common";
+import dayjs from "dayjs";
+
+export const CONSOLE_STYLE_Reset = "\x1b[0m";
+export const CONSOLE_STYLE_Bright = "\x1b[1m";
+export const CONSOLE_STYLE_Dim = "\x1b[2m";
+export const CONSOLE_STYLE_Underscore = "\x1b[4m";
+export const CONSOLE_STYLE_Blink = "\x1b[5m";
+export const CONSOLE_STYLE_Reverse = "\x1b[7m";
+export const CONSOLE_STYLE_Hidden = "\x1b[8m";
+
+export const CONSOLE_STYLE_FgBlack = "\x1b[30m";
+export const CONSOLE_STYLE_FgRed = "\x1b[31m";
+export const CONSOLE_STYLE_FgGreen = "\x1b[32m";
+export const CONSOLE_STYLE_FgYellow = "\x1b[33m";
+export const CONSOLE_STYLE_FgBlue = "\x1b[34m";
+export const CONSOLE_STYLE_FgMagenta = "\x1b[35m";
+export const CONSOLE_STYLE_FgCyan = "\x1b[36m";
+export const CONSOLE_STYLE_FgWhite = "\x1b[37m";
+export const CONSOLE_STYLE_FgGray = "\x1b[90m";
+export const CONSOLE_STYLE_FgOrange = "\x1b[38;5;208m";
+export const CONSOLE_STYLE_FgLightGreen = "\x1b[38;5;119m";
+export const CONSOLE_STYLE_FgLightBlue = "\x1b[38;5;117m";
+export const CONSOLE_STYLE_FgViolet = "\x1b[38;5;141m";
+export const CONSOLE_STYLE_FgBrown = "\x1b[38;5;130m";
+export const CONSOLE_STYLE_FgPink = "\x1b[38;5;219m";
+
+export const CONSOLE_STYLE_BgBlack = "\x1b[40m";
+export const CONSOLE_STYLE_BgRed = "\x1b[41m";
+export const CONSOLE_STYLE_BgGreen = "\x1b[42m";
+export const CONSOLE_STYLE_BgYellow = "\x1b[43m";
+export const CONSOLE_STYLE_BgBlue = "\x1b[44m";
+export const CONSOLE_STYLE_BgMagenta = "\x1b[45m";
+export const CONSOLE_STYLE_BgCyan = "\x1b[46m";
+export const CONSOLE_STYLE_BgWhite = "\x1b[47m";
+export const CONSOLE_STYLE_BgGray = "\x1b[100m";
+
+const consoleModuleColors = [
+ CONSOLE_STYLE_FgCyan,
+ CONSOLE_STYLE_FgGreen,
+ CONSOLE_STYLE_FgLightGreen,
+ CONSOLE_STYLE_FgBlue,
+ CONSOLE_STYLE_FgLightBlue,
+ CONSOLE_STYLE_FgMagenta,
+ CONSOLE_STYLE_FgOrange,
+ CONSOLE_STYLE_FgViolet,
+ CONSOLE_STYLE_FgBrown,
+ CONSOLE_STYLE_FgPink,
+];
+
+const consoleLevelColors : Record = {
+ "INFO": CONSOLE_STYLE_FgCyan,
+ "WARN": CONSOLE_STYLE_FgYellow,
+ "ERROR": CONSOLE_STYLE_FgRed,
+ "DEBUG": CONSOLE_STYLE_FgGray,
+};
+
+class Logger {
+
+ /**
+ * DOCKGE_HIDE_LOG=debug_monitor,info_monitor
+ *
+ * Example:
+ * [
+ * "debug_monitor", // Hide all logs that level is debug and the module is monitor
+ * "info_monitor",
+ * ]
+ */
+ hideLog : Record = {
+ info: [],
+ warn: [],
+ error: [],
+ debug: [],
+ };
+
+ /**
+ *
+ */
+ constructor() {
+ if (typeof process !== "undefined" && process.env.DOCKGE_HIDE_LOG) {
+ const list = process.env.DOCKGE_HIDE_LOG.split(",").map(v => v.toLowerCase());
+
+ for (const pair of list) {
+ // split first "_" only
+ const values = pair.split(/_(.*)/s);
+
+ if (values.length >= 2) {
+ this.hideLog[values[0]].push(values[1]);
+ }
+ }
+
+ this.debug("server", "DOCKGE_HIDE_LOG is set");
+ this.debug("server", this.hideLog);
+ }
+ }
+
+ /**
+ * Write a message to the log
+ * @param module The module the log comes from
+ * @param msg Message to write
+ * @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
+ */
+ log(module: string, msg: unknown, level: string) {
+ if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
+ return;
+ }
+
+ module = module.toUpperCase();
+ level = level.toUpperCase();
+
+ let now;
+ if (dayjs.tz) {
+ now = dayjs.tz(new Date()).format();
+ } else {
+ now = dayjs().format();
+ }
+
+ const levelColor = consoleLevelColors[level];
+ const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
+
+ let timePart = CONSOLE_STYLE_FgCyan + now + CONSOLE_STYLE_Reset;
+ const modulePart = "[" + moduleColor + module + CONSOLE_STYLE_Reset + "]";
+ const levelPart = levelColor + `${level}:` + CONSOLE_STYLE_Reset;
+
+ if (level === "INFO") {
+ console.info(timePart, modulePart, levelPart, msg);
+ } else if (level === "WARN") {
+ console.warn(timePart, modulePart, levelPart, msg);
+ } else if (level === "ERROR") {
+ let msgPart : unknown;
+ if (typeof msg === "string") {
+ msgPart = CONSOLE_STYLE_FgRed + msg + CONSOLE_STYLE_Reset;
+ } else {
+ msgPart = msg;
+ }
+ console.error(timePart, modulePart, levelPart, msgPart);
+ } else if (level === "DEBUG") {
+ if (isDev) {
+ timePart = CONSOLE_STYLE_FgGray + now + CONSOLE_STYLE_Reset;
+ let msgPart : unknown;
+ if (typeof msg === "string") {
+ msgPart = CONSOLE_STYLE_FgGray + msg + CONSOLE_STYLE_Reset;
+ } else {
+ msgPart = msg;
+ }
+ console.debug(timePart, modulePart, levelPart, msgPart);
+ }
+ } else {
+ console.log(timePart, modulePart, msg);
+ }
+ }
+
+ /**
+ * Log an INFO message
+ * @param module Module log comes from
+ * @param msg Message to write
+ */
+ info(module: string, msg: unknown) {
+ this.log(module, msg, "info");
+ }
+
+ /**
+ * Log a WARN message
+ * @param module Module log comes from
+ * @param msg Message to write
+ */
+ warn(module: string, msg: unknown) {
+ this.log(module, msg, "warn");
+ }
+
+ /**
+ * Log an ERROR message
+ * @param module Module log comes from
+ * @param msg Message to write
+ */
+ error(module: string, msg: unknown) {
+ this.log(module, msg, "error");
+ }
+
+ /**
+ * Log a DEBUG message
+ * @param module Module log comes from
+ * @param msg Message to write
+ */
+ debug(module: string, msg: unknown) {
+ this.log(module, msg, "debug");
+ }
+
+ /**
+ * Log an exception as an ERROR
+ * @param module Module log comes from
+ * @param exception The exception to include
+ * @param msg The message to write
+ */
+ exception(module: string, exception: unknown, msg: unknown) {
+ let finalMessage = exception;
+
+ if (msg) {
+ finalMessage = `${msg}: ${exception}`;
+ }
+
+ this.log(module, finalMessage, "error");
+ }
+}
+
+export const log = new Logger();
diff --git a/backend/migrations/2023-10-20-0829-setting-table.ts b/backend/migrations/2023-10-20-0829-setting-table.ts
new file mode 100644
index 0000000..56c5d6a
--- /dev/null
+++ b/backend/migrations/2023-10-20-0829-setting-table.ts
@@ -0,0 +1,14 @@
+import { Knex } from "knex";
+
+export async function up(knex: Knex): Promise {
+ return knex.schema.createTable("setting", (table) => {
+ table.increments("id");
+ table.string("key", 200).notNullable().unique().collate("utf8_general_ci");
+ table.text("value");
+ table.string("type", 20);
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ return knex.schema.dropTable("setting");
+}
diff --git a/backend/migrations/2023-10-20-0829-user-table.ts b/backend/migrations/2023-10-20-0829-user-table.ts
new file mode 100644
index 0000000..ca84b00
--- /dev/null
+++ b/backend/migrations/2023-10-20-0829-user-table.ts
@@ -0,0 +1,19 @@
+import { Knex } from "knex";
+
+export async function up(knex: Knex): Promise {
+ // Create the user table
+ return knex.schema.createTable("user", (table) => {
+ table.increments("id");
+ table.string("username", 255).notNullable().unique().collate("utf8_general_ci");
+ table.string("password", 255);
+ table.boolean("active").notNullable().defaultTo(true);
+ table.string("timezone", 150);
+ table.string("twofa_secret", 64);
+ table.boolean("twofa_status").notNullable().defaultTo(false);
+ table.string("twofa_last_token", 6);
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ return knex.schema.dropTable("user");
+}
diff --git a/backend/models/user.ts b/backend/models/user.ts
new file mode 100644
index 0000000..1a91c34
--- /dev/null
+++ b/backend/models/user.ts
@@ -0,0 +1,46 @@
+import jwt from "jsonwebtoken";
+import { R } from "redbean-node";
+import { BeanModel } from "redbean-node/dist/bean-model";
+import { generatePasswordHash, shake256, SHAKE256_LENGTH } from "../password-hash";
+
+export class User extends BeanModel {
+ /**
+ * Reset user password
+ * Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
+ * @param {number} userID ID of user to update
+ * @param {string} newPassword Users new password
+ * @returns {Promise}
+ */
+ static async resetPassword(userID : number, newPassword : string) {
+ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
+ generatePasswordHash(newPassword),
+ userID
+ ]);
+ }
+
+ /**
+ * Reset this users password
+ * @param {string} newPassword
+ * @returns {Promise}
+ */
+ async resetPassword(newPassword : string) {
+ await User.resetPassword(this.id, newPassword);
+ this.password = newPassword;
+ }
+
+ /**
+ * Create a new JWT for a user
+ * @param {User} user The User to create a JsonWebToken for
+ * @param {string} jwtSecret The key used to sign the JsonWebToken
+ * @returns {string} the JsonWebToken as a string
+ */
+ static createJWT(user : User, jwtSecret : string) {
+ return jwt.sign({
+ username: user.username,
+ h: shake256(user.password, SHAKE256_LENGTH),
+ }, jwtSecret);
+ }
+
+}
+
+export default User;
diff --git a/backend/password-hash.ts b/backend/password-hash.ts
new file mode 100644
index 0000000..fb8d42d
--- /dev/null
+++ b/backend/password-hash.ts
@@ -0,0 +1,47 @@
+import bcrypt from "bcryptjs";
+import crypto from "crypto";
+const saltRounds = 10;
+
+/**
+ * Hash a password
+ * @param {string} password Password to hash
+ * @returns {string} Hash
+ */
+export function generatePasswordHash(password : string) {
+ return bcrypt.hashSync(password, saltRounds);
+}
+
+/**
+ * Verify a password against a hash
+ * @param {string} password Password to verify
+ * @param {string} hash Hash to verify against
+ * @returns {boolean} Does the password match the hash?
+ */
+export function verifyPassword(password, hash) {
+ return bcrypt.compareSync(password, hash);
+}
+
+/**
+ * Does the hash need to be rehashed?
+ * @param {string} hash Hash to check
+ * @returns {boolean} Needs to be rehashed?
+ */
+export function needRehashPassword(hash : string) : boolean {
+ return false;
+}
+
+export const SHAKE256_LENGTH = 16;
+
+/**
+ * @param {string} data The data to be hashed
+ * @param {number} len Output length of the hash
+ * @returns {string} The hashed data in hex format
+ */
+export function shake256(data, len) {
+ if (!data) {
+ return "";
+ }
+ return crypto.createHash("shake256", { outputLength: len })
+ .update(data)
+ .digest("hex");
+}
diff --git a/backend/rate-limiter.ts b/backend/rate-limiter.ts
new file mode 100644
index 0000000..3c43abe
--- /dev/null
+++ b/backend/rate-limiter.ts
@@ -0,0 +1,75 @@
+// "limit" is bugged in Typescript, use "limiter-es6-compat" instead
+// See https://github.com/jhurliman/node-rate-limiter/issues/80
+import { RateLimiter } from "limiter-es6-compat";
+import { log } from "./log";
+
+class KumaRateLimiter {
+
+ errorMessage : string;
+ rateLimiter : RateLimiter;
+
+ /**
+ * @param {object} config Rate limiter configuration object
+ */
+ constructor(config) {
+ this.errorMessage = config.errorMessage;
+ this.rateLimiter = new RateLimiter(config);
+ }
+
+ /**
+ * Callback for pass
+ * @callback passCB
+ * @param {object} err Too many requests
+ */
+
+ /**
+ * Should the request be passed through
+ * @param {passCB} callback Callback function to call with decision
+ * @param {number} num Number of tokens to remove
+ * @returns {Promise} Should the request be allowed?
+ */
+ async pass(callback, num = 1) {
+ const remainingRequests = await this.removeTokens(num);
+ log.info("rate-limit", "remaining requests: " + remainingRequests);
+ if (remainingRequests < 0) {
+ if (callback) {
+ callback({
+ ok: false,
+ msg: this.errorMessage,
+ });
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove a given number of tokens
+ * @param {number} num Number of tokens to remove
+ * @returns {Promise} Number of remaining tokens
+ */
+ async removeTokens(num = 1) {
+ return await this.rateLimiter.removeTokens(num);
+ }
+}
+
+export const loginRateLimiter = new KumaRateLimiter({
+ tokensPerInterval: 20,
+ interval: "minute",
+ fireImmediately: true,
+ errorMessage: "Too frequently, try again later."
+});
+
+export const apiRateLimiter = new KumaRateLimiter({
+ tokensPerInterval: 60,
+ interval: "minute",
+ fireImmediately: true,
+ errorMessage: "Too frequently, try again later."
+});
+
+export const twoFaRateLimiter = new KumaRateLimiter({
+ tokensPerInterval: 30,
+ interval: "minute",
+ fireImmediately: true,
+ errorMessage: "Too frequently, try again later."
+});
diff --git a/backend/router.ts b/backend/router.ts
new file mode 100644
index 0000000..b004477
--- /dev/null
+++ b/backend/router.ts
@@ -0,0 +1,6 @@
+import { DockgeServer } from "./dockge-server";
+import { Express, Router as ExpressRouter } from "express";
+
+export abstract class Router {
+ abstract create(app : Express, server : DockgeServer): ExpressRouter;
+}
diff --git a/backend/routers/main-router.ts b/backend/routers/main-router.ts
new file mode 100644
index 0000000..8d791db
--- /dev/null
+++ b/backend/routers/main-router.ts
@@ -0,0 +1,23 @@
+import { DockgeServer } from "../dockgeServer";
+import { Router } from "../router";
+import express, { Express, Router as ExpressRouter } from "express";
+
+export class MainRouter extends Router {
+ create(app: Express, server: DockgeServer): ExpressRouter {
+ const router = express.Router();
+
+ router.get("/", (req, res) => {
+ res.send(server.indexHTML);
+ });
+
+ // Robots.txt
+ router.get("/robots.txt", async (_request, response) => {
+ let txt = "User-agent: *\nDisallow: /";
+ response.setHeader("Content-Type", "text/plain");
+ response.send(txt);
+ });
+
+ return router;
+ }
+
+}
diff --git a/backend/settings.ts b/backend/settings.ts
new file mode 100644
index 0000000..fed94a7
--- /dev/null
+++ b/backend/settings.ts
@@ -0,0 +1,174 @@
+import { R } from "redbean-node";
+import { log } from "./log";
+
+export class Settings {
+
+ /**
+ * Example:
+ * {
+ * key1: {
+ * value: "value2",
+ * timestamp: 12345678
+ * },
+ * key2: {
+ * value: 2,
+ * timestamp: 12345678
+ * },
+ * }
+ * @type {{}}
+ */
+ static cacheList = {
+
+ };
+
+ static cacheCleaner = null;
+
+ /**
+ * Retrieve value of setting based on key
+ * @param {string} key Key of setting to retrieve
+ * @returns {Promise} Value
+ */
+ static async get(key) {
+
+ // Start cache clear if not started yet
+ if (!Settings.cacheCleaner) {
+ Settings.cacheCleaner = setInterval(() => {
+ log.debug("settings", "Cache Cleaner is just started.");
+ for (key in Settings.cacheList) {
+ if (Date.now() - Settings.cacheList[key].timestamp > 60 * 1000) {
+ log.debug("settings", "Cache Cleaner deleted: " + key);
+ delete Settings.cacheList[key];
+ }
+ }
+
+ }, 60 * 1000);
+ }
+
+ // Query from cache
+ if (key in Settings.cacheList) {
+ const v = Settings.cacheList[key].value;
+ log.debug("settings", `Get Setting (cache): ${key}: ${v}`);
+ return v;
+ }
+
+ const value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
+ key,
+ ]);
+
+ try {
+ const v = JSON.parse(value);
+ log.debug("settings", `Get Setting: ${key}: ${v}`);
+
+ Settings.cacheList[key] = {
+ value: v,
+ timestamp: Date.now()
+ };
+
+ return v;
+ } catch (e) {
+ return value;
+ }
+ }
+
+ /**
+ * Sets the specified setting to specified value
+ * @param {string} key Key of setting to set
+ * @param {any} value Value to set to
+ * @param {?string} type Type of setting
+ * @returns {Promise}
+ */
+ static async set(key, value, type = null) {
+
+ let bean = await R.findOne("setting", " `key` = ? ", [
+ key,
+ ]);
+ if (!bean) {
+ bean = R.dispense("setting");
+ bean.key = key;
+ }
+ bean.type = type;
+ bean.value = JSON.stringify(value);
+ await R.store(bean);
+
+ Settings.deleteCache([ key ]);
+ }
+
+ /**
+ * Get settings based on type
+ * @param {string} type The type of setting
+ * @returns {Promise} Settings
+ */
+ static async getSettings(type) {
+ const list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
+ type,
+ ]);
+
+ const result = {};
+
+ for (const row of list) {
+ try {
+ result[row.key] = JSON.parse(row.value);
+ } catch (e) {
+ result[row.key] = row.value;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Set settings based on type
+ * @param {string} type Type of settings to set
+ * @param {object} data Values of settings
+ * @returns {Promise}
+ */
+ static async setSettings(type, data) {
+ const keyList = Object.keys(data);
+
+ const promiseList = [];
+
+ for (const key of keyList) {
+ let bean = await R.findOne("setting", " `key` = ? ", [
+ key
+ ]);
+
+ if (bean == null) {
+ bean = R.dispense("setting");
+ bean.type = type;
+ bean.key = key;
+ }
+
+ if (bean.type === type) {
+ bean.value = JSON.stringify(data[key]);
+ promiseList.push(R.store(bean));
+ }
+ }
+
+ await Promise.all(promiseList);
+
+ Settings.deleteCache(keyList);
+ }
+
+ /**
+ * Delete selected keys from settings cache
+ * @param {string[]} keyList Keys to remove
+ * @returns {void}
+ */
+ static deleteCache(keyList) {
+ for (const key of keyList) {
+ delete Settings.cacheList[key];
+ }
+ }
+
+ /**
+ * Stop the cache cleaner if running
+ * @returns {void}
+ */
+ static stopCacheCleaner() {
+ if (Settings.cacheCleaner) {
+ clearInterval(Settings.cacheCleaner);
+ Settings.cacheCleaner = null;
+ }
+ }
+}
+
diff --git a/backend/socket-handler.ts b/backend/socket-handler.ts
new file mode 100644
index 0000000..aaf6060
--- /dev/null
+++ b/backend/socket-handler.ts
@@ -0,0 +1,6 @@
+import { DockgeServer } from "./dockge-server";
+import { DockgeSocket } from "./util-server";
+
+export abstract class SocketHandler {
+ abstract create(socket : DockgeSocket, server : DockgeServer): void;
+}
diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts
new file mode 100644
index 0000000..e945bef
--- /dev/null
+++ b/backend/socket-handlers/docker-socket-handler.ts
@@ -0,0 +1,262 @@
+import { SocketHandler } from "../socket-handler.js";
+import { DockgeServer } from "../dockge-server";
+import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
+import { Stack } from "../stack";
+
+// @ts-ignore
+import composerize from "composerize";
+
+export class DockerSocketHandler extends SocketHandler {
+ create(socket : DockgeSocket, server : DockgeServer) {
+
+ socket.on("deployStack", async (name : unknown, composeYAML : unknown, isAdd : unknown, callback) => {
+ try {
+ checkLogin(socket);
+ const stack = this.saveStack(socket, server, name, composeYAML, isAdd);
+ await stack.deploy(socket);
+ server.sendStackList();
+ callback({
+ ok: true,
+ msg: "Deployed",
+ });
+ stack.joinCombinedTerminal(socket);
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ socket.on("saveStack", async (name : unknown, composeYAML : unknown, isAdd : unknown, callback) => {
+ try {
+ checkLogin(socket);
+ this.saveStack(socket, server, name, composeYAML, isAdd);
+ callback({
+ ok: true,
+ "msg": "Saved"
+ });
+ server.sendStackList();
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ socket.on("deleteStack", async (name : unknown, callback) => {
+ try {
+ checkLogin(socket);
+ if (typeof(name) !== "string") {
+ throw new ValidationError("Name must be a string");
+ }
+ const stack = Stack.getStack(server, name);
+
+ try {
+ await stack.delete(socket);
+ } catch (e) {
+ server.sendStackList();
+ throw e;
+ }
+
+ server.sendStackList();
+ callback({
+ ok: true,
+ msg: "Deleted"
+ });
+
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ socket.on("getStack", (stackName : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(stackName) !== "string") {
+ throw new ValidationError("Stack name must be a string");
+ }
+
+ const stack = Stack.getStack(server, stackName);
+
+ stack.joinCombinedTerminal(socket);
+
+ callback({
+ ok: true,
+ stack: stack.toJSON(),
+ });
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // requestStackList
+ socket.on("requestStackList", async (callback) => {
+ try {
+ checkLogin(socket);
+ server.sendStackList();
+ callback({
+ ok: true,
+ msg: "Updated"
+ });
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // startStack
+ socket.on("startStack", async (stackName : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(stackName) !== "string") {
+ throw new ValidationError("Stack name must be a string");
+ }
+
+ const stack = Stack.getStack(server, stackName);
+ await stack.start(socket);
+ callback({
+ ok: true,
+ msg: "Started"
+ });
+ server.sendStackList();
+
+ stack.joinCombinedTerminal(socket);
+
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // stopStack
+ socket.on("stopStack", async (stackName : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(stackName) !== "string") {
+ throw new ValidationError("Stack name must be a string");
+ }
+
+ const stack = Stack.getStack(server, stackName);
+ await stack.stop(socket);
+ callback({
+ ok: true,
+ msg: "Stopped"
+ });
+ server.sendStackList();
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // restartStack
+ socket.on("restartStack", async (stackName : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(stackName) !== "string") {
+ throw new ValidationError("Stack name must be a string");
+ }
+
+ const stack = Stack.getStack(server, stackName);
+ await stack.restart(socket);
+ callback({
+ ok: true,
+ msg: "Restarted"
+ });
+ server.sendStackList();
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // updateStack
+ socket.on("updateStack", async (stackName : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(stackName) !== "string") {
+ throw new ValidationError("Stack name must be a string");
+ }
+
+ const stack = Stack.getStack(server, stackName);
+ await stack.update(socket);
+ callback({
+ ok: true,
+ msg: "Updated"
+ });
+ server.sendStackList();
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // Services status
+ socket.on("serviceStatusList", async (stackName : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(stackName) !== "string") {
+ throw new ValidationError("Stack name must be a string");
+ }
+
+ const stack = Stack.getStack(server, stackName);
+ const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
+ callback({
+ ok: true,
+ serviceStatusList,
+ });
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // getExternalNetworkList
+ socket.on("getDockerNetworkList", async (callback) => {
+ try {
+ checkLogin(socket);
+ const dockerNetworkList = server.getDockerNetworkList();
+ callback({
+ ok: true,
+ dockerNetworkList,
+ });
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // composerize
+ socket.on("composerize", async (dockerRunCommand : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(dockerRunCommand) !== "string") {
+ throw new ValidationError("dockerRunCommand must be a string");
+ }
+
+ const composeTemplate = composerize(dockerRunCommand);
+ callback({
+ ok: true,
+ composeTemplate,
+ });
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+ }
+
+ saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack {
+ // Check types
+ if (typeof(name) !== "string") {
+ throw new ValidationError("Name must be a string");
+ }
+ if (typeof(composeYAML) !== "string") {
+ throw new ValidationError("Compose YAML must be a string");
+ }
+ if (typeof(isAdd) !== "boolean") {
+ throw new ValidationError("isAdd must be a boolean");
+ }
+
+ const stack = new Stack(server, name, composeYAML);
+ stack.save(isAdd);
+ return stack;
+ }
+
+}
+
diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts
new file mode 100644
index 0000000..194d093
--- /dev/null
+++ b/backend/socket-handlers/main-socket-handler.ts
@@ -0,0 +1,295 @@
+import { SocketHandler } from "../socket-handler.js";
+import { Socket } from "socket.io";
+import { DockgeServer } from "../dockge-server";
+import { log } from "../log";
+import { R } from "redbean-node";
+import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter";
+import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash";
+import { User } from "../models/user";
+import { checkLogin, DockgeSocket, doubleCheckPassword } from "../util-server";
+import { passwordStrength } from "check-password-strength";
+import jwt from "jsonwebtoken";
+import { Settings } from "../settings";
+
+export class MainSocketHandler extends SocketHandler {
+ create(socket : DockgeSocket, server : DockgeServer) {
+
+ // ***************************
+ // Public Socket API
+ // ***************************
+
+ // Setup
+ socket.on("setup", async (username, password, callback) => {
+ try {
+ if (passwordStrength(password).value === "Too weak") {
+ throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
+ }
+
+ if ((await R.knex("user").count("id as count").first()).count !== 0) {
+ throw new Error("Dockge has been initialized. If you want to run setup again, please delete the database.");
+ }
+
+ const user = R.dispense("user");
+ user.username = username;
+ user.password = generatePasswordHash(password);
+ await R.store(user);
+
+ server.needSetup = false;
+
+ callback({
+ ok: true,
+ msg: "successAdded",
+ msgi18n: true,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ // Login by token
+ socket.on("loginByToken", async (token, callback) => {
+ const clientIP = await server.getClientIP(socket);
+
+ log.info("auth", `Login by token. IP=${clientIP}`);
+
+ try {
+ const decoded = jwt.verify(token, server.jwtSecret);
+
+ log.info("auth", "Username from JWT: " + decoded.username);
+
+ const user = await R.findOne("user", " username = ? AND active = 1 ", [
+ decoded.username,
+ ]) as User;
+
+ if (user) {
+ // Check if the password changed
+ if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
+ throw new Error("The token is invalid due to password change or old token");
+ }
+
+ log.debug("auth", "afterLogin");
+ await server.afterLogin(socket, user);
+ log.debug("auth", "afterLogin ok");
+
+ log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
+
+ callback({
+ ok: true,
+ });
+ } else {
+
+ log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`);
+
+ callback({
+ ok: false,
+ msg: "authUserInactiveOrDeleted",
+ msgi18n: true,
+ });
+ }
+ } catch (error) {
+ log.error("auth", `Invalid token. IP=${clientIP}`);
+ if (error.message) {
+ log.error("auth", error.message, `IP=${clientIP}`);
+ }
+ callback({
+ ok: false,
+ msg: "authInvalidToken",
+ msgi18n: true,
+ });
+ }
+
+ });
+
+ // Login
+ socket.on("login", async (data, callback) => {
+ const clientIP = await server.getClientIP(socket);
+
+ log.info("auth", `Login by username + password. IP=${clientIP}`);
+
+ // Checking
+ if (typeof callback !== "function") {
+ return;
+ }
+
+ if (!data) {
+ return;
+ }
+
+ // Login Rate Limit
+ if (!await loginRateLimiter.pass(callback)) {
+ log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
+ return;
+ }
+
+ const user = await this.login(data.username, data.password);
+
+ if (user) {
+ if (user.twofa_status === 0) {
+ server.afterLogin(socket, user);
+
+ log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
+
+ callback({
+ ok: true,
+ token: User.createJWT(user, server.jwtSecret),
+ });
+ }
+
+ if (user.twofa_status === 1 && !data.token) {
+
+ log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
+
+ callback({
+ tokenRequired: true,
+ });
+ }
+
+ if (data.token) {
+ const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
+
+ if (user.twofa_last_token !== data.token && verify) {
+ server.afterLogin(socket, user);
+
+ await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
+ data.token,
+ socket.userID,
+ ]);
+
+ log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
+
+ callback({
+ ok: true,
+ token: User.createJWT(user, server.jwtSecret),
+ });
+ } else {
+
+ log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
+
+ callback({
+ ok: false,
+ msg: "authInvalidToken",
+ msgi18n: true,
+ });
+ }
+ }
+ } else {
+
+ log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
+
+ callback({
+ ok: false,
+ msg: "authIncorrectCreds",
+ msgi18n: true,
+ });
+ }
+
+ });
+
+ // Change Password
+ socket.on("changePassword", async (password, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (! password.newPassword) {
+ throw new Error("Invalid new password");
+ }
+
+ if (passwordStrength(password.newPassword).value === "Too weak") {
+ throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
+ }
+
+ let user = await doubleCheckPassword(socket, password.currentPassword);
+ await user.resetPassword(password.newPassword);
+
+ callback({
+ ok: true,
+ msg: "Password has been updated successfully.",
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("getSettings", async (callback) => {
+ try {
+ checkLogin(socket);
+ const data = await Settings.getSettings("general");
+
+ callback({
+ ok: true,
+ data: data,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("setSettings", async (data, currentPassword, callback) => {
+ try {
+ checkLogin(socket);
+
+ // If currently is disabled auth, don't need to check
+ // Disabled Auth + Want to Disable Auth => No Check
+ // Disabled Auth + Want to Enable Auth => No Check
+ // Enabled Auth + Want to Disable Auth => Check!!
+ // Enabled Auth + Want to Enable Auth => No Check
+ const currentDisabledAuth = await Settings.get("disableAuth");
+ if (!currentDisabledAuth && data.disableAuth) {
+ await doubleCheckPassword(socket, currentPassword);
+ }
+
+ console.log(data);
+
+ await Settings.setSettings("general", data);
+
+ callback({
+ ok: true,
+ msg: "Saved"
+ });
+
+ server.sendInfo(socket);
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+ }
+
+ async login(username : string, password : string) {
+ if (typeof username !== "string" || typeof password !== "string") {
+ return null;
+ }
+
+ const user = await R.findOne("user", " username = ? AND active = 1 ", [
+ username,
+ ]);
+
+ if (user && verifyPassword(password, user.password)) {
+ // Upgrade the hash to bcrypt
+ if (needRehashPassword(user.password)) {
+ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
+ generatePasswordHash(password),
+ user.id,
+ ]);
+ }
+ return user;
+ }
+
+ return null;
+ }
+}
diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts
new file mode 100644
index 0000000..f647dfb
--- /dev/null
+++ b/backend/socket-handlers/terminal-socket-handler.ts
@@ -0,0 +1,151 @@
+import { SocketHandler } from "../socket-handler.js";
+import { DockgeServer } from "../dockge-server";
+import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
+import { log } from "../log";
+import yaml from "yaml";
+import path from "path";
+import fs from "fs";
+import {
+ allowedCommandList,
+ allowedRawKeys,
+ getComposeTerminalName, getContainerExecTerminalName,
+ isDev,
+ PROGRESS_TERMINAL_ROWS
+} from "../util-common";
+import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal";
+import { Stack } from "../stack";
+
+export class TerminalSocketHandler extends SocketHandler {
+ create(socket : DockgeSocket, server : DockgeServer) {
+
+ socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(terminalName) !== "string") {
+ throw new Error("Terminal name must be a string.");
+ }
+
+ if (typeof(cmd) !== "string") {
+ throw new Error("Command must be a string.");
+ }
+
+ let terminal = Terminal.getTerminal(terminalName);
+ if (terminal instanceof InteractiveTerminal) {
+ //log.debug("terminalInput", "Terminal found, writing to terminal.");
+ terminal.write(cmd);
+ } else {
+ throw new Error("Terminal not found or it is not a Interactive Terminal.");
+ }
+ } catch (e) {
+ errorCallback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ // Main Terminal
+ socket.on("mainTerminal", async (terminalName : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ // TODO: Reset the name here, force one main terminal for now
+ terminalName = "console";
+
+ if (typeof(terminalName) !== "string") {
+ throw new ValidationError("Terminal name must be a string.");
+ }
+
+ log.debug("deployStack", "Terminal name: " + terminalName);
+
+ let terminal = Terminal.getTerminal(terminalName);
+
+ if (!terminal) {
+ terminal = new MainTerminal(server, terminalName);
+ terminal.rows = 50;
+ log.debug("deployStack", "Terminal created");
+ }
+
+ terminal.join(socket);
+ terminal.start();
+
+ callback({
+ ok: true,
+ });
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // Interactive Terminal for containers
+ socket.on("interactiveTerminal", async (stackName : unknown, serviceName : unknown, shell : unknown, callback) => {
+ try {
+ checkLogin(socket);
+
+ if (typeof(stackName) !== "string") {
+ throw new ValidationError("Stack name must be a string.");
+ }
+
+ if (typeof(serviceName) !== "string") {
+ throw new ValidationError("Service name must be a string.");
+ }
+
+ if (typeof(shell) !== "string") {
+ throw new ValidationError("Shell must be a string.");
+ }
+
+ log.debug("interactiveTerminal", "Stack name: " + stackName);
+ log.debug("interactiveTerminal", "Service name: " + serviceName);
+
+ // Get stack
+ const stack = Stack.getStack(server, stackName);
+ stack.joinContainerTerminal(socket, serviceName, shell);
+
+ callback({
+ ok: true,
+ });
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // Join Output Terminal
+ socket.on("terminalJoin", async (terminalName : unknown, callback) => {
+ if (typeof(callback) !== "function") {
+ log.debug("console", "Callback is not a function.");
+ return;
+ }
+
+ try {
+ checkLogin(socket);
+ if (typeof(terminalName) !== "string") {
+ throw new ValidationError("Terminal name must be a string.");
+ }
+
+ let buffer : string = Terminal.getTerminal(terminalName)?.getBuffer() ?? "";
+
+ if (!buffer) {
+ log.debug("console", "No buffer found.");
+ }
+
+ callback({
+ ok: true,
+ buffer,
+ });
+ } catch (e) {
+ callbackError(e, callback);
+ }
+ });
+
+ // Close Terminal
+ socket.on("terminalClose", async (terminalName : unknown, callback : unknown) => {
+
+ });
+
+ // TODO: Resize Terminal
+ socket.on("terminalResize", async (rows : unknown) => {
+
+ });
+ }
+}
diff --git a/backend/stack.ts b/backend/stack.ts
new file mode 100644
index 0000000..5a15fc1
--- /dev/null
+++ b/backend/stack.ts
@@ -0,0 +1,356 @@
+import { DockgeServer } from "./dockge-server";
+import fs from "fs";
+import { log } from "./log";
+import yaml from "yaml";
+import { DockgeSocket, ValidationError } from "./util-server";
+import path from "path";
+import {
+ COMBINED_TERMINAL_COLS,
+ COMBINED_TERMINAL_ROWS,
+ CREATED_FILE,
+ CREATED_STACK,
+ EXITED, getCombinedTerminalName,
+ getComposeTerminalName, getContainerExecTerminalName,
+ PROGRESS_TERMINAL_ROWS,
+ RUNNING, TERMINAL_ROWS,
+ UNKNOWN
+} from "./util-common";
+import { InteractiveTerminal, Terminal } from "./terminal";
+import childProcess from "child_process";
+
+export class Stack {
+
+ name: string;
+ protected _status: number = UNKNOWN;
+ protected _composeYAML?: string;
+ protected _configFilePath?: string;
+ protected server: DockgeServer;
+
+ protected combinedTerminal? : Terminal;
+
+ protected static managedStackList: Map = new Map();
+
+ constructor(server : DockgeServer, name : string, composeYAML? : string) {
+ this.name = name;
+ this.server = server;
+ this._composeYAML = composeYAML;
+ }
+
+ toJSON() : object {
+ let obj = this.toSimpleJSON();
+ return {
+ ...obj,
+ composeYAML: this.composeYAML,
+ };
+ }
+
+ toSimpleJSON() : object {
+ return {
+ name: this.name,
+ status: this._status,
+ tags: [],
+ isManagedByDockge: this.isManagedByDockge,
+ };
+ }
+
+ /**
+ * Get the status of the stack from `docker compose ps --format json`
+ */
+ ps() : object {
+ let res = childProcess.execSync("docker compose ps --format json", {
+ cwd: this.path
+ });
+ return JSON.parse(res.toString());
+ }
+
+ get isManagedByDockge() : boolean {
+ return fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
+ }
+
+ get status() : number {
+ return this._status;
+ }
+
+ validate() {
+ // Check name, allows [a-z][A-Z][0-9] _ - only
+ if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
+ throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only");
+ }
+
+ // Check YAML format
+ yaml.parse(this.composeYAML);
+ }
+
+ get composeYAML() : string {
+ if (this._composeYAML === undefined) {
+ try {
+ this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8");
+ } catch (e) {
+ this._composeYAML = "";
+ }
+ }
+ return this._composeYAML;
+ }
+
+ get path() : string {
+ return path.join(this.server.stacksDir, this.name);
+ }
+
+ get fullPath() : string {
+ let dir = this.path;
+
+ // Compose up via node-pty
+ let fullPathDir;
+
+ // if dir is relative, make it absolute
+ if (!path.isAbsolute(dir)) {
+ fullPathDir = path.join(process.cwd(), dir);
+ } else {
+ fullPathDir = dir;
+ }
+ return fullPathDir;
+ }
+
+ /**
+ * Save the stack to the disk
+ * @param isAdd
+ */
+ save(isAdd : boolean) {
+ this.validate();
+
+ let dir = this.path;
+
+ // Check if the name is used if isAdd
+ if (isAdd) {
+ if (fs.existsSync(dir)) {
+ throw new ValidationError("Stack name already exists");
+ }
+
+ // Create the stack folder
+ fs.mkdirSync(dir);
+ } else {
+ if (!fs.existsSync(dir)) {
+ throw new ValidationError("Stack not found");
+ }
+ }
+
+ // Write or overwrite the compose.yaml
+ fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
+ }
+
+ async deploy(socket? : DockgeSocket) : Promise {
+ const terminalName = getComposeTerminalName(this.name);
+ let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path);
+ if (exitCode !== 0) {
+ throw new Error("Failed to deploy, please check the terminal output for more information.");
+ }
+ return exitCode;
+ }
+
+ async delete(socket?: DockgeSocket) : Promise {
+ const terminalName = getComposeTerminalName(this.name);
+ let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans", "--rmi", "all" ], this.path);
+ if (exitCode !== 0) {
+ throw new Error("Failed to delete, please check the terminal output for more information.");
+ }
+
+ // Remove the stack folder
+ fs.rmSync(this.path, {
+ recursive: true,
+ force: true
+ });
+
+ return exitCode;
+ }
+
+ static getStackList(server : DockgeServer, useCacheForManaged = false) : Map {
+ let stacksDir = server.stacksDir;
+ let stackList : Map;
+
+ if (useCacheForManaged && this.managedStackList.size > 0) {
+ stackList = this.managedStackList;
+ } else {
+ stackList = new Map();
+
+ // Scan the stacks directory, and get the stack list
+ let filenameList = fs.readdirSync(stacksDir);
+
+ for (let filename of filenameList) {
+ try {
+ let stack = this.getStack(server, filename);
+ stack._status = CREATED_FILE;
+ stackList.set(filename, stack);
+ } catch (e) {
+ log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
+ }
+ }
+
+ // Cache by copying
+ this.managedStackList = new Map(stackList);
+ }
+
+ // Also get the list from `docker compose ls --all --format json`
+ let res = childProcess.execSync("docker compose ls --all --format json");
+ let composeList = JSON.parse(res.toString());
+
+ for (let composeStack of composeList) {
+
+ // Skip the dockge stack
+ // TODO: Could be self managed?
+ if (composeStack.Name === "dockge") {
+ continue;
+ }
+
+ let stack = stackList.get(composeStack.Name);
+
+ // This stack probably is not managed by Dockge, but we still want to show it
+ if (!stack) {
+ stack = new Stack(server, composeStack.Name);
+ stackList.set(composeStack.Name, stack);
+ }
+
+ stack._status = this.statusConvert(composeStack.Status);
+ stack._configFilePath = composeStack.ConfigFiles;
+ }
+
+ return stackList;
+ }
+
+ /**
+ * Get the status list, it will be used to update the status of the stacks
+ * Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
+ */
+ static getStatusList() : Map {
+ let statusList = new Map();
+
+ let res = childProcess.execSync("docker compose ls --all --format json");
+ let composeList = JSON.parse(res.toString());
+
+ for (let composeStack of composeList) {
+ statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
+ }
+
+ return statusList;
+ }
+
+ /**
+ * Convert the status string from `docker compose ls` to the status number
+ * @param status
+ */
+ static statusConvert(status : string) : number {
+ if (status.startsWith("created")) {
+ return CREATED_STACK;
+ } else if (status.startsWith("running")) {
+ return RUNNING;
+ } else if (status.startsWith("exited")) {
+ return EXITED;
+ } else {
+ return UNKNOWN;
+ }
+ }
+
+ static getStack(server: DockgeServer, stackName: string) : Stack {
+ let dir = path.join(server.stacksDir, stackName);
+
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
+ // Maybe it is a stack managed by docker compose directly
+ let stackList = this.getStackList(server);
+ let stack = stackList.get(stackName);
+
+ if (stack) {
+ return stack;
+ } else {
+ // Really not found
+ throw new ValidationError("Stack not found");
+ }
+ }
+
+ let stack = new Stack(server, stackName);
+ stack._status = UNKNOWN;
+ stack._configFilePath = path.resolve(dir);
+ return stack;
+ }
+
+ async start(socket: DockgeSocket) {
+ const terminalName = getComposeTerminalName(this.name);
+ let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path);
+ if (exitCode !== 0) {
+ throw new Error("Failed to start, please check the terminal output for more information.");
+ }
+ return exitCode;
+ }
+
+ async stop(socket: DockgeSocket) : Promise {
+ const terminalName = getComposeTerminalName(this.name);
+ let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "stop" ], this.path);
+ if (exitCode !== 0) {
+ throw new Error("Failed to stop, please check the terminal output for more information.");
+ }
+ return exitCode;
+ }
+
+ async restart(socket: DockgeSocket) : Promise {
+ const terminalName = getComposeTerminalName(this.name);
+ let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "restart" ], this.path);
+ if (exitCode !== 0) {
+ throw new Error("Failed to restart, please check the terminal output for more information.");
+ }
+ return exitCode;
+ }
+
+ async update(socket: DockgeSocket) {
+ const terminalName = getComposeTerminalName(this.name);
+ let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path);
+ if (exitCode !== 0) {
+ throw new Error("Failed to pull, please check the terminal output for more information.");
+ }
+ exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path);
+ if (exitCode !== 0) {
+ throw new Error("Failed to restart, please check the terminal output for more information.");
+ }
+ return exitCode;
+ }
+
+ async joinCombinedTerminal(socket: DockgeSocket) {
+ const terminalName = getCombinedTerminalName(this.name);
+ const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path);
+ terminal.rows = COMBINED_TERMINAL_ROWS;
+ terminal.cols = COMBINED_TERMINAL_COLS;
+ terminal.join(socket);
+ terminal.start();
+ }
+
+ async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) {
+ const terminalName = getContainerExecTerminalName(this.name, serviceName, index);
+ let terminal = Terminal.getTerminal(terminalName);
+
+ if (!terminal) {
+ terminal = new InteractiveTerminal(this.server, terminalName, "docker", [ "compose", "exec", serviceName, shell ], this.path);
+ terminal.rows = TERMINAL_ROWS;
+ log.debug("joinContainerTerminal", "Terminal created");
+ }
+
+ terminal.join(socket);
+ terminal.start();
+ }
+
+ async getServiceStatusList() {
+ let statusList = new Map();
+
+ let res = childProcess.execSync("docker compose ps --format json", {
+ cwd: this.path,
+ });
+
+ let lines = res.toString().split("\n");
+
+ for (let line of lines) {
+ try {
+ let obj = JSON.parse(line);
+ statusList.set(obj.Service, obj.State);
+ } catch (e) {
+ }
+ }
+
+ return statusList;
+ }
+}
diff --git a/backend/terminal.ts b/backend/terminal.ts
new file mode 100644
index 0000000..2a31f17
--- /dev/null
+++ b/backend/terminal.ts
@@ -0,0 +1,230 @@
+import { DockgeServer } from "./dockge-server";
+import * as os from "node:os";
+import * as pty from "@homebridge/node-pty-prebuilt-multiarch";
+import { LimitQueue } from "./utils/limit-queue";
+import { DockgeSocket } from "./util-server";
+import {
+ allowedCommandList, allowedRawKeys,
+ getComposeTerminalName,
+ getCryptoRandomInt,
+ PROGRESS_TERMINAL_ROWS,
+ TERMINAL_COLS,
+ TERMINAL_ROWS
+} from "./util-common";
+import { sync as commandExistsSync } from "command-exists";
+import { log } from "./log";
+
+/**
+ * Terminal for running commands, no user interaction
+ */
+export class Terminal {
+
+ protected static terminalMap : Map = new Map();
+
+ protected _ptyProcess? : pty.IPty;
+ protected server : DockgeServer;
+ protected buffer : LimitQueue = new LimitQueue(100);
+ protected _name : string;
+
+ protected file : string;
+ protected args : string | string[];
+ protected cwd : string;
+ protected callback? : (exitCode : number) => void;
+
+ protected _rows : number = TERMINAL_ROWS;
+ protected _cols : number = TERMINAL_COLS;
+
+ constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) {
+ this.server = server;
+ this._name = name;
+ //this._name = "terminal-" + Date.now() + "-" + getCryptoRandomInt(0, 1000000);
+ this.file = file;
+ this.args = args;
+ this.cwd = cwd;
+
+ Terminal.terminalMap.set(this.name, this);
+ }
+
+ get rows() {
+ return this._rows;
+ }
+
+ set rows(rows : number) {
+ this._rows = rows;
+ try {
+ this.ptyProcess?.resize(this.cols, this.rows);
+ } catch (e) {
+ log.debug("Terminal", "Failed to resize terminal: " + e.message);
+ }
+ }
+
+ get cols() {
+ return this._cols;
+ }
+
+ set cols(cols : number) {
+ this._cols = cols;
+ try {
+ this.ptyProcess?.resize(this.cols, this.rows);
+ } catch (e) {
+ log.debug("Terminal", "Failed to resize terminal: " + e.message);
+ }
+ }
+
+ public start() {
+ if (this._ptyProcess) {
+ return;
+ }
+
+ this._ptyProcess = pty.spawn(this.file, this.args, {
+ name: this.name,
+ cwd: this.cwd,
+ cols: TERMINAL_COLS,
+ rows: this.rows,
+ });
+
+ // On Data
+ this._ptyProcess.onData((data) => {
+ this.buffer.push(data);
+ if (this.server.io) {
+ this.server.io.to(this.name).emit("terminalWrite", this.name, data);
+ }
+ });
+
+ // On Exit
+ this._ptyProcess.onExit((res) => {
+ this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode);
+
+ // Remove room
+ this.server.io.in(this.name).socketsLeave(this.name);
+
+ Terminal.terminalMap.delete(this.name);
+ log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
+
+ if (this.callback) {
+ this.callback(res.exitCode);
+ }
+ });
+ }
+
+ public onExit(callback : (exitCode : number) => void) {
+ this.callback = callback;
+ }
+
+ public join(socket : DockgeSocket) {
+ socket.join(this.name);
+ }
+
+ public leave(socket : DockgeSocket) {
+ socket.leave(this.name);
+ }
+
+ public get ptyProcess() {
+ return this._ptyProcess;
+ }
+
+ public get name() {
+ return this._name;
+ }
+
+ /**
+ * Get the terminal output string
+ */
+ getBuffer() : string {
+ if (this.buffer.length === 0) {
+ return "";
+ }
+ return this.buffer.join("");
+ }
+
+ close() {
+ this._ptyProcess?.kill();
+ }
+
+ /**
+ * Get a running and non-exited terminal
+ * @param name
+ */
+ public static getTerminal(name : string) : Terminal | undefined {
+ return Terminal.terminalMap.get(name);
+ }
+
+ public static getOrCreateTerminal(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) : Terminal {
+ // Since exited terminal will be removed from the map, it is safe to get the terminal from the map
+ let terminal = Terminal.getTerminal(name);
+ if (!terminal) {
+ terminal = new Terminal(server, name, file, args, cwd);
+ }
+ return terminal;
+ }
+
+ public static exec(server : DockgeServer, socket : DockgeSocket | undefined, terminalName : string, file : string, args : string | string[], cwd : string) : Promise {
+ const terminal = new Terminal(server, terminalName, file, args, cwd);
+ terminal.rows = PROGRESS_TERMINAL_ROWS;
+
+ if (socket) {
+ terminal.join(socket);
+ }
+
+ return new Promise((resolve) => {
+ terminal.onExit((exitCode : number) => {
+ resolve(exitCode);
+ });
+ terminal.start();
+ });
+ }
+}
+
+/**
+ * Interactive terminal
+ * Mainly used for container exec
+ */
+export class InteractiveTerminal extends Terminal {
+ public write(input : string) {
+ this.ptyProcess?.write(input);
+ }
+
+ resetCWD() {
+ const cwd = process.cwd();
+ this.ptyProcess?.write(`cd "${cwd}"\r`);
+ }
+}
+
+/**
+ * User interactive terminal that use bash or powershell with limited commands such as docker, ls, cd, dir
+ */
+export class MainTerminal extends InteractiveTerminal {
+ constructor(server : DockgeServer, name : string) {
+ let shell;
+
+ if (os.platform() === "win32") {
+ if (commandExistsSync("pwsh.exe")) {
+ shell = "pwsh.exe";
+ } else {
+ shell = "powershell.exe";
+ }
+ } else {
+ shell = "bash";
+ }
+ super(server, name, shell, [], server.stacksDir);
+ }
+
+ public write(input : string) {
+ // For like Ctrl + C
+ if (allowedRawKeys.includes(input)) {
+ super.write(input);
+ return;
+ }
+
+ // Check if the command is allowed
+ const cmdParts = input.split(" ");
+ const executable = cmdParts[0].trim();
+ log.debug("console", "Executable: " + executable);
+ log.debug("console", "Executable length: " + executable.length);
+
+ if (!allowedCommandList.includes(executable)) {
+ throw new Error("Command not allowed.");
+ }
+ super.write(input);
+ }
+}
diff --git a/backend/util-common.ts b/backend/util-common.ts
new file mode 100644
index 0000000..576d8d8
--- /dev/null
+++ b/backend/util-common.ts
@@ -0,0 +1,337 @@
+/*
+ * Common utilities for backend and frontend
+ */
+import { Document } from "yaml";
+
+// Init dayjs
+import dayjs from "dayjs";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+import relativeTime from "dayjs/plugin/relativeTime";
+dayjs.extend(utc);
+dayjs.extend(timezone);
+dayjs.extend(relativeTime);
+
+let randomBytes : (numBytes: number) => Uint8Array;
+initRandomBytes();
+
+async function initRandomBytes() {
+ if (typeof window !== "undefined" && window.crypto) {
+ randomBytes = function randomBytes(numBytes: number) {
+ const bytes = new Uint8Array(numBytes);
+ for (let i = 0; i < numBytes; i += 65536) {
+ window.crypto.getRandomValues(bytes.subarray(i, i + Math.min(numBytes - i, 65536)));
+ }
+ return bytes;
+ };
+ } else {
+ randomBytes = (await import("node:crypto")).randomBytes;
+ }
+}
+
+// Stack Status
+export const UNKNOWN = 0;
+export const CREATED_FILE = 1;
+export const CREATED_STACK = 2;
+export const RUNNING = 3;
+export const EXITED = 4;
+
+export function statusName(status : number) : string {
+ switch (status) {
+ case CREATED_FILE:
+ return "draft";
+ case CREATED_STACK:
+ return "created_stack";
+ case RUNNING:
+ return "running";
+ case EXITED:
+ return "exited";
+ default:
+ return "unknown";
+ }
+}
+
+export function statusNameShort(status : number) : string {
+ switch (status) {
+ case CREATED_FILE:
+ return "inactive";
+ case CREATED_STACK:
+ return "inactive";
+ case RUNNING:
+ return "active";
+ case EXITED:
+ return "exited";
+ default:
+ return "?";
+ }
+}
+
+export function statusColor(status : number) : string {
+ switch (status) {
+ case CREATED_FILE:
+ return "dark";
+ case CREATED_STACK:
+ return "dark";
+ case RUNNING:
+ return "primary";
+ case EXITED:
+ return "danger";
+ default:
+ return "secondary";
+ }
+}
+
+export const isDev = process.env.NODE_ENV === "development";
+export const TERMINAL_COLS = 105;
+export const TERMINAL_ROWS = 10;
+export const PROGRESS_TERMINAL_ROWS = 8;
+
+export const COMBINED_TERMINAL_COLS = 56;
+export const COMBINED_TERMINAL_ROWS = 15;
+
+export const ERROR_TYPE_VALIDATION = 1;
+
+export const allowedCommandList : string[] = [
+ "docker",
+ "ls",
+ "cd",
+ "dir",
+];
+
+export const allowedRawKeys = [
+ "\u0003", // Ctrl + C
+];
+
+/**
+ * Generate a decimal integer number from a string
+ * @param str Input
+ * @param length Default is 10 which means 0 - 9
+ */
+export function intHash(str : string, length = 10) : number {
+ // A simple hashing function (you can use more complex hash functions if needed)
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash += str.charCodeAt(i);
+ }
+ // Normalize the hash to the range [0, 10]
+ return (hash % length + length) % length; // Ensure the result is non-negative
+}
+
+/**
+ * Delays for specified number of seconds
+ * @param ms Number of milliseconds to sleep for
+ */
+export function sleep(ms: number) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Generate a random alphanumeric string of fixed length
+ * @param length Length of string to generate
+ * @returns string
+ */
+export function genSecret(length = 64) {
+ let secret = "";
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ const charsLength = chars.length;
+ for ( let i = 0; i < length; i++ ) {
+ secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1));
+ }
+ return secret;
+}
+
+/**
+ * Get a random integer suitable for use in cryptography between upper
+ * and lower bounds.
+ * @param min Minimum value of integer
+ * @param max Maximum value of integer
+ * @returns Cryptographically suitable random integer
+ */
+export function getCryptoRandomInt(min: number, max: number):number {
+ // synchronous version of: https://github.com/joepie91/node-random-number-csprng
+
+ const range = max - min;
+ if (range >= Math.pow(2, 32)) {
+ console.log("Warning! Range is too large.");
+ }
+
+ let tmpRange = range;
+ let bitsNeeded = 0;
+ let bytesNeeded = 0;
+ let mask = 1;
+
+ while (tmpRange > 0) {
+ if (bitsNeeded % 8 === 0) {
+ bytesNeeded += 1;
+ }
+ bitsNeeded += 1;
+ mask = mask << 1 | 1;
+ tmpRange = tmpRange >>> 1;
+ }
+
+ const bytes = randomBytes(bytesNeeded);
+ let randomValue = 0;
+
+ for (let i = 0; i < bytesNeeded; i++) {
+ randomValue |= bytes[i] << 8 * i;
+ }
+
+ randomValue = randomValue & mask;
+
+ if (randomValue <= range) {
+ return min + randomValue;
+ } else {
+ return getCryptoRandomInt(min, max);
+ }
+}
+
+export function getComposeTerminalName(stack : string) {
+ return "compose-" + stack;
+}
+
+export function getCombinedTerminalName(stack : string) {
+ return "combined-" + stack;
+}
+
+export function getContainerTerminalName(container : string) {
+ return "container-" + container;
+}
+
+export function getContainerExecTerminalName(stackName : string, container : string, index : number) {
+ return "container-exec-" + container + "-" + index;
+}
+
+export function copyYAMLComments(doc : Document, src : Document) {
+ doc.comment = src.comment;
+ doc.commentBefore = src.commentBefore;
+
+ if (doc && doc.contents && src && src.contents) {
+ // @ts-ignore
+ copyYAMLCommentsItems(doc.contents.items, src.contents.items);
+ }
+}
+
+/**
+ * Copy yaml comments from srcItems to items
+ * Typescript is super annoying here, so I have to use any here
+ * TODO: Since comments are belong to the array index, the comments will be lost if the order of the items is changed or removed or added.
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function copyYAMLCommentsItems(items : any, srcItems : any) {
+ if (!items || !srcItems) {
+ return;
+ }
+
+ for (let i = 0; i < items.length; i++) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const item : any = items[i];
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const srcItem : any = srcItems[i];
+
+ if (!srcItem) {
+ continue;
+ }
+
+ if (item.key && srcItem.key) {
+ item.key.comment = srcItem.key.comment;
+ item.key.commentBefore = srcItem.key.commentBefore;
+ }
+
+ if (srcItem.comment) {
+ item.comment = srcItem.comment;
+ }
+
+ if (item.value && srcItem.value) {
+ if (typeof item.value === "object" && typeof srcItem.value === "object") {
+ item.value.comment = srcItem.value.comment;
+ item.value.commentBefore = srcItem.value.commentBefore;
+
+ if (item.value.items && srcItem.value.items) {
+ copyYAMLCommentsItems(item.value.items, srcItem.value.items);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Possible Inputs:
+ * ports:
+ * - "3000"
+ * - "3000-3005"
+ * - "8000:8000"
+ * - "9090-9091:8080-8081"
+ * - "49100:22"
+ * - "8000-9000:80"
+ * - "127.0.0.1:8001:8001"
+ * - "127.0.0.1:5000-5010:5000-5010"
+ * - "6060:6060/udp"
+ * @param input
+ * @param defaultHostname
+ */
+export function parseDockerPort(input : string, defaultHostname : string = "localhost") {
+ let hostname = defaultHostname;
+ let port;
+ let display;
+
+ const parts = input.split("/");
+ const part1 = parts[0];
+ let protocol = parts[1] || "tcp";
+
+ // Split the last ":"
+ const lastColon = part1.lastIndexOf(":");
+
+ if (lastColon === -1) {
+ // No colon, so it's just a port or port range
+ // Check if it's a port range
+ const dash = part1.indexOf("-");
+ if (dash === -1) {
+ // No dash, so it's just a port
+ port = part1;
+ } else {
+ // Has dash, so it's a port range, use the first port
+ port = part1.substring(0, dash);
+ }
+
+ display = part1;
+
+ } else {
+ // Has colon, so it's a port mapping
+ let hostPart = part1.substring(0, lastColon);
+ display = hostPart;
+
+ // Check if it's a port range
+ const dash = part1.indexOf("-");
+
+ if (dash !== -1) {
+ // Has dash, so it's a port range, use the first port
+ hostPart = part1.substring(0, dash);
+ }
+
+ // Check if it has a ip (ip:port)
+ const colon = hostPart.indexOf(":");
+
+ if (colon !== -1) {
+ // Has colon, so it's a ip:port
+ hostname = hostPart.substring(0, colon);
+ port = hostPart.substring(colon + 1);
+ } else {
+ // No colon, so it's just a port
+ port = hostPart;
+ }
+ }
+
+ let portInt = parseInt(port);
+
+ if (portInt == 443) {
+ protocol = "https";
+ } else if (protocol === "tcp") {
+ protocol = "http";
+ }
+
+ return {
+ url: protocol + "://" + hostname + ":" + portInt,
+ display: display,
+ };
+}
diff --git a/backend/util-server.ts b/backend/util-server.ts
new file mode 100644
index 0000000..241782c
--- /dev/null
+++ b/backend/util-server.ts
@@ -0,0 +1,79 @@
+import { Socket } from "socket.io";
+import { Terminal } from "./terminal";
+import { randomBytes } from "crypto";
+import { log } from "./log";
+import { ERROR_TYPE_VALIDATION } from "./util-common";
+import { R } from "redbean-node";
+import { verifyPassword } from "./password-hash";
+
+export interface DockgeSocket extends Socket {
+ userID: number;
+ consoleTerminal? : Terminal;
+}
+
+// For command line arguments, so they are nullable
+export interface Arguments {
+ sslKey? : string;
+ sslCert? : string;
+ sslKeyPassphrase? : string;
+ port? : number;
+ hostname? : string;
+ dataDir? : string;
+ stacksDir? : string;
+}
+
+// Some config values are required
+export interface Config extends Arguments {
+ dataDir : string;
+ stacksDir : string;
+}
+
+export function checkLogin(socket : DockgeSocket) {
+ if (!socket.userID) {
+ throw new Error("You are not logged in.");
+ }
+}
+
+export class ValidationError extends Error {
+ constructor(message : string) {
+ super(message);
+ }
+}
+
+export function callbackError(error : unknown, callback : unknown) {
+ if (typeof(callback) !== "function") {
+ log.error("console", "Callback is not a function");
+ return;
+ }
+
+ if (error instanceof Error) {
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ } else if (error instanceof ValidationError) {
+ callback({
+ ok: false,
+ type: ERROR_TYPE_VALIDATION,
+ msg: error.message,
+ });
+ } else {
+ log.debug("console", "Unknown error: " + error);
+ }
+}
+
+export async function doubleCheckPassword(socket : DockgeSocket, currentPassword : unknown) {
+ if (typeof currentPassword !== "string") {
+ throw new Error("Wrong data type?");
+ }
+
+ let user = await R.findOne("user", " id = ? AND active = 1 ", [
+ socket.userID,
+ ]);
+
+ if (!user || !verifyPassword(currentPassword, user.password)) {
+ throw new Error("Incorrect current password");
+ }
+
+ return user;
+}
diff --git a/backend/utils/limit-queue.ts b/backend/utils/limit-queue.ts
new file mode 100644
index 0000000..8a1a95d
--- /dev/null
+++ b/backend/utils/limit-queue.ts
@@ -0,0 +1,24 @@
+/**
+ * Limit Queue
+ * The first element will be removed when the length exceeds the limit
+ */
+export class LimitQueue extends Array {
+ __limit;
+ __onExceed = null;
+
+ constructor(limit: number) {
+ super();
+ this.__limit = limit;
+ }
+
+ push(value : T) {
+ super.push(value);
+ if (this.length > this.__limit) {
+ const item = this.shift();
+ if (this.__onExceed) {
+ this.__onExceed(item);
+ }
+ }
+ }
+
+}
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..018a6cb
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,18 @@
+version: "3.8"
+services:
+ dockge:
+ image: louislam/dockge:1
+ restart: unless-stopped
+ ports:
+ # Host Port : Container Port
+ - 5001:5001
+ volumes:
+ # Docker Socket
+ - /var/run/docker.sock:/var/run/docker.sock
+ # Dockge Config
+ - ./data:/app/data
+ # Your stacks directory in the host (The paths inside container must be the same as the host)
+ - /opt/stacks:/opt/stacks
+ environment:
+ # Tell Dockge where is your stacks directory
+ - DOCKGE_STACKS_DIR=/opt/stacks
diff --git a/docker/Base.Dockerfile b/docker/Base.Dockerfile
new file mode 100644
index 0000000..7c89d95
--- /dev/null
+++ b/docker/Base.Dockerfile
@@ -0,0 +1,39 @@
+FROM node:20-bookworm-slim
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+
+# COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
+
+RUN apt update && apt install --yes --no-install-recommends \
+ curl \
+ ca-certificates \
+ gnupg \
+ unzip \
+ dumb-init \
+ && install -m 0755 -d /etc/apt/keyrings \
+ && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
+ && chmod a+r /etc/apt/keyrings/docker.gpg \
+ && echo \
+ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
+ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
+ tee /etc/apt/sources.list.d/docker.list > /dev/null \
+ && apt update \
+ && apt --yes --no-install-recommends install \
+ docker-ce-cli \
+ docker-compose-plugin \
+ && rm -rf /var/lib/apt/lists/* \
+ && npm install pnpm -g \
+ && pnpm install -g tsx
+
+# ensures that /var/run/docker.sock exists
+# changes the ownership of /var/run/docker.sock
+RUN touch /var/run/docker.sock && chown node:node /var/run/docker.sock
+
+# Full Base Image
+# MariaDB, Chromium and fonts
+#FROM base-slim AS base
+#ENV DOCKGE_ENABLE_EMBEDDED_MARIADB=1
+#RUN apt update && \
+# apt --yes --no-install-recommends install mariadb-server && \
+# rm -rf /var/lib/apt/lists/* && \
+# apt --yes autoremove
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..fa70174
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,29 @@
+############################################
+# Build
+############################################
+FROM louislam/dockge:base AS build
+WORKDIR /app
+COPY --chown=node:node ./package.json ./package.json
+COPY --chown=node:node ./pnpm-lock.yaml ./pnpm-lock.yaml
+RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
+
+############################################
+# ⭐ Main Image
+############################################
+FROM louislam/dockge:base AS release
+WORKDIR /app
+COPY --chown=node:node . .
+COPY --from=build /app/node_modules /app/node_modules
+RUN mkdir ./data
+
+VOLUME /app/data
+EXPOSE 5001
+ENTRYPOINT ["/usr/bin/dumb-init", "--"]
+CMD ["tsx", "./backend/index.ts"]
+
+
+############################################
+# Mark as Nightly
+############################################
+FROM release AS nightly
+RUN pnpm run mark-as-nightly
diff --git a/extra/mark-as-nightly.ts b/extra/mark-as-nightly.ts
new file mode 100644
index 0000000..afb863f
--- /dev/null
+++ b/extra/mark-as-nightly.ts
@@ -0,0 +1,22 @@
+import pkg from "../package.json";
+import fs from "fs";
+import dayjs from "dayjs";
+
+const oldVersion = pkg.version;
+const newVersion = oldVersion + "-nightly-" + dayjs().format("YYYYMMDDHHmmss");
+
+console.log("Old Version: " + oldVersion);
+console.log("New Version: " + newVersion);
+
+if (newVersion) {
+ // Process package.json
+ pkg.version = newVersion;
+ //pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
+ //pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
+ fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
+
+ // Process README.md
+ if (fs.existsSync("README.md")) {
+ fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
+ }
+}
diff --git a/extra/templates/mariadb/compose.yaml b/extra/templates/mariadb/compose.yaml
new file mode 100644
index 0000000..14aad83
--- /dev/null
+++ b/extra/templates/mariadb/compose.yaml
@@ -0,0 +1,9 @@
+version: "3.8"
+services:
+ mariadb:
+ image: mariadb:latest
+ restart: unless-stopped
+ ports:
+ - 3306:3306
+ environment:
+ - MARIADB_ROOT_PASSWORD=123456
diff --git a/extra/templates/nginx-proxy-manager/compose.yaml b/extra/templates/nginx-proxy-manager/compose.yaml
new file mode 100644
index 0000000..49d5813
--- /dev/null
+++ b/extra/templates/nginx-proxy-manager/compose.yaml
@@ -0,0 +1,12 @@
+version: '3.8'
+services:
+ nginx-proxy-manager:
+ image: 'jc21/nginx-proxy-manager:latest'
+ restart: unless-stopped
+ ports:
+ - '80:80'
+ - '81:81'
+ - '443:443'
+ volumes:
+ - ./data:/data
+ - ./letsencrypt:/etc/letsencrypt
diff --git a/extra/templates/uptime-kuma/compose.yaml b/extra/templates/uptime-kuma/compose.yaml
new file mode 100644
index 0000000..8027975
--- /dev/null
+++ b/extra/templates/uptime-kuma/compose.yaml
@@ -0,0 +1,9 @@
+version: '3.8'
+services:
+ uptime-kuma:
+ image: louislam/uptime-kuma:1
+ volumes:
+ - ./data:/app/data
+ ports:
+ - "3001:3001"
+ restart: always
diff --git a/frontend/components.d.ts b/frontend/components.d.ts
new file mode 100644
index 0000000..bafec4c
--- /dev/null
+++ b/frontend/components.d.ts
@@ -0,0 +1,30 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+declare module 'vue' {
+ export interface GlobalComponents {
+ About: typeof import('./src/components/settings/About.vue')['default']
+ Appearance: typeof import('./src/components/settings/Appearance.vue')['default']
+ ArrayInput: typeof import('./src/components/ArrayInput.vue')['default']
+ ArraySelect: typeof import('./src/components/ArraySelect.vue')['default']
+ BModal: typeof import('bootstrap-vue-next')['BModal']
+ Confirm: typeof import('./src/components/Confirm.vue')['default']
+ Container: typeof import('./src/components/Container.vue')['default']
+ General: typeof import('./src/components/settings/General.vue')['default']
+ HiddenInput: typeof import('./src/components/HiddenInput.vue')['default']
+ Login: typeof import('./src/components/Login.vue')['default']
+ NetworkInput: typeof import('./src/components/NetworkInput.vue')['default']
+ RouterLink: typeof import('vue-router')['RouterLink']
+ RouterView: typeof import('vue-router')['RouterView']
+ Security: typeof import('./src/components/settings/Security.vue')['default']
+ StackList: typeof import('./src/components/StackList.vue')['default']
+ StackListItem: typeof import('./src/components/StackListItem.vue')['default']
+ Terminal: typeof import('./src/components/Terminal.vue')['default']
+ TwoFADialog: typeof import('./src/components/TwoFADialog.vue')['default']
+ Uptime: typeof import('./src/components/Uptime.vue')['default']
+ }
+}
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..bd46fe2
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+ Dockge
+
+
+
+
+
+
+
+
diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png
new file mode 100644
index 0000000..271b56a
Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
new file mode 100644
index 0000000..6622262
Binary files /dev/null and b/frontend/public/favicon.ico differ
diff --git a/frontend/public/icon-192x192.png b/frontend/public/icon-192x192.png
new file mode 100644
index 0000000..aa7af8c
Binary files /dev/null and b/frontend/public/icon-192x192.png differ
diff --git a/frontend/public/icon-512x512.png b/frontend/public/icon-512x512.png
new file mode 100644
index 0000000..a58944c
Binary files /dev/null and b/frontend/public/icon-512x512.png differ
diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg
new file mode 100644
index 0000000..b8e2df2
--- /dev/null
+++ b/frontend/public/icon.svg
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json
new file mode 100644
index 0000000..e62d262
--- /dev/null
+++ b/frontend/public/manifest.json
@@ -0,0 +1,19 @@
+{
+ "name": "Dockge",
+ "short_name": "Dockge",
+ "start_url": "/",
+ "background_color": "#fff",
+ "display": "standalone",
+ "icons": [
+ {
+ "src": "icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..aef300e
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/frontend/src/components/ArrayInput.vue b/frontend/src/components/ArrayInput.vue
new file mode 100644
index 0000000..2271083
--- /dev/null
+++ b/frontend/src/components/ArrayInput.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+ Long syntax is not supported here. Please use the YAML editor.
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ArraySelect.vue b/frontend/src/components/ArraySelect.vue
new file mode 100644
index 0000000..563bcbd
--- /dev/null
+++ b/frontend/src/components/ArraySelect.vue
@@ -0,0 +1,125 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+ Long syntax is not supported here. Please use the YAML editor.
+
+
+
+
+
+
+
diff --git a/frontend/src/components/Confirm.vue b/frontend/src/components/Confirm.vue
new file mode 100644
index 0000000..e855b67
--- /dev/null
+++ b/frontend/src/components/Confirm.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue
new file mode 100644
index 0000000..3e242ec
--- /dev/null
+++ b/frontend/src/components/Container.vue
@@ -0,0 +1,273 @@
+
+
+
+
+
{{ name }}
+
+ {{ imageName }}:{{ imageTag }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No networks available. You need to add internal networks or enable external networks in the right side first.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/HiddenInput.vue b/frontend/src/components/HiddenInput.vue
new file mode 100644
index 0000000..fb86a39
--- /dev/null
+++ b/frontend/src/components/HiddenInput.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue
new file mode 100644
index 0000000..9667d7a
--- /dev/null
+++ b/frontend/src/components/Login.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/NetworkInput.vue b/frontend/src/components/NetworkInput.vue
new file mode 100644
index 0000000..57f3c3f
--- /dev/null
+++ b/frontend/src/components/NetworkInput.vue
@@ -0,0 +1,223 @@
+
+
+
Internal Networks
+
+
+
+
+
External Networks
+
+
+ No External Networks
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/StackList.vue b/frontend/src/components/StackList.vue
new file mode 100644
index 0000000..90edb2f
--- /dev/null
+++ b/frontend/src/components/StackList.vue
@@ -0,0 +1,438 @@
+
+
+
+
+
+ {{ $t("addFirstStackMsg") }}
+
+
+
+
+
+
+
+ {{ $t("pauseStackMsg") }}
+
+
+
+
+
+
diff --git a/frontend/src/components/StackListItem.vue b/frontend/src/components/StackListItem.vue
new file mode 100644
index 0000000..ee0a8aa
--- /dev/null
+++ b/frontend/src/components/StackListItem.vue
@@ -0,0 +1,154 @@
+
+
+
+ {{ stackName }}
+
+
+
+
+
+
diff --git a/frontend/src/components/Terminal.vue b/frontend/src/components/Terminal.vue
new file mode 100644
index 0000000..fe767a0
--- /dev/null
+++ b/frontend/src/components/Terminal.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/TwoFADialog.vue b/frontend/src/components/TwoFADialog.vue
new file mode 100644
index 0000000..6ded47a
--- /dev/null
+++ b/frontend/src/components/TwoFADialog.vue
@@ -0,0 +1,203 @@
+
+
+
+
+ {{ $t("confirmEnableTwoFAMsg") }}
+
+
+
+ {{ $t("confirmDisableTwoFAMsg") }}
+
+
+
+
+
+
diff --git a/frontend/src/components/Uptime.vue b/frontend/src/components/Uptime.vue
new file mode 100644
index 0000000..84c2c27
--- /dev/null
+++ b/frontend/src/components/Uptime.vue
@@ -0,0 +1,54 @@
+
+ {{ statusName }}
+
+
+
+
+
diff --git a/frontend/src/components/settings/About.vue b/frontend/src/components/settings/About.vue
new file mode 100644
index 0000000..aed0bb5
--- /dev/null
+++ b/frontend/src/components/settings/About.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
Dockge
+
{{ $t("Version") }}: {{ $root.info.version }}
+
{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}
+
+
+ ⚠️ {{ $t("Frontend Version do not match backend version!") }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/settings/Appearance.vue b/frontend/src/components/settings/Appearance.vue
new file mode 100644
index 0000000..c974b6e
--- /dev/null
+++ b/frontend/src/components/settings/Appearance.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/settings/General.vue b/frontend/src/components/settings/General.vue
new file mode 100644
index 0000000..f9cf5c3
--- /dev/null
+++ b/frontend/src/components/settings/General.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
diff --git a/frontend/src/components/settings/Security.vue b/frontend/src/components/settings/Security.vue
new file mode 100644
index 0000000..0ece48a
--- /dev/null
+++ b/frontend/src/components/settings/Security.vue
@@ -0,0 +1,205 @@
+
+
+
+
+
+
+ {{ $t("Current User") }}: {{ $root.username }}
+
+
+
+ {{ $t("Change Password") }}
+
+
+
+
+
+
+ {{ $t("Two Factor Authentication") }}
+
+
+
+
+
+
+
+
+
{{ $t("Advanced") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("Please use this option carefully!") }}
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
new file mode 100644
index 0000000..8799143
--- /dev/null
+++ b/frontend/src/i18n.ts
@@ -0,0 +1,36 @@
+// @ts-ignore Performance issue when using "vue-i18n", so we use "vue-i18n/dist/vue-i18n.esm-browser.prod.js", but typescript doesn't like that.
+import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
+import en from "./lang/en.json";
+
+const languageList = {
+
+};
+
+let messages = {
+ en,
+};
+
+for (let lang in languageList) {
+ messages[lang] = {
+ languageName: languageList[lang]
+ };
+}
+
+const rtlLangs = [ "fa", "ar-SY", "ur" ];
+
+export const currentLocale = () => localStorage.locale
+ || languageList[navigator.language] && navigator.language
+ || languageList[navigator.language.substring(0, 2)] && navigator.language.substring(0, 2)
+ || "en";
+
+export const localeDirection = () => {
+ return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr";
+};
+
+export const i18n = createI18n({
+ locale: currentLocale(),
+ fallbackLocale: "en",
+ silentFallbackWarn: true,
+ silentTranslationWarn: true,
+ messages: messages,
+});
diff --git a/frontend/src/icon.ts b/frontend/src/icon.ts
new file mode 100644
index 0000000..0599e6a
--- /dev/null
+++ b/frontend/src/icon.ts
@@ -0,0 +1,115 @@
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
+
+// Add Free Font Awesome Icons
+// https://fontawesome.com/v6/icons?d=gallery&p=2&s=solid&m=free
+// In order to add an icon, you have to:
+// 1) add the icon name in the import statement below;
+// 2) add the icon name to the library.add() statement below.
+import {
+ faArrowAltCircleUp,
+ faCog,
+ faEdit,
+ faEye,
+ faEyeSlash,
+ faList,
+ faPause,
+ faStop,
+ faPlay,
+ faPlus,
+ faSearch,
+ faTachometerAlt,
+ faTimes,
+ faTimesCircle,
+ faTrash,
+ faCheckCircle,
+ faStream,
+ faSave,
+ faExclamationCircle,
+ faBullhorn,
+ faArrowsAltV,
+ faUnlink,
+ faQuestionCircle,
+ faImages,
+ faUpload,
+ faCopy,
+ faCheck,
+ faFile,
+ faAward,
+ faLink,
+ faChevronDown,
+ faSignOutAlt,
+ faPen,
+ faExternalLinkSquareAlt,
+ faSpinner,
+ faUndo,
+ faPlusCircle,
+ faAngleDown,
+ faWrench,
+ faHeartbeat,
+ faFilter,
+ faInfoCircle,
+ faClone,
+ faCertificate,
+ faTerminal, faWarehouse, faHome, faRocket,
+ faRotate,
+ faCloudArrowDown, faArrowsRotate,
+} from "@fortawesome/free-solid-svg-icons";
+
+library.add(
+ faArrowAltCircleUp,
+ faCog,
+ faEdit,
+ faEye,
+ faEyeSlash,
+ faList,
+ faPause,
+ faStop,
+ faPlay,
+ faPlus,
+ faSearch,
+ faTachometerAlt,
+ faTimes,
+ faTimesCircle,
+ faTrash,
+ faCheckCircle,
+ faStream,
+ faSave,
+ faExclamationCircle,
+ faBullhorn,
+ faArrowsAltV,
+ faUnlink,
+ faQuestionCircle,
+ faImages,
+ faUpload,
+ faCopy,
+ faCheck,
+ faFile,
+ faAward,
+ faLink,
+ faChevronDown,
+ faSignOutAlt,
+ faPen,
+ faExternalLinkSquareAlt,
+ faSpinner,
+ faUndo,
+ faPlusCircle,
+ faAngleDown,
+ faLink,
+ faWrench,
+ faHeartbeat,
+ faFilter,
+ faInfoCircle,
+ faClone,
+ faCertificate,
+ faTerminal,
+ faWarehouse,
+ faHome,
+ faRocket,
+ faRotate,
+ faCloudArrowDown,
+ faArrowsRotate,
+);
+
+export { FontAwesomeIcon };
+
diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json
new file mode 100644
index 0000000..2df544b
--- /dev/null
+++ b/frontend/src/lang/en.json
@@ -0,0 +1,53 @@
+{
+ "languageName": "English",
+ "authIncorrectCreds": "Incorrect username or password.",
+ "PasswordsDoNotMatch": "Passwords do not match.",
+ "signedInDisp": "Signed in as {0}",
+ "signedInDispDisabled": "Auth Disabled.",
+ "home": "Home",
+ "console": "Console",
+ "registry": "Registry",
+ "compose": "Compose",
+ "addFirstStackMsg": "Compose your first stack!",
+ "stackName" : "Stack Name",
+ "deployStack": "Deploy",
+ "deleteStack": "Delete",
+ "stopStack": "Stop",
+ "restartStack": "Restart",
+ "updateStack": "Update",
+ "startStack": "Start",
+ "editStack": "Edit",
+ "discardStack": "Discard",
+ "saveStackDraft": "Save",
+ "notAvailableShort" : "N/A",
+ "deleteStackMsg": "Are you sure you want to delete this stack?",
+ "stackNotManagedByDockgeMsg": "This stack is not managed by Dockge.",
+ "primaryHostname": "Primary Hostname",
+ "general": "General",
+ "container": "Container | Containers",
+ "scanFolder": "Scan Stacks Folder",
+ "dockerImage": "Image",
+ "restartPolicyUnlessStopped": "Unless Stopped",
+ "restartPolicyAlways": "Always",
+ "restartPolicyOnFailure": "On Failure",
+ "restartPolicyNo": "No",
+ "environmentVariable": "Environment Variable | Environment Variables",
+ "restartPolicy": "Restart Policy",
+ "containerName": "Container Name",
+ "port": "Port | Ports",
+ "volume": "Volume | Volumes",
+ "network": "Network | Networks",
+ "dependsOn": "Container Dependency | Container Dependencies",
+ "addListItem": "Add {0}",
+ "deleteContainer": "Delete",
+ "addContainer": "Add Container",
+ "addNetwork": "Add Network",
+ "disableauth.message1": "Are you sure want to disable authentication?",
+ "disableauth.message2": "It is designed for scenarios where you intend to implement third-party authentication in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.",
+ "passwordNotMatchMsg": "The repeat password does not match.",
+ "autoGet": "Auto Get",
+ "add": "Add",
+ "applyToYAML": "Apply to YAML",
+ "createExternalNetwork": "Create",
+ "addInternalNetwork": "Add"
+}
diff --git a/frontend/src/layouts/EmptyLayout.vue b/frontend/src/layouts/EmptyLayout.vue
new file mode 100644
index 0000000..825ec93
--- /dev/null
+++ b/frontend/src/layouts/EmptyLayout.vue
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/frontend/src/layouts/Layout.vue b/frontend/src/layouts/Layout.vue
new file mode 100644
index 0000000..3d13e2a
--- /dev/null
+++ b/frontend/src/layouts/Layout.vue
@@ -0,0 +1,304 @@
+
+
+
+
+ {{ $root.socketIO.connectionErrorMsg }}
+
+
+
+
+
+
+
+ Dockge
+
+
+
+ {{ $t("New Update") }}
+
+
+
+ -
+
+ {{ $t("home") }}
+
+
+
+ -
+
+ {{ $t("console") }}
+
+
+
+ -
+
+
+
{{ $root.usernameFirstChar }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
new file mode 100644
index 0000000..662517c
--- /dev/null
+++ b/frontend/src/main.ts
@@ -0,0 +1,101 @@
+// Dayjs init inside this, so it has to be the first import
+import "../../backend/util-common";
+
+import { createApp, defineComponent, h } from "vue";
+import App from "./App.vue";
+import { router } from "./router";
+import { FontAwesomeIcon } from "./icon.js";
+import { i18n } from "./i18n";
+
+// Dependencies
+import "bootstrap";
+import Toast, { POSITION, useToast } from "vue-toastification";
+import "xterm/lib/xterm.js";
+
+// CSS
+import "vue-toastification/dist/index.css";
+import "xterm/css/xterm.css";
+import "./styles/main.scss";
+
+// Minxins
+import socket from "./mixins/socket";
+import lang from "./mixins/lang";
+import theme from "./mixins/theme";
+
+const app = createApp(rootApp());
+
+app.use(Toast, {
+ position: POSITION.BOTTOM_RIGHT,
+ showCloseButtonOnHover: true,
+});
+app.use(router);
+app.use(i18n);
+app.component("FontAwesomeIcon", FontAwesomeIcon);
+app.mount("#app");
+
+/**
+ * Root Vue component
+ */
+function rootApp() {
+ const toast = useToast();
+
+ return defineComponent({
+ mixins: [
+ socket,
+ lang,
+ theme,
+ ],
+ data() {
+ return {
+ loggedIn: false,
+ allowLoginDialog: false,
+ username: null,
+ };
+ },
+ computed: {
+
+ },
+ methods: {
+
+ /**
+ * Show success or error toast dependant on response status code
+ * @param {object} res Response object
+ * @returns {void}
+ */
+ toastRes(res) {
+ let msg = res.msg;
+ if (res.msgi18n) {
+ if (msg != null && typeof msg === "object") {
+ msg = this.$t(msg.key, msg.values);
+ } else {
+ msg = this.$t(msg);
+ }
+ }
+
+ if (res.ok) {
+ toast.success(msg);
+ } else {
+ toast.error(msg);
+ }
+ },
+ /**
+ * Show a success toast
+ * @param {string} msg Message to show
+ * @returns {void}
+ */
+ toastSuccess(msg : string) {
+ toast.success(this.$t(msg));
+ },
+
+ /**
+ * Show an error toast
+ * @param {string} msg Message to show
+ * @returns {void}
+ */
+ toastError(msg : string) {
+ toast.error(this.$t(msg));
+ },
+ },
+ render: () => h(App),
+ });
+}
diff --git a/frontend/src/mixins/lang.ts b/frontend/src/mixins/lang.ts
new file mode 100644
index 0000000..1a9ab93
--- /dev/null
+++ b/frontend/src/mixins/lang.ts
@@ -0,0 +1,39 @@
+import { currentLocale } from "../i18n";
+import { setPageLocale } from "../util-frontend";
+import { defineComponent } from "vue";
+const langModules = import.meta.glob("../lang/*.json");
+
+export default defineComponent({
+ data() {
+ return {
+ language: currentLocale(),
+ };
+ },
+
+ watch: {
+ async language(lang) {
+ await this.changeLang(lang);
+ },
+ },
+
+ async created() {
+ if (this.language !== "en") {
+ await this.changeLang(this.language);
+ }
+ },
+
+ methods: {
+ /**
+ * Change the application language
+ * @param {string} lang Language code to switch to
+ * @returns {Promise}
+ */
+ async changeLang(lang : string) {
+ const message = (await langModules["../lang/" + lang + ".json"]()).default;
+ this.$i18n.setLocaleMessage(lang, message);
+ this.$i18n.locale = lang;
+ localStorage.locale = lang;
+ setPageLocale();
+ }
+ }
+});
diff --git a/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts
new file mode 100644
index 0000000..42e4698
--- /dev/null
+++ b/frontend/src/mixins/socket.ts
@@ -0,0 +1,317 @@
+import { io } from "socket.io-client";
+import { Socket } from "socket.io-client";
+import { defineComponent } from "vue";
+import jwtDecode from "jwt-decode";
+import { Terminal } from "xterm";
+
+let socket : Socket;
+
+let terminalMap : Map = new Map();
+
+export default defineComponent({
+ data() {
+ return {
+ socketIO: {
+ token: null,
+ firstConnect: true,
+ connected: false,
+ connectCount: 0,
+ initedSocketIO: false,
+ connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`,
+ showReverseProxyGuide: true,
+ },
+ info: {
+
+ },
+ remember: (localStorage.remember !== "0"),
+ loggedIn: false,
+ allowLoginDialog: false,
+ username: null,
+ stackList: {},
+ composeTemplate: "",
+ };
+ },
+ computed: {
+ usernameFirstChar() {
+ if (typeof this.username == "string" && this.username.length >= 1) {
+ return this.username.charAt(0).toUpperCase();
+ } else {
+ return "🐻";
+ }
+ },
+
+ /**
+ * Frontend Version
+ * It should be compiled to a static value while building the frontend.
+ * Please see ./frontend/vite.config.ts, it is defined via vite.js
+ * @returns {string}
+ */
+ frontendVersion() {
+ // eslint-disable-next-line no-undef
+ return FRONTEND_VERSION;
+ },
+
+ /**
+ * Are both frontend and backend in the same version?
+ * @returns {boolean}
+ */
+ isFrontendBackendVersionMatched() {
+ if (!this.info.version) {
+ return true;
+ }
+ return this.info.version === this.frontendVersion;
+ },
+
+ },
+ watch: {
+ remember() {
+ localStorage.remember = (this.remember) ? "1" : "0";
+ },
+
+ // Reload the SPA if the server version is changed.
+ "info.version"(to, from) {
+ if (from && from !== to) {
+ window.location.reload();
+ }
+ },
+ },
+ created() {
+ this.initSocketIO();
+ },
+ mounted() {
+ return;
+
+ },
+ methods: {
+ /**
+ * Initialize connection to socket server
+ * @param bypass Should the check for if we
+ * are on a status page be bypassed?
+ */
+ initSocketIO(bypass = false) {
+ // No need to re-init
+ if (this.socketIO.initedSocketIO) {
+ return;
+ }
+
+ this.socketIO.initedSocketIO = true;
+ let url : string;
+ const env = process.env.NODE_ENV || "production";
+ if (env === "development" || localStorage.dev === "dev") {
+ url = location.protocol + "//" + location.hostname + ":5001";
+ } else {
+ url = location.protocol + "//" + location.host;
+ }
+
+ socket = io(url, {
+ transports: [ "websocket", "polling" ]
+ });
+
+ socket.on("connect", () => {
+ console.log("Connected to the socket server");
+
+ this.socketIO.connectCount++;
+ this.socketIO.connected = true;
+ this.socketIO.showReverseProxyGuide = false;
+ const token = this.storage().token;
+
+ if (token) {
+ if (token !== "autoLogin") {
+ console.log("Logging in by token");
+ this.loginByToken(token);
+ } else {
+ // Timeout if it is not actually auto login
+ setTimeout(() => {
+ if (! this.loggedIn) {
+ this.allowLoginDialog = true;
+ this.storage().removeItem("token");
+ }
+ }, 5000);
+ }
+ } else {
+ this.allowLoginDialog = true;
+ }
+
+ this.socketIO.firstConnect = false;
+ });
+
+ socket.on("disconnect", () => {
+ console.log("disconnect");
+ this.socketIO.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
+ this.socketIO.connected = false;
+ });
+
+ socket.on("connect_error", (err) => {
+ console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
+ this.socketIO.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("Reconnecting...")}`;
+ this.socketIO.showReverseProxyGuide = true;
+ this.socketIO.connected = false;
+ this.socketIO.firstConnect = false;
+ });
+
+ // Custom Events
+
+ socket.on("info", (info) => {
+ this.info = info;
+ });
+
+ socket.on("autoLogin", () => {
+ this.loggedIn = true;
+ this.storage().token = "autoLogin";
+ this.socketIO.token = "autoLogin";
+ this.allowLoginDialog = false;
+ this.afterLogin();
+ });
+
+ socket.on("setup", () => {
+ console.log("setup");
+ this.$router.push("/setup");
+ });
+
+ socket.on("terminalWrite", (terminalName, data) => {
+ const terminal = terminalMap.get(terminalName);
+ if (!terminal) {
+ //console.error("Terminal not found: " + terminalName);
+ return;
+ }
+ terminal.write(data);
+ });
+
+ socket.on("stackList", (res) => {
+ if (res.ok) {
+ this.stackList = res.stackList;
+ }
+ });
+
+ socket.on("stackStatusList", (res) => {
+ if (res.ok) {
+ for (let stackName in res.stackStatusList) {
+ const stackObj = this.stackList[stackName];
+ if (stackObj) {
+ stackObj.status = res.stackStatusList[stackName];
+ }
+ }
+ }
+ });
+ },
+
+ /**
+ * The storage currently in use
+ * @returns Current storage
+ */
+ storage() : Storage {
+ return (this.remember) ? localStorage : sessionStorage;
+ },
+
+ getSocket() : Socket {
+ return socket;
+ },
+
+ /**
+ * Get payload of JWT cookie
+ * @returns {(object | undefined)} JWT payload
+ */
+ getJWTPayload() {
+ const jwtToken = this.storage().token;
+
+ if (jwtToken && jwtToken !== "autoLogin") {
+ return jwtDecode(jwtToken);
+ }
+ return undefined;
+ },
+
+ /**
+ * Send request to log user in
+ * @param {string} username Username to log in with
+ * @param {string} password Password to log in with
+ * @param {string} token User token
+ * @param {loginCB} callback Callback to call with result
+ * @returns {void}
+ */
+ login(username : string, password : string, token : string, callback) {
+ this.getSocket().emit("login", {
+ username,
+ password,
+ token,
+ }, (res) => {
+ if (res.tokenRequired) {
+ callback(res);
+ }
+
+ if (res.ok) {
+ this.storage().token = res.token;
+ this.socketIO.token = res.token;
+ this.loggedIn = true;
+ this.username = this.getJWTPayload()?.username;
+
+ this.afterLogin();
+
+ // Trigger Chrome Save Password
+ history.pushState({}, "");
+ }
+
+ callback(res);
+ });
+ },
+
+ /**
+ * Log in using a token
+ * @param {string} token Token to log in with
+ * @returns {void}
+ */
+ loginByToken(token : string) {
+ socket.emit("loginByToken", token, (res) => {
+ this.allowLoginDialog = true;
+
+ if (! res.ok) {
+ this.logout();
+ } else {
+ this.loggedIn = true;
+ this.username = this.getJWTPayload()?.username;
+ this.afterLogin();
+ }
+ });
+ },
+
+ /**
+ * Log out of the web application
+ * @returns {void}
+ */
+ logout() {
+ socket.emit("logout", () => { });
+ this.storage().removeItem("token");
+ this.socketIO.token = null;
+ this.loggedIn = false;
+ this.username = null;
+ this.clearData();
+ },
+
+ /**
+ * @returns {void}
+ */
+ clearData() {
+
+ },
+
+ afterLogin() {
+
+ },
+
+ bindTerminal(terminalName : string, terminal : Terminal) {
+ // Load terminal, get terminal screen
+ socket.emit("terminalJoin", terminalName, (res) => {
+ if (res.ok) {
+ terminal.write(res.buffer);
+ terminalMap.set(terminalName, terminal);
+ } else {
+ this.toastRes(res);
+ }
+ });
+ },
+
+ unbindTerminal(terminalName : string) {
+ terminalMap.delete(terminalName);
+ },
+
+ }
+});
diff --git a/frontend/src/mixins/theme.ts b/frontend/src/mixins/theme.ts
new file mode 100644
index 0000000..3982d04
--- /dev/null
+++ b/frontend/src/mixins/theme.ts
@@ -0,0 +1,80 @@
+import { defineComponent } from "vue";
+
+export default defineComponent({
+ data() {
+ return {
+ system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
+ userTheme: localStorage.theme,
+ statusPageTheme: "light",
+ forceStatusPageTheme: false,
+ path: "",
+ };
+ },
+
+ computed: {
+ theme() {
+ if (this.userTheme === "auto") {
+ return this.system;
+ }
+ return this.userTheme;
+ },
+
+ isDark() {
+ return this.theme === "dark";
+ }
+ },
+
+ watch: {
+ "$route.fullPath"(path) {
+ this.path = path;
+ },
+
+ userTheme(to, from) {
+ localStorage.theme = to;
+ },
+
+ styleElapsedTime(to, from) {
+ localStorage.styleElapsedTime = to;
+ },
+
+ theme(to, from) {
+ document.body.classList.remove(from);
+ document.body.classList.add(this.theme);
+ this.updateThemeColorMeta();
+ },
+
+ userHeartbeatBar(to, from) {
+ localStorage.heartbeatBarTheme = to;
+ },
+
+ heartbeatBarTheme(to, from) {
+ document.body.classList.remove(from);
+ document.body.classList.add(this.heartbeatBarTheme);
+ }
+ },
+
+ mounted() {
+ // Default Dark
+ if (! this.userTheme) {
+ this.userTheme = "dark";
+ }
+
+ document.body.classList.add(this.theme);
+ this.updateThemeColorMeta();
+ },
+
+ methods: {
+ /**
+ * Update the theme color meta tag
+ * @returns {void}
+ */
+ updateThemeColorMeta() {
+ if (this.theme === "dark") {
+ document.querySelector("#theme-color").setAttribute("content", "#161B22");
+ } else {
+ document.querySelector("#theme-color").setAttribute("content", "#5cdd8b");
+ }
+ }
+ }
+});
+
diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue
new file mode 100644
index 0000000..1a12e78
--- /dev/null
+++ b/frontend/src/pages/Compose.vue
@@ -0,0 +1,599 @@
+
+
+
+
Compose
+
{{ stack.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t("general") }}
+
+
+
+
+
+
+
+
+
+
+
{{ $tc("container", 2) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Terminal
+
+
+
+
+
compose.yaml
+
+
+
+
+ {{ yamlError }}
+
+
+
+
+
+
{{ $tc("volume", 2) }}
+
+
+
+
+
+
{{ $tc("network", 2) }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t("stackNotManagedByDockgeMsg") }}
+
+
+
+
+ {{ $t("deleteStackMsg") }}
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Console.vue b/frontend/src/pages/Console.vue
new file mode 100644
index 0000000..95f9977
--- /dev/null
+++ b/frontend/src/pages/Console.vue
@@ -0,0 +1,48 @@
+
+
+
+
Console
+
+
+
+ Allowed commands:
+
+ {{ command }}
+
+
+ ,
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/ContainerTerminal.vue b/frontend/src/pages/ContainerTerminal.vue
new file mode 100644
index 0000000..3eb4f7f
--- /dev/null
+++ b/frontend/src/pages/ContainerTerminal.vue
@@ -0,0 +1,63 @@
+
+
+
+
Terminal - {{ serviceName }} ({{ stackName }})
+
+
+ Switch to sh
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Dashboard.vue b/frontend/src/pages/Dashboard.vue
new file mode 100644
index 0000000..7b653af
--- /dev/null
+++ b/frontend/src/pages/Dashboard.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+ {{ $t("compose") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/DashboardHome.vue b/frontend/src/pages/DashboardHome.vue
new file mode 100644
index 0000000..d32ce45
--- /dev/null
+++ b/frontend/src/pages/DashboardHome.vue
@@ -0,0 +1,231 @@
+
+
+
+
+ {{ $t("home") }}
+
+
+
+
+
+
{{ $t("active") }}
+ {{ activeNum }}
+
+
+
{{ $t("exited") }}
+ {{ exitedNum }}
+
+
+
{{ $t("inactive") }}
+ {{ inactiveNum }}
+
+
+
+
+
Docker Run
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue
new file mode 100644
index 0000000..e25cac7
--- /dev/null
+++ b/frontend/src/pages/Settings.vue
@@ -0,0 +1,252 @@
+
+
+
+ {{ $t("Settings") }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Setup.vue b/frontend/src/pages/Setup.vue
new file mode 100644
index 0000000..d8ec406
--- /dev/null
+++ b/frontend/src/pages/Setup.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/router.ts b/frontend/src/router.ts
new file mode 100644
index 0000000..0c0d65c
--- /dev/null
+++ b/frontend/src/router.ts
@@ -0,0 +1,90 @@
+import { createRouter, createWebHistory } from "vue-router";
+
+import Layout from "./layouts/Layout.vue";
+import Setup from "./pages/Setup.vue";
+import Dashboard from "./pages/Dashboard.vue";
+import DashboardHome from "./pages/DashboardHome.vue";
+import Console from "./pages/Console.vue";
+import Compose from "./pages/Compose.vue";
+import ContainerTerminal from "./pages/ContainerTerminal.vue";
+
+const Settings = () => import("./pages/Settings.vue");
+
+// Settings - Sub Pages
+import Appearance from "./components/settings/Appearance.vue";
+import General from "./components/settings/General.vue";
+const Security = () => import("./components/settings/Security.vue");
+import About from "./components/settings/About.vue";
+
+const routes = [
+ {
+ path: "/empty",
+ component: Layout,
+ children: [
+ {
+ path: "",
+ component: Dashboard,
+ children: [
+ {
+ name: "DashboardHome",
+ path: "/",
+ component: DashboardHome,
+ children: [
+ {
+ path: "/compose",
+ component: Compose,
+ },
+ {
+ path: "/compose/:stackName",
+ name: "compose",
+ component: Compose,
+ props: true,
+ },
+ {
+ path: "/terminal/:stackName/:serviceName/:type",
+ component: ContainerTerminal,
+ name: "containerTerminal",
+ },
+ ]
+ },
+ {
+ path: "/console",
+ component: Console,
+ },
+ {
+ path: "/settings",
+ component: Settings,
+ children: [
+ {
+ path: "general",
+ component: General,
+ },
+ {
+ path: "appearance",
+ component: Appearance,
+ },
+ {
+ path: "security",
+ component: Security,
+ },
+ {
+ path: "about",
+ component: About,
+ },
+ ]
+ },
+ ]
+ },
+ ]
+ },
+ {
+ path: "/setup",
+ component: Setup,
+ },
+];
+
+export const router = createRouter({
+ linkActiveClass: "active",
+ history: createWebHistory(),
+ routes,
+});
diff --git a/frontend/src/styles/localization.scss b/frontend/src/styles/localization.scss
new file mode 100644
index 0000000..97be377
--- /dev/null
+++ b/frontend/src/styles/localization.scss
@@ -0,0 +1,9 @@
+html[lang='fa'] {
+ #app {
+ font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
+ }
+}
+
+ul.multiselect__content {
+ padding-left: 0 !important;
+}
diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss
new file mode 100644
index 0000000..e69cac3
--- /dev/null
+++ b/frontend/src/styles/main.scss
@@ -0,0 +1,697 @@
+@import "vars.scss";
+@import "bootstrap/scss/bootstrap";
+@import "bootstrap-vue-next/dist/bootstrap-vue-next.css";
+
+#app {
+ font-family: BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
+}
+
+h1 {
+ font-size: 32px;
+}
+
+h2 {
+ font-size: 26px;
+}
+
+textarea.form-control {
+ border-radius: 19px;
+}
+
+::-webkit-scrollbar {
+ width: 10px;
+}
+
+.bg-maintenance {
+ color: white !important;
+ background-color: $maintenance !important;
+}
+
+.bg-dark {
+ color: white;
+}
+
+.text-maintenance {
+ color: $maintenance !important;
+}
+
+::placeholder {
+ color: $dark-font-color3 !important;
+}
+
+.incident a,
+.bg-maintenance a {
+ color: inherit;
+}
+
+.list-group {
+ border-radius: 0.75rem;
+
+ .dark & {
+ .list-group-item {
+ background-color: $dark-bg2;
+ color: $dark-font-color;
+ border-color: $dark-border-color;
+ }
+ }
+}
+
+
+// optgroup
+optgroup {
+ color: #b1b1b1;
+ option {
+ color: #212529;
+ }
+}
+
+.dark {
+ optgroup {
+ color: #535864;
+ option {
+ color: $dark-font-color;
+ }
+ }
+}
+
+// Scrollbar
+::-webkit-scrollbar-thumb {
+ background: #ccc;
+ border-radius: 20px;
+}
+
+.modal {
+ backdrop-filter: blur(3px);
+}
+
+.modal-content {
+ border-radius: 1rem;
+ box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
+
+ .dark & {
+ box-shadow: 0 15px 70px rgb(0 0 0);
+ background-color: $dark-bg;
+ }
+}
+
+.VuePagination__count {
+ font-size: 13px;
+ text-align: center;
+}
+
+.shadow-box {
+ box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
+ padding: 10px;
+ border-radius: 10px;
+
+ &.big-padding {
+ padding: 20px;
+ }
+}
+
+.btn {
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+.btn-sm {
+ border-radius: 25px;
+}
+
+.btn-primary {
+ color: white;
+ background: $primary-gradient;
+
+ &:hover, &:active, &:focus, &.active {
+ color: white;
+ background: $primary-gradient-active;
+ border-color: $highlight;
+ }
+
+ .dark & {
+ color: $dark-font-color2;
+ }
+}
+
+.btn-normal {
+ $bg-color: #F5F5F5;
+
+ background-color: $bg-color;
+ border-color: $bg-color;
+
+ &:hover {
+ $hover-color: darken($bg-color, 3%);
+ background-color: $hover-color;
+ border-color: $hover-color;
+ }
+}
+
+.btn-warning {
+ color: white;
+
+ &:hover, &:active, &:focus, &.active {
+ color: white;
+ }
+}
+
+.btn-info {
+ color: white;
+
+ &:hover, &:active, &:focus, &.active {
+ color: white;
+ }
+}
+
+.btn-dark {
+ background-color: #161B22;
+}
+
+.btn-outline-normal {
+ padding: 4px 10px;
+ border: 1px solid #ced4da;
+ border-radius: 25px;
+ background-color: transparent;
+
+ .dark & {
+ color: $dark-font-color;
+ border: 1px solid $dark-font-color2;
+ }
+
+ &.active {
+ background-color: $highlight-white;
+
+ .dark & {
+ background-color: $dark-font-color2;
+ }
+ }
+}
+
+@media (max-width: 550px) {
+ .table-shadow-box {
+ padding: 10px !important;
+
+ thead {
+ display: none;
+ }
+
+ tbody {
+ .shadow-box {
+ background-color: white;
+ }
+ }
+
+ tr {
+ margin-top: 0 !important;
+ padding: 4px 10px !important;
+ display: block;
+ margin-bottom: 6px;
+
+ td:first-child {
+ font-weight: bold;
+ }
+
+ td:nth-child(-n+3) {
+ text-align: center;
+ }
+
+ td:last-child {
+ text-align: left;
+ }
+
+ td {
+ border-bottom: 1px solid $dark-font-color;
+ display: block;
+ padding: 4px;
+
+ .badge {
+ margin: auto;
+ display: block;
+ width: 30%;
+ }
+ }
+ }
+ }
+}
+
+// Dark Theme override here
+.dark {
+ background-color: #090c10;
+ color: $dark-font-color;
+
+ mark, .mark {
+ background-color: #b6ad86;
+ }
+
+ &::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
+ background: $dark-border-color;
+ }
+
+ .shadow-box {
+ &:not(.alert) {
+ background-color: $dark-bg;
+ }
+ }
+
+ .form-check-input {
+ background-color: $dark-bg2;
+ border-color: $dark-border-color;
+ }
+
+ .input-group-text {
+ background-color: #282f39;
+ border-color: $dark-border-color;
+ color: $dark-font-color;
+ }
+
+ .form-check-input:checked {
+ border-color: $primary; // Re-apply bootstrap border
+ }
+
+ .form-switch .form-check-input {
+ background-color: #232f3b;
+ }
+
+ a:not(.btn),
+ .table,
+ .nav-link {
+ color: $dark-font-color;
+
+ &.btn-info {
+ color: white;
+ }
+ }
+
+ .incident a,
+ .bg-maintenance a {
+ color: inherit;
+ }
+
+ .form-control,
+ .form-control:focus,
+ .form-select,
+ .form-select:focus {
+ color: $dark-font-color;
+ background-color: $dark-bg2;
+ }
+
+ .form-select:disabled {
+ color: rgba($dark-font-color, 0.7);
+ background-color: $dark-bg;
+ }
+
+ .form-control, .form-select {
+ border-color: $dark-border-color;
+ }
+
+ .form-control:disabled, .form-control[readonly] {
+ background-color: #232f3b;
+ opacity: 1;
+ }
+
+ .table-hover > tbody > tr:hover > * {
+ --bs-table-accent-bg: #070a10;
+ color: $dark-font-color;
+ }
+
+ .nav-pills .nav-link.active, .nav-pills .show > .nav-link {
+ color: $dark-font-color2;
+ background: $primary-gradient;
+
+ &:hover {
+ background: $primary-gradient-active;
+ }
+ }
+
+ .bg-primary {
+ color: $dark-font-color2;
+ background: $primary-gradient;
+ }
+
+ .btn-secondary {
+ color: white;
+ }
+
+ .btn-normal {
+ $bg-color: $dark-header-bg;
+
+ color: $dark-font-color;
+ background-color: $bg-color;
+ border-color: $bg-color;
+
+ &:hover {
+ $hover-color: darken($bg-color, 3%);
+ background-color: $hover-color;
+ border-color: $hover-color;
+ }
+ }
+
+ .btn-warning {
+ color: $dark-font-color2;
+
+ &:hover, &:active, &:focus, &.active {
+ color: $dark-font-color2;
+ }
+ }
+
+ .btn-close {
+ box-shadow: none;
+ filter: invert(1);
+
+ &:hover {
+ opacity: 0.6;
+ }
+ }
+
+ .modal-header {
+ border-color: $dark-bg;
+ }
+
+ .modal-footer {
+ border-color: $dark-bg;
+ }
+
+ // Pagination
+ .page-item.disabled .page-link {
+ background-color: $dark-bg;
+ border-color: $dark-border-color;
+ }
+
+ .page-link {
+ background-color: $dark-bg;
+ border-color: $dark-border-color;
+ color: $dark-font-color;
+ }
+
+ .stack-list {
+ .item {
+ &:hover {
+ background-color: $dark-bg2;
+ }
+
+ &.active {
+ background-color: $dark-bg2;
+ }
+ }
+ }
+
+ @media (max-width: 550px) {
+ .table-shadow-box {
+ tbody {
+ .shadow-box {
+ background-color: $dark-bg2;
+
+ td {
+ border-bottom: 1px solid $dark-border-color;
+ }
+ }
+ }
+ }
+ }
+
+ .alert {
+ &.bg-info,
+ &.bg-warning,
+ &.bg-danger,
+ &.bg-maintenance,
+ &.bg-light {
+ color: $dark-font-color2;
+ }
+ }
+}
+
+// Floating Label
+.form-floating > .form-control:focus ~ label::after, .form-floating > .form-control:not(:placeholder-shown) ~ label::after, .form-floating > .form-control-plaintext ~ label::after, .form-floating > .form-select ~ label::after {
+ background-color: transparent;
+
+
+}
+.form-floating > label {
+ .dark & {
+ color: $dark-font-color3 !important;
+ }
+}
+
+
+/*
+ * Transitions
+ */
+
+// page-change
+.slide-fade-enter-active {
+ transition: all 0.2s $easing-in;
+}
+
+.slide-fade-leave-active {
+ transition: all 0.2s $easing-in;
+}
+
+.slide-fade-enter-from,
+.slide-fade-leave-to {
+ transform: translateY(50px);
+ opacity: 0;
+}
+
+.slide-fade-right-enter-active {
+ transition: all 0.2s $easing-in;
+}
+
+.slide-fade-right-leave-active {
+ transition: all 0.2s $easing-in;
+}
+
+.slide-fade-right-enter-from,
+.slide-fade-right-leave-to {
+ transform: translateX(50px);
+ opacity: 0;
+}
+
+.slide-fade-up-enter-active {
+ transition: all 0.2s $easing-in;
+}
+
+.slide-fade-up-leave-active {
+ transition: all 0.2s $easing-in;
+}
+
+.slide-fade-up-enter-from,
+.slide-fade-up-leave-to {
+ transform: translateY(-50px);
+ opacity: 0;
+}
+
+.stack-list {
+ &.scrollbar {
+ overflow-y: auto;
+ }
+
+ @media (max-width: 770px) {
+ &.scrollbar {
+ height: calc(100% - 97px);
+ }
+ }
+
+ .item {
+ display: flex;
+ align-items: center;
+ height: 52px;
+ text-decoration: none;
+ border-radius: 10px;
+ transition: all ease-in-out 0.15s;
+ width: 100%;
+ padding: 0 8px;
+
+ &.disabled {
+ opacity: 0.3;
+ }
+
+ &:hover {
+ background-color: $highlight-white;
+ }
+
+ &.active {
+ background-color: #cdf8f4;
+ }
+
+ .title {
+ display: inline-block;
+ margin-top: -4px;
+ }
+ }
+}
+
+.alert-success {
+ color: #122f21;
+ background-color: $primary;
+ border-color: $primary;
+}
+
+.alert-info {
+ color: #055160;
+ background-color: #cff4fc;
+ border-color: #cff4fc;
+}
+
+.alert-danger {
+ color: #842029;
+ background-color: #f8d7da;
+ border-color: #f8d7da;
+}
+
+.btn-success {
+ color: #fff;
+ background-color: #4caf50;
+ border-color: #4caf50;
+}
+
+
+[contenteditable=true] {
+ transition: all $easing-in 0.2s;
+ background-color: rgba(239, 239, 239, 0.7);
+ border-radius: 8px;
+
+ &.no-bg {
+ background-color: transparent !important;
+ }
+
+ &:focus {
+ outline: 0 solid #eee;
+ background-color: rgba(245, 245, 245, 0.9);
+ }
+
+ &:hover {
+ background-color: rgba(239, 239, 239, 0.8);
+ }
+
+ .dark & {
+ background-color: rgba(239, 239, 239, 0.2);
+ }
+
+ /*
+ &::after {
+ margin-left: 5px;
+ content: "🖊️";
+ font-size: 13px;
+ color: #eee;
+ }
+ */
+
+}
+
+.action {
+ transition: all $easing-in 0.2s;
+
+ &:hover {
+ cursor: pointer;
+ transform: scale(1.2);
+ }
+}
+
+.vue-image-crop-upload .vicp-wrap {
+ border-radius: 10px !important;
+}
+
+.spinner {
+ color: $primary;
+}
+
+.prism-editor__textarea {
+ outline: none !important;
+}
+
+h5.settings-subheading::after {
+ content: "";
+ display: block;
+ width: 50%;
+ padding-top: 8px;
+ border-bottom: 1px solid $dark-border-color;
+}
+
+/* required class */
+.code-editor, .css-editor {
+ /* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
+
+ border-radius: 1rem;
+ padding: 10px 5px;
+ border: 1px solid #ced4da;
+
+ .dark & {
+ background: $dark-bg2;
+ border: 1px solid $dark-border-color;
+ }
+}
+
+
+$shadow-box-padding: 20px;
+
+.shadow-box-with-fixed-bottom-bar {
+ padding-top: $shadow-box-padding;
+ padding-bottom: 0;
+ padding-right: $shadow-box-padding;
+ padding-left: $shadow-box-padding;
+}
+
+.fixed-bottom-bar {
+ position: sticky;
+ bottom: 0;
+ margin-left: -$shadow-box-padding;
+ margin-right: -$shadow-box-padding;
+ z-index: 100;
+ background-color: rgba(white, 0.2);
+ backdrop-filter: blur(2px);
+ border-radius: 0 0 10px 10px;
+
+ .dark & {
+ background-color: rgba($dark-header-bg, 0.9);
+ }
+}
+
+@media (max-width: 770px) {
+ .toast-container {
+ margin-bottom: 100px !important;
+ }
+}
+
+@media (max-width: 550px) {
+ .toast-container {
+ margin-bottom: 126px !important;
+ }
+}
+
+.main-terminal {
+ .xterm-viewport {
+ border-radius: 10px;
+ background-color: $dark-bg !important;
+ }
+}
+
+code {
+ padding: .2em .4em;
+ margin: 0;
+ font-size: 85%;
+ white-space: break-spaces;
+ background-color: rgba(239, 239, 239, 0.15);
+
+ border-radius: 6px;
+ font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ color: black;
+
+ .dark & {
+ color: $dark-font-color;
+ }
+}
+
+// Vue Prism Editor bug - workaround
+// https://github.com/koca/vue-prism-editor/issues/87
+/*
+.prism-editor__textarea {
+ width: 999999px !important;
+}
+.prism-editor__editor {
+ white-space: pre !important;
+}
+.prism-editor__container {
+ overflow-x: scroll !important;
+}*/
+
+// Localization
+@import "localization.scss";
diff --git a/frontend/src/styles/vars.scss b/frontend/src/styles/vars.scss
new file mode 100644
index 0000000..5595da2
--- /dev/null
+++ b/frontend/src/styles/vars.scss
@@ -0,0 +1,26 @@
+$primary: #74c2ff;
+$danger: #dc3545;
+$warning: #f8a306;
+$maintenance: #1747f5;
+$link-color: #111;
+$border-radius: 50rem;
+
+$highlight: #9dd1ff;
+$highlight-white: #e7faec;
+
+$dark-font-color: #b1b8c0;
+$dark-font-color2: #020b05;
+$dark-font-color3: #575c62;
+$dark-bg: #0d1117;
+$dark-bg2: #070a10;
+$dark-border-color: #1d2634;
+$dark-header-bg: #161b22;
+
+$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
+$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
+$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);
+
+$dropdown-border-radius: 0.5rem;
+
+$primary-gradient: linear-gradient(135deg, #74c2ff 0%, #74c2ff 75%, #86e6a9);
+$primary-gradient-active: linear-gradient(135deg, #74c2ff 0%, #74c2ff 50%, #86e6a9);
diff --git a/frontend/src/util-frontend.ts b/frontend/src/util-frontend.ts
new file mode 100644
index 0000000..e55ebf0
--- /dev/null
+++ b/frontend/src/util-frontend.ts
@@ -0,0 +1,215 @@
+import dayjs from "dayjs";
+import timezones from "timezones-list";
+import { localeDirection, currentLocale } from "./i18n";
+import { POSITION } from "vue-toastification";
+
+/**
+ * Returns the offset from UTC in hours for the current locale.
+ * @param {string} timeZone Timezone to get offset for
+ * @returns {number} The offset from UTC in hours.
+ *
+ * Generated by Trelent
+ */
+function getTimezoneOffset(timeZone) {
+ const now = new Date();
+ const tzString = now.toLocaleString("en-US", {
+ timeZone,
+ });
+ const localString = now.toLocaleString("en-US");
+ const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000;
+ const offset = diff + now.getTimezoneOffset() / 60;
+ return -offset;
+}
+
+/**
+ * Returns a list of timezones sorted by their offset from UTC.
+ * @returns {object[]} A list of the given timezones sorted by their offset from UTC.
+ *
+ * Generated by Trelent
+ */
+export function timezoneList() {
+ let result = [];
+
+ for (let timezone of timezones) {
+ try {
+ let display = dayjs().tz(timezone.tzCode).format("Z");
+
+ result.push({
+ name: `(UTC${display}) ${timezone.tzCode}`,
+ value: timezone.tzCode,
+ time: getTimezoneOffset(timezone.tzCode),
+ });
+ } catch (e) {
+ // Skipping not supported timezone.tzCode by dayjs
+ }
+ }
+
+ result.sort((a, b) => {
+ if (a.time > b.time) {
+ return 1;
+ }
+
+ if (b.time > a.time) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ return result;
+}
+
+/**
+ * Set the locale of the HTML page
+ * @returns {void}
+ */
+export function setPageLocale() {
+ const html = document.documentElement;
+ html.setAttribute("lang", currentLocale() );
+ html.setAttribute("dir", localeDirection() );
+}
+
+/**
+ * Get the base URL
+ * Mainly used for dev, because the backend and the frontend are in different ports.
+ * @returns {string} Base URL
+ */
+export function getResBaseURL() {
+ const env = process.env.NODE_ENV;
+ if (env === "development" && isDevContainer()) {
+ return location.protocol + "//" + getDevContainerServerHostname();
+ } else if (env === "development" || localStorage.dev === "dev") {
+ return location.protocol + "//" + location.hostname + ":3001";
+ } else {
+ return "";
+ }
+}
+
+/**
+ * Are we currently running in a dev container?
+ * @returns {boolean} Running in dev container?
+ */
+export function isDevContainer() {
+ // eslint-disable-next-line no-undef
+ return (typeof DEVCONTAINER === "string" && DEVCONTAINER === "1");
+}
+
+/**
+ * Supports GitHub Codespaces only currently
+ * @returns {string} Dev container server hostname
+ */
+export function getDevContainerServerHostname() {
+ if (!isDevContainer()) {
+ return "";
+ }
+
+ // eslint-disable-next-line no-undef
+ return CODESPACE_NAME + "-3001." + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN;
+}
+
+/**
+ * Regex pattern fr identifying hostnames and IP addresses
+ * @param {boolean} mqtt whether or not the regex should take into
+ * account the fact that it is an mqtt uri
+ * @returns {RegExp} The requested regex
+ */
+export function hostNameRegexPattern(mqtt = false) {
+ // mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect)
+ const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?";
+ // Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
+ const ipRegexPattern = `((^${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$))`;
+ // Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
+ const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])(\\.)?$`;
+
+ return `${ipRegexPattern}|${hostNameRegexPattern}`;
+}
+
+/**
+ * Get the tag color options
+ * Shared between components
+ * @param {any} self Component
+ * @returns {object[]} Colour options
+ */
+export function colorOptions(self) {
+ return [
+ { name: self.$t("Gray"),
+ color: "#4B5563" },
+ { name: self.$t("Red"),
+ color: "#DC2626" },
+ { name: self.$t("Orange"),
+ color: "#D97706" },
+ { name: self.$t("Green"),
+ color: "#059669" },
+ { name: self.$t("Blue"),
+ color: "#2563EB" },
+ { name: self.$t("Indigo"),
+ color: "#4F46E5" },
+ { name: self.$t("Purple"),
+ color: "#7C3AED" },
+ { name: self.$t("Pink"),
+ color: "#DB2777" },
+ ];
+}
+
+/**
+ * Loads the toast timeout settings from storage.
+ * @returns {object} The toast plugin options object.
+ */
+export function loadToastSettings() {
+ return {
+ position: POSITION.BOTTOM_RIGHT,
+ containerClassName: "toast-container",
+ showCloseButtonOnHover: true,
+
+ filterBeforeCreate: (toast, toasts) => {
+ if (toast.timeout === 0) {
+ return false;
+ } else {
+ return toast;
+ }
+ },
+ };
+}
+
+/**
+ * Get timeout for success toasts
+ * @returns {(number|boolean)} Timeout in ms. If false timeout disabled.
+ */
+export function getToastSuccessTimeout() {
+ let successTimeout = 20000;
+
+ if (localStorage.toastSuccessTimeout !== undefined) {
+ const parsedTimeout = parseInt(localStorage.toastSuccessTimeout);
+ if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
+ successTimeout = parsedTimeout;
+ }
+ }
+
+ if (successTimeout === -1) {
+ successTimeout = false;
+ }
+
+ return successTimeout;
+}
+
+/**
+ * Get timeout for error toasts
+ * @returns {(number|boolean)} Timeout in ms. If false timeout disabled.
+ */
+export function getToastErrorTimeout() {
+ let errorTimeout = -1;
+
+ if (localStorage.toastErrorTimeout !== undefined) {
+ const parsedTimeout = parseInt(localStorage.toastErrorTimeout);
+ if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
+ errorTimeout = parsedTimeout;
+ }
+ }
+
+ if (errorTimeout === -1) {
+ errorTimeout = false;
+ }
+
+ return errorTimeout;
+}
+
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..899b0bc
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +1,7 @@
+///
+
+declare module "*.vue" {
+ import type { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..a640d82
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,36 @@
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+import Components from "unplugin-vue-components/vite";
+import { BootstrapVueNextResolver } from "unplugin-vue-components/resolvers";
+import viteCompression from "vite-plugin-compression";
+import "vue";
+
+const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ server: {
+ port: 5000,
+ },
+ define: {
+ "FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
+ },
+ root: "./frontend",
+ build: {
+ outDir: "../frontend-dist",
+ },
+ plugins: [
+ vue(),
+ Components({
+ resolvers: [ BootstrapVueNextResolver() ],
+ }),
+ viteCompression({
+ algorithm: "gzip",
+ filter: viteCompressionFilter,
+ }),
+ viteCompression({
+ algorithm: "brotliCompress",
+ filter: viteCompressionFilter,
+ }),
+ ],
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3175afc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,79 @@
+{
+ "name": "dockge",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "fmt": "eslint \"**/*.{ts,vue}\" --fix",
+ "lint": "eslint \"**/*.{ts,vue}\"",
+ "start": "tsx ./backend/index.ts",
+ "dev:backend": "cross-env NODE_ENV=development tsx watch ./backend/index.ts",
+ "dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
+ "build:frontend": "vite build --config ./frontend/vite.config.ts",
+ "build:docker-base": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:base -f ./docker/Base.Dockerfile . --push",
+ "build:docker": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:1.0.0 -f ./docker/Dockerfile . --push",
+ "build:docker-nightly": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:nightly --target nightly -f ./docker/Dockerfile . --push",
+ "start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest",
+ "mark-as-nightly": "tsx ./extra/mark-as-nightly.ts"
+ },
+ "dependencies": {
+ "@homebridge/node-pty-prebuilt-multiarch": "~0.11.10",
+ "@louislam/sqlite3": "~15.1.6",
+ "bcryptjs": "~2.4.3",
+ "check-password-strength": "~2.0.7",
+ "command-exists": "~1.2.9",
+ "compare-versions": "~6.1.0",
+ "composerize": "~1.4.1",
+ "croner": "~7.0.4",
+ "dayjs": "~1.11.10",
+ "express": "~4.18.2",
+ "express-static-gzip": "~2.1.7",
+ "http-graceful-shutdown": "~3.1.13",
+ "jsonwebtoken": "~9.0.2",
+ "jwt-decode": "~3.1.2",
+ "knex": "~2.5.1",
+ "limiter-es6-compat": "~2.1.2",
+ "mysql2": "^3.6.3",
+ "redbean-node": "0.3.2",
+ "socket.io": "~4.7.2",
+ "socket.io-client": "~4.7.2",
+ "timezones-list": "~3.0.2",
+ "ts-command-line-args": "~2.5.1",
+ "tsx": "~3.14.0",
+ "type-fest": "~4.3.3",
+ "yaml": "~2.3.4"
+ },
+ "devDependencies": {
+ "@fortawesome/fontawesome-svg-core": "6.4.2",
+ "@fortawesome/free-regular-svg-icons": "6.4.2",
+ "@fortawesome/free-solid-svg-icons": "6.4.2",
+ "@fortawesome/vue-fontawesome": "3.0.3",
+ "@types/bootstrap": "~5.2.8",
+ "@types/command-exists": "~1.2.3",
+ "@types/express": "~4.17.21",
+ "@types/jsonwebtoken": "~9.0.4",
+ "@typescript-eslint/eslint-plugin": "~6.8.0",
+ "@typescript-eslint/parser": "~6.8.0",
+ "@vitejs/plugin-vue": "~4.3.4",
+ "bootstrap": "5.3.2",
+ "bootstrap-vue-next": "~0.14.10",
+ "cross-env": "~7.0.3",
+ "eslint": "~8.50.0",
+ "eslint-plugin-jsdoc": "~46.8.2",
+ "eslint-plugin-vue": "~9.17.0",
+ "prismjs": "~1.29.0",
+ "sass": "~1.68.0",
+ "typescript": "~5.2.2",
+ "unplugin-vue-components": "~0.25.2",
+ "vite": "~4.5.0",
+ "vite-plugin-compression": "~0.5.1",
+ "vue": "~3.3.8",
+ "vue-eslint-parser": "~9.3.2",
+ "vue-i18n": "~9.5.0",
+ "vue-prism-editor": "2.0.0-alpha.2",
+ "vue-qrcode": "~2.2.0",
+ "vue-router": "~4.2.5",
+ "vue-toastification": "2.0.0-rc.5",
+ "xterm": "~5.4.0-beta.37",
+ "xterm-addon-web-links": "~0.9.0"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..f7e7d75
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,4477 @@
+lockfileVersion: '6.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+dependencies:
+ '@homebridge/node-pty-prebuilt-multiarch':
+ specifier: ~0.11.10
+ version: 0.11.10
+ '@louislam/sqlite3':
+ specifier: ~15.1.6
+ version: 15.1.6
+ bcryptjs:
+ specifier: ~2.4.3
+ version: 2.4.3
+ check-password-strength:
+ specifier: ~2.0.7
+ version: 2.0.7
+ command-exists:
+ specifier: ~1.2.9
+ version: 1.2.9
+ compare-versions:
+ specifier: ~6.1.0
+ version: 6.1.0
+ composerize:
+ specifier: ~1.4.1
+ version: 1.4.1
+ croner:
+ specifier: ~7.0.4
+ version: 7.0.4
+ dayjs:
+ specifier: ~1.11.10
+ version: 1.11.10
+ express:
+ specifier: ~4.18.2
+ version: 4.18.2
+ express-static-gzip:
+ specifier: ~2.1.7
+ version: 2.1.7
+ http-graceful-shutdown:
+ specifier: ~3.1.13
+ version: 3.1.13
+ jsonwebtoken:
+ specifier: ~9.0.2
+ version: 9.0.2
+ jwt-decode:
+ specifier: ~3.1.2
+ version: 3.1.2
+ knex:
+ specifier: ~2.5.1
+ version: 2.5.1(mysql2@3.6.3)
+ limiter-es6-compat:
+ specifier: ~2.1.2
+ version: 2.1.2
+ mysql2:
+ specifier: ^3.6.3
+ version: 3.6.3
+ redbean-node:
+ specifier: 0.3.2
+ version: 0.3.2(mysql2@3.6.3)
+ socket.io:
+ specifier: ~4.7.2
+ version: 4.7.2
+ socket.io-client:
+ specifier: ~4.7.2
+ version: 4.7.2
+ timezones-list:
+ specifier: ~3.0.2
+ version: 3.0.2
+ ts-command-line-args:
+ specifier: ~2.5.1
+ version: 2.5.1
+ tsx:
+ specifier: ~3.14.0
+ version: 3.14.0
+ type-fest:
+ specifier: ~4.3.3
+ version: 4.3.3
+ yaml:
+ specifier: ~2.3.4
+ version: 2.3.4
+
+devDependencies:
+ '@fortawesome/fontawesome-svg-core':
+ specifier: 6.4.2
+ version: 6.4.2
+ '@fortawesome/free-regular-svg-icons':
+ specifier: 6.4.2
+ version: 6.4.2
+ '@fortawesome/free-solid-svg-icons':
+ specifier: 6.4.2
+ version: 6.4.2
+ '@fortawesome/vue-fontawesome':
+ specifier: 3.0.3
+ version: 3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.8)
+ '@types/bootstrap':
+ specifier: ~5.2.8
+ version: 5.2.8
+ '@types/command-exists':
+ specifier: ~1.2.3
+ version: 1.2.3
+ '@types/express':
+ specifier: ~4.17.21
+ version: 4.17.21
+ '@types/jsonwebtoken':
+ specifier: ~9.0.4
+ version: 9.0.4
+ '@typescript-eslint/eslint-plugin':
+ specifier: ~6.8.0
+ version: 6.8.0(@typescript-eslint/parser@6.8.0)(eslint@8.50.0)(typescript@5.2.2)
+ '@typescript-eslint/parser':
+ specifier: ~6.8.0
+ version: 6.8.0(eslint@8.50.0)(typescript@5.2.2)
+ '@vitejs/plugin-vue':
+ specifier: ~4.3.4
+ version: 4.3.4(vite@4.5.0)(vue@3.3.8)
+ bootstrap:
+ specifier: 5.3.2
+ version: 5.3.2(@popperjs/core@2.11.8)
+ bootstrap-vue-next:
+ specifier: ~0.14.10
+ version: 0.14.10(vue@3.3.8)
+ cross-env:
+ specifier: ~7.0.3
+ version: 7.0.3
+ eslint:
+ specifier: ~8.50.0
+ version: 8.50.0
+ eslint-plugin-jsdoc:
+ specifier: ~46.8.2
+ version: 46.8.2(eslint@8.50.0)
+ eslint-plugin-vue:
+ specifier: ~9.17.0
+ version: 9.17.0(eslint@8.50.0)
+ prismjs:
+ specifier: ~1.29.0
+ version: 1.29.0
+ sass:
+ specifier: ~1.68.0
+ version: 1.68.0
+ typescript:
+ specifier: ~5.2.2
+ version: 5.2.2
+ unplugin-vue-components:
+ specifier: ~0.25.2
+ version: 0.25.2(vue@3.3.8)
+ vite:
+ specifier: ~4.5.0
+ version: 4.5.0(sass@1.68.0)
+ vite-plugin-compression:
+ specifier: ~0.5.1
+ version: 0.5.1(vite@4.5.0)
+ vue:
+ specifier: ~3.3.8
+ version: 3.3.8(typescript@5.2.2)
+ vue-eslint-parser:
+ specifier: ~9.3.2
+ version: 9.3.2(eslint@8.50.0)
+ vue-i18n:
+ specifier: ~9.5.0
+ version: 9.5.0(vue@3.3.8)
+ vue-prism-editor:
+ specifier: 2.0.0-alpha.2
+ version: 2.0.0-alpha.2(vue@3.3.8)
+ vue-qrcode:
+ specifier: ~2.2.0
+ version: 2.2.0(qrcode@1.5.3)(vue@3.3.8)
+ vue-router:
+ specifier: ~4.2.5
+ version: 4.2.5(vue@3.3.8)
+ vue-toastification:
+ specifier: 2.0.0-rc.5
+ version: 2.0.0-rc.5(vue@3.3.8)
+ xterm:
+ specifier: ~5.4.0-beta.37
+ version: 5.4.0-beta.37
+ xterm-addon-web-links:
+ specifier: ~0.9.0
+ version: 0.9.0(xterm@5.4.0-beta.37)
+
+packages:
+
+ /@aashutoshrathi/word-wrap@1.2.6:
+ resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /@antfu/utils@0.7.6:
+ resolution: {integrity: sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==}
+ dev: true
+
+ /@babel/helper-string-parser@7.22.5:
+ resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-validator-identifier@7.22.20:
+ resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/parser@7.23.0:
+ resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+ dependencies:
+ '@babel/types': 7.23.0
+ dev: true
+
+ /@babel/types@7.23.0:
+ resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-string-parser': 7.22.5
+ '@babel/helper-validator-identifier': 7.22.20
+ to-fast-properties: 2.0.0
+ dev: true
+
+ /@es-joy/jsdoccomment@0.40.1:
+ resolution: {integrity: sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==}
+ engines: {node: '>=16'}
+ dependencies:
+ comment-parser: 1.4.0
+ esquery: 1.5.0
+ jsdoc-type-pratt-parser: 4.0.0
+ dev: true
+
+ /@esbuild/android-arm64@0.18.20:
+ resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/android-arm@0.18.20:
+ resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/android-x64@0.18.20:
+ resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/darwin-arm64@0.18.20:
+ resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/darwin-x64@0.18.20:
+ resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/freebsd-arm64@0.18.20:
+ resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/freebsd-x64@0.18.20:
+ resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-arm64@0.18.20:
+ resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-arm@0.18.20:
+ resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-ia32@0.18.20:
+ resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-loong64@0.18.20:
+ resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-mips64el@0.18.20:
+ resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-ppc64@0.18.20:
+ resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-riscv64@0.18.20:
+ resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-s390x@0.18.20:
+ resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/linux-x64@0.18.20:
+ resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/netbsd-x64@0.18.20:
+ resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/openbsd-x64@0.18.20:
+ resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/sunos-x64@0.18.20:
+ resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/win32-arm64@0.18.20:
+ resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/win32-ia32@0.18.20:
+ resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ optional: true
+
+ /@esbuild/win32-x64@0.18.20:
+ resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ optional: true
+
+ /@eslint-community/eslint-utils@4.4.0(eslint@8.50.0):
+ resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ dependencies:
+ eslint: 8.50.0
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /@eslint-community/regexpp@4.10.0:
+ resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+ dev: true
+
+ /@eslint/eslintrc@2.1.3:
+ resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.3.4
+ espree: 9.6.1
+ globals: 13.23.0
+ ignore: 5.2.4
+ import-fresh: 3.3.0
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@eslint/js@8.50.0:
+ resolution: {integrity: sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: true
+
+ /@floating-ui/core@1.5.0:
+ resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==}
+ dependencies:
+ '@floating-ui/utils': 0.1.6
+ dev: true
+
+ /@floating-ui/dom@1.5.3:
+ resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==}
+ dependencies:
+ '@floating-ui/core': 1.5.0
+ '@floating-ui/utils': 0.1.6
+ dev: true
+
+ /@floating-ui/utils@0.1.6:
+ resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
+ dev: true
+
+ /@floating-ui/vue@1.0.2(vue@3.3.8):
+ resolution: {integrity: sha512-sImlAl9mAoCKZLNlwWz2P2ZMJIDlOEDXrRD6aD2sIHAka1LPC+nWtB+D3lPe7IE7FGWSbwBPTnlSdlABa3Fr0A==}
+ dependencies:
+ '@floating-ui/dom': 1.5.3
+ vue-demi: 0.14.6(vue@3.3.8)
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+ dev: true
+
+ /@fortawesome/fontawesome-common-types@6.4.2:
+ resolution: {integrity: sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dev: true
+
+ /@fortawesome/fontawesome-svg-core@6.4.2:
+ resolution: {integrity: sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.4.2
+ dev: true
+
+ /@fortawesome/free-regular-svg-icons@6.4.2:
+ resolution: {integrity: sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.4.2
+ dev: true
+
+ /@fortawesome/free-solid-svg-icons@6.4.2:
+ resolution: {integrity: sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.4.2
+ dev: true
+
+ /@fortawesome/vue-fontawesome@3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.8):
+ resolution: {integrity: sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==}
+ peerDependencies:
+ '@fortawesome/fontawesome-svg-core': ~1 || ~6
+ vue: '>= 3.0.0 < 4'
+ dependencies:
+ '@fortawesome/fontawesome-svg-core': 6.4.2
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /@gar/promisify@1.1.3:
+ resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@homebridge/node-pty-prebuilt-multiarch@0.11.10:
+ resolution: {integrity: sha512-ttOE8QQRq/aRXDoKD2rfYEF50AiDLM9LPTohqCog1Z78g8k3Zqk15R/EHfTl/8cfw4l0fxt3y0dWL56wq79p2A==}
+ requiresBuild: true
+ dependencies:
+ nan: 2.18.0
+ prebuild-install: 7.1.1
+ dev: false
+
+ /@humanwhocodes/config-array@0.11.13:
+ resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
+ engines: {node: '>=10.10.0'}
+ dependencies:
+ '@humanwhocodes/object-schema': 2.0.1
+ debug: 4.3.4
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@humanwhocodes/module-importer@1.0.1:
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+ dev: true
+
+ /@humanwhocodes/object-schema@2.0.1:
+ resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
+ dev: true
+
+ /@intlify/core-base@9.5.0:
+ resolution: {integrity: sha512-y3ufM1RJbI/DSmJf3lYs9ACq3S/iRvaSsE3rPIk0MGH7fp+JxU6rdryv/EYcwfcr3Y1aHFlCBir6S391hRZ57w==}
+ engines: {node: '>= 16'}
+ dependencies:
+ '@intlify/message-compiler': 9.5.0
+ '@intlify/shared': 9.5.0
+ dev: true
+
+ /@intlify/message-compiler@9.5.0:
+ resolution: {integrity: sha512-CAhVNfEZcOVFg0/5MNyt+OFjvs4J/ARjCj2b+54/FvFP0EDJI5lIqMTSDBE7k0atMROSP0SvWCkwu/AZ5xkK1g==}
+ engines: {node: '>= 16'}
+ dependencies:
+ '@intlify/shared': 9.5.0
+ source-map-js: 1.0.2
+ dev: true
+
+ /@intlify/shared@9.5.0:
+ resolution: {integrity: sha512-tAxV14LMXZDZbu32XzLMTsowNlgJNmLwWHYzvMUl6L8gvQeoYiZONjY7AUsqZW8TOZDX9lfvF6adPkk9FSRdDA==}
+ engines: {node: '>= 16'}
+ dev: true
+
+ /@isaacs/cliui@8.0.2:
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: /string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: /strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: /wrap-ansi@7.0.0
+ dev: false
+
+ /@jridgewell/sourcemap-codec@1.4.15:
+ resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
+ dev: true
+
+ /@louislam/sqlite3@15.1.6:
+ resolution: {integrity: sha512-cVf7hcMrfywYnycatLvorngTFpL3BSWvEy7/NrEfcTyQX8xxj9fdeD553oCTv5fIAk85fluo6mzPq89V3YzrVA==}
+ requiresBuild: true
+ peerDependenciesMeta:
+ node-gyp:
+ optional: true
+ dependencies:
+ '@mapbox/node-pre-gyp': 1.0.11
+ node-addon-api: 4.3.0
+ tar: 6.2.0
+ optionalDependencies:
+ node-gyp: 8.4.1
+ transitivePeerDependencies:
+ - bluebird
+ - encoding
+ - supports-color
+ dev: false
+
+ /@mapbox/node-pre-gyp@1.0.11:
+ resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
+ hasBin: true
+ dependencies:
+ detect-libc: 2.0.2
+ https-proxy-agent: 5.0.1
+ make-dir: 3.1.0
+ node-fetch: 2.7.0
+ nopt: 5.0.0
+ npmlog: 5.0.1
+ rimraf: 3.0.2
+ semver: 7.5.4
+ tar: 6.2.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+ dev: false
+
+ /@nodelib/fs.scandir@2.1.5:
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+ dev: true
+
+ /@nodelib/fs.stat@2.0.5:
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /@nodelib/fs.walk@1.2.8:
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.15.0
+ dev: true
+
+ /@npmcli/fs@1.1.1:
+ resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==}
+ requiresBuild: true
+ dependencies:
+ '@gar/promisify': 1.1.3
+ semver: 7.5.4
+ dev: false
+ optional: true
+
+ /@npmcli/move-file@1.1.2:
+ resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==}
+ engines: {node: '>=10'}
+ deprecated: This functionality has been moved to @npmcli/fs
+ requiresBuild: true
+ dependencies:
+ mkdirp: 1.0.4
+ rimraf: 3.0.2
+ dev: false
+ optional: true
+
+ /@pkgjs/parseargs@0.11.0:
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@popperjs/core@2.11.8:
+ resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+ dev: true
+
+ /@rollup/pluginutils@5.0.5:
+ resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ dependencies:
+ '@types/estree': 1.0.5
+ estree-walker: 2.0.2
+ picomatch: 2.3.1
+ dev: true
+
+ /@socket.io/component-emitter@3.1.0:
+ resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==}
+ dev: false
+
+ /@tootallnate/once@1.1.2:
+ resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
+ engines: {node: '>= 6'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@types/body-parser@1.19.5:
+ resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
+ dependencies:
+ '@types/connect': 3.4.38
+ '@types/node': 20.8.10
+ dev: true
+
+ /@types/bootstrap@5.2.8:
+ resolution: {integrity: sha512-14do+aWZPc1w3G+YevSsy8eas1XEPhTOUNBhQX/r12YKn7ySssATJusBQ/HCQAd2nq54U8vvrftHSb1YpeJUXg==}
+ dependencies:
+ '@popperjs/core': 2.11.8
+ dev: true
+
+ /@types/command-exists@1.2.3:
+ resolution: {integrity: sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==}
+ dev: true
+
+ /@types/connect@3.4.38:
+ resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+ dependencies:
+ '@types/node': 20.8.10
+ dev: true
+
+ /@types/cookie@0.4.1:
+ resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
+ dev: false
+
+ /@types/cors@2.8.16:
+ resolution: {integrity: sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==}
+ dependencies:
+ '@types/node': 20.8.10
+ dev: false
+
+ /@types/estree@1.0.5:
+ resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
+ dev: true
+
+ /@types/express-serve-static-core@4.17.41:
+ resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==}
+ dependencies:
+ '@types/node': 20.8.10
+ '@types/qs': 6.9.9
+ '@types/range-parser': 1.2.6
+ '@types/send': 0.17.3
+ dev: true
+
+ /@types/express@4.17.21:
+ resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
+ dependencies:
+ '@types/body-parser': 1.19.5
+ '@types/express-serve-static-core': 4.17.41
+ '@types/qs': 6.9.9
+ '@types/serve-static': 1.15.4
+ dev: true
+
+ /@types/http-errors@2.0.3:
+ resolution: {integrity: sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==}
+ dev: true
+
+ /@types/json-schema@7.0.14:
+ resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==}
+ dev: true
+
+ /@types/jsonwebtoken@9.0.4:
+ resolution: {integrity: sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==}
+ dependencies:
+ '@types/node': 20.8.10
+ dev: true
+
+ /@types/mime@1.3.4:
+ resolution: {integrity: sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==}
+ dev: true
+
+ /@types/mime@3.0.3:
+ resolution: {integrity: sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ==}
+ dev: true
+
+ /@types/node@20.3.3:
+ resolution: {integrity: sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==}
+ dev: false
+
+ /@types/node@20.8.10:
+ resolution: {integrity: sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==}
+ dependencies:
+ undici-types: 5.26.5
+
+ /@types/qs@6.9.9:
+ resolution: {integrity: sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==}
+ dev: true
+
+ /@types/range-parser@1.2.6:
+ resolution: {integrity: sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==}
+ dev: true
+
+ /@types/semver@7.5.4:
+ resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==}
+ dev: true
+
+ /@types/send@0.17.3:
+ resolution: {integrity: sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==}
+ dependencies:
+ '@types/mime': 1.3.4
+ '@types/node': 20.8.10
+ dev: true
+
+ /@types/serve-static@1.15.4:
+ resolution: {integrity: sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==}
+ dependencies:
+ '@types/http-errors': 2.0.3
+ '@types/mime': 3.0.3
+ '@types/node': 20.8.10
+ dev: true
+
+ /@types/web-bluetooth@0.0.18:
+ resolution: {integrity: sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==}
+ dev: true
+
+ /@typescript-eslint/eslint-plugin@6.8.0(@typescript-eslint/parser@6.8.0)(eslint@8.50.0)(typescript@5.2.2):
+ resolution: {integrity: sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@eslint-community/regexpp': 4.10.0
+ '@typescript-eslint/parser': 6.8.0(eslint@8.50.0)(typescript@5.2.2)
+ '@typescript-eslint/scope-manager': 6.8.0
+ '@typescript-eslint/type-utils': 6.8.0(eslint@8.50.0)(typescript@5.2.2)
+ '@typescript-eslint/utils': 6.8.0(eslint@8.50.0)(typescript@5.2.2)
+ '@typescript-eslint/visitor-keys': 6.8.0
+ debug: 4.3.4
+ eslint: 8.50.0
+ graphemer: 1.4.0
+ ignore: 5.2.4
+ natural-compare: 1.4.0
+ semver: 7.5.4
+ ts-api-utils: 1.0.3(typescript@5.2.2)
+ typescript: 5.2.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@typescript-eslint/parser@6.8.0(eslint@8.50.0)(typescript@5.2.2):
+ resolution: {integrity: sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/scope-manager': 6.8.0
+ '@typescript-eslint/types': 6.8.0
+ '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2)
+ '@typescript-eslint/visitor-keys': 6.8.0
+ debug: 4.3.4
+ eslint: 8.50.0
+ typescript: 5.2.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@typescript-eslint/scope-manager@6.8.0:
+ resolution: {integrity: sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dependencies:
+ '@typescript-eslint/types': 6.8.0
+ '@typescript-eslint/visitor-keys': 6.8.0
+ dev: true
+
+ /@typescript-eslint/type-utils@6.8.0(eslint@8.50.0)(typescript@5.2.2):
+ resolution: {integrity: sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2)
+ '@typescript-eslint/utils': 6.8.0(eslint@8.50.0)(typescript@5.2.2)
+ debug: 4.3.4
+ eslint: 8.50.0
+ ts-api-utils: 1.0.3(typescript@5.2.2)
+ typescript: 5.2.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@typescript-eslint/types@6.8.0:
+ resolution: {integrity: sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dev: true
+
+ /@typescript-eslint/typescript-estree@6.8.0(typescript@5.2.2):
+ resolution: {integrity: sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@typescript-eslint/types': 6.8.0
+ '@typescript-eslint/visitor-keys': 6.8.0
+ debug: 4.3.4
+ globby: 11.1.0
+ is-glob: 4.0.3
+ semver: 7.5.4
+ ts-api-utils: 1.0.3(typescript@5.2.2)
+ typescript: 5.2.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@typescript-eslint/utils@6.8.0(eslint@8.50.0)(typescript@5.2.2):
+ resolution: {integrity: sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0)
+ '@types/json-schema': 7.0.14
+ '@types/semver': 7.5.4
+ '@typescript-eslint/scope-manager': 6.8.0
+ '@typescript-eslint/types': 6.8.0
+ '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2)
+ eslint: 8.50.0
+ semver: 7.5.4
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+ dev: true
+
+ /@typescript-eslint/visitor-keys@6.8.0:
+ resolution: {integrity: sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ dependencies:
+ '@typescript-eslint/types': 6.8.0
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /@vitejs/plugin-vue@4.3.4(vite@4.5.0)(vue@3.3.8):
+ resolution: {integrity: sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.0.0
+ vue: ^3.2.25
+ dependencies:
+ vite: 4.5.0(sass@1.68.0)
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /@vue/compiler-core@3.3.8:
+ resolution: {integrity: sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==}
+ dependencies:
+ '@babel/parser': 7.23.0
+ '@vue/shared': 3.3.8
+ estree-walker: 2.0.2
+ source-map-js: 1.0.2
+ dev: true
+
+ /@vue/compiler-dom@3.3.8:
+ resolution: {integrity: sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==}
+ dependencies:
+ '@vue/compiler-core': 3.3.8
+ '@vue/shared': 3.3.8
+ dev: true
+
+ /@vue/compiler-sfc@3.3.8:
+ resolution: {integrity: sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==}
+ dependencies:
+ '@babel/parser': 7.23.0
+ '@vue/compiler-core': 3.3.8
+ '@vue/compiler-dom': 3.3.8
+ '@vue/compiler-ssr': 3.3.8
+ '@vue/reactivity-transform': 3.3.8
+ '@vue/shared': 3.3.8
+ estree-walker: 2.0.2
+ magic-string: 0.30.5
+ postcss: 8.4.31
+ source-map-js: 1.0.2
+ dev: true
+
+ /@vue/compiler-ssr@3.3.8:
+ resolution: {integrity: sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==}
+ dependencies:
+ '@vue/compiler-dom': 3.3.8
+ '@vue/shared': 3.3.8
+ dev: true
+
+ /@vue/devtools-api@6.5.1:
+ resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==}
+ dev: true
+
+ /@vue/reactivity-transform@3.3.8:
+ resolution: {integrity: sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==}
+ dependencies:
+ '@babel/parser': 7.23.0
+ '@vue/compiler-core': 3.3.8
+ '@vue/shared': 3.3.8
+ estree-walker: 2.0.2
+ magic-string: 0.30.5
+ dev: true
+
+ /@vue/reactivity@3.3.8:
+ resolution: {integrity: sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==}
+ dependencies:
+ '@vue/shared': 3.3.8
+ dev: true
+
+ /@vue/runtime-core@3.3.8:
+ resolution: {integrity: sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==}
+ dependencies:
+ '@vue/reactivity': 3.3.8
+ '@vue/shared': 3.3.8
+ dev: true
+
+ /@vue/runtime-dom@3.3.8:
+ resolution: {integrity: sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==}
+ dependencies:
+ '@vue/runtime-core': 3.3.8
+ '@vue/shared': 3.3.8
+ csstype: 3.1.2
+ dev: true
+
+ /@vue/server-renderer@3.3.8(vue@3.3.8):
+ resolution: {integrity: sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==}
+ peerDependencies:
+ vue: 3.3.8
+ dependencies:
+ '@vue/compiler-ssr': 3.3.8
+ '@vue/shared': 3.3.8
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /@vue/shared@3.3.8:
+ resolution: {integrity: sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==}
+ dev: true
+
+ /@vueuse/core@10.5.0(vue@3.3.8):
+ resolution: {integrity: sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==}
+ dependencies:
+ '@types/web-bluetooth': 0.0.18
+ '@vueuse/metadata': 10.5.0
+ '@vueuse/shared': 10.5.0(vue@3.3.8)
+ vue-demi: 0.14.6(vue@3.3.8)
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+ dev: true
+
+ /@vueuse/metadata@10.5.0:
+ resolution: {integrity: sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==}
+ dev: true
+
+ /@vueuse/shared@10.5.0(vue@3.3.8):
+ resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==}
+ dependencies:
+ vue-demi: 0.14.6(vue@3.3.8)
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+ dev: true
+
+ /abbrev@1.1.1:
+ resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
+ requiresBuild: true
+ dev: false
+
+ /accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+ dev: false
+
+ /acorn-jsx@5.3.2(acorn@8.11.2):
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+ dependencies:
+ acorn: 8.11.2
+ dev: true
+
+ /acorn@8.11.2:
+ resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+ dev: true
+
+ /agent-base@6.0.2:
+ resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+ engines: {node: '>= 6.0.0'}
+ dependencies:
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /agentkeepalive@4.5.0:
+ resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
+ engines: {node: '>= 8.0.0'}
+ requiresBuild: true
+ dependencies:
+ humanize-ms: 1.2.1
+ dev: false
+ optional: true
+
+ /aggregate-error@3.1.0:
+ resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dependencies:
+ clean-stack: 2.2.0
+ indent-string: 4.0.0
+ dev: false
+ optional: true
+
+ /ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+ dev: true
+
+ /ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ /ansi-regex@6.0.1:
+ resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /ansi-styles@3.2.1:
+ resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
+ engines: {node: '>=4'}
+ dependencies:
+ color-convert: 1.9.3
+ dev: false
+
+ /ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+ dependencies:
+ color-convert: 2.0.1
+
+ /ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+ dev: true
+
+ /aproba@2.0.0:
+ resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
+ dev: false
+
+ /are-docs-informative@0.0.2:
+ resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
+ engines: {node: '>=14'}
+ dev: true
+
+ /are-we-there-yet@2.0.0:
+ resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
+ engines: {node: '>=10'}
+ dependencies:
+ delegates: 1.0.0
+ readable-stream: 3.6.2
+ dev: false
+
+ /are-we-there-yet@3.0.1:
+ resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ requiresBuild: true
+ dependencies:
+ delegates: 1.0.0
+ readable-stream: 3.6.2
+ dev: false
+ optional: true
+
+ /argparse@1.0.10:
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+ requiresBuild: true
+ dependencies:
+ sprintf-js: 1.0.3
+ dev: false
+
+ /argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ dev: true
+
+ /array-back@3.1.0:
+ resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /array-back@4.0.2:
+ resolution: {integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /array-flatten@1.1.1:
+ resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
+ dev: false
+
+ /array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /await-lock@2.2.2:
+ resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
+ dev: false
+
+ /balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ /base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ dev: false
+
+ /base64id@2.0.0:
+ resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
+ engines: {node: ^4.5.0 || >= 5.9}
+ dev: false
+
+ /bcryptjs@2.4.3:
+ resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
+ dev: false
+
+ /binary-extensions@2.2.0:
+ resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /bl@4.1.0:
+ resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+ dependencies:
+ buffer: 5.7.1
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ dev: false
+
+ /body-parser@1.20.1:
+ resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+ 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.11.0
+ raw-body: 2.5.1
+ type-is: 1.6.18
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+ dev: true
+
+ /bootstrap-vue-next@0.14.10(vue@3.3.8):
+ resolution: {integrity: sha512-0czOUVVi8nSA3M9dm48jJnMUB6vP7sYrMxfbFjINE/MEwVbsi4b1Gq41k6gFJcwE54fj7pYQW9q8LP5fqcq0Lw==}
+ peerDependencies:
+ vue: ^3.3.4
+ dependencies:
+ '@floating-ui/vue': 1.0.2(vue@3.3.8)
+ '@vueuse/core': 10.5.0(vue@3.3.8)
+ vue: 3.3.8(typescript@5.2.2)
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ dev: true
+
+ /bootstrap@5.3.2(@popperjs/core@2.11.8):
+ resolution: {integrity: sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==}
+ peerDependencies:
+ '@popperjs/core': ^2.11.8
+ dependencies:
+ '@popperjs/core': 2.11.8
+ dev: true
+
+ /brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ /brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+ dependencies:
+ balanced-match: 1.0.2
+
+ /braces@3.0.2:
+ resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
+ engines: {node: '>=8'}
+ dependencies:
+ fill-range: 7.0.1
+ dev: true
+
+ /buffer-equal-constant-time@1.0.1:
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
+ dev: false
+
+ /buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ dev: false
+
+ /buffer@5.7.1:
+ resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+ dev: false
+
+ /builtin-modules@3.3.0:
+ resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
+ /cacache@15.3.0:
+ resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==}
+ engines: {node: '>= 10'}
+ requiresBuild: true
+ dependencies:
+ '@npmcli/fs': 1.1.1
+ '@npmcli/move-file': 1.1.2
+ chownr: 2.0.0
+ fs-minipass: 2.1.0
+ glob: 7.2.3
+ infer-owner: 1.0.4
+ lru-cache: 6.0.0
+ minipass: 3.3.6
+ minipass-collect: 1.0.2
+ minipass-flush: 1.0.5
+ minipass-pipeline: 1.2.4
+ mkdirp: 1.0.4
+ p-map: 4.0.0
+ promise-inflight: 1.0.1
+ rimraf: 3.0.2
+ ssri: 8.0.1
+ tar: 6.2.0
+ unique-filename: 1.1.1
+ transitivePeerDependencies:
+ - bluebird
+ dev: false
+ optional: true
+
+ /call-bind@1.0.5:
+ resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==}
+ dependencies:
+ function-bind: 1.1.2
+ get-intrinsic: 1.2.2
+ set-function-length: 1.1.1
+ dev: false
+
+ /callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
+ /chalk@2.4.2:
+ resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ ansi-styles: 3.2.1
+ escape-string-regexp: 1.0.5
+ supports-color: 5.5.0
+ dev: false
+
+ /chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ /check-password-strength@2.0.7:
+ resolution: {integrity: sha512-VyklBkB6dOKnCIh63zdVr7QKVMN9/npwUqNAXxWrz8HabVZH/n/d+lyNm1O/vbXFJlT/Hytb5ouYKYGkoeZirQ==}
+ dev: false
+
+ /chokidar@3.5.3:
+ resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
+ engines: {node: '>= 8.10.0'}
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.2
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
+ /chownr@1.1.4:
+ resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
+ dev: false
+
+ /chownr@2.0.0:
+ resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /clean-stack@2.2.0:
+ resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /cliui@6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
+ dev: true
+
+ /color-convert@1.9.3:
+ resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
+ dependencies:
+ color-name: 1.1.3
+ dev: false
+
+ /color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+ dependencies:
+ color-name: 1.1.4
+
+ /color-name@1.1.3:
+ resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
+ dev: false
+
+ /color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ /color-support@1.1.3:
+ resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
+ hasBin: true
+ dev: false
+
+ /colorette@2.0.19:
+ resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
+ dev: false
+
+ /command-exists@1.2.9:
+ resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==}
+ dev: false
+
+ /command-line-args@5.2.1:
+ resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==}
+ engines: {node: '>=4.0.0'}
+ dependencies:
+ array-back: 3.1.0
+ find-replace: 3.0.0
+ lodash.camelcase: 4.3.0
+ typical: 4.0.0
+ dev: false
+
+ /command-line-usage@6.1.3:
+ resolution: {integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==}
+ engines: {node: '>=8.0.0'}
+ dependencies:
+ array-back: 4.0.2
+ chalk: 2.4.2
+ table-layout: 1.0.2
+ typical: 5.2.0
+ dev: false
+
+ /commander@10.0.1:
+ resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
+ engines: {node: '>=14'}
+ dev: false
+
+ /commander@9.5.0:
+ resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
+ engines: {node: ^12.20.0 || >=14}
+ dev: false
+
+ /comment-parser@1.4.0:
+ resolution: {integrity: sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==}
+ engines: {node: '>= 12.0.0'}
+ dev: true
+
+ /compare-versions@6.1.0:
+ resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==}
+ dev: false
+
+ /composerize@1.4.1:
+ resolution: {integrity: sha512-63bKZMgOE8Fd6jBSmkZFdOtKM5A46TafrWgNHVXIBfTu/YBBZw91xkjALG3JiuOatX4tMK04uM37O4HGypnkfQ==}
+ hasBin: true
+ dependencies:
+ core-js: 2.6.12
+ deepmerge: 2.2.1
+ invariant: 2.2.4
+ yamljs: 0.3.0
+ yargs-parser: 13.1.2
+ dev: false
+
+ /concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ /console-control-strings@1.1.0:
+ resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
+ dev: false
+
+ /content-disposition@0.5.4:
+ resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
+ /content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /cookie-signature@1.0.6:
+ resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
+ dev: false
+
+ /cookie@0.4.2:
+ resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /cookie@0.5.0:
+ resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /core-js@2.6.12:
+ resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==}
+ deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
+ requiresBuild: true
+ dev: false
+
+ /cors@2.8.5:
+ resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
+ engines: {node: '>= 0.10'}
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+ dev: false
+
+ /croner@7.0.4:
+ resolution: {integrity: sha512-P8Zd88km8oQ0xH8Es0u75GtOnFyCNopuAhlFv5kAnbcTuXd0xNvRTgnxnJEs63FicCOsHTL7rpu4BHzY3cMq4w==}
+ engines: {node: '>=6.0'}
+ dev: false
+
+ /cross-env@7.0.3:
+ resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
+ engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
+ hasBin: true
+ dependencies:
+ cross-spawn: 7.0.3
+ dev: true
+
+ /cross-spawn@7.0.3:
+ resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
+ engines: {node: '>= 8'}
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ /cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: true
+
+ /csstype@3.1.2:
+ resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
+ dev: true
+
+ /dayjs@1.11.10:
+ resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
+ dev: false
+
+ /debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.0.0
+ dev: false
+
+ /debug@4.3.4:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+
+ /decamelize@1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+
+ /decompress-response@6.0.0:
+ resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ mimic-response: 3.1.0
+ dev: false
+
+ /deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+ dev: false
+
+ /deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ dev: true
+
+ /deepmerge@2.2.1:
+ resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /define-data-property@1.1.1:
+ resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ get-intrinsic: 1.2.2
+ gopd: 1.0.1
+ has-property-descriptors: 1.0.1
+ dev: false
+
+ /delegates@1.0.0:
+ resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
+ dev: false
+
+ /denque@2.1.0:
+ resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
+ engines: {node: '>=0.10'}
+ dev: false
+
+ /depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
+ /destroy@1.2.0:
+ resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+ dev: false
+
+ /detect-libc@2.0.2:
+ resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /dijkstrajs@1.0.3:
+ resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
+ dev: true
+
+ /dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+ dependencies:
+ path-type: 4.0.0
+ dev: true
+
+ /doctrine@3.0.0:
+ resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ esutils: 2.0.3
+ dev: true
+
+ /eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ dev: false
+
+ /ecdsa-sig-formatter@1.0.11:
+ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
+ /ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+ dev: false
+
+ /emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ requiresBuild: true
+
+ /emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ dev: false
+
+ /encode-utf8@1.0.3:
+ resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==}
+ dev: true
+
+ /encodeurl@1.0.2:
+ resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
+ /encoding@0.1.13:
+ resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
+ requiresBuild: true
+ dependencies:
+ iconv-lite: 0.6.3
+ dev: false
+ optional: true
+
+ /end-of-stream@1.4.4:
+ resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
+ dependencies:
+ once: 1.4.0
+ dev: false
+
+ /engine.io-client@6.5.2:
+ resolution: {integrity: sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==}
+ dependencies:
+ '@socket.io/component-emitter': 3.1.0
+ debug: 4.3.4
+ engine.io-parser: 5.2.1
+ ws: 8.11.0
+ xmlhttprequest-ssl: 2.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /engine.io-parser@5.2.1:
+ resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==}
+ engines: {node: '>=10.0.0'}
+ dev: false
+
+ /engine.io@6.5.3:
+ resolution: {integrity: sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==}
+ engines: {node: '>=10.2.0'}
+ dependencies:
+ '@types/cookie': 0.4.1
+ '@types/cors': 2.8.16
+ '@types/node': 20.8.10
+ accepts: 1.3.8
+ base64id: 2.0.0
+ cookie: 0.4.2
+ cors: 2.8.5
+ debug: 4.3.4
+ engine.io-parser: 5.2.1
+ ws: 8.11.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /err-code@2.0.3:
+ resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /esbuild@0.18.20:
+ resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/android-arm': 0.18.20
+ '@esbuild/android-arm64': 0.18.20
+ '@esbuild/android-x64': 0.18.20
+ '@esbuild/darwin-arm64': 0.18.20
+ '@esbuild/darwin-x64': 0.18.20
+ '@esbuild/freebsd-arm64': 0.18.20
+ '@esbuild/freebsd-x64': 0.18.20
+ '@esbuild/linux-arm': 0.18.20
+ '@esbuild/linux-arm64': 0.18.20
+ '@esbuild/linux-ia32': 0.18.20
+ '@esbuild/linux-loong64': 0.18.20
+ '@esbuild/linux-mips64el': 0.18.20
+ '@esbuild/linux-ppc64': 0.18.20
+ '@esbuild/linux-riscv64': 0.18.20
+ '@esbuild/linux-s390x': 0.18.20
+ '@esbuild/linux-x64': 0.18.20
+ '@esbuild/netbsd-x64': 0.18.20
+ '@esbuild/openbsd-x64': 0.18.20
+ '@esbuild/sunos-x64': 0.18.20
+ '@esbuild/win32-arm64': 0.18.20
+ '@esbuild/win32-ia32': 0.18.20
+ '@esbuild/win32-x64': 0.18.20
+
+ /escalade@3.1.1:
+ resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+ dev: false
+
+ /escape-string-regexp@1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+ dev: false
+
+ /escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /eslint-plugin-jsdoc@46.8.2(eslint@8.50.0):
+ resolution: {integrity: sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ dependencies:
+ '@es-joy/jsdoccomment': 0.40.1
+ are-docs-informative: 0.0.2
+ comment-parser: 1.4.0
+ debug: 4.3.4
+ escape-string-regexp: 4.0.0
+ eslint: 8.50.0
+ esquery: 1.5.0
+ is-builtin-module: 3.2.1
+ semver: 7.5.4
+ spdx-expression-parse: 3.0.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /eslint-plugin-vue@9.17.0(eslint@8.50.0):
+ resolution: {integrity: sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==}
+ engines: {node: ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0)
+ eslint: 8.50.0
+ natural-compare: 1.4.0
+ nth-check: 2.1.1
+ postcss-selector-parser: 6.0.13
+ semver: 7.5.4
+ vue-eslint-parser: 9.3.2(eslint@8.50.0)
+ xml-name-validator: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /eslint-scope@7.2.2:
+ resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+ dev: true
+
+ /eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: true
+
+ /eslint@8.50.0:
+ resolution: {integrity: sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ hasBin: true
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0)
+ '@eslint-community/regexpp': 4.10.0
+ '@eslint/eslintrc': 2.1.3
+ '@eslint/js': 8.50.0
+ '@humanwhocodes/config-array': 0.11.13
+ '@humanwhocodes/module-importer': 1.0.1
+ '@nodelib/fs.walk': 1.2.8
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.3
+ debug: 4.3.4
+ doctrine: 3.0.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 7.2.2
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
+ esquery: 1.5.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 6.0.1
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ globals: 13.23.0
+ graphemer: 1.4.0
+ ignore: 5.2.4
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ is-path-inside: 3.0.3
+ js-yaml: 4.1.0
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.3
+ strip-ansi: 6.0.1
+ text-table: 0.2.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /esm@3.2.25:
+ resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /espree@9.6.1:
+ resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ acorn: 8.11.2
+ acorn-jsx: 5.3.2(acorn@8.11.2)
+ eslint-visitor-keys: 3.4.3
+ dev: true
+
+ /esquery@1.5.0:
+ resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
+ engines: {node: '>=0.10'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: true
+
+ /esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: true
+
+ /estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+ dev: true
+
+ /estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+ dev: true
+
+ /esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /expand-template@2.0.3:
+ resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /express-static-gzip@2.1.7:
+ resolution: {integrity: sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==}
+ dependencies:
+ serve-static: 1.15.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /express@4.18.2:
+ resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
+ engines: {node: '>= 0.10.0'}
+ dependencies:
+ accepts: 1.3.8
+ array-flatten: 1.1.1
+ body-parser: 1.20.1
+ content-disposition: 0.5.4
+ content-type: 1.0.5
+ cookie: 0.5.0
+ cookie-signature: 1.0.6
+ debug: 2.6.9
+ depd: 2.0.0
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 1.2.0
+ fresh: 0.5.2
+ http-errors: 2.0.0
+ merge-descriptors: 1.0.1
+ methods: 1.1.2
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ path-to-regexp: 0.1.7
+ proxy-addr: 2.0.7
+ qs: 6.11.0
+ range-parser: 1.2.1
+ safe-buffer: 5.2.1
+ send: 0.18.0
+ serve-static: 1.15.0
+ setprototypeof: 1.2.0
+ statuses: 2.0.1
+ type-is: 1.6.18
+ utils-merge: 1.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ dev: true
+
+ /fast-glob@3.3.2:
+ resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+ engines: {node: '>=8.6.0'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.5
+ dev: true
+
+ /fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+ dev: true
+
+ /fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ dev: true
+
+ /fastq@1.15.0:
+ resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
+ dependencies:
+ reusify: 1.0.4
+ dev: true
+
+ /file-entry-cache@6.0.1:
+ resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+ dependencies:
+ flat-cache: 3.1.1
+ dev: true
+
+ /fill-range@7.0.1:
+ resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: true
+
+ /finalhandler@1.2.0:
+ resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ debug: 2.6.9
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.1
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /find-replace@3.0.0:
+ resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==}
+ engines: {node: '>=4.0.0'}
+ dependencies:
+ array-back: 3.1.0
+ dev: false
+
+ /find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+ dev: true
+
+ /find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+ dev: true
+
+ /flat-cache@3.1.1:
+ resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==}
+ engines: {node: '>=12.0.0'}
+ dependencies:
+ flatted: 3.2.9
+ keyv: 4.5.4
+ rimraf: 3.0.2
+ dev: true
+
+ /flatted@3.2.9:
+ resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
+ dev: true
+
+ /foreground-child@3.1.1:
+ resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
+ engines: {node: '>=14'}
+ dependencies:
+ cross-spawn: 7.0.3
+ signal-exit: 4.1.0
+ dev: false
+
+ /forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /fresh@0.5.2:
+ resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /fs-constants@1.0.0:
+ resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+ dev: false
+
+ /fs-extra@10.1.0:
+ resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.1.0
+ universalify: 2.0.1
+ dev: true
+
+ /fs-minipass@2.1.0:
+ resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+
+ /fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ requiresBuild: true
+
+ /fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ optional: true
+
+ /function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ /gauge@3.0.2:
+ resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ aproba: 2.0.0
+ color-support: 1.1.3
+ console-control-strings: 1.1.0
+ has-unicode: 2.0.1
+ object-assign: 4.1.1
+ signal-exit: 3.0.7
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wide-align: 1.1.5
+ dev: false
+
+ /gauge@4.0.4:
+ resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ requiresBuild: true
+ dependencies:
+ aproba: 2.0.0
+ color-support: 1.1.3
+ console-control-strings: 1.1.0
+ has-unicode: 2.0.1
+ signal-exit: 3.0.7
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wide-align: 1.1.5
+ dev: false
+ optional: true
+
+ /generate-function@2.3.1:
+ resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
+ dependencies:
+ is-property: 1.0.2
+ dev: false
+
+ /get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+ dev: true
+
+ /get-intrinsic@1.2.2:
+ resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==}
+ dependencies:
+ function-bind: 1.1.2
+ has-proto: 1.0.1
+ has-symbols: 1.0.3
+ hasown: 2.0.0
+ dev: false
+
+ /get-package-type@0.1.0:
+ resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
+ engines: {node: '>=8.0.0'}
+ dev: false
+
+ /get-tsconfig@4.7.2:
+ resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==}
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+ dev: false
+
+ /getopts@2.3.0:
+ resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==}
+ dev: false
+
+ /github-from-package@0.0.0:
+ resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
+ dev: false
+
+ /glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob@10.3.10:
+ resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+ dependencies:
+ foreground-child: 3.1.1
+ jackspeak: 2.3.6
+ minimatch: 9.0.3
+ minipass: 7.0.4
+ path-scurry: 1.10.1
+ dev: false
+
+ /glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ requiresBuild: true
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
+ /globals@13.23.0:
+ resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==}
+ engines: {node: '>=8'}
+ dependencies:
+ type-fest: 0.20.2
+ dev: true
+
+ /globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.2
+ ignore: 5.2.4
+ merge2: 1.4.1
+ slash: 3.0.0
+ dev: true
+
+ /gopd@1.0.1:
+ resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
+ dependencies:
+ get-intrinsic: 1.2.2
+ dev: false
+
+ /graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ /graphemer@1.4.0:
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+ dev: true
+
+ /has-flag@3.0.0:
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+ engines: {node: '>=4'}
+ dev: false
+
+ /has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ /has-property-descriptors@1.0.1:
+ resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==}
+ dependencies:
+ get-intrinsic: 1.2.2
+ dev: false
+
+ /has-proto@1.0.1:
+ resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
+ engines: {node: '>= 0.4'}
+ dev: false
+
+ /has-symbols@1.0.3:
+ resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
+ engines: {node: '>= 0.4'}
+ dev: false
+
+ /has-unicode@2.0.1:
+ resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
+ dev: false
+
+ /hasown@2.0.0:
+ resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ function-bind: 1.1.2
+
+ /http-cache-semantics@4.1.1:
+ resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /http-errors@2.0.0:
+ resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.1
+ toidentifier: 1.0.1
+ dev: false
+
+ /http-graceful-shutdown@3.1.13:
+ resolution: {integrity: sha512-Ci5LRufQ8AtrQ1U26AevS8QoMXDOhnAHCJI3eZu1com7mZGHxREmw3dNj85ftpQokQCvak8nI2pnFS8zyM1M+Q==}
+ engines: {node: '>=4.0.0'}
+ dependencies:
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /http-proxy-agent@4.0.1:
+ resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
+ engines: {node: '>= 6'}
+ requiresBuild: true
+ dependencies:
+ '@tootallnate/once': 1.1.2
+ agent-base: 6.0.2
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+ optional: true
+
+ /https-proxy-agent@5.0.1:
+ resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+ engines: {node: '>= 6'}
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /humanize-ms@1.2.1:
+ resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+ requiresBuild: true
+ dependencies:
+ ms: 2.1.3
+ dev: false
+ optional: true
+
+ /iconv-lite@0.4.24:
+ resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ safer-buffer: 2.1.2
+ dev: false
+
+ /iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+ requiresBuild: true
+ dependencies:
+ safer-buffer: 2.1.2
+ dev: false
+
+ /ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+ dev: false
+
+ /ignore@5.2.4:
+ resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
+ engines: {node: '>= 4'}
+ dev: true
+
+ /immutable@4.3.4:
+ resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
+ dev: true
+
+ /import-fresh@3.3.0:
+ resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+ engines: {node: '>=6'}
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+ dev: true
+
+ /imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ /indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /infer-owner@1.0.4:
+ resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ requiresBuild: true
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ /inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ /ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ requiresBuild: true
+ dev: false
+
+ /interpret@2.2.0:
+ resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==}
+ engines: {node: '>= 0.10'}
+ dev: false
+
+ /invariant@2.2.4:
+ resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: false
+
+ /ip@2.0.0:
+ resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+ dev: false
+
+ /is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+ dependencies:
+ binary-extensions: 2.2.0
+ dev: true
+
+ /is-builtin-module@3.2.1:
+ resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
+ engines: {node: '>=6'}
+ dependencies:
+ builtin-modules: 3.3.0
+ dev: true
+
+ /is-core-module@2.13.1:
+ resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
+ dependencies:
+ hasown: 2.0.0
+
+ /is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+
+ /is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+
+ /is-lambda@1.0.1:
+ resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+ dev: true
+
+ /is-path-inside@3.0.3:
+ resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /is-property@1.0.2:
+ resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
+ dev: false
+
+ /isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ requiresBuild: true
+
+ /jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+ dev: false
+
+ /js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ dev: false
+
+ /js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+ dependencies:
+ argparse: 2.0.1
+ dev: true
+
+ /jsdoc-type-pratt-parser@4.0.0:
+ resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==}
+ engines: {node: '>=12.0.0'}
+ dev: true
+
+ /json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ dev: true
+
+ /json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ dev: true
+
+ /json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+ dev: true
+
+ /jsonfile@6.1.0:
+ resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+ dev: true
+
+ /jsonwebtoken@9.0.2:
+ resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
+ engines: {node: '>=12', npm: '>=6'}
+ dependencies:
+ jws: 3.2.2
+ lodash.includes: 4.3.0
+ lodash.isboolean: 3.0.3
+ lodash.isinteger: 4.0.4
+ lodash.isnumber: 3.0.3
+ lodash.isplainobject: 4.0.6
+ lodash.isstring: 4.0.1
+ lodash.once: 4.1.1
+ ms: 2.1.3
+ semver: 7.5.4
+ dev: false
+
+ /just-performance-es6-compat@4.3.2:
+ resolution: {integrity: sha512-bNPuqRaV8PrqYVivdN8FsKi0rmXaQ0Y63oasFUQFJooJPH0r26LdfBAfoodhUQI6I6cek9yNo+tnGiF0R3pIHw==}
+ dev: false
+
+ /jwa@1.4.1:
+ resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
+ dependencies:
+ buffer-equal-constant-time: 1.0.1
+ ecdsa-sig-formatter: 1.0.11
+ safe-buffer: 5.2.1
+ dev: false
+
+ /jws@3.2.2:
+ resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
+ dependencies:
+ jwa: 1.4.1
+ safe-buffer: 5.2.1
+ dev: false
+
+ /jwt-decode@3.1.2:
+ resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
+ dev: false
+
+ /keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ dependencies:
+ json-buffer: 3.0.1
+ dev: true
+
+ /knex@2.4.2(mysql2@3.6.3):
+ resolution: {integrity: sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg==}
+ engines: {node: '>=12'}
+ hasBin: true
+ peerDependencies:
+ better-sqlite3: '*'
+ mysql: '*'
+ mysql2: '*'
+ pg: '*'
+ pg-native: '*'
+ sqlite3: '*'
+ tedious: '*'
+ peerDependenciesMeta:
+ better-sqlite3:
+ optional: true
+ mysql:
+ optional: true
+ mysql2:
+ optional: true
+ pg:
+ optional: true
+ pg-native:
+ optional: true
+ sqlite3:
+ optional: true
+ tedious:
+ optional: true
+ dependencies:
+ colorette: 2.0.19
+ commander: 9.5.0
+ debug: 4.3.4
+ escalade: 3.1.1
+ esm: 3.2.25
+ get-package-type: 0.1.0
+ getopts: 2.3.0
+ interpret: 2.2.0
+ lodash: 4.17.21
+ mysql2: 3.6.3
+ pg-connection-string: 2.5.0
+ rechoir: 0.8.0
+ resolve-from: 5.0.0
+ tarn: 3.0.2
+ tildify: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /knex@2.5.1(mysql2@3.6.3):
+ resolution: {integrity: sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==}
+ engines: {node: '>=12'}
+ hasBin: true
+ peerDependencies:
+ better-sqlite3: '*'
+ mysql: '*'
+ mysql2: '*'
+ pg: '*'
+ pg-native: '*'
+ sqlite3: '*'
+ tedious: '*'
+ peerDependenciesMeta:
+ better-sqlite3:
+ optional: true
+ mysql:
+ optional: true
+ mysql2:
+ optional: true
+ pg:
+ optional: true
+ pg-native:
+ optional: true
+ sqlite3:
+ optional: true
+ tedious:
+ optional: true
+ dependencies:
+ colorette: 2.0.19
+ commander: 10.0.1
+ debug: 4.3.4
+ escalade: 3.1.1
+ esm: 3.2.25
+ get-package-type: 0.1.0
+ getopts: 2.3.0
+ interpret: 2.2.0
+ lodash: 4.17.21
+ mysql2: 3.6.3
+ pg-connection-string: 2.6.1
+ rechoir: 0.8.0
+ resolve-from: 5.0.0
+ tarn: 3.0.2
+ tildify: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ dev: true
+
+ /limiter-es6-compat@2.1.2:
+ resolution: {integrity: sha512-rAi6LCOubXfnojSePboCylTSJWi9m0hlnZJ6AYsBv0clRFr4PJAZd8DtDVhXSN5Ed5nXa6pULHu9sJxoxpWWww==}
+ dependencies:
+ just-performance-es6-compat: 4.3.2
+ dev: false
+
+ /local-pkg@0.4.3:
+ resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
+ engines: {node: '>=14'}
+ dev: true
+
+ /locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-locate: 4.1.0
+ dev: true
+
+ /locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+ dependencies:
+ p-locate: 5.0.0
+ dev: true
+
+ /lodash.camelcase@4.3.0:
+ resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
+ dev: false
+
+ /lodash.includes@4.3.0:
+ resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
+ dev: false
+
+ /lodash.isboolean@3.0.3:
+ resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
+ dev: false
+
+ /lodash.isinteger@4.0.4:
+ resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
+ dev: false
+
+ /lodash.isnumber@3.0.3:
+ resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
+ dev: false
+
+ /lodash.isplainobject@4.0.6:
+ resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
+ dev: false
+
+ /lodash.isstring@4.0.1:
+ resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
+ dev: false
+
+ /lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ dev: true
+
+ /lodash.once@4.1.1:
+ resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
+ dev: false
+
+ /lodash@4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
+ /long@5.2.3:
+ resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
+ dev: false
+
+ /loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+ dependencies:
+ js-tokens: 4.0.0
+ dev: false
+
+ /lru-cache@10.0.1:
+ resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==}
+ engines: {node: 14 || >=16.14}
+ dev: false
+
+ /lru-cache@6.0.0:
+ resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+ engines: {node: '>=10'}
+ dependencies:
+ yallist: 4.0.0
+
+ /lru-cache@7.18.3:
+ resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /lru-cache@8.0.5:
+ resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
+ engines: {node: '>=16.14'}
+ dev: false
+
+ /magic-string@0.30.5:
+ resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
+ engines: {node: '>=12'}
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.4.15
+ dev: true
+
+ /make-dir@3.1.0:
+ resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
+ engines: {node: '>=8'}
+ dependencies:
+ semver: 6.3.1
+ dev: false
+
+ /make-fetch-happen@9.1.0:
+ resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==}
+ engines: {node: '>= 10'}
+ requiresBuild: true
+ dependencies:
+ agentkeepalive: 4.5.0
+ cacache: 15.3.0
+ http-cache-semantics: 4.1.1
+ http-proxy-agent: 4.0.1
+ https-proxy-agent: 5.0.1
+ is-lambda: 1.0.1
+ lru-cache: 6.0.0
+ minipass: 3.3.6
+ minipass-collect: 1.0.2
+ minipass-fetch: 1.4.1
+ minipass-flush: 1.0.5
+ minipass-pipeline: 1.2.4
+ negotiator: 0.6.3
+ promise-retry: 2.0.1
+ socks-proxy-agent: 6.2.1
+ ssri: 8.0.1
+ transitivePeerDependencies:
+ - bluebird
+ - supports-color
+ dev: false
+ optional: true
+
+ /media-typer@0.3.0:
+ resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /merge-descriptors@1.0.1:
+ resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
+ dev: false
+
+ /merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /methods@1.1.2:
+ resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /micromatch@4.0.5:
+ resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
+ engines: {node: '>=8.6'}
+ dependencies:
+ braces: 3.0.2
+ picomatch: 2.3.1
+ dev: true
+
+ /mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-db: 1.52.0
+ dev: false
+
+ /mime@1.6.0:
+ resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: false
+
+ /mimic-response@3.1.0:
+ resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ dependencies:
+ brace-expansion: 1.1.11
+
+ /minimatch@9.0.3:
+ resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: false
+
+ /minipass-collect@1.0.2:
+ resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
+ engines: {node: '>= 8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /minipass-fetch@1.4.1:
+ resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ minipass-sized: 1.0.3
+ minizlib: 2.1.2
+ optionalDependencies:
+ encoding: 0.1.13
+ dev: false
+ optional: true
+
+ /minipass-flush@1.0.5:
+ resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
+ engines: {node: '>= 8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /minipass-pipeline@1.2.4:
+ resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /minipass-sized@1.0.3:
+ resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
+ engines: {node: '>=8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /minipass@3.3.6:
+ resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
+ engines: {node: '>=8'}
+ dependencies:
+ yallist: 4.0.0
+ dev: false
+
+ /minipass@5.0.0:
+ resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /minipass@7.0.4:
+ resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dev: false
+
+ /minizlib@2.1.2:
+ resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ minipass: 3.3.6
+ yallist: 4.0.0
+ dev: false
+
+ /mkdirp-classic@0.5.3:
+ resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+ dev: false
+
+ /mkdirp@1.0.4:
+ resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dev: false
+
+ /ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+ dev: false
+
+ /ms@2.1.2:
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+
+ /ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ dev: false
+
+ /mysql2@3.6.3:
+ resolution: {integrity: sha512-qYd/1CDuW1KYZjD4tzg2O8YS3X/UWuGH8ZMHyMeggMTXL3yOdMisbwZ5SNkHzDGlZXKYLAvV8tMrEH+NUMz3fw==}
+ engines: {node: '>= 8.0'}
+ dependencies:
+ denque: 2.1.0
+ generate-function: 2.3.1
+ iconv-lite: 0.6.3
+ long: 5.2.3
+ lru-cache: 8.0.5
+ named-placeholders: 1.1.3
+ seq-queue: 0.0.5
+ sqlstring: 2.3.3
+ dev: false
+
+ /named-placeholders@1.1.3:
+ resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
+ engines: {node: '>=12.0.0'}
+ dependencies:
+ lru-cache: 7.18.3
+ dev: false
+
+ /nan@2.18.0:
+ resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==}
+ dev: false
+
+ /nanoid@3.3.7:
+ resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: true
+
+ /napi-build-utils@1.0.2:
+ resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
+ dev: false
+
+ /natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ dev: true
+
+ /negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+ requiresBuild: true
+ dev: false
+
+ /node-abi@3.51.0:
+ resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==}
+ engines: {node: '>=10'}
+ dependencies:
+ semver: 7.5.4
+ dev: false
+
+ /node-addon-api@4.3.0:
+ resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
+ dev: false
+
+ /node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+ dependencies:
+ whatwg-url: 5.0.0
+ dev: false
+
+ /node-gyp@8.4.1:
+ resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==}
+ engines: {node: '>= 10.12.0'}
+ hasBin: true
+ requiresBuild: true
+ dependencies:
+ env-paths: 2.2.1
+ glob: 7.2.3
+ graceful-fs: 4.2.11
+ make-fetch-happen: 9.1.0
+ nopt: 5.0.0
+ npmlog: 6.0.2
+ rimraf: 3.0.2
+ semver: 7.5.4
+ tar: 6.2.0
+ which: 2.0.2
+ transitivePeerDependencies:
+ - bluebird
+ - supports-color
+ dev: false
+ optional: true
+
+ /nopt@5.0.0:
+ resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
+ engines: {node: '>=6'}
+ hasBin: true
+ dependencies:
+ abbrev: 1.1.1
+ dev: false
+
+ /normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /npmlog@5.0.1:
+ resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
+ dependencies:
+ are-we-there-yet: 2.0.0
+ console-control-strings: 1.1.0
+ gauge: 3.0.2
+ set-blocking: 2.0.0
+ dev: false
+
+ /npmlog@6.0.2:
+ resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ requiresBuild: true
+ dependencies:
+ are-we-there-yet: 3.0.1
+ console-control-strings: 1.1.0
+ gauge: 4.0.4
+ set-blocking: 2.0.0
+ dev: false
+ optional: true
+
+ /nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+ dependencies:
+ boolbase: 1.0.0
+ dev: true
+
+ /object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /object-inspect@1.13.1:
+ resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
+ dev: false
+
+ /on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ ee-first: 1.1.1
+ dev: false
+
+ /once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ requiresBuild: true
+ dependencies:
+ wrappy: 1.0.2
+
+ /optionator@0.9.3:
+ resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ '@aashutoshrathi/word-wrap': 1.2.6
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ dev: true
+
+ /p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+ dependencies:
+ p-try: 2.2.0
+ dev: true
+
+ /p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ yocto-queue: 0.1.0
+ dev: true
+
+ /p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-limit: 2.3.0
+ dev: true
+
+ /p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+ dependencies:
+ p-limit: 3.1.0
+ dev: true
+
+ /p-map@4.0.0:
+ resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+ dependencies:
+ aggregate-error: 3.1.0
+ dev: false
+ optional: true
+
+ /p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+ dependencies:
+ callsites: 3.1.0
+ dev: true
+
+ /parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
+ /path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+ requiresBuild: true
+
+ /path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ /path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ /path-scurry@1.10.1:
+ resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ lru-cache: 10.0.1
+ minipass: 7.0.4
+ dev: false
+
+ /path-to-regexp@0.1.7:
+ resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
+ dev: false
+
+ /path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /pg-connection-string@2.5.0:
+ resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==}
+ dev: false
+
+ /pg-connection-string@2.6.1:
+ resolution: {integrity: sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==}
+ dev: false
+
+ /picocolors@1.0.0:
+ resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
+ dev: true
+
+ /picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+ dev: true
+
+ /pngjs@5.0.0:
+ resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+ engines: {node: '>=10.13.0'}
+ dev: true
+
+ /postcss-selector-parser@6.0.13:
+ resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+ dev: true
+
+ /postcss@8.4.31:
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.7
+ picocolors: 1.0.0
+ source-map-js: 1.0.2
+ dev: true
+
+ /prebuild-install@7.1.1:
+ resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ detect-libc: 2.0.2
+ expand-template: 2.0.3
+ github-from-package: 0.0.0
+ minimist: 1.2.8
+ mkdirp-classic: 0.5.3
+ napi-build-utils: 1.0.2
+ node-abi: 3.51.0
+ pump: 3.0.0
+ rc: 1.2.8
+ simple-get: 4.0.1
+ tar-fs: 2.1.1
+ tunnel-agent: 0.6.0
+ dev: false
+
+ /prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+ dev: true
+
+ /prismjs@1.29.0:
+ resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /promise-inflight@1.0.1:
+ resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
+ requiresBuild: true
+ peerDependencies:
+ bluebird: '*'
+ peerDependenciesMeta:
+ bluebird:
+ optional: true
+ dev: false
+ optional: true
+
+ /promise-retry@2.0.1:
+ resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+ dependencies:
+ err-code: 2.0.3
+ retry: 0.12.0
+ dev: false
+ optional: true
+
+ /proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+ dev: false
+
+ /pump@3.0.0:
+ resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
+ dependencies:
+ end-of-stream: 1.4.4
+ once: 1.4.0
+ dev: false
+
+ /punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /qrcode@1.5.3:
+ resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+ dependencies:
+ dijkstrajs: 1.0.3
+ encode-utf8: 1.0.3
+ pngjs: 5.0.0
+ yargs: 15.4.1
+ dev: true
+
+ /qs@6.11.0:
+ resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
+ engines: {node: '>=0.6'}
+ dependencies:
+ side-channel: 1.0.4
+ dev: false
+
+ /queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ dev: true
+
+ /range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /raw-body@2.5.1:
+ resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.0
+ iconv-lite: 0.4.24
+ unpipe: 1.0.0
+ dev: false
+
+ /rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.8
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+ dev: false
+
+ /readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+ dev: false
+
+ /readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+ dependencies:
+ picomatch: 2.3.1
+ dev: true
+
+ /rechoir@0.8.0:
+ resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==}
+ engines: {node: '>= 10.13.0'}
+ dependencies:
+ resolve: 1.22.8
+ dev: false
+
+ /redbean-node@0.3.2(mysql2@3.6.3):
+ resolution: {integrity: sha512-39VMxPWPpPicRlU4FSJJnJuUMoxw5/4envFthHtKnLe+3qWTBje3RMrJTFZcQGLruWQ/s2LgeYzdd+d0O+p+uQ==}
+ dependencies:
+ '@types/node': 20.3.3
+ await-lock: 2.2.2
+ dayjs: 1.11.10
+ glob: 10.3.10
+ knex: 2.4.2(mysql2@3.6.3)
+ lodash: 4.17.21
+ transitivePeerDependencies:
+ - better-sqlite3
+ - mysql
+ - mysql2
+ - pg
+ - pg-native
+ - sqlite3
+ - supports-color
+ - tedious
+ dev: false
+
+ /reduce-flatten@2.0.0:
+ resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /require-main-filename@2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+ dev: true
+
+ /resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+ dev: false
+
+ /resolve@1.22.8:
+ resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
+ hasBin: true
+ dependencies:
+ is-core-module: 2.13.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ /retry@0.12.0:
+ resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
+ engines: {node: '>= 4'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /reusify@1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ dev: true
+
+ /rimraf@3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+
+ /rollup@3.29.4:
+ resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
+ engines: {node: '>=14.18.0', npm: '>=8.0.0'}
+ hasBin: true
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
+ /run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ dependencies:
+ queue-microtask: 1.2.3
+ dev: true
+
+ /safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ dev: false
+
+ /safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ dev: false
+
+ /sass@1.68.0:
+ resolution: {integrity: sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+ dependencies:
+ chokidar: 3.5.3
+ immutable: 4.3.4
+ source-map-js: 1.0.2
+ dev: true
+
+ /semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+ dev: false
+
+ /semver@7.5.4:
+ resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ lru-cache: 6.0.0
+
+ /send@0.18.0:
+ resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
+ engines: {node: '>= 0.8.0'}
+ 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
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /seq-queue@0.0.5:
+ resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
+ dev: false
+
+ /serve-static@1.15.0:
+ resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 0.18.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+
+ /set-function-length@1.1.1:
+ resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ define-data-property: 1.1.1
+ get-intrinsic: 1.2.2
+ gopd: 1.0.1
+ has-property-descriptors: 1.0.1
+ dev: false
+
+ /setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+ dev: false
+
+ /shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+ dependencies:
+ shebang-regex: 3.0.0
+
+ /shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ /side-channel@1.0.4:
+ resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
+ dependencies:
+ call-bind: 1.0.5
+ get-intrinsic: 1.2.2
+ object-inspect: 1.13.1
+ dev: false
+
+ /signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ dev: false
+
+ /signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+ dev: false
+
+ /simple-concat@1.0.1:
+ resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
+ dev: false
+
+ /simple-get@4.0.1:
+ resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
+ dependencies:
+ decompress-response: 6.0.0
+ once: 1.4.0
+ simple-concat: 1.0.1
+ dev: false
+
+ /slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /smart-buffer@4.2.0:
+ resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
+ engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /socket.io-adapter@2.5.2:
+ resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==}
+ dependencies:
+ ws: 8.11.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+ dev: false
+
+ /socket.io-client@4.7.2:
+ resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ '@socket.io/component-emitter': 3.1.0
+ debug: 4.3.4
+ engine.io-client: 6.5.2
+ socket.io-parser: 4.2.4
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /socket.io-parser@4.2.4:
+ resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ '@socket.io/component-emitter': 3.1.0
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /socket.io@4.7.2:
+ resolution: {integrity: sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==}
+ engines: {node: '>=10.2.0'}
+ dependencies:
+ accepts: 1.3.8
+ base64id: 2.0.0
+ cors: 2.8.5
+ debug: 4.3.4
+ engine.io: 6.5.3
+ socket.io-adapter: 2.5.2
+ socket.io-parser: 4.2.4
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ dev: false
+
+ /socks-proxy-agent@6.2.1:
+ resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==}
+ engines: {node: '>= 10'}
+ requiresBuild: true
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.3.4
+ socks: 2.7.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+ optional: true
+
+ /socks@2.7.1:
+ resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==}
+ engines: {node: '>= 10.13.0', npm: '>= 3.0.0'}
+ requiresBuild: true
+ dependencies:
+ ip: 2.0.0
+ smart-buffer: 4.2.0
+ dev: false
+ optional: true
+
+ /source-map-js@1.0.2:
+ resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /source-map-support@0.5.21:
+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+ dev: false
+
+ /source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /spdx-exceptions@2.3.0:
+ resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
+ dev: true
+
+ /spdx-expression-parse@3.0.1:
+ resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
+ dependencies:
+ spdx-exceptions: 2.3.0
+ spdx-license-ids: 3.0.16
+ dev: true
+
+ /spdx-license-ids@3.0.16:
+ resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==}
+ dev: true
+
+ /sprintf-js@1.0.3:
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+ requiresBuild: true
+ dev: false
+
+ /sqlstring@2.3.3:
+ resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /ssri@8.0.1:
+ resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
+ engines: {node: '>= 8'}
+ requiresBuild: true
+ dependencies:
+ minipass: 3.3.6
+ dev: false
+ optional: true
+
+ /statuses@2.0.1:
+ resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
+ /string-format@2.0.0:
+ resolution: {integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==}
+ dev: false
+
+ /string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ /string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+ dev: false
+
+ /string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ requiresBuild: true
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
+ /strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-regex: 5.0.1
+
+ /strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-regex: 6.0.1
+ dev: false
+
+ /strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /supports-color@5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+ dependencies:
+ has-flag: 3.0.0
+ dev: false
+
+ /supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+ dependencies:
+ has-flag: 4.0.0
+
+ /supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ /table-layout@1.0.2:
+ resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==}
+ engines: {node: '>=8.0.0'}
+ dependencies:
+ array-back: 4.0.2
+ deep-extend: 0.6.0
+ typical: 5.2.0
+ wordwrapjs: 4.0.1
+ dev: false
+
+ /tar-fs@2.1.1:
+ resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
+ dependencies:
+ chownr: 1.1.4
+ mkdirp-classic: 0.5.3
+ pump: 3.0.0
+ tar-stream: 2.2.0
+ dev: false
+
+ /tar-stream@2.2.0:
+ resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
+ engines: {node: '>=6'}
+ dependencies:
+ bl: 4.1.0
+ end-of-stream: 1.4.4
+ fs-constants: 1.0.0
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ dev: false
+
+ /tar@6.2.0:
+ resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ chownr: 2.0.0
+ fs-minipass: 2.1.0
+ minipass: 5.0.0
+ minizlib: 2.1.2
+ mkdirp: 1.0.4
+ yallist: 4.0.0
+ dev: false
+
+ /tarn@3.0.2:
+ resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
+ engines: {node: '>=8.0.0'}
+ dev: false
+
+ /text-table@0.2.0:
+ resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+ dev: true
+
+ /tildify@2.0.0:
+ resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /timezones-list@3.0.2:
+ resolution: {integrity: sha512-I698hm6Jp/xxkwyTSOr39pZkYKETL8LDJeSIhjxXBfPUAHM5oZNuQ4o9UK3PSkDBOkjATecSOBb3pR1IkIBUsg==}
+ dev: false
+
+ /to-fast-properties@2.0.0:
+ resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ is-number: 7.0.0
+ dev: true
+
+ /toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+ dev: false
+
+ /tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ dev: false
+
+ /ts-api-utils@1.0.3(typescript@5.2.2):
+ resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
+ engines: {node: '>=16.13.0'}
+ peerDependencies:
+ typescript: '>=4.2.0'
+ dependencies:
+ typescript: 5.2.2
+ dev: true
+
+ /ts-command-line-args@2.5.1:
+ resolution: {integrity: sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==}
+ hasBin: true
+ dependencies:
+ chalk: 4.1.2
+ command-line-args: 5.2.1
+ command-line-usage: 6.1.3
+ string-format: 2.0.0
+ dev: false
+
+ /tslib@2.6.2:
+ resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+ requiresBuild: true
+ dev: true
+
+ /tsx@3.14.0:
+ resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==}
+ hasBin: true
+ dependencies:
+ esbuild: 0.18.20
+ get-tsconfig: 4.7.2
+ source-map-support: 0.5.21
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: false
+
+ /tunnel-agent@0.6.0:
+ resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
+ /type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ prelude-ls: 1.2.1
+ dev: true
+
+ /type-fest@0.20.2:
+ resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /type-fest@4.3.3:
+ resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==}
+ engines: {node: '>=16'}
+ dev: false
+
+ /type-is@1.6.18:
+ resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ media-typer: 0.3.0
+ mime-types: 2.1.35
+ dev: false
+
+ /typescript@5.2.2:
+ resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+ dev: true
+
+ /typical@4.0.0:
+ resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /typical@5.2.0:
+ resolution: {integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
+ /unique-filename@1.1.1:
+ resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==}
+ requiresBuild: true
+ dependencies:
+ unique-slug: 2.0.2
+ dev: false
+ optional: true
+
+ /unique-slug@2.0.2:
+ resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==}
+ requiresBuild: true
+ dependencies:
+ imurmurhash: 0.1.4
+ dev: false
+ optional: true
+
+ /universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+ dev: true
+
+ /unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
+ /unplugin-vue-components@0.25.2(vue@3.3.8):
+ resolution: {integrity: sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/parser': ^7.15.8
+ '@nuxt/kit': ^3.2.2
+ vue: 2 || 3
+ peerDependenciesMeta:
+ '@babel/parser':
+ optional: true
+ '@nuxt/kit':
+ optional: true
+ dependencies:
+ '@antfu/utils': 0.7.6
+ '@rollup/pluginutils': 5.0.5
+ chokidar: 3.5.3
+ debug: 4.3.4
+ fast-glob: 3.3.2
+ local-pkg: 0.4.3
+ magic-string: 0.30.5
+ minimatch: 9.0.3
+ resolve: 1.22.8
+ unplugin: 1.5.0
+ vue: 3.3.8(typescript@5.2.2)
+ transitivePeerDependencies:
+ - rollup
+ - supports-color
+ dev: true
+
+ /unplugin@1.5.0:
+ resolution: {integrity: sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==}
+ dependencies:
+ acorn: 8.11.2
+ chokidar: 3.5.3
+ webpack-sources: 3.2.3
+ webpack-virtual-modules: 0.5.0
+ dev: true
+
+ /uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ dependencies:
+ punycode: 2.3.1
+ dev: true
+
+ /util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ /utils-merge@1.0.1:
+ resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
+ engines: {node: '>= 0.4.0'}
+ dev: false
+
+ /vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
+ /vite-plugin-compression@0.5.1(vite@4.5.0):
+ resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
+ peerDependencies:
+ vite: '>=2.0.0'
+ dependencies:
+ chalk: 4.1.2
+ debug: 4.3.4
+ fs-extra: 10.1.0
+ vite: 4.5.0(sass@1.68.0)
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /vite@4.5.0(sass@1.68.0):
+ resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': '>= 14'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ esbuild: 0.18.20
+ postcss: 8.4.31
+ rollup: 3.29.4
+ sass: 1.68.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
+ /vue-demi@0.14.6(vue@3.3.8):
+ resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ peerDependencies:
+ '@vue/composition-api': ^1.0.0-rc.1
+ vue: ^3.0.0-0 || ^2.6.0
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+ dependencies:
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /vue-eslint-parser@9.3.2(eslint@8.50.0):
+ resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==}
+ engines: {node: ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: '>=6.0.0'
+ dependencies:
+ debug: 4.3.4
+ eslint: 8.50.0
+ eslint-scope: 7.2.2
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
+ esquery: 1.5.0
+ lodash: 4.17.21
+ semver: 7.5.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /vue-i18n@9.5.0(vue@3.3.8):
+ resolution: {integrity: sha512-NiI3Ph1qMstNf7uhYh8trQBOBFLxeJgcOxBq51pCcZ28Vs18Y7BDS58r8HGDKCYgXdLUYqPDXdKatIF4bvBVZg==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ vue: ^3.0.0
+ dependencies:
+ '@intlify/core-base': 9.5.0
+ '@intlify/shared': 9.5.0
+ '@vue/devtools-api': 6.5.1
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /vue-prism-editor@2.0.0-alpha.2(vue@3.3.8):
+ resolution: {integrity: sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ vue: ^3.0.0
+ dependencies:
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /vue-qrcode@2.2.0(qrcode@1.5.3)(vue@3.3.8):
+ resolution: {integrity: sha512-pEwy/IznxEY5MXptFLaxbGdeDWIJRgU5VhBcFmg1avDjD2z2jjWAGE5dlDwqagXtUjcgkvFSSQ40boog1maLuw==}
+ peerDependencies:
+ qrcode: ^1.5.0
+ vue: ^2.7.0 || ^3.0.0
+ dependencies:
+ qrcode: 1.5.3
+ tslib: 2.6.2
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /vue-router@4.2.5(vue@3.3.8):
+ resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==}
+ peerDependencies:
+ vue: ^3.2.0
+ dependencies:
+ '@vue/devtools-api': 6.5.1
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /vue-toastification@2.0.0-rc.5(vue@3.3.8):
+ resolution: {integrity: sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==}
+ peerDependencies:
+ vue: ^3.0.2
+ dependencies:
+ vue: 3.3.8(typescript@5.2.2)
+ dev: true
+
+ /vue@3.3.8(typescript@5.2.2):
+ resolution: {integrity: sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ dependencies:
+ '@vue/compiler-dom': 3.3.8
+ '@vue/compiler-sfc': 3.3.8
+ '@vue/runtime-dom': 3.3.8
+ '@vue/server-renderer': 3.3.8(vue@3.3.8)
+ '@vue/shared': 3.3.8
+ typescript: 5.2.2
+ dev: true
+
+ /webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ dev: false
+
+ /webpack-sources@3.2.3:
+ resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
+ engines: {node: '>=10.13.0'}
+ dev: true
+
+ /webpack-virtual-modules@0.5.0:
+ resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
+ dev: true
+
+ /whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+ dev: false
+
+ /which-module@2.0.1:
+ resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+ dev: true
+
+ /which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+
+ /wide-align@1.1.5:
+ resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
+ dependencies:
+ string-width: 4.2.3
+ dev: false
+
+ /wordwrapjs@4.0.1:
+ resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==}
+ engines: {node: '>=8.0.0'}
+ dependencies:
+ reduce-flatten: 2.0.0
+ typical: 5.2.0
+ dev: false
+
+ /wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: true
+
+ /wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: false
+
+ /wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+ dev: false
+
+ /wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ requiresBuild: true
+
+ /ws@8.11.0:
+ resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ^5.0.2
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+ dev: false
+
+ /xml-name-validator@4.0.0:
+ resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /xmlhttprequest-ssl@2.0.0:
+ resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==}
+ engines: {node: '>=0.4.0'}
+ dev: false
+
+ /xterm-addon-web-links@0.9.0(xterm@5.4.0-beta.37):
+ resolution: {integrity: sha512-LIzi4jBbPlrKMZF3ihoyqayWyTXAwGfu4yprz1aK2p71e9UKXN6RRzVONR0L+Zd+Ik5tPVI9bwp9e8fDTQh49Q==}
+ peerDependencies:
+ xterm: ^5.0.0
+ dependencies:
+ xterm: 5.4.0-beta.37
+ dev: true
+
+ /xterm@5.4.0-beta.37:
+ resolution: {integrity: sha512-ys+mXqLFrJc7khmYN/MgBnfLv38NgXfkwkEXsCZKHGqn3h2xUBvTvsrSEWO3NQeDPLj4zMr1RwqTblMK9St3BA==}
+ dev: true
+
+ /y18n@4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+ dev: true
+
+ /yallist@4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+
+ /yaml@2.3.4:
+ resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
+ engines: {node: '>= 14'}
+ dev: false
+
+ /yamljs@0.3.0:
+ resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==}
+ hasBin: true
+ requiresBuild: true
+ dependencies:
+ argparse: 1.0.10
+ glob: 7.2.3
+ dev: false
+
+ /yargs-parser@13.1.2:
+ resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==}
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: false
+
+ /yargs-parser@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: true
+
+ /yargs@15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.3
+ which-module: 2.0.1
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+ dev: true
+
+ /yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+ dev: true
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..6eff920
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "module": "ESNext",
+ "target": "ESNext",
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}