#!/usr/bin/env node /** * ncicd * CI builder for NİBGAT® and NİBGAT®. * * @author nibgat */ import dotenv from "dotenv"; import cp from "child_process"; import chalk from "chalk"; import { Spinner } from "@topcli/spinner"; import figlet from "figlet"; import path from "path"; import clearConsole from "clear-any-console"; import fs from "fs"; import inquirer from "inquirer"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; import constants from "./constants/index.js"; import utils from "./utils/index.js"; import { generateProject } from "./actions/index.js"; import os from "os"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); dotenv.config(); const input = constants.input; const flags = constants.flags; const { buildCommand, environment, nodeVersion, generateRsa, repository, targetPath, privateKey, updateConf, command, remove, branch, update, setup, clear, debug, name, port, file, bind, logs, list } = flags; let params = { buildCommand: null, nodeVersion: null, environment: null, targetPath: null, repository: null, command: null, branch: null, name: null, env: null, rsaKeys: { privateKey: null, publicKey: null } }; export const DIR = process.env.NODE_ENV === "development" ? path.join(__dirname, "../") : path.join(os.homedir(), ".ncicd"); if (process.env.NODE_ENV !== "development" && !fs.existsSync(DIR)) { fs.mkdirSync(DIR, { recursive: true }); } const _setup = () => { clearConsole(); if (!port) { console.log( chalk.red( "Port is required (-p or --port)." ) ); return; } try { const spinnerYarn = new Spinner().start("Yarn is installing"); cp.execSync("npm i -g yarn", { stdio: "inherit" }); spinnerYarn.succeed("Yarn is installed."); const spinnerPM2 = new Spinner().start("PM2 is installing"); cp.execSync("npm i -g pm2", { stdio: "inherit" }); spinnerPM2.succeed("PM2 is installed."); const spinnerConfigFile = new Spinner().start("Config file creating"); if (!fs.existsSync(path.join(DIR, "ncicd.conf"))) { cp.execSync(`${process.platform === "win32" ? "type NUL >" : "touch"} ${path.join(DIR, "ncicd.conf")}`, { stdio: "inherit" }); fs.writeFileSync(path.join(DIR, "ncicd.conf"), JSON.stringify([], null, 4) + "\n"); spinnerConfigFile.succeed("Config file is created."); } else { spinnerConfigFile.succeed("Config file already exists."); } const spinnerNCICDConfFile = new Spinner().start("N - CI & CD app config file creating"); if (!fs.existsSync(path.join(DIR, "ncicd.conf.json"))) { cp.execSync(`${process.platform === "win32" ? "type NUL >" : "touch"} ${path.join(DIR, "ncicd.conf.json")}`, { stdio: "inherit" }); fs.writeFileSync(path.join(DIR, "ncicd.conf.json"), JSON.stringify({ bind: bind ? bind : "0.0.0.0", PORT: port }, null, 4) + "\n"); } if (!fs.existsSync(path.join(DIR, "privateKey.pem"))) { fs.writeFileSync(path.join(DIR, "privateKey.pem"), "", { mode: "600" }); fs.chmodSync(path.join(DIR, "privateKey.pem"), "0600"); } spinnerNCICDConfFile.succeed("N - CI & CD app config file checked/created."); const spinnerNCICDApp = new Spinner().start("N - CI & CD app running"); try { cp.execSync("pm2 delete ncicd-app", { stdio: "ignore" }); } catch { // Ignore if it doesn't exist } cp.execSync(`pm2 start npx --name ncicd-app --time --log-date-format "YYYY-MM-DDTHH:mm:ss.SSSZ" -l "${path.join(DIR, "ncicd-app-combined.log")}" -- tsx "${path.join(__dirname, "ncicd.js")}"`, { stdio: "inherit" }); if (process.platform !== "win32") { cp.execSync("pm2 startup", { stdio: "inherit" }); } cp.execSync("pm2 save", { stdio: "inherit" }); spinnerNCICDApp.succeed("N - CI & CD app is successfully running."); console.log( chalk.green( "Setup processes is successfully completed." ) ); } catch (err) { if (err && err.message) { console.log( chalk.red( err.message ) ); throw new Error(err.message); } else { console.log( chalk.red( err ) ); throw new Error(err); } } }; const _runMultiple = async (location) => { const file = fs.readFileSync(location).toString(); const json = JSON.parse(file); try { await utils.asyncForEach(json, async (item) => { await _runSingle(item); }); console.log( chalk.green("All projects successfully up.") ); } catch (err) { console.log( chalk.red(err) ); } }; const _runSingle = async (data: typeof params) => { const file = fs.readFileSync(path.join(DIR, "ncicd.conf")).toString(); const json = JSON.parse(file); let newJSON = JSON.parse(JSON.stringify(json)); console.log( chalk.cyan(`N - CI & CD Working on ${data.name}.`) ); if (data.environment) { try { data.env = fs.readFileSync(String(data.environment)).toString(); } catch (err) { console.log( data.environment, chalk.red(err) ); } } let customRSA = false; if (privateKey) { try { const privateKeyFileData = fs.readFileSync(String(privateKey)).toString(); data.rsaKeys.privateKey = privateKeyFileData; } catch { data.rsaKeys.privateKey = privateKey; } } else { if (!(data.rsaKeys.privateKey && data.rsaKeys.privateKey.length)) { customRSA = true; const rsaKeys = await _getRSA(data.name); data.rsaKeys = { publicKey: rsaKeys.pubKey, privateKey: rsaKeys.key }; } } const alreadyExistsIndex = newJSON.findIndex((item: any) => item.name === data.name); if (alreadyExistsIndex !== -1) { if (!privateKey && newJSON[alreadyExistsIndex].rsaKeys) { data.rsaKeys = newJSON[alreadyExistsIndex].rsaKeys; } newJSON[alreadyExistsIndex] = { ...newJSON[alreadyExistsIndex], ...data }; } else { newJSON.push(data); } fs.writeFileSync(path.join(DIR, "ncicd.conf"), JSON.stringify(newJSON, null, 4) + "\n"); console.log(`${data.name} - Your Public Key:`, data.rsaKeys.publicKey); await inquirer.prompt([ { name: "publicKeyIsReady", message: `${data.name} - Do you add your public key to your repo ? (yes):` } ]) .then(async (answers) => { if (answers.publicKeyIsReady === "yes") { const generatedProject = await generateProject(data, customRSA); if (typeof generatedProject !== "boolean" || !generatedProject) { return; } else { console.log( chalk.green(`${data.name} successfully up.`) ); } } else { throw new Error("Public key is must."); } }); return; }; const _getRSA = async (name: string, returnRSA?: boolean) => { const newRSA = await utils.generateSSHKeygen(path.join(DIR, "privateKey.pem"), name); if (!returnRSA) { console.log( `${name} - Your RSA Public Key:`, chalk.blue( newRSA.pubKey ), `${name} - Your RSA Private Key:`, chalk.blue( newRSA.key ) ); } return newRSA; }; (async () => { utils.init({ clear }); console.log( chalk.cyan( figlet.textSync("NİBGAT®", { horizontalLayout: "full" }) ) ); console.log( figlet.textSync("N - CI & CD", { width: 80 }) ); input.includes("help") && constants.showHelp(0); debug && console.log(flags); console.log("\n\n"); const isList = list || input.includes("list") || input.includes("ls"); if (isList) { const confPath = path.join(DIR, "ncicd.conf"); if (!fs.existsSync(confPath)) { console.log(chalk.yellow("No configuration found (ncicd.conf does not exist).")); return; } try { const confContent = fs.readFileSync(confPath).toString(); const confJson = JSON.parse(confContent); if (!confJson || confJson.length === 0) { console.log(chalk.cyan("No projects configured yet.")); return; } const tableData = confJson.map((item: any) => ({ Name: item.name || "-", Branch: item.branch || "-", Node_Version: item.nodeVersion || "-", Target_Path: item.targetPath || "-", Build_Command: item.buildCommand || "-" })); console.log(chalk.green(`Configured Projects (${confJson.length}):`)); console.table(tableData); } catch (e: any) { console.log(chalk.red(`Error reading ncicd.conf: ${e.message}`)); } return; } if (logs) { const serviceName = input.length > 0 ? input[0] : ""; const confPath = path.join(DIR, "ncicd.conf"); let services: any[] = []; services.push({ name: "ncicd-app", logPath: path.join(DIR, "ncicd-app-combined.log") }); if (fs.existsSync(confPath)) { try { const list = JSON.parse(fs.readFileSync(confPath).toString()); list.forEach((item: any) => { services.push({ name: item.name, logPath: path.join(item.targetPath, item.name, `${item.name}-combined.log`) }); }); } catch (e: any) { console.log( chalk.red( `Error parsing ncicd.conf: ${e.message}` ) ); } } if (serviceName) { services = services.filter(s => s.name === serviceName); if (services.length === 0) { console.log(chalk.red(`Service ${serviceName} not found.`)); return; } } let allLines: { service: string; line: string; dateStr: string }[] = []; services.forEach(s => { if (fs.existsSync(s.logPath)) { const content = fs.readFileSync(s.logPath, 'utf8'); const lines = content.split('\n').filter(l => l.trim() !== ''); lines.forEach(line => { let dateStr = ""; if (line.startsWith("20")) { dateStr = line.substring(0, 24); } allLines.push({ service: s.name, line: line, dateStr: dateStr }); }); } }); allLines.sort((a, b) => a.dateStr.localeCompare(b.dateStr)); allLines.forEach(item => { console.log(chalk.blue(`[${item.service}]`) + ` ${item.line}`); }); return; } if (update) { const spinnerUpdate = new Spinner().start("Updating ncicd..."); try { const confPath = path.join(DIR, "ncicd.conf.json"); let portToUse = 8088; let bindToUse = "0.0.0.0"; if (fs.existsSync(confPath)) { const confContent = fs.readFileSync(confPath).toString(); const confJson = JSON.parse(confContent); if (confJson.PORT) portToUse = confJson.PORT; if (confJson.bind) bindToUse = confJson.bind; } cp.execSync("npm i -g ncicd", { stdio: "inherit" }); cp.execSync(`ncicd --setup -p ${portToUse} --bind ${bindToUse}`, { stdio: "inherit" }); spinnerUpdate.succeed("ncicd successfully updated and restarted."); } catch (e) { spinnerUpdate.failed("Failed to update ncicd."); console.error(e); } return; } if (remove) { if (typeof remove !== 'string' || remove.trim() === '') { console.log( chalk.red( "You must provide a service name to remove. (Ex. --remove projectName)" ) ); return; } const serviceName = remove; const { isConfirmed } = await inquirer.prompt([ { type: "confirm", name: "isConfirmed", message: `Are you sure you want to remove the service '${serviceName}' from PM2 and ncicd.conf?`, default: false } ]); if (!isConfirmed) { console.log( chalk.cyan( "Service removal cancelled." ) ); return; } const { deleteDirectory } = await inquirer.prompt([ { type: "confirm", name: "deleteDirectory", message: `Delete the project directory?`, default: false } ]); const spinnerRemove = new Spinner().start(`Removing service ${serviceName}...`); try { try { cp.execSync(`pm2 delete ${serviceName}`, { stdio: "inherit", maxBuffer: 1024 * 1024 * 50 }); cp.execSync("pm2 startup", { stdio: "inherit", maxBuffer: 1024 * 1024 * 50 }); cp.execSync("pm2 save", { stdio: "inherit", maxBuffer: 1024 * 1024 * 50 }); } catch (e: any) { console.log( chalk.yellow( `Warning from PM2 (service might not exist): ${e.message}` ) ); } const confPath = path.join(DIR, "ncicd.conf"); if (fs.existsSync(confPath)) { const confContent = fs.readFileSync(confPath).toString(); const confJson = JSON.parse(confContent); const projectConfig = confJson.find((item: any) => item.name === serviceName); const updatedConf = confJson.filter((item: any) => item.name !== serviceName); if (confJson.length === updatedConf.length) { spinnerRemove.failed(`Service '${serviceName}' not found in ncicd.conf.`); return; } fs.writeFileSync(confPath, JSON.stringify(updatedConf, null, 4) + "\n"); if (deleteDirectory && projectConfig && projectConfig.targetPath) { const projectPath = path.join(projectConfig.targetPath, projectConfig.name); if (fs.existsSync(projectPath)) { fs.rmSync(projectPath, { recursive: true, force: true }); spinnerRemove.succeed(`Service '${serviceName}' and its directory successfully removed.`); } else { spinnerRemove.succeed(`Service '${serviceName}' successfully removed (directory not found).`); } } else { spinnerRemove.succeed(`Service '${serviceName}' successfully removed from PM2 and ncicd.conf.`); } } else { spinnerRemove.failed("ncicd.conf not found."); } } catch (e) { spinnerRemove.failed(`Failed to remove service '${serviceName}'.`); console.error(e); } return; } if (updateConf) { if (!name) { console.log( chalk.red( "You must provide projectName (-n or --name)." ) ); return; } const file = fs.readFileSync(path.join(DIR, "ncicd.conf")).toString(); const json = JSON.parse(file); const index = json.findIndex((item: any) => item.name === name); if (index === -1) { console.log( chalk.red( `Project ${name} not found in ncicd.conf.` ) ); return; } const [ key, value ] = String(updateConf).split(":"); if (!key || !value) { console.log( chalk.red( "Invalid updateConf format. Expected KEY:VALUE." ) ); return; } json[index][key] = value; fs.writeFileSync(path.join(DIR, "ncicd.conf"), JSON.stringify(json, null, 4) + "\n"); console.log( chalk.green( `Successfully updated ${key} to ${value} for project ${name}.` ) ); return; } if (setup) { setTimeout(() => { _setup(); }, 2000); return; } if (generateRsa) { _getRSA("ncicd"); return; } if (file) { _runMultiple(file); return; } if (repository || branch || targetPath || name) { if (!repository) { console.log( chalk.red( "You must be provide repository url (-r or --repository)." ) ); return; } if (!branch) { console.log( chalk.red( "You must be provide branch (-b or --branch)." ) ); return; } if (!targetPath) { console.log( chalk.red( "You must be provide targetPath (-t or --targetPath)." ) ); return; } if (!name) { console.log( chalk.red( "You must be provide projectName (-n or --name)." ) ); return; } const file = fs.readFileSync(path.join(DIR, "ncicd.conf")).toString(); const json = JSON.parse(file); let newJSON = JSON.parse(JSON.stringify(json)); let env = ""; if (environment) { try { env = fs.readFileSync(String(environment)).toString(); } catch (err) { console.log( environment, chalk.red(err) ); } } if (process.argv.indexOf("-ev") !== -1 || process.argv.indexOf("--environmentVariable") !== -1) { process.argv.forEach((envItem, envIndex) => { if (envItem === "-ev" || envItem === "--environmentVariable") { env = `${env}\n${process.argv[envIndex + 1]}`; } }); } if (buildCommand) { params.buildCommand = buildCommand; } params.nodeVersion = nodeVersion; params.environment = environment; params.repository = repository; params.targetPath = targetPath; params.command = command; params.branch = branch; params.name = name; params.env = env; let customRSA = false; if (privateKey) { try { const privateKeyFileData = fs.readFileSync(String(privateKey)).toString(); params.rsaKeys.privateKey = privateKeyFileData; } catch { params.rsaKeys.privateKey = privateKey; } } else { customRSA = true; const rsaKeys = await _getRSA(params.name); params.rsaKeys = { publicKey: rsaKeys.pubKey, privateKey: rsaKeys.key }; } const alreadyExistsIndex = newJSON.findIndex((item: any) => item.name === params.name); if (alreadyExistsIndex !== -1) { if (!privateKey && newJSON[alreadyExistsIndex].rsaKeys) { params.rsaKeys = newJSON[alreadyExistsIndex].rsaKeys; } newJSON[alreadyExistsIndex] = { ...newJSON[alreadyExistsIndex], ...params }; } else { newJSON.push(params); } fs.writeFileSync(path.join(DIR, "ncicd.conf"), JSON.stringify(newJSON, null, 4) + "\n"); console.log("Your Public Key:", params.rsaKeys.publicKey); await inquirer.prompt([ { name: "publicKeyIsReady", message: "Have you added your public key to your repository ? (yes):" } ]) .then(async (answers) => { if (answers.publicKeyIsReady === "yes") { const generatedProject = await generateProject(params, customRSA); if (typeof generatedProject !== "boolean" || !generatedProject) { return; } } else { throw new Error("Public key is must."); } }); return; } console.log( chalk.red( "You did't choose any action." ) ); })();