This commit is contained in:
Louis Lam 2023-10-26 13:23:45 +08:00
parent 5f70fa6baf
commit 7d1da2ad99
34 changed files with 1650 additions and 510 deletions

17
.dockerignore Normal file
View File

@ -0,0 +1,17 @@
# Should be identical to .gitignore
.env
node_modules
dist
frontend-dist
.idea
data
tmp
/private
# Docker extra
docker
frontend
.editorconfig
.eslintrc.cjs
.gitignore
README.md

View File

@ -92,5 +92,6 @@ module.exports = {
"one-var": [ "error", "never" ],
"max-statements-per-line": [ "error", { "max": 1 }],
"@typescript-eslint/ban-ts-comment": "off",
"prefer-const" : "off",
},
};

4
.gitignore vendored
View File

@ -1,8 +1,10 @@
# dotenv environment variable files
# Should update .dockerignore as well
.env
node_modules
dist
frontend-dist
.idea
data
tmp
/private

View File

@ -1,21 +1,33 @@
<div align="center" width="100%">
<img src="./frontend/public/icon.svg" width="128" alt="" />
</div>
# Dockge
## Features
A fancy, easy-to-use and reactive docker stack (`docker-compose.yml`) manager.
- Easy-to-use
- Fancy UI
- Focus on `docker-compose` stack management
## ⭐ Features
- Focus on `docker-compose.yml` stack management
- Interactive editor for `docker-compose.yml` files
- Easy to expose your service to the internet with https
- Interactive web terminal for containers and any docker commands
- Reactive - Everything is just responsive. Progress and terminal output are in real-time
- Easy-to-use & fancy UI - If you love Uptime Kuma's UI, you will love this too
## Installation
## Motivations
- Want to build my dream web-based container manager
- Want to try next gen runtime like Deno or Bun, but I chose Deno because at this moment, Deno is more stable and Jetbrains IDE support is better.
- Full TypeScript and ES module
- Try DaisyUI + TailwindCSS
- Try ES Module and TypeScript in 2023
- I have been using Portainer for some time, but I am sometimes not satisfied with it. For example, sometimes when I deploy a stack, it keeps spinning the loading icon for a few minutes, and I don't know what's going on.
## Dockge?
If you love this project, please consider giving this project a ⭐.
Naming idea is coming from Twitch emotes. There are many emotes sound like this such as `bedge` and `sadge`.
## More Ideas?
- Container file manager
- App store for yaml templates
- Container stats
- Get app icons
- Switch Docker context
- Zero-config private docker registry

View File

