@runeya/runeya
Version:
Monitor processes as a stack
1,289 lines (1,261 loc) • 200 kB
JavaScript
#!/usr/bin/env node
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __typeError = (msg) => {
throw TypeError(msg);
};
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
// ../../common/socket-server/src/CustomObservable.js
var require_CustomObservable = __commonJS({
"../../common/socket-server/src/CustomObservable.js"(exports2, module2) {
"use strict";
function CustomObservable() {
this.funcs = [];
}
CustomObservable.prototype.subscribe = function(fun) {
this.funcs.push(fun);
};
CustomObservable.prototype.next = function(...value) {
this.funcs.forEach((f) => f(...value));
};
CustomObservable.prototype.off = function(fun) {
this.funcs = this.funcs.filter((f) => f !== fun);
};
CustomObservable.prototype.destroy = function() {
this.funcs = [];
};
module2.exports = CustomObservable;
}
});
// ../../common/socket-server/src/sockets.js
var require_sockets = __commonJS({
"../../common/socket-server/src/sockets.js"(exports2, module2) {
"use strict";
var WebSocket = require("ws");
var CustomObservable = require_CustomObservable();
var events = {};
module2.exports = {
/** @type {WebSocket.Server | null} */
io: null,
emit(channel, ...data) {
events[channel]?.next(...data);
this.io?.clients.forEach((ws) => {
ws.send(JSON.stringify({ channel, data }));
});
},
on: (event, cb) => {
if (!events[event]) events[event] = new CustomObservable();
events[event].subscribe(cb);
},
off(event, cb) {
events[event]?.off(cb);
},
/**
*
* @param {*} server
*/
connect(server) {
this.io = new WebSocket.Server({ noServer: true, path: "/socket" });
this.io.on("connection", function message(ws) {
ws.on("message", function message2(event) {
const { channel, data } = JSON.parse(event?.toString());
events[channel]?.next(...data);
});
});
const self = this;
server.on("upgrade", function upgrade(request, socket, head) {
if (request.url === "/socket") {
self.io?.handleUpgrade(request, socket, head, function done(ws) {
self.io?.emit("connection", ws, request);
});
}
});
}
};
}
});
// ../../common/socket-server/src/index.js
var require_src = __commonJS({
"../../common/socket-server/src/index.js"(exports2, module2) {
"use strict";
module2.exports = {
sockets: require_sockets()
};
}
});
// helpers/args.js
var require_args = __commonJS({
"helpers/args.js"(exports2, module2) {
"use strict";
var path = require("path");
var yargs = require("yargs/yargs");
var { hideBin } = require("yargs/helpers");
var { existsSync } = require("fs");
var yarg = yargs(hideBin(process.argv)).usage("Usage: <path-to-your-stack> [options]").alias("pe", "pull-env").describe("pe", "Pull env from a service (need --environment and --service)").default("pe", false).alias("e", "environment").describe("e", "Choose your environment").default("e", void 0).alias("s", "service").describe("s", "Service").default("s", void 0).alias("ss", "services").describe("ss", "Services").default("ss", []).boolean(["pe"]).string(["e", "s"]).array(["ss"]).help("h").alias("h", "help").parse();
var rootPath = path.resolve(yarg["_"]?.[0] || ".");
var args2 = Object.assign({
rootPath,
initialCwd: "",
runeyaConfigPath: path.resolve(rootPath, ".runeya"),
runeyaGlobalConfigPath: path.resolve(require("os").homedir(), ".runeya-global")
}, yarg);
if (!existsSync(args2.rootPath)) {
console.error("Error: Runeya was launched with an invalid root path that does not exist. Please check your launch command. \nPath: ", args2.rootPath);
process.exit(1);
}
if (args2.rootPath) {
args2.initialCwd = process.cwd();
process.chdir(args2.rootPath);
}
if (args2.pullEnv && !args2.service) {
console.error("Error: --service needed");
process.exit(1);
}
if (args2.pullEnv && !args2.environment) {
console.error("Error: --environment needed");
process.exit(1);
}
module2.exports = args2;
}
});
// helpers/migrateStackMonitor.js
var require_migrateStackMonitor = __commonJS({
"helpers/migrateStackMonitor.js"(exports2, module2) {
"use strict";
var pathfs = require("path");
var { existsSync } = require("fs");
var { cp } = require("fs/promises");
var args2 = require_args();
module2.exports = async function migrateStackMonitor2() {
const localLegacyPath = pathfs.resolve(args2.runeyaConfigPath, "../.stackmonitor");
const localNewPath = args2.runeyaConfigPath;
if (existsSync(localLegacyPath) && !existsSync(localNewPath)) {
console.log("Legacy path found, copy to new path");
await cp(localLegacyPath, localNewPath, { recursive: true, force: true });
}
const globalLegacyPath = pathfs.resolve(args2.runeyaGlobalConfigPath, "../.stackmonitor");
const globalNewPath = args2.runeyaGlobalConfigPath;
if (existsSync(globalLegacyPath) && !existsSync(globalNewPath)) {
console.log("Legacy path found, copy to new path");
await cp(globalLegacyPath, globalNewPath, { recursive: true, force: true });
}
if (existsSync(localLegacyPath) && !existsSync(pathfs.resolve(args2.runeyaConfigPath, "dbs/overrides"))) {
console.log("Legacy path found, copy to new path");
await cp(pathfs.resolve(localLegacyPath, "dbs/overrides"), pathfs.resolve(args2.runeyaConfigPath, "dbs/overrides"), { recursive: true, force: true });
}
if (existsSync(localLegacyPath) && !existsSync(pathfs.resolve(args2.runeyaConfigPath, "dbs/encryption-key.json"))) {
console.log("Legacy path found, copy to new path");
await cp(pathfs.resolve(localLegacyPath, "dbs/encryption-key.json"), pathfs.resolve(args2.runeyaConfigPath, "dbs/encryption-key.json"), { recursive: true, force: true });
}
};
}
});
// helpers/conflictStorage.js
var require_conflictStorage = __commonJS({
"helpers/conflictStorage.js"(exports2, module2) {
"use strict";
var pendingConflicts = [];
function storeConflict(conflict) {
const conflictId = Date.now().toString();
pendingConflicts.push({
id: conflictId,
timestamp: Date.now(),
...conflict
});
if (pendingConflicts.length > 20) {
pendingConflicts.shift();
}
return conflictId;
}
function getPendingConflicts() {
return pendingConflicts;
}
function removeConflict(conflictId) {
const index = pendingConflicts.findIndex((c) => c.id === conflictId);
if (index !== -1) {
pendingConflicts.splice(index, 1);
}
}
module2.exports = {
storeConflict,
getPendingConflicts,
removeConflict
};
}
});
// helpers/reencrypt-nodered.js
var require_reencrypt_nodered = __commonJS({
"helpers/reencrypt-nodered.js"(exports2, module2) {
"use strict";
var crypto2 = require("crypto");
var { readFile, writeFile } = require("fs/promises");
var encryptionAlgorithm = "aes-256-ctr";
module2.exports = async function decryptCreds(oldSecret, newScret, path) {
const oldKey = crypto2.createHash("sha256").update(oldSecret).digest();
const newKey = crypto2.createHash("sha256").update(newScret).digest();
const cipher = JSON.parse(await readFile(path, "utf-8"));
let flows = cipher["$"];
const vector = Buffer.from(flows.substring(0, 32), "hex");
flows = flows.substring(32);
const decipher = crypto2.createDecipheriv(encryptionAlgorithm, oldKey, vector);
const decrypted = decipher.update(flows, "base64", "utf8") + decipher.final("utf8");
const newVector = crypto2.randomBytes(16);
const newCipher = crypto2.createCipheriv(encryptionAlgorithm, newKey, newVector);
const encrypted = newCipher.update(decrypted, "utf8", "base64") + newCipher.final("base64");
await writeFile(path, JSON.stringify({
"$": newVector.toString("hex") + encrypted
}, null, 2), "utf-8");
};
}
});
// ../../modules/bugs/backend/routes.js
var require_routes = __commonJS({
"../../modules/bugs/backend/routes.js"(exports2, module2) {
"use strict";
var express = require("express");
var router = express.Router();
var pathfs = require("path");
var { fork } = require("child_process");
module2.exports = (runeya) => {
router.get("/bugs/:service", async (req, res) => {
const service = runeya.findService(req.params.service);
if (!service) return res.status(404).send("SERVICE_NOT_FOUND");
const ts = fork(pathfs.resolve(__dirname, "checkJsFork"));
ts.on("message", (results) => {
res.json(results);
ts.kill("SIGKILL");
});
ts.send(req.query.cwd || service.getRootPath() || __dirname);
return null;
});
return router;
};
}
});
// ../../modules/bugs/backend/index.js
var require_backend = __commonJS({
"../../modules/bugs/backend/index.js"(exports2, module2) {
"use strict";
var { existsSync } = require("fs");
var pathfs = require("path");
var plugin = {
enabled: true,
name: "Bugs",
displayName: "Bugs",
description: "Find bugs across a whole javascript project",
icon: "fas fa-bug",
placements: ["service"],
order: 4,
export: null,
hidden: async (service) => {
if (!service) return true;
const npm = new service.Stack.npm(service);
const serviceIsNpm = !!npm.getNpmPaths(service);
return !serviceIsNpm;
},
routes: require_routes()
};
module2.exports = plugin;
}
});
// ../../modules/configuration/backend/index.js
var require_backend2 = __commonJS({
"../../modules/configuration/backend/index.js"(exports2, module2) {
"use strict";
var plugin = {
enabled: true,
name: "Configuration",
displayName: "Configuration",
description: "Show all configurations used to launch the given service",
icon: "fas fa-cog",
export: null,
placements: ["service"],
order: 5
};
module2.exports = plugin;
}
});
// ../../modules/documentation/backend/Documentation.js
var require_Documentation = __commonJS({
"../../modules/documentation/backend/Documentation.js"(exports2, module2) {
"use strict";
var PromiseB2 = require("bluebird");
var { randomUUID: randomUUID2 } = require("crypto");
var dbs2 = require_dbs();
var db = dbs2.getDb(`documentations`);
var dbDocumentationTree = dbs2.getDb(`documentations-tree`, { encrypted: true, defaultData: [] }).alasql;
var Documentation = class {
/**
* @param {import('@runeya/common-typings').NonFunctionProperties<Documentation>} documentation
*/
constructor(documentation) {
this.id = documentation.id || "";
this.text = documentation.text || "";
}
static async load(id) {
}
static async all() {
return db.alasql.select("Select * from ?");
}
static async find(envId) {
const documentations = await this.all();
return documentations.find((env) => env.id === envId);
}
async save() {
const obj = this.toStorage();
await dbs2.getDb(`documentations/${this.id}`).write(obj);
return this;
}
async update(env) {
this.transform = env.transform;
this.label = env.label;
await dbs2.getDb(`documentations/${this.id}`).write(this.toStorage());
}
async delete() {
await dbs2.getDb(`documentations/${this.id}`).delete();
}
toStorage() {
return {
id: this.id,
label: this.label,
transform: this.transform
};
}
};
module2.exports = Documentation;
}
});
// ../../modules/documentation/backend/Leaf.js
var require_Leaf = __commonJS({
"../../modules/documentation/backend/Leaf.js"(exports2, module2) {
"use strict";
var PromiseB2 = require("bluebird");
var { randomUUID: randomUUID2 } = require("crypto");
var dbs2 = require_dbs();
var { v4 } = require("uuid");
var dbLeafTree = dbs2.getDb(`leafs-leafs`, { encrypted: true, defaultData: [] }).alasql;
var Leaf = class _Leaf {
/**
* @param {import('@runeya/common-typings').NonFunctionProperties<Leaf>} leaf
*/
constructor(leaf) {
this.id = leaf.id || v4();
this.docId = leaf.docId || "";
this.serviceId = leaf.serviceId || "";
this.label = leaf.label || "";
this.position = leaf.position || -1;
this.text = leaf.text || "";
this.parentId = leaf.parentId || "";
}
static async getTree(serviceLabel) {
const leafs = await dbLeafTree.read(`Select * from ${dbLeafTree.table} ${serviceLabel ? `where serviceId = '${serviceLabel}'` : `where serviceId=''`}`);
return leafs;
}
toStorage() {
return {
docId: this.docId,
position: this.position,
serviceId: this.serviceId,
label: this.label,
text: this.text,
parentId: this.parentId
};
}
async remove() {
return dbLeafTree.write(`DELETE from ${dbLeafTree.table} where id = '${this.id}'`);
}
static async find({ id }) {
const [leaf] = await dbLeafTree.read(`Select * from ${dbLeafTree.table} where id='${id}'`);
return leaf ? new _Leaf(leaf) : null;
}
async save() {
const leafExists = await _Leaf.find({ id: this.id });
const storage = this.toStorage();
const set = dbLeafTree.buildUpdateQuery(storage);
await leafExists ? dbLeafTree.write(`update ${dbLeafTree.table} set ${set} where id='${this.id}'`) : dbLeafTree.write(`insert into ${dbLeafTree.table} (id, docId, position, serviceId, label, text, parentId) values ('${this.id}', '${this.docId}', ${this.position}, '${this.serviceId}', '${this.label.replace(/'/g, "''")}', '${this.text.replace(/'/g, "''")}', '${this.parentId}')`);
return this;
}
};
module2.exports = Leaf;
}
});
// ../../modules/documentation/backend/routes.js
var require_routes2 = __commonJS({
"../../modules/documentation/backend/routes.js"(exports2, module2) {
"use strict";
var express = require("express");
var Leaf = require_Leaf();
var PromiseB2 = require("bluebird");
var router = express.Router();
module2.exports = (runeya) => {
const { findService } = runeya;
router.get("/documentation/tree", async (req, res) => {
const service = findService(req.query.serviceId?.toString() || "");
const result = await Leaf.getTree(service?.label);
return res.send(result);
});
router.post("/documentation/tree/sort", async (req, res) => {
const leafs = await PromiseB2.mapSeries(req.body, async (_leaf, index) => {
const leaf = await Leaf.find({ id: _leaf.id });
if (!leaf) return;
leaf.position = index;
await leaf.save();
return leaf;
});
return res.send(leafs);
});
router.post("/documentation/tree", async (req, res) => {
const result = await new Leaf({
...req.body
}).save();
return res.send(result);
});
router.post("/documentation/tree/:key", async (req, res) => {
const result = await new Leaf({
...req.body,
id: req.params.key
}).save();
return res.send(result);
});
router.delete("/documentation/tree/:key", async (req, res) => {
const result = await Leaf.find({
id: req.params.key
});
if (result) return res.send(await result.remove());
return res.send();
});
return router;
};
}
});
// ../../modules/documentation/backend/index.js
var require_backend3 = __commonJS({
"../../modules/documentation/backend/index.js"(exports2, module2) {
"use strict";
var { readFile } = require("fs/promises");
var PromiseB2 = require("bluebird");
var pathfs = require("path");
var Documentation = require_Documentation();
var plugin = {
enabled: true,
name: "Documentation",
displayName: "Documentation",
description: "Read documentation for a given service",
icon: "fas fa-book",
// export: Documentation,
placements: ["service"],
order: 6,
routes: require_routes2(),
finder: async (search, runeya) => {
}
};
module2.exports = plugin;
}
});
// ../../modules/finder/backend/routes.js
var require_routes3 = __commonJS({
"../../modules/finder/backend/routes.js"(exports2, module2) {
"use strict";
var express = require("express");
var router = express.Router();
var PromiseB2 = require("bluebird");
function pluginToUrl(plugin) {
for (let i = 0; i < plugin.placements.length; i += 1) {
const placement = plugin.placements[i];
if (typeof placement !== "string") {
if (placement.position === "toolbox") {
return `/toolbox${placement.goTo?.path || placement.goTo}`;
}
if (placement.position === "sidebar") {
return `${placement.goTo?.path || placement.goTo}`;
}
}
}
return "";
}
var routes = (runeya) => {
const {
getServices,
helpers: { searchString }
} = runeya;
router.get("/finder/search", async (req, res) => {
const search = req.query.q?.toString()?.toUpperCase() || "";
const services = getServices().filter((service) => searchString(service?.label, search)).map((service) => ({
title: service.label,
description: service.description,
group: "Service",
url: `/stack-single/${service.label}`
}));
const { plugins: plugins2 } = runeya;
const _plugins = (await PromiseB2.map(Object.keys(plugins2), (key) => plugins2[key]).map(async (plugin) => [
...await plugin?.finder?.(search, runeya)?.catch?.(() => []) || [],
...searchString(plugin.name, search) ? [{
title: plugin.displayName || plugin.name,
description: plugin.description,
group: "Plugin",
icon: plugin.icon,
url: pluginToUrl(plugin) || ""
}] : []
])).flat().filter((a) => a?.url);
const result = [
...services,
..._plugins
].filter((a) => a);
res.send(result);
});
return router;
};
module2.exports = routes;
}
});
// ../../modules/finder/backend/index.js
var require_backend4 = __commonJS({
"../../modules/finder/backend/index.js"(exports2, module2) {
"use strict";
var commandExists = require("command-exists");
var plugin = {
enabled: true,
name: "Finder",
displayName: "Finder",
description: "Find all you want inside this app",
icon: "fab fa-git-alt",
export: null,
order: -1,
placements: ["global", {
position: "sidebar",
label: "Finder",
icon: "fas fa-search",
goTo: { path: "/Finder" },
active: "Finder"
}],
hidden: () => commandExists("git").then(() => false).catch(() => true),
routes: require_routes3()
};
module2.exports = plugin;
}
});
// helpers/exec.js
var require_exec = __commonJS({
"helpers/exec.js"(exports2, module2) {
"use strict";
var { exec } = require("child_process");
module2.exports = {
/**
* @param {string} cmd
* @param {import('child_process').ExecOptions} options
* @returns {Promise<string>}
*/
execAsync(cmd, options) {
return new Promise((res, rej) => {
exec(cmd, options, (err, stdout, stderr) => {
if (err) return rej(stderr || err);
return res(stdout);
});
});
},
/**
* @param {string} cmd
* @param {import('child_process').ExecOptions} options
* @returns {Promise<string>}
*/
execAsyncWithoutErr(cmd, options) {
return new Promise((res) => {
exec(cmd, options, (err, stdout) => {
res(stdout);
});
});
},
/**
* @param {string} cmd
* @param {import('child_process').ExecOptions} options
* @returns {Promise<string>}
*/
execAsyncGetError(cmd, options) {
return new Promise((res) => {
exec(cmd, options, (err, stdout, stderr) => {
res(stderr);
});
});
}
};
}
});
// ../../common/express-http-error/src/HTTPError.js
var require_HTTPError = __commonJS({
"../../common/express-http-error/src/HTTPError.js"(exports2, module2) {
"use strict";
var dayjs = require("dayjs");
var { v4: uuid } = require("uuid");
var statusMessage = {
403: "Not allowed",
404: "Resource not found",
500: "We cannot respond to your request for moment. Contact support for more information"
};
module2.exports.statusMessage = statusMessage;
var HTTPError = class extends Error {
/**
*
* @param {string} message
* @param {number | string} code
* @param {string} errorId
* @param {string} date
* @param {string} stack
*/
constructor(message, code = 500, errorId = uuid(), date = dayjs().format("YYYY-MM-DD HH:mm:ss"), stack = "") {
super(message);
this.code = code || 500;
this.errorId = errorId;
this.date = date;
this.message = process.env.NODE_ENV === "production" ? statusMessage[this.code] || message?.toString() || message : message?.toString() || message || statusMessage[this.code];
this.originalMessage = message;
this.originalStack = stack || new Error().stack;
}
};
module2.exports = HTTPError;
}
});
// ../../common/express-http-error/src/index.js
var require_src2 = __commonJS({
"../../common/express-http-error/src/index.js"(exports2, module2) {
"use strict";
var HTTPError = require_HTTPError();
module2.exports = HTTPError;
}
});
// ../../modules/git/backend/Git.js
var require_Git = __commonJS({
"../../modules/git/backend/Git.js"(exports2, module2) {
"use strict";
var { existsSync } = require("fs");
var pathfs = require("path");
var { execAsync, execAsyncWithoutErr } = require_exec();
var HTTPError = require_src2();
var Git = (runeya) => {
const { findService } = runeya;
const searchGit = (path) => {
if (existsSync(pathfs.resolve(path, ".git"))) return path;
const parentPath = pathfs.resolve(path, "..");
if (parentPath === pathfs.resolve("/")) return null;
return searchGit(parentPath);
};
const getGitRootPath = (service) => searchGit(service.getRootPath());
async function requirements(service) {
if (!service) throw new Error("Git Error: Service not found");
if (!service.git) throw new Error(`Git Error - ${service?.label}: Git option not set`);
const path = getGitRootPath(service);
if (!path) return false;
if (!existsSync(path)) return false;
return true;
}
return {
/**
* @param {string} serviceName
* @param {{graphOnAll?: boolean}} param1
*/
async getGraph(serviceName, { graphOnAll } = {}) {
const service = findService(serviceName);
if (!await requirements(service)) return [];
const cmd = `git -c color.ui=always log --decorate=full --oneline --graph ${graphOnAll ? "--all" : ""} -500`;
const result = await execAsync(cmd, { cwd: getGitRootPath(service), env: process.env });
return result.split("\n");
},
/** @param {string} serviceName */
async getBranches(serviceName, fetch = false) {
const service = findService(serviceName);
if (!await requirements(service)) return [];
if (fetch) await this.fetch(serviceName);
const origin = await this.getOrigin(serviceName);
const unmergeableBranches = ["dev", "develop", "main", "master"];
const currentBranch = await this.getCurrentBranch(service.label);
const mergedBranches = await execAsyncWithoutErr("git branch --no-color --merged develop", { cwd: getGitRootPath(service) }).then((branches) => branches.trim().split("\n").map((a) => a.replace("*", "").trim()));
const localBranches = await execAsyncWithoutErr("git branch --no-color", { cwd: getGitRootPath(service) }).then((res) => res.toString().trim().split("\n").map((branch) => {
const name = branch.replace("*", "").trim();
return {
name,
isRemote: false,
isCurrentBranch: currentBranch === name,
canDelete: !unmergeableBranches.includes(name) && currentBranch !== name,
merged: mergedBranches.includes(name) && !unmergeableBranches.includes(name) && currentBranch !== name
};
}));
const remoteBranches = await execAsyncWithoutErr("git branch --no-color -r", { cwd: getGitRootPath(service) }).then((res) => res.toString().trim().split("\n").map((branch) => {
const name = branch.replace("*", "").trim();
return {
name: name.replace(`${origin}/`, ""),
isRemote: true,
canDelete: false,
merged: false
};
}).filter((remoteBranch) => !localBranches.find((localBranch) => remoteBranch.name === localBranch.name)));
return [
...localBranches,
...remoteBranches
];
},
/** @param {string} serviceName */
async getCurrentBranch(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "";
return (await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: getGitRootPath(service) }).catch((err) => {
}))?.trim();
},
/**
*
* @param {string} serviceName
* @param {string} branchName
* @param {boolean} shouldPush
* @returns
*/
async addBranch(serviceName, branchName, shouldPush = false) {
if (!branchName) throw new Error("Branch name is empty");
const service = findService(serviceName);
if (!await requirements(service)) return "";
const res = (await execAsync(`git checkout -b ${branchName}`, { cwd: getGitRootPath(service) }))?.trim();
if (shouldPush) await this.push(serviceName);
return res;
},
/** @param {string} serviceName */
async getStatus(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return [];
return execAsyncWithoutErr("git -c color.status=no status -s", { cwd: getGitRootPath(service) }).then((res) => res.toString().trim().split("\n")?.filter((a) => a));
},
/** @param {string} serviceName */
async getDiff(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "";
return execAsync("git diff --minimal", { cwd: getGitRootPath(service) });
},
/**
* @param {string} serviceName
* @param {string} branchName
*/
async changeBranch(serviceName, branchName) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
await execAsync(`git checkout ${branchName}`, { cwd: getGitRootPath(service) });
return "ok";
},
/**
* @param {string} serviceName
* @param {string} branchName
*/
async deleteBranch(serviceName, branchName) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
await execAsync(`git branch --delete ${branchName}`, { cwd: getGitRootPath(service) });
return "ok";
},
/**
* @param {string} serviceName
* @param {string} branchName
*/
async remoteDelta(serviceName, branchName) {
const service = findService(serviceName);
if (!await requirements(service)) return 0;
const upstream = await execAsync("git rev-parse --abbrev-ref --symbolic-full-name @{u}", { cwd: getGitRootPath(service) }).catch(() => {
});
if (!upstream) {
throw new HTTPError("BRANCH_NOT_PUSHED", 500.12578);
}
await execAsync(`git fetch origin ${branchName}`, { cwd: getGitRootPath(service) });
const localCommit = await execAsync(`git log --oneline ${branchName}`, { cwd: getGitRootPath(service) });
const remoteCommit = await execAsync(`git log --oneline origin/${branchName}`, { cwd: getGitRootPath(service) });
return localCommit.trim().split("\n").length - remoteCommit.trim().split("\n").length;
},
/** @param {string} serviceName */
async fetch(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
await execAsync("git fetch", { cwd: getGitRootPath(service) });
return "ok";
},
/** @param {string} serviceName */
async reset(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
await execAsync("git reset --hard", { cwd: getGitRootPath(service) });
return "ok";
},
/** @param {string} serviceName */
async pull(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
const origin = await this.getOrigin(serviceName);
const currentBranch = await this.getCurrentBranch(service.label);
await execAsync(`git pull ${origin} ${currentBranch}`, { cwd: getGitRootPath(service) });
return "ok";
},
/** @param {string} serviceName */
async getOrigin(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "";
return (await execAsync("git remote -v", { cwd: getGitRootPath(service) })).trim().split("\n").find((a) => a?.includes("fetch"))?.split(" ")[0]?.trim() || "";
},
/** @param {string} serviceName */
async push(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
const origin = await this.getOrigin(serviceName);
const currentBranch = await this.getCurrentBranch(service.label);
await execAsync(`git push ${origin} ${currentBranch}`, { cwd: getGitRootPath(service) });
return "ok";
},
/** @param {string} serviceName */
async stash(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
await execAsync("git add .", { cwd: getGitRootPath(service) });
await execAsync("git stash", { cwd: getGitRootPath(service) });
return "ok";
},
/** @param {string} serviceName */
async stashPop(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
await execAsync("git stash pop", { cwd: getGitRootPath(service) });
await execAsync("git reset HEAD", { cwd: getGitRootPath(service) });
return "ok";
},
/** @param {string} serviceName */
async stashList(serviceName) {
const service = findService(serviceName);
if (!await requirements(service)) return "";
const list = await execAsync("git stash show", { cwd: getGitRootPath(service) }).catch(() => "");
return list;
},
/**
* @param {string} serviceName
* @param {string} filePath
*/
async checkoutFile(serviceName, filePath) {
const service = findService(serviceName);
if (!await requirements(service)) return "ko";
await execAsync(`git checkout ${filePath}`, { cwd: getGitRootPath(service) });
return "ok";
}
};
};
module2.exports = Git;
}
});
// ../../modules/git/backend/routes.js
var require_routes4 = __commonJS({
"../../modules/git/backend/routes.js"(exports2, module2) {
"use strict";
var express = require("express");
var router = express.Router();
module2.exports = (runeya) => {
const { git } = runeya;
router.get("/git/:service/graph", async (req, res) => {
const graph = await git.getGraph(req.params.service, { graphOnAll: req.query.graphOnAll === "true" });
res.json(graph);
});
router.get("/git/:service/branches", async (req, res) => {
const branches = await git.getBranches(req.params.service);
res.json(branches);
});
router.get("/git/:service/status", async (req, res) => {
const status = await git.getStatus(req.params.service);
res.json(status);
});
router.get("/git/:service/diff", async (req, res) => {
const diff = await git.getDiff(req.params.service);
res.json(diff);
});
router.post("/git/:service/branch/:branchName/change", async (req, res) => {
await git.changeBranch(req.params.service, req.params.branchName).then((result) => res.json(result));
});
router.delete("/git/:service/branch/:branchName", async (req, res) => {
await git.deleteBranch(req.params.service, req.params.branchName).then((result) => res.json(result));
});
router.get("/git/:service/branch/:branchName/remote-delta", async (req, res) => {
await git.remoteDelta(req.params.service, req.params.branchName).then((result) => res.json(result));
});
router.post("/git/:service/fetch", async (req, res) => {
await git.fetch(req.params.service).then((result) => res.json(result));
});
router.delete("/git/:service/reset", async (req, res) => {
await git.reset(req.params.service).then((result) => res.json(result));
});
router.get("/git/:service/current-branch", async (req, res) => {
const currentBranch = await git.getCurrentBranch(req.params.service);
res.json(currentBranch);
});
router.post("/git/:service/add-branch", async (req, res) => {
const currentBranch = await git.addBranch(req.params.service, req.body.name, !!req.body.shouldPush);
res.json(currentBranch);
});
router.post("/git/:service/pull", async (req, res) => {
await git.pull(req.params.service).then((result) => res.json(result));
});
router.post("/git/:service/stash", async (req, res) => {
await git.stash(req.params.service).then((result) => res.json(result));
});
router.post("/git/:service/stash-pop", async (req, res) => {
await git.stashPop(req.params.service).then((result) => res.json(result));
});
router.post("/git/:service/stash-list", async (req, res) => {
await git.stashList(req.params.service).then((result) => res.json(result));
});
router.delete("/git/:service/checkout/:file", async (req, res) => {
await git.checkoutFile(req.params.service, req.params.file.trim()).then((result) => res.json(result));
});
return router;
};
}
});
// ../../modules/git/backend/index.js
var require_backend5 = __commonJS({
"../../modules/git/backend/index.js"(exports2, module2) {
"use strict";
var commandExists = require("command-exists");
var plugin = {
enabled: true,
name: "Git",
displayName: "Git",
description: "View and manage git across whole projects",
icon: "fab fa-git-alt",
export: require_Git(),
placements: ["service", {
position: "sidebar",
label: "GIT",
icon: "fab fa-git-alt",
goTo: { path: "/Git-NotUpToDate" },
active: "Git-NotUpToDate"
}],
order: 2,
hidden: (service, stack, placement) => {
if (placement === "sidebar") return commandExists("git").then(() => false).catch(() => true);
if (placement === "service" && service?.git?.remote) return false;
return true;
},
routes: require_routes4()
};
module2.exports = plugin;
}
});
// ../../modules/github/backend/routes.js
var require_routes5 = __commonJS({
"../../modules/github/backend/routes.js"(exports2, module2) {
"use strict";
var express = require("express");
var { Octokit } = require("fix-esm").require("@octokit/core");
var { restEndpointMethods } = require("fix-esm").require("@octokit/plugin-rest-endpoint-methods");
var router = express.Router();
var MyOctokit = Octokit.plugin(restEndpointMethods);
var octokit = process.env.RUNEYA_GH_APIKEY ? new MyOctokit({ auth: process.env.RUNEYA_GH_APIKEY }) : null;
module2.exports = (runeya) => {
const { findService } = runeya;
router.get("/github/service/:label/ready", async (req, res) => {
requirements();
if (!octokit) return null;
const { data: { login } } = await octokit.rest.users.getAuthenticated();
return res.send(login);
});
router.post("/github/service/:label/apikey", async (req, res) => {
const { apikey } = req.body;
if (!apikey) return res.status(400).send("apikey not found in body");
const _octokit = new Octokit.Octokit({ auth: apikey });
const { data: { login } } = await _octokit.rest.users.getAuthenticated();
octokit = _octokit;
return res.send(login);
});
router.get("/github/service/:label/whoami", async (req, res) => {
requirements();
if (!octokit) return;
const { data: { login } } = await octokit.rest.users.getAuthenticated();
res.json({ loggedAs: login });
});
router.get("/github/service/:label/pull-requests", async (req, res) => {
const service = findService(req.params.label);
if (!service) return res.status(404).send("Service not found");
const [owner, repo] = `${new URL(service.git.home).pathname.replace(".git", "")}`.split("/").slice(-2);
requirements();
if (!octokit) return null;
const { data } = await octokit.rest.pulls.list({ owner, repo });
return res.json(data);
});
return router;
};
function requirements() {
if (!octokit && !process.env.RUNEYA_GH_APIKEY) {
throw new Error("github api is not initialized");
} else if (!octokit) {
octokit = new MyOctokit({ auth: process.env.RUNEYA_GH_APIKEY });
}
}
}
});
// ../../modules/github/backend/index.js
var require_backend6 = __commonJS({
"../../modules/github/backend/index.js"(exports2, module2) {
"use strict";
var plugin = {
enabled: true,
name: "Github",
displayName: "Github",
description: "View pull requests for a given service",
icon: "fab fa-github",
export: null,
placements: ["service"],
order: 3,
/** @param {import('../../../servers/server/models/Service')} service */
hidden: async (service) => {
if (!service) return true;
return !service.git?.remote?.includes("github.com");
},
routes: require_routes5()
};
module2.exports = plugin;
}
});
// ../../modules/logs/backend/routes.js
var require_routes6 = __commonJS({
"../../modules/logs/backend/routes.js"(exports2, module2) {
"use strict";
var express = require("express");
var { v4 } = require("uuid");
var router = express.Router();
module2.exports = (runeya) => {
const history = runeya.getSave("history.json", {
/** @type {History[]} */
history: []
});
const { findService, Socket } = runeya;
router.get("/logs/:service/logs", (req, res) => {
const service = findService(req.params.service);
res.send(service ? service.store : []);
});
router.get("/logs/:service/autocomplete", (req, res) => {
const msg = req.query.message;
if (!msg && !req.query.force) return res.json([]);
const historyToSend = groupBy(history.data.history, "raw").sort((a, b) => a.timestamp - b.timestamp).filter((a) => a?.raw?.startsWith(msg) || a.cmd?.startsWith(msg)).slice(-10);
return res.json(historyToSend);
});
router.post("/logs/:service/prompt", async (req, res) => {
const service = findService(req.params.service);
let command = req.body.command || {};
const pid = req.body.pid ? +req.body.pid : void 0;
if (!command.spawnCmd) command.spawnCmd = "\n";
let result = {
id: v4(),
pid,
cmd: command.spawnCmd,
args: [],
raw: command.spawnCmd,
timestamp: Date.now(),
service: service.label || ""
};
if (pid) service.respondToProcess(pid, command.spawnCmd);
else if (command) {
const { spawnProcess, launchMessage } = await service.launchProcess(
{ spawnCmd: command.spawnCmd, spawnArgs: command.spawnArgs || [], spawnOptions: command.spawnOptions || {} },
false
);
result = {
...result,
pid: spawnProcess?.pid,
raw: launchMessage.raw,
timestamp: launchMessage.timestamp
};
history.data.history.push(result);
history.save();
}
res.json(result);
});
router.post("/logs/:service/terminate", (req, res) => {
const service = findService(req.params.service);
const {
/** @type {number | undefined} */
pid,
/** @type {boolean | undefined} */
forceKill
} = req.body;
if (!pid) throw new Error("Pid is required");
if (pid) service.terminate(pid, !!forceKill);
Socket.emit("logs:update", []);
res.send("ok");
});
router.delete("/logs/:service/logs", (req, res) => {
const service = findService(req.params.service);
service.store = [];
Socket.emit("logs:clear", { label: service.label });
res.send(service.store);
});
return router;
};
var groupBy = (xs, key) => {
const group = xs.reduce((rv, x) => {
if (!rv[x[key]]) rv[x[key]] = { ...x, number: 0, timestamps: [] };
rv[x[key]].timestamps.push(x.timestamp);
rv[x[key]].timestamp = x.timestamp;
return rv;
}, {});
return Object.keys(group).map((key2) => group[key2]);
};
}
});
// ../../modules/logs/backend/index.js
var require_backend7 = __commonJS({
"../../modules/logs/backend/index.js"(exports2, module2) {
"use strict";
var dayjs = require("dayjs");
var plugin = {
enabled: true,
name: "Logs",
displayName: "Logs",
description: "Show, parse and communicate with logs produced by yours commands",
icon: "fas fa-terminal",
export: null,
placements: ["service"],
order: 1,
/**
* @param {*} search
* @param {import('@runeya/common-typings').Runeya} runeya
*/
finder: (search, runeya) => {
const services = runeya.getServices()?.flatMap((s) => s.store?.slice(-300).reverse().map((line) => ({ log: line, service: s })))?.filter(({ log }) => runeya.helpers.searchString(log.raw, search));
return [
...services.map((service) => ({
icon: "fas fa-terminal",
title: service.log.raw,
group: "Logs",
description: `Log from ${service.service.label}`,
secondaryTitle: service.service.label,
secondaryDescription: dayjs(service.log.timestamp).format("YYYY-DD-MM HH:mm:ss"),
url: { path: `/stack-single/${encodeURIComponent(service.service.label)}`, query: { tab: "Logs" } }
}))
];
},
routes: require_routes6()
};
module2.exports = plugin;
}
});
// helpers/specialCaracters.helper.js
var require_specialCaracters_helper = __commonJS({
"helpers/specialCaracters.helper.js"(exports2, module2) {
"use strict";
module2.exports = {};
module2.exports.specialCaracters = [
["\xE0", "à", "agrave"],
["\xC0", "À", "agrave"],
["\xE1", "á", "aacute"],
["\xC1", "Á", "aacute"],
["\xE2", "â", "acirc"],
["\xC2", "Â", "acirc"],
["\xE3", "ã", "atilde"],
["\xC3", "Ã", "atilde"],
["\xE4", "ä", "auml"],
["\xC4", "Ä", "auml"],
["\xE5", "å", "aring"],
["\xC5", "Å", "aring"],
["\xE6", "æ", "aelig"],
["\xC6", "Æ", "aelig"],
["\xE8", "è", "egrave"],
["\xC8", "È", "egrave"],
["\xE9", "é", "eacute"],
["\xC9", "É", "eacute"],
["\xEA", "ê", "ecirc"],
["\xCA", "Ê", "ecirc"],
["\xEB", "ë", "euml"],
["\xCB", "Ë", "euml"],
["\xEC", "ì", "igrave"],
["\xCC", "Ì", "igrave"],
["\xED", "í", "iacute"],
["\xCD", "Í", "iacute"],
["\xEE", "î", "icirc"],
["\xCE", "Î", "icirc"],
["\xEF", "ï", "iuml"],
["\xCF", "Ï", "iuml"],
["\xF2", "ò", "ograve"],
["\xD2", "Ò", "ograve"],
["\xF3", "ó", "oacute"],
["\xD3", "Ó", "oacute"],
["\xF4", "ô", "ocirc"],
["\xD4", "Ô", "ocirc"],
["\xF5", "õ", "otilde"],
["\xD5", "Õ", "otilde"],
["\xF6", "ö", "ouml"],
["\xD6", "Ö", "ouml"],
["\xF8", "ø", "oslash"],
["\xD8", "Ø", "oslash"],
["\xF9", "ù", "ugrave"],
["\xD9", "Ù", "ugrave"],
["\xFA", "ú", "uacute"],
["\xDA", "Ú", "uacute"],
["\xFB", "û", "ucirc"],
["\xDB", "Û", "ucirc"],
["\xFC", "ü", "uuml"],
["\xDC", "Ü", "uuml"],
["\xF1", "ñ", "ntilde"],
["\xD1", "Ñ", "ntilde"],
["\xE7", "ç", "ccedil"],
["\xC7", "Ç", "ccedil"],
["\xFD", "ý", "yacute"],
["\xDD", "Ý", "yacute"],
["\xDF", "ß", "szlig"],
["\xAB", "«"],
["\xBB", "»"],
["&", "&"],
["<", "<"],
[">", ">"],
['"', """],
["\xA7", "¶"],
["\xA9", "©"]
];
}
});
// helpers/stringTransformer.helper.js
var require_stringTransformer_helper = __commonJS({
"helpers/stringTransformer.helper.js"(exports2, module2) {
"use strict";
var slugify = require("slugify").default;
var { specialCaracters } = require_specialCaracters_helper();
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
function numberToLetters(number, result = "") {
let charIndex = number % alphabet.length;
let quotient = number / alphabet.length;
if (charIndex - 1 === -1) {
charIndex = alphabet.length;
quotient -= 1;
}
result = alphabet.charAt(charIndex - 1) + result;
return quotient >= 1 ? numberToLetters(+quotient, result) : result;
}
function transformBeginingNumber(str) {
let transformedString = str;
const beginningNumber = Number.parseInt(transformedString, 10);
const isBeginningWithNumber = Number.isInteger(beginningNumber);
if (isBeginningWithNumber) {
const hasAlreadySeparator = transformedString.split(beginningNumber.toString())?.[1]?.charAt(0) === "_";
transformedString = transformedString.replace(beginningNumber.toString(), `${numberToLetters(beginningNumber)}${hasAlreadySeparator ? "" : "_"}`);
}
return transformedString;
}
module2.exports.humanStringToKey = (str, separator = "_") => {
let transformedString = str.trim().toLowerCase();
specialCaracters.forEach((sc) => {
const [char, eacute, slug] = sc;
if (eacute) transformedString = transformedString.replaceAll(eacute.toLowerCase(), char);
if (slug) transformedString = transformedString.replaceAll(slug.toLowerCase(), char);
});
transformedString = transformBeginingNumber(transformedString);
slugify.extend({
"'": separator,
"-": "_",
"(": separator,
")": separator
});
return slugify(transformedString, separator);
};
module2.exports.