diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4f85cc3 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6ac287f..1a292dc 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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", }, }; diff --git a/.gitignore b/.gitignore index 25b978e..5ca0888 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ -# dotenv environment variable files +# Should update .dockerignore as well .env node_modules dist +frontend-dist .idea data tmp +/private diff --git a/README.md b/README.md index f87f6a6..82c81b6 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,33 @@ +
+ +
+ # 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 diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index b2b27db..fd13e22 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -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({ + // Define all possible arguments + let args = parse({ 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, + }); + } } diff --git a/backend/routers/main-router.ts b/backend/routers/main-router.ts index fd36da6..cdba197 100644 --- a/backend/routers/main-router.ts +++ b/backend/routers/main-router.ts @@ -7,7 +7,7 @@ export class MainRouter extends Router { const router = express.Router(); router.get("/", (req, res) => { - + res.send(server.indexHTML); }); return router; diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts index 55df1a0..755b412 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -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) { - callback({ - ok: false, - msg: e.message, - }); + 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(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; + } } + diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts index 60dd58a..fa00f2b 100644 --- a/backend/socket-handlers/main-socket-handler.ts +++ b/backend/socket-handlers/main-socket-handler.ts @@ -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) { diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts new file mode 100644 index 0000000..1bbb1a9 --- /dev/null +++ b/backend/socket-handlers/terminal-socket-handler.ts @@ -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) { + + } + }); + } +} diff --git a/backend/stack.ts b/backend/stack.ts new file mode 100644 index 0000000..893f205 --- /dev/null +++ b/backend/stack.ts @@ -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 { + 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); + } +} diff --git a/backend/terminal.ts b/backend/terminal.ts index 9f95b01..3092420 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -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 = new LimitQueue(100); + protected static terminalMap : Map = new Map(); - constructor(server : DockgeServer) { + 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; + + 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); + } } diff --git a/backend/util-common.ts b/backend/util-common.ts index 3bfbf6c..3fd88be 100644 --- a/backend/util-common.ts +++ b/backend/util-common.ts @@ -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) +export function getComposeTerminalName(stack : string) { + return "compose-" + stack; +} - // 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 getContainerTerminalName(container : string) { + return "container-" + container; +} + +export function getContainerExecTerminalName(container : string, index : number) { + return "container-exec-" + container + "-" + index; +} - // Node - : function () { - // eslint-disable-next-line @typescript-eslint/no-var-requires - return randomBytes; - } -)(); diff --git a/backend/util-server.ts b/backend/util-server.ts index 29171a2..2da3f11 100644 --- a/backend/util-server.ts +++ b/backend/util-server.ts @@ -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); + } +} diff --git a/docker/Base.Dockerfile b/docker/Base.Dockerfile index 6124b56..7c89d95 100644 --- a/docker/Base.Dockerfile +++ b/docker/Base.Dockerfile @@ -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 diff --git a/docker/Dockerfile b/docker/Dockerfile index 7d4163e..868ded2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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"] diff --git a/docker/compose.yaml b/docker/compose.yaml new file mode 100644 index 0000000..e69de29 diff --git a/frontend/components.d.ts b/frontend/components.d.ts index d96ecad..93e131e 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -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'] } } diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue index b10a072..ad90bad 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/components/Login.vue @@ -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({}, ""); } diff --git a/frontend/src/components/StackList.vue b/frontend/src/components/StackList.vue index 284af44..0e31bfa 100644 --- a/frontend/src/components/StackList.vue +++ b/frontend/src/components/StackList.vue @@ -26,7 +26,7 @@
- +
@@ -40,12 +40,12 @@ - - {{ $t("selectedMonitorCount", [ selectedMonitorCount ]) }} + + {{ $t("selectedStackCount", [ selectedStackCount ]) }} -
+
{{ $t("addFirstStackMsg") }}
@@ -53,7 +53,7 @@ - {{ $t("pauseMonitorMsg") }} + {{ $t("pauseStackMsg") }} @@ -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%; } diff --git a/frontend/src/components/StackListItem.vue b/frontend/src/components/StackListItem.vue index 62e4ead..e436c8d 100644 --- a/frontend/src/components/StackListItem.vue +++ b/frontend/src/components/StackListItem.vue @@ -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" />
- +
-
+
- - - - - {{ monitorName }} + + {{ stackName }}
-
- +
+
-
- -
-
- -
-
- -
- - -
- -
-
+ + + + diff --git a/frontend/src/components/Uptime.vue b/frontend/src/components/Uptime.vue new file mode 100644 index 0000000..39869dd --- /dev/null +++ b/frontend/src/components/Uptime.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/frontend/src/icon.ts b/frontend/src/icon.ts index e1e757c..98fcf1c 100644 --- a/frontend/src/icon.ts +++ b/frontend/src/icon.ts @@ -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 }; diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index 443db89..57a07e0 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -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" } diff --git a/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts index 47a467b..3f99e7e 100644 --- a/frontend/src/mixins/socket.ts +++ b/frontend/src/mixins/socket.ts @@ -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 = 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"); - terminal.write(res.buffer); + 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/pages/Compose.vue b/frontend/src/pages/Compose.vue index d190ce1..8186984 100644 --- a/frontend/src/pages/Compose.vue +++ b/frontend/src/pages/Compose.vue @@ -1,16 +1,224 @@ - diff --git a/frontend/src/pages/Console.vue b/frontend/src/pages/Console.vue index 3c66cb5..1b96e12 100644 --- a/frontend/src/pages/Console.vue +++ b/frontend/src/pages/Console.vue @@ -3,9 +3,13 @@

Console

-
-
+
+

+ Allowed commands: docker, ls, cd +

+ +
@@ -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 { diff --git a/frontend/src/pages/EditStack.vue b/frontend/src/pages/EditStack.vue deleted file mode 100644 index aadf393..0000000 --- a/frontend/src/pages/EditStack.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/frontend/src/router.ts b/frontend/src/router.ts index 9470552..e730314 100644 --- a/frontend/src/router.ts +++ b/frontend/src/router.ts @@ -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, }, ] diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss index 3f64be0..0823482 100644 --- a/frontend/src/styles/main.scss +++ b/frontend/src/styles/main.scss @@ -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"; diff --git a/frontend/src/util-frontend.ts b/frontend/src/util-frontend.ts index 1e30160..2b3f9ba 100644 --- a/frontend/src/util-frontend.ts +++ b/frontend/src/util-frontend.ts @@ -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) => { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index da84018..5521b42 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ }, root: "./frontend", build: { - outDir: "../dist", + outDir: "../frontend-dist", }, plugins: [ vue(), diff --git a/package.json b/package.json index c5dc78c..5dedf09 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e360fa..3c0c053 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==}