diff --git a/CHANGELOG b/CHANGELOG index adf7817..f718ed9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +# 1.6.9 + +- Add support for local envvars defined in [group]/env.kv in the format "KEY=value\n" +- Experiment with sessionId +- Don't ask about accessibility and environment when deleting envvars + # 1.6.6 - Fix .ssh/config when calling `register` multiple times diff --git a/package-lock.json b/package-lock.json index 1a4ad5d..27a5ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "@merrymake/cli", - "version": "1.6.0", + "version": "1.6.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@merrymake/cli", - "version": "1.6.0", + "version": "1.6.8", "license": "ISC", "dependencies": { "@merrymake/detect-project-type": "^1.2.0", "@merrymake/ext2mime": "^1.0.0", "express": "^4.17.3", + "express-session": "^1.17.3", "uuid": "^9.0.0" }, "bin": { @@ -20,6 +21,7 @@ }, "devDependencies": { "@types/express": "^4.17.13", + "@types/express-session": "^1.17.10", "@types/node": "^16.7.4", "@types/uuid": "^9.0.1", "pkg": "^5.8.1" @@ -219,6 +221,15 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.10.tgz", + "integrity": "sha512-U32bC/s0ejXijw5MAzyaV4tuZopCh/K7fPoUDyNbsRXHvPSeymygYD1RFL99YOLhF5PNOkzswvOTRaVHdL1zMw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", @@ -767,6 +778,32 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -1443,6 +1480,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1650,6 +1695,14 @@ } ] }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2138,6 +2191,17 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -2430,6 +2494,15 @@ "@types/send": "*" } }, + "@types/express-session": { + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.10.tgz", + "integrity": "sha512-U32bC/s0ejXijw5MAzyaV4tuZopCh/K7fPoUDyNbsRXHvPSeymygYD1RFL99YOLhF5PNOkzswvOTRaVHdL1zMw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/http-errors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", @@ -2851,6 +2924,28 @@ "vary": "~1.1.2" } }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, "fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3326,6 +3421,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3472,6 +3572,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3826,6 +3931,14 @@ "mime-types": "~2.1.24" } }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 4784515..9680118 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@merrymake/cli", - "version": "1.6.8", + "version": "1.6.9", "description": "", "main": "index.js", "scripts": { @@ -26,10 +26,12 @@ "@merrymake/detect-project-type": "^1.2.0", "@merrymake/ext2mime": "^1.0.0", "express": "^4.17.3", + "express-session": "^1.17.3", "uuid": "^9.0.0" }, "devDependencies": { "@types/express": "^4.17.13", + "@types/express-session": "^1.17.10", "@types/node": "^16.7.4", "@types/uuid": "^9.0.1", "pkg": "^5.8.1" diff --git a/questions.js b/questions.js index ca1a674..8c0442f 100644 --- a/questions.js +++ b/questions.js @@ -418,7 +418,10 @@ function envvar_key(org, group, overwrite, key, currentValue) { return __awaiter(this, void 0, void 0, function* () { try { let value = yield (0, prompt_1.shortText)("Value", "The value...", ""); - return envvar_key_value(org, group, overwrite, key, value); + if (value !== "") + return envvar_key_value(org, group, overwrite, key, value); + else + return envvar_key_value_access_visible(org, group, overwrite, key, value, ["--prod", "--test"], "--public"); } catch (e) { throw e; diff --git a/questions.ts b/questions.ts index f8c06de..69b624b 100644 --- a/questions.ts +++ b/questions.ts @@ -589,7 +589,18 @@ async function envvar_key( ) { try { let value = await shortText("Value", "The value...", ""); - return envvar_key_value(org, group, overwrite, key, value); + if (value !== "") + return envvar_key_value(org, group, overwrite, key, value); + else + return envvar_key_value_access_visible( + org, + group, + overwrite, + key, + value, + ["--prod", "--test"], + "--public" + ); } catch (e) { throw e; } diff --git a/simulator.js b/simulator.js index 610f2a7..475fc7a 100644 --- a/simulator.js +++ b/simulator.js @@ -20,6 +20,12 @@ const child_process_1 = require("child_process"); const detect_project_type_1 = require("@merrymake/detect-project-type"); const http_1 = __importDefault(require("http")); const prompt_1 = require("./prompt"); +const express_session_1 = __importDefault(require("express-session")); +const MILLISECONDS = 1; +const SECONDS = 1000 * MILLISECONDS; +const MINUTES = 60 * SECONDS; +const HOURS = 60 * MINUTES; +const DEFAULT_TIMEOUT = 5 * MINUTES; class Run { constructor(port) { this.port = port; @@ -30,6 +36,12 @@ class Run { let teams = (0, utils_1.directoryNames)(new utils_1.Path(pathToRoot), ["event-catalogue"]).map((x) => x.name); const app = (0, express_1.default)(); const server = http_1.default.createServer(app); + app.use((0, express_session_1.default)({ + secret: "something that is kept or meant to be kept unknown or unseen by others", + resave: false, + saveUninitialized: true, + cookie: { maxAge: 12 * HOURS }, + })); app.use((req, res, next) => { if (req.is("multipart/form-data") || req.is("application/x-www-form-urlencoded")) { @@ -40,12 +52,13 @@ class Run { } }); let hooks; - app.post("/trace/:traceId/:event", (req, res) => __awaiter(this, void 0, void 0, function* () { + app.post("/trace/:sessionId/:traceId/:event", (req, res) => __awaiter(this, void 0, void 0, function* () { try { let traceId = req.params.traceId; + let sessionId = req.params.sessionId; let event = req.params.event; let payload = req.body; - this.runService(pathToRoot, this.port, event, payload, traceId, hooks, req.headers["content-type"]); + this.runService(pathToRoot, this.port, event, payload, traceId, sessionId, hooks, req.headers["content-type"]); res.send("Done"); } catch (e) { @@ -60,9 +73,11 @@ class Run { let event = req.params.event; hooks = new PublicHooks(pathToRoot); let payload = Buffer.from(JSON.stringify(req.query)); - processFolders(pathToRoot, teams, hooks); - let traceId = "s" + Math.random(); - let response = yield this.runWithReply(pathToRoot, this.port, res, event, payload, traceId, hooks, req.headers["content-type"]); + processFolders(pathToRoot, null, teams, hooks); + loadLocalEnvvars(pathToRoot); + let traceId = "t" + Math.random(); + let sessionId = req.session.id; + let response = yield this.runWithReply(pathToRoot, this.port, res, event, payload, traceId, sessionId, hooks, req.headers["content-type"]); } catch (e) { if (e.data !== undefined) @@ -80,9 +95,11 @@ class Run { ? Buffer.from(JSON.stringify(req.body)) : Buffer.from(req.body) : req.body; - processFolders(pathToRoot, teams, hooks); - let traceId = "s" + Math.random(); - let response = yield this.runWithReply(pathToRoot, this.port, res, event, payload, traceId, hooks, req.headers["content-type"]); + processFolders(pathToRoot, null, teams, hooks); + loadLocalEnvvars(pathToRoot); + let traceId = "t" + Math.random(); + let sessionId = req.session.id; + let response = yield this.runWithReply(pathToRoot, this.port, res, event, payload, traceId, sessionId, hooks, req.headers["content-type"]); } catch (e) { if (e.data !== undefined) @@ -112,7 +129,7 @@ class Run { }); }); } - runService(pathToRoot, port, event, payload, traceId, hooks, contentType) { + runService(pathToRoot, port, event, payload, traceId, sessionId, hooks, contentType) { var _a; if (event === "$reply") { let rs = pendingReplies[traceId]; @@ -128,6 +145,7 @@ class Run { let envelope = JSON.stringify({ messageId, traceId, + sessionId, }); Object.keys(rivers).forEach((river) => { let services = rivers[river]; @@ -136,7 +154,7 @@ class Run { const args = [...rest, `'${service.action}'`, `'${envelope}'`]; const options = { cwd: service.dir, - env: Object.assign(Object.assign({}, process.env), { RAPIDS: `http://localhost:${port}/trace/${traceId}` }), + env: Object.assign(Object.assign(Object.assign({}, process.env), (envvars[service.group] || {})), { RAPIDS: `http://localhost:${port}/trace/${sessionId}/${traceId}` }), shell: "sh", }; if (process.env["DEBUG"]) @@ -155,13 +173,13 @@ class Run { }); }); } - runWithReply(pathToRoot, port, resp, event, payload, traceId, hooks, contentType) { + runWithReply(pathToRoot, port, resp, event, payload, traceId, sessionId, hooks, contentType) { return __awaiter(this, void 0, void 0, function* () { try { let rivers = hooks.riversFor(event); if (rivers === undefined) return reply(resp, HTTP.CLIENT_ERROR.NO_HOOKS, undefined); - this.runService(pathToRoot, port, event, payload, traceId, hooks, contentType); + this.runService(pathToRoot, port, event, payload, traceId, sessionId, hooks, contentType); pendingReplies[traceId] = { resp, replies: [] }; yield sleep(rivers.waitFor || MAX_WAIT); let pending = pendingReplies[traceId]; @@ -180,6 +198,7 @@ exports.Run = Run; const MAX_WAIT = 30000; const Reset = "\x1b[0m"; const FgRed = "\x1b[31m"; +let envvars = {}; let pendingReplies = {}; class PublicHooks { constructor(pathToRoot) { @@ -200,11 +219,7 @@ class PublicHooks { return this.hooks[event]; } } -const MILLISECONDS = 1; -const SECONDS = 1000 * MILLISECONDS; -const MINUTES = 60 * SECONDS; -const DEFAULT_TIMEOUT = 5 * MINUTES; -function processFolder(folder, hooks) { +function processFolder(group, folder, hooks) { if (fs_1.default.existsSync(`${folder}/mist.json`)) { let projectType; let cmd; @@ -232,6 +247,7 @@ function processFolder(folder, hooks) { hooks.register(event, river, { action, dir: folder.replace(/\/\//g, "/"), + group, cmd, }); }); @@ -263,19 +279,38 @@ function processFolder(folder, hooks) { hooks.register(event, river, { action, dir: folder.replace(/\/\//g, "/"), + group, cmd, }); }); } else if (!folder.endsWith(".DS_Store") && fs_1.default.lstatSync(folder).isDirectory()) { - processFolders(folder, fs_1.default.readdirSync(folder), hooks); + processFolders(folder, group, fs_1.default.readdirSync(folder), hooks); } } -function processFolders(prefix, folders, hooks) { +function processFolders(prefix, group, folders, hooks) { folders - .filter((x) => !x.startsWith("(deleted) ")) - .forEach((folder) => processFolder(prefix + folder + "/", hooks)); + .filter((x) => !x.startsWith("(deleted) ") && !x.endsWith(".DS_Store")) + .forEach((folder) => processFolder(group || folder, prefix + folder + "/", hooks)); +} +function loadLocalEnvvars(pathToRoot) { + fs_1.default.readdirSync(pathToRoot) + .filter((x) => !x.startsWith("(deleted) ") && !x.endsWith(".DS_Store")) + .forEach((group) => { + if (fs_1.default.existsSync(pathToRoot + "/" + group + "/env.kv")) { + envvars[group] = {}; + fs_1.default.readFileSync(pathToRoot + "/" + group + "/env.kv") + .toString() + .split(/\r?\n/) + .forEach((x) => { + if (!x.includes("=")) + return; + let b = x.split("="); + envvars[group][b[0]] = b[1]; + }); + } + }); } let spacerTimer; function timedOutput(str) { diff --git a/simulator.ts b/simulator.ts index 7da3b3d..5646974 100644 --- a/simulator.ts +++ b/simulator.ts @@ -10,6 +10,13 @@ import { } from "@merrymake/detect-project-type"; import http from "http"; import { YELLOW, NORMAL_COLOR } from "./prompt"; +import session from "express-session"; + +const MILLISECONDS = 1; +const SECONDS = 1000 * MILLISECONDS; +const MINUTES = 60 * SECONDS; +const HOURS = 60 * MINUTES; +const DEFAULT_TIMEOUT = 5 * MINUTES; export class Run { constructor(private port: number) {} @@ -24,6 +31,16 @@ export class Run { const app = express(); const server = http.createServer(app); + app.use( + session({ + secret: + "something that is kept or meant to be kept unknown or unseen by others", + resave: false, + saveUninitialized: true, + cookie: { maxAge: 12 * HOURS }, + }) + ); + app.use((req, res, next) => { if ( req.is("multipart/form-data") || @@ -37,9 +54,10 @@ export class Run { let hooks: PublicHooks; - app.post("/trace/:traceId/:event", async (req, res) => { + app.post("/trace/:sessionId/:traceId/:event", async (req, res) => { try { let traceId = req.params.traceId; + let sessionId = req.params.sessionId; let event = req.params.event; let payload: Buffer = req.body; this.runService( @@ -48,6 +66,7 @@ export class Run { event, payload, traceId, + sessionId, hooks, req.headers["content-type"] ); @@ -63,8 +82,10 @@ export class Run { let event = req.params.event; hooks = new PublicHooks(pathToRoot); let payload: Buffer = Buffer.from(JSON.stringify(req.query)); - processFolders(pathToRoot, teams, hooks); - let traceId = "s" + Math.random(); + processFolders(pathToRoot, null, teams, hooks); + loadLocalEnvvars(pathToRoot); + let traceId = "t" + Math.random(); + let sessionId = req.session.id; let response = await this.runWithReply( pathToRoot, this.port, @@ -72,6 +93,7 @@ export class Run { event, payload, traceId, + sessionId, hooks, req.headers["content-type"] ); @@ -90,8 +112,10 @@ export class Run { ? Buffer.from(JSON.stringify(req.body)) : Buffer.from(req.body) : req.body; - processFolders(pathToRoot, teams, hooks); - let traceId = "s" + Math.random(); + processFolders(pathToRoot, null, teams, hooks); + loadLocalEnvvars(pathToRoot); + let traceId = "t" + Math.random(); + let sessionId = req.session.id; let response = await this.runWithReply( pathToRoot, this.port, @@ -99,6 +123,7 @@ export class Run { event, payload, traceId, + sessionId, hooks, req.headers["content-type"] ); @@ -157,6 +182,7 @@ export class Run { event: string, payload: Buffer, traceId: string, + sessionId: string, hooks: PublicHooks, contentType: string | undefined ) { @@ -173,6 +199,7 @@ export class Run { let envelope = JSON.stringify({ messageId, traceId, + sessionId, }); Object.keys(rivers).forEach((river) => { let services = rivers[river]; @@ -183,7 +210,8 @@ export class Run { cwd: service.dir, env: { ...process.env, - RAPIDS: `http://localhost:${port}/trace/${traceId}`, + ...(envvars[service.group] || {}), + RAPIDS: `http://localhost:${port}/trace/${sessionId}/${traceId}`, }, shell: "sh", }; @@ -214,6 +242,7 @@ export class Run { event: string, payload: Buffer, traceId: string, + sessionId: string, hooks: PublicHooks, contentType: string | undefined ) { @@ -227,6 +256,7 @@ export class Run { event, payload, traceId, + sessionId, hooks, contentType ); @@ -248,7 +278,10 @@ const MAX_WAIT = 30000; const Reset = "\x1b[0m"; const FgRed = "\x1b[31m"; +let envvars: { [group: string]: { [key: string]: string } } = {}; + interface Hook { + group: string; dir: string; cmd: string; action: string; @@ -294,12 +327,7 @@ class PublicHooks { } } -const MILLISECONDS = 1; -const SECONDS = 1000 * MILLISECONDS; -const MINUTES = 60 * SECONDS; -const DEFAULT_TIMEOUT = 5 * MINUTES; - -function processFolder(folder: string, hooks: PublicHooks) { +function processFolder(group: string, folder: string, hooks: PublicHooks) { if (fs.existsSync(`${folder}/mist.json`)) { let projectType: ProjectType; let cmd: string; @@ -327,6 +355,7 @@ function processFolder(folder: string, hooks: PublicHooks) { hooks.register(event, river, { action, dir: folder.replace(/\/\//g, "/"), + group, cmd, }); }); @@ -357,6 +386,7 @@ function processFolder(folder: string, hooks: PublicHooks) { hooks.register(event, river, { action, dir: folder.replace(/\/\//g, "/"), + group, cmd, }); }); @@ -364,14 +394,39 @@ function processFolder(folder: string, hooks: PublicHooks) { !folder.endsWith(".DS_Store") && fs.lstatSync(folder).isDirectory() ) { - processFolders(folder, fs.readdirSync(folder), hooks); + processFolders(folder, group, fs.readdirSync(folder), hooks); } } -function processFolders(prefix: string, folders: string[], hooks: PublicHooks) { +function processFolders( + prefix: string, + group: string | null, + folders: string[], + hooks: PublicHooks +) { folders - .filter((x) => !x.startsWith("(deleted) ")) - .forEach((folder) => processFolder(prefix + folder + "/", hooks)); + .filter((x) => !x.startsWith("(deleted) ") && !x.endsWith(".DS_Store")) + .forEach((folder) => + processFolder(group || folder, prefix + folder + "/", hooks) + ); +} + +function loadLocalEnvvars(pathToRoot: string) { + fs.readdirSync(pathToRoot) + .filter((x) => !x.startsWith("(deleted) ") && !x.endsWith(".DS_Store")) + .forEach((group) => { + if (fs.existsSync(pathToRoot + "/" + group + "/env.kv")) { + envvars[group] = {}; + fs.readFileSync(pathToRoot + "/" + group + "/env.kv") + .toString() + .split(/\r?\n/) + .forEach((x) => { + if (!x.includes("=")) return; + let b = x.split("="); + envvars[group][b[0]] = b[1]; + }); + } + }); } let spacerTimer: undefined | NodeJS.Timeout;