@ -20,27 +20,20 @@ import { R } from "redbean-node";
import { genSecret, isDev } from "./util-common";
import { generatePasswordHash } from "./password-hash";
import { Bean } from "redbean-node/dist/bean";
import { DockgeSocket } from "./util-server";
import { Arguments, Config, DockgeSocket } from "./util-server";
import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler";
import { Terminal } from "./terminal";
export interface Arguments {
sslKey? : string;
sslCert? : string;
sslKeyPassphrase? : string;
port? : number;
hostname? : string;
dataDir? : string;
}
import expressStaticGzip from "express-static-gzip";
import path from "path";
import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler";
import { Stack } from "./stack";
export class DockgeServer {
app : Express;
httpServer : http.Server;
packageJSON : PackageJson;
io : socketIO.Server;
config : Arguments;
indexHTML : string;
terminal : Terminal;
config : Config;
indexHTML : string = "";
/**
* List of express routers
@ -55,6 +48,7 @@ export class DockgeServer {
socketHandlerList : SocketHandler[] = [
new MainSocketHandler(),
new DockerSocketHandler(),
new TerminalSocketHandler(),
];
/**
@ -64,10 +58,20 @@ export class DockgeServer {
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/uptime-kuma/issues");
};
process.addListener("unhandledRejection", unexpectedErrorHandler);
process.addListener("uncaughtException", unexpectedErrorHandler);
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}
@ -75,8 +79,8 @@ export class DockgeServer {
// Log NODE ENV
log.info("server", "NODE_ENV: " + process.env.NODE_ENV);
// Load arguments
const args = this.config = parse<Arguments>({
// Define all possible arguments
let args = parse<Arguments>({
sslKey: {
type: String,
optional: true,
@ -103,21 +107,101 @@ export class DockgeServer {
}
});
// Load from environment variables or default values if args are not set
args.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
args.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined;
args.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined;
args.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001;
args.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
args.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
this.config = args as Config;
log.debug("server", args);
// 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/";
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();
this.terminal = new Terminal(this);
// 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", (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);
}
});
this.io.on("disconnect", () => {
});
}
prepareServer() {
}
/**
@ -157,64 +241,6 @@ export class DockgeServer {
this.needSetup = true;
}
// Create express
this.app = express();
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);
}
try {
this.indexHTML = fs.readFileSync("./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 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
for (const router of this.routerList) {
this.app.use(router.create(this.app, this));
}
let cors = undefined;
if (isDev) {
cors = {
origin: "*",
};
}
// Create Socket.io
this.io = new socketIO.Server(this.httpServer, {
cors,
});
this.io.on("connection", (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);
}
});
// Listen
this.httpServer.listen(5001, this.config.hostname, () => {
if (this.config.hostname) {
@ -349,14 +375,21 @@ export class DockgeServer {
* 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`);
}
if (! fs.existsSync(this.config.dataDir)) {
fs.mkdirSync(this.config.dataDir, { recursive: true });
// Create data/stacks directory
this.stacksDir = path.join(this.config.dataDir, "stacks");
if (!fs.existsSync(this.stacksDir)) {
fs.mkdirSync(this.stacksDir, { recursive: true });
}
log.info("server", `Data Dir: ${this.config.dataDir}`);
}
@ -378,4 +411,19 @@ export class DockgeServer {
await R.store(jwtSecretBean);
return jwtSecretBean;
}
sendStackList(socket : DockgeSocket) {
let room = socket.userID.toString();
let stackList = Stack.getStackList(this);
let list = {};
for (let stack of stackList) {
list[stack.name] = stack.toSimpleJSON();
}
this.io.to(room).emit("stackList", {
ok: true,
stackList: list,
});
}
}

View File

@ -7,7 +7,7 @@ export class MainRouter extends Router {
const router = express.Router();
router.get("/", (req, res) => {
res.send(server.indexHTML);
});
return router;

View File

@ -1,61 +1,84 @@
import { SocketHandler } from "../socket-handler.js";
import { DockgeServer } from "../dockge-server";
import { checkLogin, DockgeSocket } from "../util-server";
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
import { log } from "../log";
const allowedCommandList : string[] = [
"docker",
];
import yaml from "yaml";
import path from "path";
import fs from "fs";
import {
allowedCommandList,
allowedRawKeys,
getComposeTerminalName,
isDev,
PROGRESS_TERMINAL_ROWS
} from "../util-common";
import { Terminal } from "../terminal";
import { Stack } from "../stack";
export class DockerSocketHandler extends SocketHandler {
create(socket : DockgeSocket, server : DockgeServer) {
socket.on("composeUp", async (compose, callback) => {
});
socket.on("terminalInput", async (cmd : unknown, errorCallback) => {
try {
checkLogin(socket);
if (typeof(cmd) !== "string") {
throw new Error("Command must be a string.");
}
// Check if the command is allowed
const cmdParts = cmd.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.");
}
server.terminal.write(cmd);
} catch (e) {
errorCallback({
ok: false,
msg: e.message,
});
}
});
// Setup
socket.on("getTerminalBuffer", async (callback) => {
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);
callback({
ok: true,
buffer: server.terminal.getBuffer(),
});
} 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: false,
msg: e.message,
ok: true,
"msg": "Saved"
});
server.sendStackList(socket);
} 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);
callback({
ok: true,
stack: stack.toJSON(),
});
} 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;
}
}

View File

@ -71,7 +71,7 @@ export class MainSocketHandler extends SocketHandler {
}
log.debug("auth", "afterLogin");
await this.afterLogin(socket, user);
await this.afterLogin(server, socket, user);
log.debug("auth", "afterLogin ok");
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
@ -128,7 +128,7 @@ export class MainSocketHandler extends SocketHandler {
if (user) {
if (user.twofa_status === 0) {
this.afterLogin(socket, user);
this.afterLogin(server, socket, user);
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
@ -151,7 +151,7 @@ export class MainSocketHandler extends SocketHandler {
const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
if (user.twofa_last_token !== data.token && verify) {
this.afterLogin(socket, user);
this.afterLogin(server, socket, user);
await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
data.token,
@ -189,10 +189,15 @@ export class MainSocketHandler extends SocketHandler {
});
}
async afterLogin(socket : DockgeSocket, user : User) {
async afterLogin(server: DockgeServer, socket : DockgeSocket, user : User) {
socket.userID = user.id;
socket.join(user.id + "");
socket.join("terminal");
socket.join(user.id.toString());
try {
server.sendStackList(socket);
} catch (e) {
log.error("server", e);
}
}
async login(username : string, password : string) {

View File

@ -0,0 +1,111 @@
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, isDev } from "../util-common";
import { Terminal } from "../terminal";
export class TerminalSocketHandler extends SocketHandler {
create(socket : DockgeSocket, server : DockgeServer) {
socket.on("terminalInputRaw", async (key : unknown) => {
try {
checkLogin(socket);
if (typeof(key) !== "string") {
throw new Error("Key must be a string.");
}
if (allowedRawKeys.includes(key)) {
server.terminal.write(key);
}
} catch (e) {
}
});
socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback : unknown) => {
try {
checkLogin(socket);
if (typeof(cmd) !== "string") {
throw new Error("Command must be a string.");
}
// Check if the command is allowed
const cmdParts = cmd.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.");
}
server.terminal.write(cmd);
} catch (e) {
if (typeof(errorCallback) === "function") {
errorCallback({
ok: false,
msg: e.message,
});
}
}
});
// Create Terminal
socket.on("terminalCreate", async (terminalName : unknown, callback : unknown) => {
});
// Join Terminal
socket.on("terminalJoin", async (terminalName : unknown, callback : unknown) => {
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) => {
});
// Resize Terminal
socket.on("terminalResize", async (rows : unknown) => {
try {
checkLogin(socket);
if (typeof(rows) !== "number") {
throw new Error("Rows must be a number.");
}
log.debug("console", "Resize terminal to " + rows + " rows.");
server.terminal.resize(rows);
} catch (e) {
}
});
}
}

158
backend/stack.ts Normal file
View File

@ -0,0 +1,158 @@
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 { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "./util-common";
import { Terminal } from "./terminal";
export class Stack {
name: string;
protected _composeYAML?: string;
protected server: DockgeServer;
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,
tags: [],
};
}
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<number> {
const terminalName = getComposeTerminalName(this.name);
log.debug("deployStack", "Terminal name: " + terminalName);
const terminal = new Terminal(this.server, terminalName, "docker-compose", [ "up", "-d" ], this.path);
log.debug("deployStack", "Terminal created");
terminal.rows = PROGRESS_TERMINAL_ROWS;
if (socket) {
terminal.join(socket);
log.debug("deployStack", "Terminal joined");
} else {
log.debug("deployStack", "No socket, not joining");
}
return new Promise((resolve, reject) => {
terminal.onExit((exitCode : number) => {
if (exitCode === 0) {
resolve(exitCode);
} else {
reject(new Error("Failed to deploy, please check the terminal output for more information."));
}
});
terminal.start();
});
}
static getStackList(server : DockgeServer) : Stack[] {
let stacksDir = server.stacksDir;
let stackList : Stack[] = [];
// Scan the stacks directory, and get the stack list
let filenameList = fs.readdirSync(stacksDir);
log.debug("stack", filenameList);
for (let filename of filenameList) {
let relativePath = path.join(stacksDir, filename);
if (fs.statSync(relativePath).isDirectory()) {
let stack = new Stack(server, filename);
stackList.push(stack);
}
}
return stackList;
}
static getStack(server: DockgeServer, stackName: string) : Stack {
let dir = path.join(server.stacksDir, stackName);
if (!fs.existsSync(dir)) {
throw new ValidationError("Stack not found");
}
return new Stack(server, stackName);
}
}

View File

@ -1,43 +1,151 @@
import { DockgeServer } from "./dockge-server";
import * as os from "node:os";
import * as pty from "node-pty";
import * as pty from "@homebridge/node-pty-prebuilt-multiarch";
import { LimitQueue } from "./utils/limit-queue";
import { DockgeSocket } from "./util-server";
import { getCryptoRandomInt, TERMINAL_COLS, TERMINAL_ROWS } from "./util-common";
import { sync as commandExistsSync } from "command-exists";
import { log } from "./log";
const shell = os.platform() === "win32" ? "pwsh.exe" : "bash";
/**
* Terminal for running commands, no user interaction
*/
export class Terminal {
ptyProcess;
private server : DockgeServer;
private buffer : LimitQueue<string> = new LimitQueue(100);
protected static terminalMap : Map<string, Terminal> = new Map();
constructor(server : DockgeServer) {
protected _ptyProcess? : pty.IPty;
protected server : DockgeServer;
protected buffer : LimitQueue<string> = 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;
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;
this.ptyProcess = pty.spawn(shell, [], {
name: "dockge-terminal",
cwd: "./tmp",
});
// this.ptyProcess.write("npm remove lodash\r");
//this.ptyProcess.write("npm install lodash\r");
this.ptyProcess.onData((data) => {
this.buffer.push(data);
this.server.io.to("terminal").emit("commandOutput", data);
});
Terminal.terminalMap.set(this.name, this);
}
write(input : string) {
this.ptyProcess.write(input);
get rows() {
return this._rows;
}
set rows(rows : number) {
this._rows = rows;
this.ptyProcess?.resize(TERMINAL_COLS, rows);
}
public start() {
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);
if (this.callback) {
this.callback(res.exitCode);
}
Terminal.terminalMap.delete(this.name);
log.debug("Terminal", "Terminal " + this.name + " exited with code " + 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 for re-connecting
* Get the terminal output string
*/
getBuffer() : string {
if (this.buffer.length === 0) {
return "";
}
return this.buffer.join("");
}
close() {
this._ptyProcess?.kill();
}
public static getTerminal(name : string) : Terminal | undefined {
return Terminal.terminalMap.get(name);
}
}
/**
* 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, cwd : string = "./") {
let shell;
if (commandExistsSync("pwsh")) {
shell = "pwsh";
} else if (os.platform() === "win32") {
shell = "powershell.exe";
} else {
shell = "bash";
}
super(server, name, shell, [], cwd);
}
}

View File

@ -1,12 +1,38 @@
import dayjs from "dayjs";
// For loading dayjs plugins, don't remove event though it is not used in this file
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { randomBytes } from "crypto";
let randomBytes : (numBytes: number) => Uint8Array;
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;
}
export const isDev = process.env.NODE_ENV === "development";
export const TERMINAL_COLS = 80;
export const TERMINAL_ROWS = 10;
export const PROGRESS_TERMINAL_ROWS = 8;
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
@ -54,7 +80,6 @@ export function genSecret(length = 64) {
* @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;
@ -76,11 +101,11 @@ export function getCryptoRandomInt(min: number, max: number):number {
tmpRange = tmpRange >>> 1;
}
const randomBytes = getRandomBytes(bytesNeeded);
const bytes = randomBytes(bytesNeeded);
let randomValue = 0;
for (let i = 0; i < bytesNeeded; i++) {
randomValue |= randomBytes[i] << 8 * i;
randomValue |= bytes[i] << 8 * i;
}
randomValue = randomValue & mask;
@ -92,27 +117,15 @@ export function getCryptoRandomInt(min: number, max: number):number {
}
}
/**
* Returns either the NodeJS crypto.randomBytes() function or its
* browser equivalent implemented via window.crypto.getRandomValues()
*/
const getRandomBytes = (
(typeof window !== "undefined" && window.crypto)
// Browsers
? function () {
return (numBytes: number) => {
const randomBytes = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i += 65536) {
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
}
return randomBytes;
};
export function getComposeTerminalName(stack : string) {
return "compose-" + stack;
}
// Node
: function () {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return randomBytes;
export function getContainerTerminalName(container : string) {
return "container-" + container;
}
)();
export function getContainerExecTerminalName(container : string, index : number) {
return "container-exec-" + container + "-" + index;
}

View File

@ -1,7 +1,27 @@
import { Socket } from "socket.io";
import { Terminal } from "./terminal";
import { randomBytes } from "crypto";
import { log } from "./log";
import { ERROR_TYPE_VALIDATION } from "./util-common";
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;
}
// Some config values are required
export interface Config extends Arguments {
dataDir : string;
}
export function checkLogin(socket : DockgeSocket) {
@ -9,3 +29,31 @@ export function checkLogin(socket : DockgeSocket) {
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);
}
}

View File

@ -1,20 +1,39 @@
FROM debian:bookworm-slim
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/
# 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 \
&& rm -rf /var/lib/apt/lists/*
RUN curl https://bun.sh/install | bash -s "bun-v1.0.3"
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
#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

View File

@ -1,15 +1,9 @@
FROM debian:bookworm-slim
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
RUN apt update && apt install --yes --no-install-recommends \
curl \
ca-certificates \
unzip \
&& rm -rf /var/lib/apt/lists/*
RUN curl https://bun.sh/install | bash -s "bun-v1.0.3"
FROM louislam/dockge:base
WORKDIR /app
COPY . .
RUN bun install --production --frozen-lockfile
COPY --chown=node:node . .
RUN pnpm install --prod --frozen-lockfile && \
mkdir ./data \
VOLUME /app/data
EXPOSE 5001
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["tsx", "./backend/index.ts"]

0
docker/compose.yaml Normal file
View File

View File

@ -7,14 +7,13 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BDropdown: typeof import('bootstrap-vue-next')['BDropdown']
BDropdownItem: typeof import('bootstrap-vue-next')['BDropdownItem']
Confirm: typeof import('./src/components/Confirm.vue')['default']
Login: typeof import('./src/components/Login.vue')['default']
MonitorListItem: typeof import('./src/components/MonitorListItem.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StackList: typeof import('./src/components/StackList.vue')['default']
StackListItem: typeof import('./src/components/StackListItem.vue')['default']
Terminal: typeof import('./src/components/Terminal.vue')['default']
Uptime: typeof import('./src/components/Uptime.vue')['default']
}
}

View File

@ -107,6 +107,8 @@ export default {
this.$root.loggedIn = true;
this.$root.username = this.$root.getJWTPayload()?.username;
this.$root.afterLogin();
// Trigger Chrome Save Password
history.pushState({}, "");
}

View File

@ -26,7 +26,7 @@
<!-- TODO -->
<div v-if="false" class="header-filter">
<!--<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />-->
<!--<StackListFilter :filterState="filterState" @update-filter="updateFilter" />-->
</div>
<!-- TODO: Selection Controls -->
@ -40,12 +40,12 @@
<button class="btn-outline-normal" @click="pauseDialog"><font-awesome-icon icon="pause" size="sm" /> {{ $t("Pause") }}</button>
<button class="btn-outline-normal" @click="resumeSelected"><font-awesome-icon icon="play" size="sm" /> {{ $t("Resume") }}</button>
<span v-if="selectedMonitorCount > 0">
{{ $t("selectedMonitorCount", [ selectedMonitorCount ]) }}
<span v-if="selectedStackCount > 0">
{{ $t("selectedStackCount", [ selectedStackCount ]) }}
</span>
</div>
</div>
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
<div ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle">
<div v-if="Object.keys($root.stackList).length === 0" class="text-center mt-3">
<router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link>
</div>
@ -53,7 +53,7 @@
<StackListItem
v-for="(item, index) in sortedStackList"
:key="index"
:monitor="item"
:stack="item"
:showPathName="filtersActive"
:isSelectMode="selectMode"
:isSelected="isSelected"
@ -64,7 +64,7 @@
</div>
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
{{ $t("pauseMonitorMsg") }}
{{ $t("pauseStackMsg") }}
</Confirm>
</template>
@ -89,7 +89,7 @@ export default {
selectMode: false,
selectAll: false,
disableSelectAllWatcher: false,
selectedMonitors: {},
selectedStacks: {},
windowTop: 0,
filterState: {
status: null,
@ -103,7 +103,7 @@ export default {
* Improve the sticky appearance of the list by increasing its
* height as user scrolls down.
* Not used on mobile.
* @returns {object} Style for monitor list
* @returns {object} Style for stack list
*/
boxStyle() {
if (window.innerWidth > 550) {
@ -119,80 +119,43 @@ export default {
},
/**
* Returns a sorted list of monitors based on the applied filters and search text.
* @returns {Array} The sorted list of monitors.
* Returns a sorted list of stacks based on the applied filters and search text.
* @returns {Array} The sorted list of stacks.
*/
sortedStackList() {
let result = Object.values(this.$root.stackList);
result = result.filter(monitor => {
result = result.filter(stack => {
// filter by search text
// finds monitor name, tag name or tag value
// finds stack name, tag name or tag value
let searchTextMatch = true;
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
searchTextMatch =
monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
stack.name.toLowerCase().includes(loweredSearchText)
|| stack.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
}
// filter by status
let statusMatch = true;
if (this.filterState.status != null && this.filterState.status.length > 0) {
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
}
statusMatch = this.filterState.status.includes(monitor.status);
}
// filter by active
let activeMatch = true;
if (this.filterState.active != null && this.filterState.active.length > 0) {
activeMatch = this.filterState.active.includes(monitor.active);
activeMatch = this.filterState.active.includes(stack.active);
}
// filter by tags
let tagsMatch = true;
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
tagsMatch = stack.tags.map(tag => tag.tag_id) // convert to array of tag IDs
.filter(stackTagId => this.filterState.tags.includes(stackTagId)) // perform Array Intersaction between filter and stack's tags
.length > 0;
}
// Hide children if not filtering
let showChild = true;
if (this.filterState.status == null && this.filterState.active == null && this.filterState.tags == null && this.searchText === "") {
if (monitor.parent !== null) {
showChild = false;
}
}
return searchTextMatch && statusMatch && activeMatch && tagsMatch && showChild;
return searchTextMatch && activeMatch && tagsMatch;
});
// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === false) {
return 1;
}
if (m2.active === false) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.name.localeCompare(m2.name);
});
@ -203,8 +166,9 @@ export default {
return document.body.classList.contains("dark");
},
monitorListStyle() {
let listHeaderHeight = 107;
stackListStyle() {
//let listHeaderHeight = 107;
let listHeaderHeight = 60;
if (this.selectMode) {
listHeaderHeight += 42;
@ -215,8 +179,8 @@ export default {
};
},
selectedMonitorCount() {
return Object.keys(this.selectedMonitors).length;
selectedStackCount() {
return Object.keys(this.selectedStacks).length;
},
/**
@ -229,8 +193,8 @@ export default {
},
watch: {
searchText() {
for (let monitor of this.sortedMonitorList) {
if (!this.selectedMonitors[monitor.id]) {
for (let stack of this.sortedStackList) {
if (!this.selectedStacks[stack.id]) {
if (this.selectAll) {
this.disableSelectAllWatcher = true;
this.selectAll = false;
@ -241,11 +205,11 @@ export default {
},
selectAll() {
if (!this.disableSelectAllWatcher) {
this.selectedMonitors = {};
this.selectedStacks = {};
if (this.selectAll) {
this.sortedMonitorList.forEach((item) => {
this.selectedMonitors[item.id] = true;
this.sortedStackList.forEach((item) => {
this.selectedStacks[item.id] = true;
});
}
} else {
@ -255,7 +219,7 @@ export default {
selectMode() {
if (!this.selectMode) {
this.selectAll = false;
this.selectedMonitors = {};
this.selectedStacks = {};
}
},
},
@ -286,7 +250,7 @@ export default {
this.searchText = "";
},
/**
* Update the MonitorList Filter
* Update the StackList Filter
* @param {object} newFilter Object with new filter
* @returns {void}
*/
@ -294,28 +258,28 @@ export default {
this.filterState = newFilter;
},
/**
* Deselect a monitor
* @param {number} id ID of monitor
* Deselect a stack
* @param {number} id ID of stack
* @returns {void}
*/
deselect(id) {
delete this.selectedMonitors[id];
delete this.selectedStacks[id];
},
/**
* Select a monitor
* @param {number} id ID of monitor
* Select a stack
* @param {number} id ID of stack
* @returns {void}
*/
select(id) {
this.selectedMonitors[id] = true;
this.selectedStacks[id] = true;
},
/**
* Determine if monitor is selected
* @param {number} id ID of monitor
* @returns {bool} Is the monitor selected?
* Determine if stack is selected
* @param {number} id ID of stack
* @returns {bool} Is the stack selected?
*/
isSelected(id) {
return id in this.selectedMonitors;
return id in this.selectedStacks;
},
/**
* Disable select mode and reset selection
@ -323,7 +287,7 @@ export default {
*/
cancelSelectMode() {
this.selectMode = false;
this.selectedMonitors = {};
this.selectedStacks = {};
},
/**
* Show dialog to confirm pause
@ -333,24 +297,24 @@ export default {
this.$refs.confirmPause.show();
},
/**
* Pause each selected monitor
* Pause each selected stack
* @returns {void}
*/
pauseSelected() {
Object.keys(this.selectedMonitors)
.filter(id => this.$root.monitorList[id].active)
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id, () => {}));
Object.keys(this.selectedStacks)
.filter(id => this.$root.stackList[id].active)
.forEach(id => this.$root.getSocket().emit("pauseStack", id, () => {}));
this.cancelSelectMode();
},
/**
* Resume each selected monitor
* Resume each selected stack
* @returns {void}
*/
resumeSelected() {
Object.keys(this.selectedMonitors)
.filter(id => !this.$root.monitorList[id].active)
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id, () => {}));
Object.keys(this.selectedStacks)
.filter(id => !this.$root.stackList[id].active)
.forEach(id => this.$root.getSocket().emit("resumeStack", id, () => {}));
this.cancelSelectMode();
},
@ -428,7 +392,7 @@ export default {
max-width: 15em;
}
.monitor-item {
.stack-item {
width: 100%;
}

View File

@ -7,68 +7,44 @@
class="form-check-input select-input"
type="checkbox"
:aria-label="$t('Check/Uncheck')"
:checked="isSelected(monitor.id)"
:checked="isSelected(stack.id)"
@click.stop="toggleSelection"
/>
</div>
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
<router-link :to="`/compose/${stack.name}`" class="item">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="col-9 col-md-8 small-padding">
<div class="info">
<Uptime :monitor="monitor" type="24" :pill="true" />
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
</span>
{{ monitorName }}
<Uptime :stack="stack" type="24" :pill="true" />
{{ stackName }}
</div>
<div v-if="monitor.tags.length > 0" class="tags">
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
<div v-if="stack.tags.length > 0" class="tags">
<!--<Tag v-for="tag in stack.tags" :key="tag" :item="tag" :size="'sm'" />-->
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
</div>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
</div>
</div>
</router-link>
</div>
<transition name="slide-fade-up">
<div v-if="!isCollapsed" class="childs">
<MonitorListItem
v-for="(item, index) in sortedChildMonitorList"
:key="index" :monitor="item"
:showPathName="showPathName"
:isSelectMode="isSelectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
:depth="depth + 1"
/>
</div>
</transition>
</div>
</template>
<script>
import Uptime from "./Uptime.vue";
export default {
components: {
Uptime
},
props: {
/** Monitor this represents */
monitor: {
/** Stack this represents */
stack: {
type: Object,
default: null,
},
/** Should the monitor name show it's parent */
/** Should the stack name show it's parent */
showPathName: {
type: Boolean,
default: false,
@ -78,22 +54,22 @@ export default {
type: Boolean,
default: false,
},
/** How many ancestors are above this monitor */
/** How many ancestors are above this stack */
depth: {
type: Number,
default: 0,
},
/** Callback to determine if monitor is selected */
/** Callback to determine if stack is selected */
isSelected: {
type: Function,
default: () => {}
},
/** Callback fired when monitor is selected */
/** Callback fired when stack is selected */
select: {
type: Function,
default: () => {}
},
/** Callback fired when monitor is deselected */
/** Callback fired when stack is deselected */
deselect: {
type: Function,
default: () => {}
@ -105,51 +81,16 @@ export default {
};
},
computed: {
sortedChildMonitorList() {
let result = Object.values(this.$root.monitorList);
result = result.filter(childMonitor => childMonitor.parent === this.monitor.id);
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.name.localeCompare(m2.name);
});
return result;
},
hasChildren() {
return this.sortedChildMonitorList.length > 0;
},
depthMargin() {
return {
marginLeft: `${31 * this.depth}px`,
};
},
monitorName() {
stackName() {
if (this.showPathName) {
return this.monitor.pathName;
return this.stack.pathName;
} else {
return this.monitor.name;
return this.stack.name;
}
}
},
@ -161,28 +102,10 @@ export default {
},
beforeMount() {
// Always unfold if monitor is accessed directly
if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) {
this.isCollapsed = false;
return;
}
// Set collapsed value based on local storage
let storage = window.localStorage.getItem("monitorCollapsed");
if (storage === null) {
return;
}
let storageObject = JSON.parse(storage);
if (storageObject[`monitor_${this.monitor.id}`] == null) {
return;
}
this.isCollapsed = storageObject[`monitor_${this.monitor.id}`];
},
methods: {
/**
* Changes the collapsed value of the current monitor and saves
* Changes the collapsed value of the current stack and saves
* it to local storage
* @returns {void}
*/
@ -190,25 +113,25 @@ export default {
this.isCollapsed = !this.isCollapsed;
// Save collapsed value into local storage
let storage = window.localStorage.getItem("monitorCollapsed");
let storage = window.localStorage.getItem("stackCollapsed");
let storageObject = {};
if (storage !== null) {
storageObject = JSON.parse(storage);
}
storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed;
storageObject[`stack_${this.stack.id}`] = this.isCollapsed;
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
window.localStorage.setItem("stackCollapsed", JSON.stringify(storageObject));
},
/**
* Toggle selection of monitor
* Toggle selection of stack
* @returns {void}
*/
toggleSelection() {
if (this.isSelected(this.monitor.id)) {
this.deselect(this.monitor.id);
if (this.isSelected(this.stack.id)) {
this.deselect(this.stack.id);
} else {
this.select(this.monitor.id);
this.select(this.stack.id);
}
},
},
@ -228,7 +151,7 @@ export default {
padding-right: 2px !important;
}
// .monitor-item {
// .stack-item {
// width: 100%;
// }

View File

@ -0,0 +1,93 @@
<template>
<div class="shadow-box">
<div v-pre ref="terminal" class="main-terminal"></div>
</div>
</template>
<script>
import { Terminal } from "xterm";
import { WebLinksAddon } from "xterm-addon-web-links";
import { TERMINAL_COLS, TERMINAL_ROWS } from "../../../backend/util-common";
export default {
/**
* @type {Terminal}
*/
terminal: null,
components: {
},
props: {
allowInput: {
type: Boolean,
default: true,
},
rows: {
type: Number,
default: TERMINAL_ROWS,
}
},
emits: [ "has-data" ],
data() {
return {
name: null,
first: true,
};
},
created() {
},
mounted() {
this.terminal = new Terminal({
fontSize: 16,
fontFamily: "monospace",
cursorBlink: this.allowInput,
cols: TERMINAL_COLS,
rows: this.rows,
});
this.terminal.loadAddon(new WebLinksAddon());
// Bind to a div
this.terminal.open(this.$refs.terminal);
this.terminal.focus();
// Notify parent component when data is received
this.terminal.onCursorMove(() => {
console.debug("onData triggered");
if (this.first) {
this.$emit("has-data");
this.first = false;
}
});
},
unmounted() {
this.$root.unbindTerminal(this.name);
this.terminal.dispose();
},
methods: {
bind(name) {
if (this.name) {
this.$root.unbindTerminal(this.name);
}
this.name = name;
this.$root.bindTerminal(this.name, this.terminal);
},
}
};
</script>
<style scoped lang="scss">
.main-terminal {
height: 100%;
}
</style>
<style lang="scss">
.terminal {
padding: 10px 15px;
height: 100%;
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<span :class="className" :title="title">{{ uptime }}</span>
</template>
<script>
export default {
props: {
/** Monitor this represents */
monitor: {
type: Object,
default: null,
},
/** Type of monitor */
type: {
type: String,
default: null,
},
/** Is this a pill? */
pill: {
type: Boolean,
default: false,
},
},
computed: {
uptime() {
return this.$t("notAvailableShort");
},
color() {
return "secondary";
},
className() {
if (this.pill) {
return `badge rounded-pill bg-${this.color}`;
}
return "";
},
title() {
if (this.type === "720") {
return `30${this.$t("-day")}`;
}
return `24${this.$t("-hour")}`;
}
},
};
</script>
<style>
.badge {
min-width: 62px;
}
</style>

View File

@ -50,7 +50,7 @@ import {
faInfoCircle,
faClone,
faCertificate,
faTerminal, faWarehouse, faHome,
faTerminal, faWarehouse, faHome, faRocket,
} from "@fortawesome/free-solid-svg-icons";
library.add(
@ -101,6 +101,7 @@ library.add(
faTerminal,
faWarehouse,
faHome,
faRocket,
);
export { FontAwesomeIcon };

View File

@ -8,5 +8,15 @@
"console": "Console",
"registry": "Registry",
"compose": "Compose",
"addFirstStackMsg": "Compose your first stack!"
"addFirstStackMsg": "Compose your first stack!",
"stackName" : "Stack Name",
"deployStack": "Deploy",
"deleteStack": "Delete",
"stopStack": "Stop",
"restartStack": "Restart",
"startStack": "Start",
"editStack": "Edit",
"discardStack": "Discard",
"saveStackDraft": "Save",
"notAvailableShort" : "N/A"
}

View File

@ -3,20 +3,13 @@ import { Socket } from "socket.io-client";
import { defineComponent } from "vue";
import jwtDecode from "jwt-decode";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { WebLinksAddon } from "xterm-addon-web-links";
const terminal = new Terminal({
fontSize: 16,
fontFamily: "monospace",
cursorBlink: true,
});
terminal.loadAddon(new FitAddon());
terminal.loadAddon(new WebLinksAddon());
let terminalInputBuffer = "";
let cursorPosition = 0;
let socket : Socket;
let terminalMap : Map<string, Terminal> = new Map();
function removeInput() {
const backspaceCount = terminalInputBuffer.length;
const backspaces = "\b \b".repeat(backspaceCount);
@ -44,7 +37,6 @@ export default defineComponent({
loggedIn: false,
allowLoginDialog: false,
username: null,
stackList: {},
};
},
@ -66,6 +58,7 @@ export default defineComponent({
this.initSocketIO();
},
mounted() {
return;
terminal.onKey(e => {
const code = e.key.charCodeAt(0);
console.debug("Encode: " + JSON.stringify(e.key));
@ -100,6 +93,7 @@ export default defineComponent({
// TODO
} else if (e.key === "\u0003") { // Ctrl + C
console.debug("Ctrl + C");
socket.emit("terminalInputRaw", e.key);
removeInput();
} else {
cursorPosition++;
@ -131,7 +125,7 @@ export default defineComponent({
}
socket = io(url, {
transports: [ "websocket", "polling" ]
});
socket.on("connect", () => {
@ -195,9 +189,20 @@ export default defineComponent({
this.$router.push("/setup");
});
socket.on("commandOutput", (data) => {
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;
}
});
},
/**
@ -212,10 +217,6 @@ export default defineComponent({
return socket;
},
getTerminal() : Terminal {
return terminal;
},
/**
* Get payload of JWT cookie
* @returns {(object | undefined)} JWT payload
@ -269,13 +270,23 @@ export default defineComponent({
},
afterLogin() {
terminal.clear();
},
bindTerminal(terminalName : string, terminal : Terminal) {
// Load terminal, get terminal screen
socket.emit("getTerminalBuffer", (res) => {
console.log("getTerminalBuffer");
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);
}
}
});

View File

@ -1,16 +1,224 @@
<template>
<transition name="slide-fade" appear>
<div>
<h1 v-if="isAdd" class="mb-3">Compose</h1>
<h1 v-else class="mb-3">Stack: {{ stack.name }}</h1>
<div class="mb-3">
<div class="btn-group" role="group">
<button v-if="isEditMode" class="btn btn-primary" :disabled="processing" @click="deployStack">
<font-awesome-icon icon="rocket" class="me-1" />
{{ $t("deployStack") }}
</button>
<button v-if="isEditMode" class="btn btn-normal" :disabled="processing" @click="saveStack">
<font-awesome-icon icon="save" class="me-1" />
{{ $t("saveStackDraft") }}
</button>
<button v-if="!isEditMode" class="btn btn-normal" :disabled="processing" @click="isEditMode = true">{{ $t("editStack") }}</button>
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
<button v-if="!isEditMode" class="btn btn-primary" :disabled="processing">{{ $t("startStack") }}</button>
<button v-if="!isEditMode" class="btn btn-primary " :disabled="processing">{{ $t("restartStack") }}</button>
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("stopStack") }}</button>
<button v-if="!isEditMode" class="btn btn-danger" :disabled="processing">{{ $t("deleteStack") }}</button>
</div>
</div>
<!-- Progress Terminal -->
<transition name="slide-fade" appear>
<Terminal
v-show="showProgressTerminal"
ref="progressTerminal"
:allow-input="false"
class="mb-3 terminal"
:rows="progressTerminalRows"
@has-data="showProgressTerminal = true"
></Terminal>
</transition>
<div class="row">
<div class="col">
<h4 class="mb-3">General</h4>
<div class="shadow-box big-padding mb-3">
<!-- Stack Name -->
<div v-if="isAdd" class="mb-3">
<label for="name" class="form-label">{{ $t("stackName") }}</label>
<input id="name" v-model="stack.name" type="text" class="form-control" required>
</div>
</div>
<h4 class="mb-3">Containers</h4>
<div class="shadow-box big-padding mb-3">
<div v-for="(service, name) in jsonConfig.services" :key="name">
{{ name }} {{ service }}
</div>
</div>
</div>
<div class="col">
<h4 class="mb-3">compose.yaml</h4>
<div class="shadow-box mb-3">
<prism-editor v-model="stack.composeYAML" class="yaml-editor" :highlight="highlighter" line-numbers :readonly="!isEditMode" @input="yamlCodeChange"></prism-editor>
</div>
<div class="mb-3">
{{ yamlError }}
</div>
<!-- <div class="shadow-box big-padding mb-3">
<div class="mb-3">
<label for="name" class="form-label"> Search Templates</label>
<input id="name" v-model="name" type="text" class="form-control" placeholder="Search..." required>
</div>
<prism-editor v-if="false" v-model="yamlConfig" class="yaml-editor" :highlight="highlighter" line-numbers @input="yamlCodeChange"></prism-editor>
</div>-->
</div>
</div>
</div>
</transition>
</template>
<script lang="ts">
export default {
<script>
import { highlight, languages } from "prismjs/components/prism-core";
import { PrismEditor } from "vue-prism-editor";
import "prismjs/components/prism-yaml";
import * as yaml from "yaml";
import "prismjs/themes/prism-tomorrow.css";
import "vue-prism-editor/dist/prismeditor.min.css";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "../../../backend/util-common";
const template = `version: "3.8"
services:
nginx:
image: nginx:latest
ports:
- "8080:8080"
`;
let yamlErrorTimeout = null;
export default {
components: {
FontAwesomeIcon,
PrismEditor,
},
data() {
return {
jsonConfig: {},
yamlError: "",
processing: true,
showProgressTerminal: false,
progressTerminalRows: PROGRESS_TERMINAL_ROWS,
stack: {},
isEditMode: false,
submitted: false,
};
},
computed: {
isAdd() {
return this.$route.path === "/compose" && !this.submitted;
},
},
watch: {
"stack.composeYAML": {
handler() {
this.yamlCodeChange();
},
deep: true,
},
},
mounted() {
if (this.isAdd) {
this.processing = false;
this.isEditMode = true;
// Default Values
this.stack = {
name: "",
composeYAML: template,
};
} else {
this.stack.name = this.$route.params.stackName;
this.loadStack();
}
},
methods: {
loadStack() {
this.$root.getSocket().emit("getStack", this.stack.name, (res) => {
if (res.ok) {
this.stack = res.stack;
this.processing = false;
} else {
this.$root.toastRes(res);
}
});
},
deployStack() {
this.processing = true;
// Bind Terminal output
const terminalName = getComposeTerminalName(this.stack.name);
this.$refs.progressTerminal.bind(terminalName);
this.$root.getSocket().emit("deployStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
this.processing = false;
this.$root.toastRes(res);
if (res.ok) {
this.$router.push("/compose/" + this.stack.name);
} else {
this.submitted = true;
}
});
},
saveStack() {
this.processing = true;
this.$root.getSocket().emit("saveStack", this.stack.name, this.stack.composeYAML, this.isAdd, (res) => {
this.processing = false;
this.$root.toastRes(res);
if (res.ok) {
this.$router.push("/compose/" + this.stack.name);
}
});
},
discardStack() {
this.loadStack();
this.isEditMode = false;
},
highlighter(code) {
return highlight(code, languages.yaml);
},
yamlCodeChange() {
try {
this.jsonConfig = yaml.parse(this.stack.composeYAML) ?? {};
this.yamlError = "";
} catch (e) {
clearTimeout(yamlErrorTimeout);
if (this.yamlError) {
this.yamlError = e.message;
} else {
yamlErrorTimeout = setTimeout(() => {
this.yamlError = e.message;
}, 3000);
}
}
},
}
};
</script>
<style scoped lang="scss">
.terminal {
height: 200px;
}
</style>

View File

@ -3,9 +3,13 @@
<div>
<h1 class="mb-3">Console</h1>
<div class="shadow-box">
<div v-pre id="terminal"></div>
<div>
<p>
Allowed commands: <code>docker</code>, <code>ls</code>, <code>cd</code>
</p>
</div>
<Terminal :allow-input="true" class="terminal"></Terminal>
</div>
</transition>
</template>
@ -14,10 +18,9 @@
export default {
components: {
},
mounted() {
this.$root.getTerminal().open(document.querySelector("#terminal"));
this.$root.terminalFit(50);
},
methods: {
@ -27,9 +30,6 @@ export default {
<style scoped lang="scss">
.terminal {
font-family: monospace;
font-size: 18px;
padding: 10px 15px;
height: calc(100vh - 200px);
}
</style>

View File

@ -1,14 +0,0 @@
<script setup lang="ts">
</script>
<template>
<transition name="slide-fade" appear>
<div>
</div>
</transition>
</template>
<style scoped lang="scss">
</style>

View File

@ -4,8 +4,8 @@ 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 EditStack from "./pages/EditStack.vue";
import Console from "./pages/Console.vue";
import Compose from "./pages/Compose.vue";
const routes = [
{
@ -23,7 +23,13 @@ const routes = [
children: [
{
path: "/compose",
component: EditStack,
component: Compose,
},
{
path: "/compose/:stackName",
name: "compose",
component: Compose,
props: true,
},
]

View File

@ -377,7 +377,7 @@ optgroup {
color: $dark-font-color;
}
.monitor-list {
.stack-list {
.item {
&:hover {
background-color: $dark-bg2;
@ -474,7 +474,7 @@ optgroup {
opacity: 0;
}
.monitor-list {
.stack-list {
&.scrollbar {
overflow-y: auto;
}
@ -653,12 +653,28 @@ $shadow-box-padding: 20px;
}
}
#terminal {
.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;
}
}
// Localization
@import "localization.scss";

View File

@ -158,7 +158,7 @@ export function colorOptions(self) {
export function loadToastSettings() {
return {
position: POSITION.BOTTOM_RIGHT,
containerClassName: "toast-container mb-5",
containerClassName: "toast-container",
showCloseButtonOnHover: true,
filterBeforeCreate: (toast, toasts) => {

View File

@ -14,7 +14,7 @@ export default defineConfig({
},
root: "./frontend",
build: {
outDir: "../dist",
outDir: "../frontend-dist",
},
plugins: [
vue(),

View File

@ -5,27 +5,33 @@
"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:frontend": "vite build --config ./frontend/vite.config.ts",
"build:docker-base": "docker build -t louislam/dockge:base -f ./docker/Base.Dockerfile .",
"build:docker": "pnpm run build:frontend && docker build -t louislam/dockge:latest -f ./docker/Dockerfile .",
"start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest"
},
"dependencies": {
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.7",
"@louislam/sqlite3": "~15.1.6",
"bcryptjs": "^2.4.3",
"check-password-strength": "^2.0.7",
"bcryptjs": "~2.4.3",
"check-password-strength": "~2.0.7",
"command-exists": "^1.2.9",
"compare-versions": "~6.1.0",
"dayjs": "^1.11.10",
"express": "~4.18.2",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^3.1.2",
"express-static-gzip": "~2.1.7",
"jsonwebtoken": "~9.0.2",
"jwt-decode": "~3.1.2",
"knex": "~2.5.1",
"limiter-es6-compat": "~2.1.2",
"mysql2": "^3.6.2",
"node-pty": "^1.0.0",
"redbean-node": "0.3.1",
"socket.io": "~4.7.2",
"socket.io-client": "~4.7.2",
"timezones-list": "^3.0.2",
"timezones-list": "~3.0.2",
"ts-command-line-args": "~2.5.1",
"tsx": "~3.14.0",
"type-fest": "~4.3.3",
@ -39,7 +45,9 @@
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/vue-fontawesome": "3.0.3",
"@types/command-exists": "~1.2.2",
"@types/express": "~4.17.20",
"@types/jsonwebtoken": "~9.0.4",
"@typescript-eslint/eslint-plugin": "~6.8.0",
"@typescript-eslint/parser": "~6.8.0",
"@vitejs/plugin-vue": "~4.3.4",
@ -49,6 +57,9 @@
"eslint": "~8.50.0",
"eslint-plugin-jsdoc": "~46.8.2",
"eslint-plugin-vue": "~9.17.0",
"monaco-editor": "^0.44.0",
"monaco-yaml": "^5.1.0",
"prismjs": "~1.29.0",
"sass": "~1.68.0",
"typescript": "~5.2.2",
"unplugin-vue-components": "^0.25.2",
@ -57,10 +68,10 @@
"vue": "~3.3.6",
"vue-eslint-parser": "^9.3.2",
"vue-i18n": "~9.5.0",
"vue-prism-editor": "~2.0.0-alpha.2",
"vue-router": "~4.2.5",
"vue-toastification": "~2.0.0-rc.5",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-web-links": "^0.9.0"
"xterm": "~5.3.0",
"xterm-addon-web-links": "~0.9.0"
}
}

View File

@ -5,15 +5,21 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@homebridge/node-pty-prebuilt-multiarch':
specifier: ~0.11.7
version: 0.11.7
'@louislam/sqlite3':
specifier: ~15.1.6
version: 15.1.6
bcryptjs:
specifier: ^2.4.3
specifier: ~2.4.3
version: 2.4.3
check-password-strength:
specifier: ^2.0.7
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
@ -23,11 +29,14 @@ dependencies:
express:
specifier: ~4.18.2
version: 4.18.2
express-static-gzip:
specifier: ~2.1.7
version: 2.1.7
jsonwebtoken:
specifier: ^9.0.2
specifier: ~9.0.2
version: 9.0.2
jwt-decode:
specifier: ^3.1.2
specifier: ~3.1.2
version: 3.1.2
knex:
specifier: ~2.5.1
@ -38,9 +47,6 @@ dependencies:
mysql2:
specifier: ^3.6.2
version: 3.6.2
node-pty:
specifier: ^1.0.0
version: 1.0.0
redbean-node:
specifier: 0.3.1
version: 0.3.1(mysql2@3.6.2)
@ -51,7 +57,7 @@ dependencies:
specifier: ~4.7.2
version: 4.7.2
timezones-list:
specifier: ^3.0.2
specifier: ~3.0.2
version: 3.0.2
ts-command-line-args:
specifier: ~2.5.1
@ -84,9 +90,15 @@ devDependencies:
'@fortawesome/vue-fontawesome':
specifier: 3.0.3
version: 3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.6)
'@types/command-exists':
specifier: ~1.2.2
version: 1.2.2
'@types/express':
specifier: ~4.17.20
version: 4.17.20
'@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)
@ -114,6 +126,15 @@ devDependencies:
eslint-plugin-vue:
specifier: ~9.17.0
version: 9.17.0(eslint@8.50.0)
monaco-editor:
specifier: ^0.44.0
version: 0.44.0
monaco-yaml:
specifier: ^5.1.0
version: 5.1.0(monaco-editor@0.44.0)
prismjs:
specifier: ~1.29.0
version: 1.29.0
sass:
specifier: ~1.68.0
version: 1.68.0
@ -138,6 +159,9 @@ devDependencies:
vue-i18n:
specifier: ~9.5.0
version: 9.5.0(vue@3.3.6)
vue-prism-editor:
specifier: ~2.0.0-alpha.2
version: 2.0.0-alpha.2(vue@3.3.6)
vue-router:
specifier: ~4.2.5
version: 4.2.5(vue@3.3.6)
@ -145,13 +169,10 @@ devDependencies:
specifier: ~2.0.0-rc.5
version: 2.0.0-rc.5(vue@3.3.6)
xterm:
specifier: ^5.3.0
specifier: ~5.3.0
version: 5.3.0
xterm-addon-fit:
specifier: ^0.8.0
version: 0.8.0(xterm@5.3.0)
xterm-addon-web-links:
specifier: ^0.9.0
specifier: ~0.9.0
version: 0.9.0(xterm@5.3.0)
packages:
@ -487,6 +508,14 @@ packages:
dev: false
optional: true
/@homebridge/node-pty-prebuilt-multiarch@0.11.7:
resolution: {integrity: sha512-aaw66RDwHZ2Xs821U6hwEl2wPDyv9PAWEzNxS32YSRaoYmbie1AREehAfjbASGgpub+7d+3l98xft3oCCUKhSw==}
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'}
@ -777,6 +806,10 @@ packages:
'@types/node': 20.8.7
dev: true
/@types/command-exists@1.2.2:
resolution: {integrity: sha512-1qKPTkjLmghE5C7UUHXGcLaG8MNftchOcLAIryUXNKahRO5beS+iJ9rIL8XD4+B8K2phjYUsPQDox1FRX4KMTQ==}
dev: true
/@types/connect@3.4.37:
resolution: {integrity: sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==}
dependencies:
@ -823,6 +856,12 @@ packages:
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.7
dev: true
/@types/mime@1.3.4:
resolution: {integrity: sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==}
dev: true
@ -1363,6 +1402,10 @@ packages:
/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}
@ -1383,6 +1426,14 @@ packages:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
/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
/blessed@0.1.81:
resolution: {integrity: sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==}
engines: {node: '>= 0.8.0'}
@ -1466,6 +1517,13 @@ packages:
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'}
@ -1567,6 +1625,10 @@ packages:
optionalDependencies:
fsevents: 2.3.3
/chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
dev: false
/chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
@ -1616,6 +1678,10 @@ packages:
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'}
@ -1801,6 +1867,13 @@ packages:
dependencies:
ms: 2.1.2
/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'}
@ -1912,6 +1985,12 @@ packages:
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:
@ -2202,6 +2281,19 @@ packages:
dev: false
optional: true
/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'}
@ -2370,6 +2462,10 @@ packages:
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'}
@ -2505,6 +2601,10 @@ packages:
dev: false
optional: true
/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'}
@ -2695,6 +2795,10 @@ packages:
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'}
@ -2743,7 +2847,6 @@ packages:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
requiresBuild: true
dev: false
optional: true
/interpret@2.2.0:
resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==}
@ -2874,6 +2977,10 @@ packages:
dev: false
optional: true
/jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: true
/jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
requiresBuild: true
@ -3219,6 +3326,11 @@ packages:
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:
@ -3230,6 +3342,10 @@ packages:
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'}
@ -3304,6 +3420,10 @@ packages:
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'}
@ -3316,6 +3436,57 @@ packages:
dev: false
optional: true
/monaco-editor@0.44.0:
resolution: {integrity: sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==}
dev: true
/monaco-languageserver-types@0.2.3:
resolution: {integrity: sha512-QyV5R7s+rJ87bX1sRioMJZULWiTnMp0Vm+RLILgMEL0SqWuBsQBSW0EZunr4xMZhv6Qun3UZNCN4JrCCLURcgQ==}
dependencies:
monaco-types: 0.1.0
vscode-languageserver-protocol: 3.17.5
dev: true
/monaco-marker-data-provider@1.1.1(monaco-editor@0.44.0):
resolution: {integrity: sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==}
peerDependencies:
monaco-editor: '>=0.30.0'
dependencies:
monaco-editor: 0.44.0
dev: true
/monaco-types@0.1.0:
resolution: {integrity: sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==}
dev: true
/monaco-worker-manager@2.0.1(monaco-editor@0.44.0):
resolution: {integrity: sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==}
peerDependencies:
monaco-editor: '>=0.30.0'
dependencies:
monaco-editor: 0.44.0
dev: true
/monaco-yaml@5.1.0(monaco-editor@0.44.0):
resolution: {integrity: sha512-DU+cgXSJdOFKQ4I4oLg0V+mHKq1dJX+7hbIE4fJsgegUf1zEHW3PNlGj6qabUU2HZIPJ5NmXEf005GU9YDzTYQ==}
peerDependencies:
monaco-editor: '>=0.36'
dependencies:
'@types/json-schema': 7.0.14
jsonc-parser: 3.2.0
monaco-editor: 0.44.0
monaco-languageserver-types: 0.2.3
monaco-marker-data-provider: 1.1.1(monaco-editor@0.44.0)
monaco-types: 0.1.0
monaco-worker-manager: 2.0.1(monaco-editor@0.44.0)
path-browserify: 1.0.1
prettier: 2.8.8
vscode-languageserver-textdocument: 1.0.11
vscode-languageserver-types: 3.17.5
vscode-uri: 3.0.8
yaml: 2.3.3
dev: true
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
@ -3364,6 +3535,10 @@ packages:
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
@ -3395,6 +3570,13 @@ packages:
dev: false
optional: true
/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
@ -3433,13 +3615,6 @@ packages:
dev: false
optional: true
/node-pty@1.0.0:
resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==}
requiresBuild: true
dependencies:
nan: 2.18.0
dev: false
/nopt@5.0.0:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'}
@ -3593,6 +3768,10 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
dev: true
/path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@ -3779,11 +3958,41 @@ packages:
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
/prettier@2.8.8:
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
engines: {node: '>=10.13.0'}
hasBin: true
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
@ -3845,6 +4054,13 @@ packages:
dev: false
optional: true
/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.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
@ -3876,6 +4092,16 @@ packages:
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
/read@1.0.7:
resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==}
engines: {node: '>=0.8'}
@ -4136,6 +4362,18 @@ packages:
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'}
@ -4339,6 +4577,11 @@ packages:
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'}
@ -4380,6 +4623,26 @@ packages:
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'}
@ -4472,6 +4735,12 @@ packages:
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
/tv4@1.3.0:
resolution: {integrity: sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==}
engines: {node: '>= 0.8.0'}
@ -4690,6 +4959,30 @@ packages:
dev: false
optional: true
/vscode-jsonrpc@8.2.0:
resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
engines: {node: '>=14.0.0'}
dev: true
/vscode-languageserver-protocol@3.17.5:
resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
dependencies:
vscode-jsonrpc: 8.2.0
vscode-languageserver-types: 3.17.5
dev: true
/vscode-languageserver-textdocument@1.0.11:
resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==}
dev: true
/vscode-languageserver-types@3.17.5:
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
dev: true
/vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
dev: true
/vue-demi@0.14.6(vue@3.3.6):
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
engines: {node: '>=12'}
@ -4735,6 +5028,15 @@ packages:
vue: 3.3.6(typescript@5.2.2)
dev: true
/vue-prism-editor@2.0.0-alpha.2(vue@3.3.6):
resolution: {integrity: sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==}
engines: {node: '>=10'}
peerDependencies:
vue: ^3.0.0
dependencies:
vue: 3.3.6(typescript@5.2.2)
dev: true
/vue-router@4.2.5(vue@3.3.6):
resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==}
peerDependencies:
@ -4884,14 +5186,6 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
/xterm-addon-fit@0.8.0(xterm@5.3.0):
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
peerDependencies:
xterm: ^5.0.0
dependencies:
xterm: 5.3.0
dev: true
/xterm-addon-web-links@0.9.0(xterm@5.3.0):
resolution: {integrity: sha512-LIzi4jBbPlrKMZF3ihoyqayWyTXAwGfu4yprz1aK2p71e9UKXN6RRzVONR0L+Zd+Ik5tPVI9bwp9e8fDTQh49Q==}
peerDependencies:
@ -4910,7 +5204,6 @@ packages:
/yaml@2.3.3:
resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==}
engines: {node: '>= 14'}
dev: false
/yamljs@0.3.0:
resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==}