UNPKG

penguins-eggs

Version:

A remaster system tool, compatible with Almalinux, Alpine, Arch, Debian, Devuan, Fedora, Manjaro, Opensuse, Ubuntu and derivatives

1,091 lines (1,090 loc) 33 kB
/** * ./src/classes/utils.ts * penguins-eggs v.25.7.x / ecmascript 2020 * author: Piero Proietti * email: piero.proietti@gmail.com * license: MIT * * Refactored Utils class - imports from modular utilities */ import chalk from 'chalk'; import dns from 'dns'; import fs from 'fs'; // pjson import { createRequire } from 'module'; import os from 'os'; import path from 'path'; import { shx, spawnSync } from '../lib/utils.js'; // libraries import Distro from './distro.js'; import Kernel from './utils.d/kernel.js'; const require = createRequire(import.meta.url); const pjson = require('../../package.json'); // __dirname const __dirname = path.dirname(new URL(import.meta.url).pathname); /** * Utils: general porpourse utils * @remarks all the utilities */ export default class Utils { /** * address */ static address() { const interfaces = os.networkInterfaces(); let address = ''; if (interfaces !== undefined) { for (const devName in interfaces) { const iface = interfaces[devName]; if (iface !== undefined) { for (const alias of iface) { if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal && // take just the first! address === '') { address = alias.address; } } } } } return address; } static broadcast() { const netmask = Utils.netmask(); const ip = Utils.address(); const ipParts = ip.split('.').map(Number); const maskParts = netmask.split('.').map(Number); const broadcastParts = ipParts.map((part, index) => // Bitwise OR tra il blocco IP e il blocco Netmask invertito (255 - mask) part | (255 - maskParts[index])); return broadcastParts.join('.'); } /** * chpasswdPath * @returns */ static chpasswdPath() { let chpasswdPath = '/usr/sbin/chpasswd'; if (fs.existsSync(chpasswdPath)) { chpasswdPath = '/usr/bin/chpasswd'; } return chpasswdPath; } /** * cidr */ static cidr() { const interfaces = os.networkInterfaces(); let cidr = ''; if (interfaces !== undefined) { for (const devName in interfaces) { const iface = interfaces[devName]; if (iface !== undefined) { for (const alias of iface) { if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal && // take just the first! cidr === '' && alias.cidr !== null) { cidr = alias.cidr; } } } } } return cidr; } /** * * @param cmd */ static commandExists(cmd) { return Boolean(shx.which(cmd)); } /** * * @param msg */ static async customConfirm(msg = 'Select yes to continue... ') { const { select } = await import('@inquirer/prompts'); const answer = await select({ message: msg, choices: [ { name: 'No', value: 'No' }, { name: 'Yes', value: 'Yes' }, ], default: 'No', }); console.log('User selected:', answer); // Debug logging return answer === 'Yes'; } /** * * @param msg */ static async customConfirmAbort(msg = 'Confirm') { const { select } = await import('@inquirer/prompts'); const answer = await select({ message: msg, choices: [ { name: 'No', value: 'No' }, { name: 'Yes', value: 'Yes' }, { name: 'Abort', value: 'Abort' }, ], default: 'Yes', }); return JSON.stringify({ confirm: answer }); } /** * * @param msg */ static async customConfirmCompanion(msg = 'Select yes to continue... ') { const { select } = await import('@inquirer/prompts'); const answer = await select({ message: msg, choices: [ { name: 'No', value: 'No' }, { name: 'Yes', value: 'Yes' }, ], default: 'No', }); return JSON.stringify({ confirm: answer }); } static async debug(cmd = 'cmd', procContinue = true) { console.log(chalk.redBright('DEBUG >>> ') + cmd + '\n'); let msg = 'Press a key to exit...'; if (procContinue) { msg = 'Press a key to continue...'; } console.log(msg); const pressKeyToExit = spawnSync('read _ ', [], { shell: true, stdio: [0, 1, 2] }); if (!procContinue) { process.exit(0); } } static error(msg = '') { console.error(pjson.shortName + ' >>> ' + chalk.bgRed(chalk.whiteBright(msg))); } /** * * @returns flag */ static flag() { let arch = "-"; if (Utils.isAppImage()) { arch += "AppImage"; } else switch (process.arch) { case "arm64": { arch += "arm64"; break; } case "ia32": { arch += "i386"; break; } case "riscv64": { arch += "riscv64"; break; } case "x64": { arch += "x86_64"; break; } // No default } const title = `${pjson.name}`; const green = ` ${title}`.padEnd(25, " "); const white = ` Perri's brewery edition `.padEnd(25, " "); const red = ` v${pjson.version}${arch} `.padStart(25, " "); return chalk.bgGreen.whiteBright(green) + chalk.bgWhite.blue(white) + chalk.bgRed.whiteBright(red); } /** * * @param bytes * @param decimals * @returns */ static formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = Math.max(decimals, 0); const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Number.parseFloat((bytes / k ** i).toFixed(dm)) + sizes[i]; } /** * * @param date */ static formatDate(date) { const d = new Date(date); let month = String(d.getMonth() + 1); let day = String(d.getDate()); const year = d.getFullYear(); let hh = String(d.getHours()); let mm = String(d.getMinutes()); if (month.length < 2) { month = '0' + month; } if (day.length < 2) { day = '0' + day; } if (hh.length < 2) { hh = '0' + hh; } if (mm.length < 2) { mm = '0' + mm; } return [year, month, day].join('-') + '_' + hh + mm; } /** * @returns gateway */ static gateway() { return shx.exec(`ip r | grep 'default' | awk '{print $3}'`, { silent: true }).stdout.trim(); // return shx.exec(`route -n | grep 'UG[ \t]' | awk '{print $2}'`, { silent: true }).stdout.trim() } /** * Get author name */ static getAuthorName() { return 'Piero Proietti piero.proietti@gmail.com'; } /** * Return the Debian version * @returns {number} Versione di Debian */ static getDebianVersion() { const cmd = "cat /etc/debian_version | /usr/bin/cut -f1 -d'.'"; const version = Number(shx.exec(cmd, { silent: true }).stdout); return version; } /** * dns */ static getDns() { return dns.getServers(); } /** * getDomain */ static getDomain() { return shx.exec('domainname', { silent: true }).stdout.trim(); // return shx.exec(`route -n | grep 'UG[ \t]' | awk '{print $2}'`, { silent: true }).stdout.trim() } /** * return the short name of the package: eggs * @returns eggs */ static getFriendName() { return pjson.shortName; } /** * Estimate the linuxfs dimension * (Refactored to use native FS instead of dd/od) * @returns {number} GB */ static getLiveRootSpace(type = 'debian-live') { let squashFs = '/run/live/medium/live/filesystem.squashfs'; if (type === 'mx') { squashFs = '/live/boot-dev/antiX/linuxfs'; } // 1. Leggiamo il tipo di compressione DIRETTAMENTE dall'header del file SquashFS // L'identificativo della compressione è a offset 20 (2 bytes, little endian) // 1=gzip, 2=lzo, 3=lzma, 4=xz, 5=lz4, 6=zstd let compressionId = 0; try { if (fs.existsSync(squashFs)) { const fd = fs.openSync(squashFs, 'r'); const buffer = Buffer.alloc(2); // Leggi 2 byte alla posizione 20 fs.readSync(fd, buffer, 0, 2, 20); fs.closeSync(fd); compressionId = buffer.readUInt16LE(0); } } catch (error) { console.error("Error reading squashfs header:", error); } // 2. Determiniamo il fattore di compressione in base all'ID letto let compression_factor = 30; // Default conservative switch (compressionId) { case 1: { // gzip compression_factor = 37; break; } case 2: { // lzo compression_factor = 52; break; } case 3: { // lzma compression_factor = 52; break; } case 4: { // xz compression_factor = 31; break; } case 5: { // lz4 compression_factor = 52; break; } case 6: { // zstd (aggiunto per completezza) compression_factor = 37; // simile a gzip come ratio medio break; } default: { compression_factor = 30; } } // 3. Calcolo dimensione Linux FS // Nota: shx.exec ritorna un oggetto, dobbiamo prendere .stdout e pulirlo let rootfs_file_size = 0; const dfCmdLinux = 'df /live/linux --output=used --total | /usr/bin/tail -n1'; const dfResultLinux = shx.exec(dfCmdLinux, { silent: true }).stdout.trim(); const linuxfs_used = Number(dfResultLinux) || 0; // Gestione caso NaN const linuxfs_file_size = (linuxfs_used * 1024 * 100) / compression_factor; // 4. Calcolo persist-root (se esiste) if (fs.existsSync('/live/persist-root')) { const dfCmdRoot = 'df /live/persist-root --output=used --total | /usr/bin/tail -n1'; const dfResultRoot = shx.exec(dfCmdRoot, { silent: true }).stdout.trim(); rootfs_file_size = (Number(dfResultRoot) || 0) * 1024; } let rootSpaceNeeded; if (type === 'mx') { rootSpaceNeeded = linuxfs_file_size + rootfs_file_size; } else { rootSpaceNeeded = linuxfs_file_size; } return rootSpaceNeeded / 1_073_741_824; // Converte in GB } // Se il metodo fa parte di una classe, usa `static`. Altrimenti, rimuovilo. static getOsRelease() { const osReleasePath = path.join('/etc', 'os-release'); // Inizializza l'oggetto con valori predefiniti const osInfo = { ID: '', VERSION_CODENAME: 'n/a', VERSION_ID: '' }; // Verifica se il file esiste if (!fs.existsSync(osReleasePath)) { console.error('/etc/os-release file does not exist.'); return osInfo; } // Leggi il contenuto del file let fileContent; try { fileContent = fs.readFileSync(osReleasePath, 'utf8'); } catch (error) { console.error('Error reading /etc/os-release:', error); return osInfo; } // Analizza ogni linea const lines = fileContent.split('\n'); for (const line of lines) { if (line.startsWith('#') || line.trim() === '') continue; const [key, value] = line.split('='); if (key && value) { const trimmedKey = key.trim(); const trimmedValue = value.trim().replaceAll('"', ''); // Popola solo le chiavi desiderate switch (trimmedKey) { case 'ID': { osInfo.ID = trimmedValue; break; } case 'VERSION_CODENAME': { osInfo.VERSION_CODENAME = trimmedValue; break; } case 'VERSION_ID': { osInfo.VERSION_ID = trimmedValue; break; } // No default } } } // capitalize distroId osInfo.ID = osInfo.ID[0].toUpperCase() + osInfo.ID.slice(1).toLowerCase(); osInfo.VERSION_CODENAME = osInfo.VERSION_CODENAME.toLowerCase(); return osInfo; } /** * return the name of the package: penguins-eggs * @returns penguins-eggs */ static getPackageName() { return pjson.shortName; } /** * return the version of the package * @returns version example 8.0.0 */ static getPackageVersion() { return pjson.version; } /** * Return postfix * @param basename * @returns eggName */ static getPostfix() { const postfix = '_' + Utils.formatDate(new Date()) + '.iso'; return postfix; } /** * * @param prefix * @param backup * @returns */ static getPrefix(prefix, backup = false) { if (backup) { if (prefix.slice(0, 7) === 'egg-of_') { prefix = 'egg-bk_' + prefix.slice(7); } else { prefix = 'egg-bk_' + prefix; } } return prefix; } /** * Return the primary user's name */ static async getPrimaryUser() { const { execSync } = require('child_process'); let primaryUser = ''; try { // Attempt to get the user from logname primaryUser = execSync('/usr/bin/logname 2>/dev/null', { encoding: 'utf-8' }).trim(); } catch { // console.log("logname failed, so we continue with other methods") } if (primaryUser === 'root') { primaryUser = ''; } if (primaryUser === '') { try { // Check if doas is installed and get the DOAS_USER execSync('command -v doas', { stdio: 'ignore' }); primaryUser = execSync('echo $DOAS_USER', { encoding: 'utf-8' }).trim(); } catch { // console.log("doas is not installed or DOAS_USER is not set, continue with the next method") } } if (primaryUser === '') { try { // Check for the SUDO_USER primaryUser = execSync('echo $SUDO_USER', { encoding: 'utf-8' }).trim(); } catch { // console.log("SUDO_USER is not set, continue with the next method") } } if (primaryUser === '') { // console.log("Fallback to the USER environment variable") primaryUser = process.env.USER || ''; } if (primaryUser === '') { primaryUser = 'dummy'; // console.error('Cannot determine the primary user.'); // process.exit(1); } return primaryUser; } /** * Count the eggs present in the nest * @returns {number} Numero degli snapshot presenti */ static getSnapshotCount(snapshot_dir = '/') { if (fs.existsSync(snapshot_dir)) { const files = fs.readdirSync(snapshot_dir); let nIsos = 0; for (const f of files) { if (f.endsWith('.iso')) { nIsos++; } } return nIsos; } return 0; } /** * Get the syze of the snapshot * @returns {string} grandezza dello snapshot in Byte */ static getSnapshotSize(snapshot_dir = '/') { let fileSizeInBytes = 0; const size = shx.exec(`/usr/bin/find ${snapshot_dir} -maxdepth 1 -type f -name '*.iso' -exec du -sc {} + | tail -1 | awk '{print $1}'`, { silent: true }).stdout.trim(); if (size === '') { fileSizeInBytes = 0; } else { fileSizeInBytes = Number(size); } return fileSizeInBytes; } /** * Calculate the space used on the disk * @returns {void} */ static getUsedSpace() { let fileSizeInBytes = 0; if (this.isLive()) { fileSizeInBytes = 0; // this.getLiveRootSpace() } else { fileSizeInBytes = Number(shx.exec(`df /home | /usr/bin/awk 'NR==2 {print $3}'`, { silent: true }).stdout); } return fileSizeInBytes; } /** * return the name of network device */ static async iface() { // return shx.exec(`ifconfig | awk 'FNR==1 { print $1 }' | tr --d :`, { silent: true }).stdout.trim() const interfaces = Object.keys(os.networkInterfaces()); let netDeviceName = ''; for (const k in interfaces) { if (interfaces[k] != 'lo') { netDeviceName = interfaces[k]; } } return netDeviceName; } static info(msg = '') { console.log(pjson.shortName + ' >>> ' + chalk.white(msg)); } /** * @deprecated Use Kernel.initramfs() instead */ static initrdImg(kernel = '') { return Kernel.initramfs(kernel); } /** * isAppImage */ static isAppImage() { return Boolean(process.env.APPIMAGE) || process.execPath.includes('.AppImage') || process.execPath.includes('/tmp/.mount_'); } /** * * @param device * @returns */ static isBlockDevice(device = '') { const cmd = `lsblk -d -o name | grep ${device}`; const result = shx.exec(cmd, { silent: true }).code; return result === 0; } /** * Detect if running inside a standard chroot environment * (Distinguishes chroot from Containers/Host) */ static isChroot() { try { // 1. Check for Debian specific chroot file // Many Debian tools (debootstrap, schroot) create this file. if (fs.existsSync('/etc/debian_chroot')) { return true; } // 2. Inode comparison method // We compare the Inode and Device ID of '/' vs '/proc/1/root'. // In a chroot (sharing the host's PID namespace via bind-mounted /proc), // PID 1 refers to the host's init process. // Therefore, if '/' is NOT the same file as '/proc/1/root', we are in a chroot. const rootStat = fs.statSync('/'); const procRootStat = fs.statSync('/proc/1/root'); // If dev or ino are different, we are inside a chroot return (rootStat.dev !== procRootStat.dev) || (rootStat.ino !== procRootStat.ino); } catch { // If /proc is not mounted or not accessible, detection via inode fails. // However, usually in eggs /proc is mounted. // If strictly needed, one could assume that if /proc/1/root is unreadable // but / exists, it might be a weird chroot state, but returning false is safer. return false; } } /** * Detect if running inside a container (Docker or LXC) */ static isContainer() { // Check for Docker's specific file if (fs.existsSync('/.dockerenv')) { return true; } // Check the cgroup file, which works for Docker, Podman, LXC, etc. try { const cgroupContent = fs.readFileSync('/proc/1/cgroup', 'utf8'); return cgroupContent.includes('/docker/') || cgroupContent.includes('/kubepods/'); } catch { return false; } } /** * Return true if i686 architecture * @remarks to move in Utils * @returns {boolean} true se l'architettura è i686 */ static isi686() { return process.arch === 'ia32'; } /** * Return true if live system * @returns {boolean} isLive */ static isLive() { let retVal = false; const paths = [ '/lib/live/mount', // debian-live '/run/live/rootfs/filesystem.squashfs', // debian trixie '/lib/live/mount/rootfs/filesystem.squashfs', // ubuntu bionic '/live/aufs', // mx-linux '/media/root-rw', // AlpineLinux '/run/archiso/airootfs', // Arch '/run/miso/sfs/livefs', // Manjarolinux '/run/rootfsbase' // Fedora ]; for (const path_ of paths) { if (Utils.isMountpoint(path_)) { retVal = true; } } return retVal; } /** * Ritorna vero se path è un mountpoint * @param path */ static isMountpoint(path = '') { const cmd = `mountpoint -q ${path}`; // return 0 if the directory is a mountpoint, non-zero if not. const result = shx.exec(cmd, { silent: true }).code; return result === 0; } /** * Controlla se è un pacchetto npm */ static isNpmPackage() { return !(this.isPackage() || this.isSources()); } /** * Check if the system uses OpenRC */ static isOpenRc() { let isOpenRc = false; if (!this.isContainer()) { isOpenRc = Utils.commandExists('openrc'); } return isOpenRc; } /** * Controlla se è un pacchetto deb * /usr/lib/penguins-eggs/bin/node */ static isPackage() { let ret = false; // if (process.execPath !== '/usr/bin/node') { if (process.execPath === '/usr/lib/penguins-eggs/bin/node') { ret = true; } return ret; } /** * return true if eggs run as root * @returns isRoot */ static isRoot(command = '') { if (process.getuid && process.getuid() === 0) { return true; } return false; } /** * Controlla se è un pacchetto sorgente */ static isSources() { let ret = false; if (__dirname.slice(0, 6) === '/home/') { ret = true; } return ret; } /** * Check if the system uses Systemd */ static isSystemd() { let isSystemd = false; if (this.isContainer()) { isSystemd = true; const distro = new Distro(); if (distro.distroId === "Devuan") { isSystemd = false; } } else { isSystemd = fs.readFileSync("/proc/1/comm").includes('systemd'); } return isSystemd; } /** * Check if the system uses SysVinit */ static isSysvinit() { let isSysvinit = false; if (this.isContainer()) { const distro = new Distro(); if (distro.distroId === "Devuan") { isSysvinit = true; } } else { isSysvinit = fs.readFileSync("/proc/1/comm").includes('init'); } return isSysvinit; } /** * get the kernel version */ static kernelVersion() { return os.release(); } /** * Usata da pacman e config credo non serva affatto */ static machineId() { let result = ''; if (fs.existsSync('/etc/machine-id')) { result = fs.readFileSync('/etc/machine-id', 'utf8').trim(); } else if (fs.existsSync('/var/lib/dbus/machine-id')) { result = fs.readFileSync('/var/lib/dbus/machine-id', 'utf8').trim(); } return result; } /** * netmask */ static netmask() { const interfaces = os.networkInterfaces(); let netmask = ''; if (interfaces !== undefined) { for (const devName in interfaces) { const iface = interfaces[devName]; if (iface !== undefined) { for (const alias of iface) { if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal && // take just the first! netmask === '') { netmask = alias.netmask; } } } } } return netmask; } /** * */ static async pressKeyToExit(warning = 'Process will end', procContinue = true) { Utils.warning(warning); let msg = 'Press a key to exit...'; if (procContinue) { msg = 'Press a key to continue...'; } console.log(msg); const pressKeyToExit = spawnSync('read _ ', [], { shell: true, stdio: [0, 1, 2] }); if (!procContinue) { process.exit(0); } } /** * */ static rootPenguin() { return path.resolve(__dirname, '../../'); } /** * * @param file * @param search * @returns value */ static searchOnFile(file = '', search = '') { const lines = fs.readFileSync(file, 'utf8').split("\n"); let value = ''; for (let line of lines) { line = line.replaceAll(/\s+/g, ' '); // Remove multiple spaces with single space if (line.includes(search)) { value = line.slice(Math.max(0, line.indexOf('=') + 1)); } } value = value.replaceAll('"', ''); // Remove " return value.trim(); } /** * * @param verbose */ static setEcho(verbose = false) { let echo = { echo: false, ignore: true }; if (verbose) { echo = { echo: true, ignore: false }; } return echo; } /* Funzione helper che crea una pausa non bloccante. * @param ms Millisecondi da attendere */ static sleep(ms = 60_000) { // console.log('wait...') return new Promise(resolve => setTimeout(resolve, ms)); } /** * Restituisce il prefisso della iso * @param distroId * @param codenameId */ static snapshotPrefix(distroId, codenameId) { let result = `egg-of_${distroId.toLowerCase()}-`; if (codenameId === 'rolling' || codenameId === '') { const releaseId = Utils.getOsRelease().VERSION_ID.trim(); if (releaseId !== '') { result += releaseId; } } else { result += `${codenameId.toLowerCase()}`; } if (!result.endsWith('-')) { result += '-'; } return result; } /** * Custom function to sort object keys * @param obj * @returns */ static sortObjectKeys(obj) { const sorted = {}; for (const key of Object.keys(obj) .sort()) { sorted[key] = obj[key]; } return sorted; } static success(msg = '') { console.log(pjson.shortName + ' >>> ' + chalk.greenBright(msg)); } /** * titles * Penguin's are gettings alive! */ static titles(command = '') { console.clear(); console.log(''); console.log(' E G G S: the reproductive system of penguins'); console.log(''); console.log(Utils.flag()); console.log('command: ' + chalk.bgBlack.white(command) + '\n'); } /** * uefiArch * @returns arch */ static uefiArch() { let arch = ''; switch (process.arch) { case 'arm64': { arch = 'arm64'; break; } case 'ia32': { arch = 'i386'; // if (shx.exec('uname -m', { silent: true }).stdout.trim() === 'x86_64') { arch = 'amd64'; } break; } case 'riscv64': { arch = 'riscv64'; break; } case 'x64': { arch = 'amd64'; break; } // No default } return arch; } /** * i386-pc, * i386-efi, * x86_64-efi, * arm64-efi, * * ATTEMZIONE: install efibootmgr * * Fedora/RHEL have i386-pc */ static uefiFormat() { let format = ''; switch (process.arch) { case 'arm64': { format = 'arm64-efi'; break; } case 'ia32': { format = 'i386-efi'; if (shx.exec('uname -m', { silent: true }).stdout.trim() === 'x86_64') { format = 'x86_64-efi'; } break; } case 'riscv64': { format = 'riscv64-efi'; break; } case 'x64': { format = 'x86_64-efi'; break; } // No default } return format; } /** * * @param command */ static useRoot(command = '') { Utils.titles(pjson.shortName + ' ' + command + ` need to run with root privileges. Please, prefix it with sudo`); } /** * * @returns */ static usrLibPath() { let path = ''; if (process.arch === 'x64') { path = 'x86_64-linux-gnu'; } else if (process.arch === 'arm64') { path = 'aarch64-linux-gnu'; } return path; } /** * restituisce uuid * @param device */ static uuid(device) { const uuid = shx.exec(`blkid -p -s UUID -o value ${device}`, { silent: true }).stdout.trim(); return uuid; } /** * * @param device * @returns */ static uuidGen() { const uuid = shx.exec(`uuidgen`, { silent: true }).stdout.trim(); return uuid; } static vmlinuz(kernel = '') { return Kernel.vmlinuz(kernel); } /** * * @param volid */ static VolidTrim(volid = 'unknown') { // // 28 + 4 .iso = 32 lunghezza max di volid if (volid.length >= 32) { volid = volid.slice(0, 32); } return volid; } /** * * @returns wardrobe */ static async wardrobe() { let wardrobe = `${os.homedir()}/.wardrobe`; if (Utils.isRoot()) { wardrobe = `/home/${await Utils.getPrimaryUser()}/.wardrobe`; } return wardrobe; } /** * * @param msg */ static warning(msg = '') { console.log(pjson.shortName + ' >>> ' + chalk.cyanBright(msg)); } /** * write a file * @param file * @param text */ static write(file, text) { text = text.trim() + '\n'; file = file.trim(); fs.writeFileSync(file, text); } /** * * @param file * @param cmd */ static writeX(file, cmd) { let text = `#!/bin/sh\n\n`; text += `# Created at: ${Utils.formatDate(new Date())}\n`; text += `# By: penguins_eggs v. ${Utils.getPackageVersion()}\n`; text += `# ==> Perri\'s Brewery edition <== \n\n`; text += cmd; Utils.write(file, text); shx.chmod('+x', file); } /** * * @param file * @param cmd */ static writeXs(file, cmds) { let cmd = ''; for (const elem of cmds) { cmd += elem + '\n'; } Utils.writeX(file, cmd); } }