UNPKG

adr-cli

Version:

Architecture Decision Records (ADR) command cli

405 lines 15.6 kB
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rename } from 'node:fs'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { markdownTable } from 'markdown-table'; import moment from 'moment'; import { Configuration } from '../utils/configurations.js'; import { Locale } from '../utils/locale.js'; import Utils from '../utils/utils.js'; import { Directory } from './directory.js'; import { Enums } from './enums.js'; const config = new Configuration(); const enums = new Enums(); const pathBase = process.cwd(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export class Adr { _add = new Add(); pathAdr = config.get('adrPath'); _locale; constructor() { this._locale = Locale.getInstance().getLocale(); } async getQuestionsToAdd() { const result = this._add.questionsForAdd(); return result; } addWithAnswers(answers) { this._add.createFile(answers); } addWithoutAnswers(answers) { this._add.createFileImediatly(answers); } createIndex() { const basePath = `${this.pathAdr}`; const splitPath = basePath.split('\\'); const fileArray = [['ADR', 'Name', 'Status']]; const pathdir = `${pathBase}\\${splitPath[0]}`; const path = `${pathdir}\\${splitPath[1]}`; let table = ''; const files = readdirSync(path); for (const file of files) { const seq = this.getSequences(file); const statusAdr = Directory.getStatusForAdr(file, path); const matches = statusAdr.matchAll(/{([^}]*)}/g); const matches2 = Array.from(matches); const textColor = matches2[0][1]; const textStatus = matches2[1][1]; let title = file.replace(/-/g, ' ').replace(/.md/g, ''); title = title.slice(5, title.length); const row = [`[ADR-${seq}](adr/${file})`, title, `$\\color{${textColor}}{${textStatus}}$`]; fileArray.push(row); } table = markdownTable(fileArray); this.validateOrCreateIndex(pathdir, table); } getSequences(string_) { try { if (string_ === undefined) { string_ = '0000'; } const seq = Number(string_.slice(0, 4)); return seq.toString().padStart(4, '0'); } catch (error) { let message = 'Unknown Error'; if (error instanceof Error) { message = error.message; console.error(chalk.red.bold(`Error in parse next sequence to adr file:${message}`)); } process.exit(); } } suppressedAdr(idToSupress, id) { const pathdir = `${pathBase}\\${this.pathAdr}`; let nameFile = Utils.getFileNameById(idToSupress); nameFile = `${pathdir}\\${nameFile}`; const newNameFile = nameFile.replace('.md', '-suppressed.md'); rename(nameFile, newNameFile, error => { if (error) { throw error; } console.log(chalk.greenBright.bold(this._locale.class.adr.Adr.suppressedAdr.okRanameAdrFile)); }); const status = new Status(); status.setStatusToAdr(idToSupress, 'superseding'); this.addSuppressingToAdr(idToSupress, id); } addSuppressingToAdr(ids, idTo) { const pathdir = `${pathBase}\\${this.pathAdr}`; let nameFile = Utils.getFileNameById(ids); nameFile = `${pathdir}\\${nameFile}`; this.addSuppressToAdr(nameFile, idTo); } addRelationToAdr(ids, idTo) { const pathdir = `${pathBase}\\${this.pathAdr}`; let nameFile = Utils.getFileNameById(ids); nameFile = `${pathdir}\\${nameFile}`; this.addRelationsToAdr(nameFile, idTo); } setDefaultRel(value, defaultValue) { return value; } addSuppressToAdr(nameFile, idTo) { const file = readFileSync(nameFile, { encoding: 'utf8', flag: 'r' }); const searchString = '* Rel:'; const re = new RegExp(`^.*\\${searchString}.*$`, 'gm'); const adrsIds = Array.from(idTo, x => x.padStart(4, '0')); const text = `${searchString} \n* Suppressed by: ${adrsIds.toString()}`; const formatted = file.replace(re, text); writeFileSync(nameFile, formatted, { mode: 0o777 }); } addRelationsToAdr(nameFile, idTo) { const file = readFileSync(nameFile, { encoding: 'utf8', flag: 'r' }); const searchString = '* Rel:'; const re = new RegExp(`^.*\\${searchString}.*$`, 'gm'); const adrsIds = Array.from(idTo, x => x.padStart(4, '0')); const text = `${searchString} ${adrsIds.toString()}`; const formatted = file.replace(re, text); writeFileSync(nameFile, formatted, { mode: 0o777 }); } validateOrCreateIndex(path, table) { try { console.log(chalk.green(this._locale.class.adr.Adr.validateOrCreateIndex.reading)); const locale = config.get('locale'); const templateIndedx = readFileSync(`${__dirname}\\..\\templates\\index-${locale}.md`, { encoding: 'utf8', flag: 'r' }); const search = /<!--MakrToAppendFiles>/g; const result = templateIndedx.replace(search, table); writeFileSync(`${path}\\index.md`, result, { mode: 0o777 }); console.log(chalk.green(this._locale.class.adr.Adr.validateOrCreateIndex.generated)); } catch (error) { console.error(chalk.red.bold(error)); process.exit(); } } } export class Status { static setStatusColor(status) { const typeOfMarkdown = config.get('markdownEngine'); if (typeOfMarkdown === 'github') { switch (status) { case 'proposed': { return `* Status: ${enums.statusColor.proposed}`; } case 'acceptance': { return `* Status: ${enums.statusColor.acceptance}`; } case 'rejection': { return `* Status: ${enums.statusColor.rejection}`; } case 'deprecation': { return `* Status: ${enums.statusColor.deprecation}`; } case 'superseding': { return `* Status: ${enums.statusColor.superseding}`; } default: { console.error(chalk.red('Error in status definitions.')); return 'false'; } } } else { switch (status) { case 'proposed': { return `* Status: ${enums.statusColorGitlab.proposed}`; } case 'acceptance': { return `* Status: ${enums.statusColorGitlab.acceptance}`; } case 'rejection': { return `* Status: ${enums.statusColorGitlab.rejection}`; } case 'deprecation': { return `* Status: ${enums.statusColorGitlab.deprecation}`; } case 'superseding': { return `* Status: ${enums.statusColorGitlab.superseding}`; } default: { console.error(chalk.red('Error in status definitions.')); return 'false'; } } } } pathAdr = config.get('adrPath'); _adr = new Adr(); async getStatus() { return this.choiceStatus(); } setStatusToAdr(id, status) { const pathdir = `${pathBase}\\${this.pathAdr}`; let nameFile = this.getFileNameById(id); nameFile = `${pathdir}\\${nameFile}`; const validateStatus = this.validateStatusType(status); if (!validateStatus) { throw new Error('Error in validate status selected, Please review status selected.'); } this.setStatusToFileAdr(nameFile, status); } setDefaultStatus(value, defaultValue) { return value; } async choiceStatus() { const choice = [ { type: 'list', name: 'status', message: 'Choice one status', choices: [ 'proposed', 'acceptance', 'rejection', 'deprecation', 'superseding', ], }, ]; return await inquirer.prompt(choice); } setStatusToFileAdr(fileName, status) { const file = readFileSync(fileName, { encoding: 'utf8', flag: 'r' }); const searchString = '* Status:'; const re = new RegExp(`^.*\\${searchString}.*$`, 'gm'); const colorStatus = Status.setStatusColor(status); const formatted = file.replace(re, colorStatus); writeFileSync(fileName, formatted, { mode: 0o777 }); } validateStatusType(status) { switch (status) { case 'proposed': case 'acceptance': case 'rejection': case 'deprecation': case 'superseding': { return true; } default: { console.error(chalk.red('Error in status definitions.')); return false; } } } getFileNameById(id) { let matchedFiles = ''; const pathdir = `${pathBase}\\${this.pathAdr}`; const files = readdirSync(pathdir); for (const file of files) { const seq = Number.parseInt(this._adr.getSequences(file), 10); if (seq === id) { matchedFiles = file; return matchedFiles; } } return matchedFiles; } } export class Add { // Template que usaremos para la creación del contenido del fichero templateAdr; pathAdr = config.get('adrPath'); _directory = new Directory(); _locale; constructor() { this._locale = Locale.getInstance().getLocale(); const locale = config.get('locale'); this.templateAdr = readFileSync(`${__dirname}\\..\\templates\\adr-${locale}.md`, { encoding: 'utf8', flag: 'r' }); } async questionsForAdd() { const qs = [{ name: 'shortTitle', type: 'input', message: this._locale.command.new.withAnswers.shortTitle, }, { name: 'contextDescription', type: 'input', message: this._locale.command.new.withAnswers.contextDescription, }]; return await inquirer.prompt(qs); } createFile(data) { const path = `${pathBase}\\${this.pathAdr}`; this.createDirectory(path); const lastAdrCreate = this._directory.getMostRecentFile(path); let seq = '0000'; if (lastAdrCreate === undefined) { console.log(chalk.yellow.bold('Get last file name to geneate sequences.')); process.exit(); } else { const nameLastAdr = lastAdrCreate[lastAdrCreate.length - 1]; seq = this.getNextSequences(nameLastAdr); } const fileName = this.getNewName(data.shortTitle); const file = `${path}\\${seq}-${fileName}.md`; if (!existsSync(path)) { mkdirSync(path, 0o777); } try { this.templateAdr = this.templateAdr.replace('$shortTitle', data.shortTitle); const date = moment().format('YYYY-MM-DD hh:mm:ss'); this.templateAdr = this.templateAdr.replace('$dateAdr', date); this.templateAdr = this.templateAdr.replace('$statusAdr', Status.setStatusColor('proposed')); this.templateAdr = this.templateAdr.replace('$contextDescription', this.checkContextValid(data.contextDescription)); writeFileSync(file, this.templateAdr, { mode: 0o777 }); } catch (error) { console.error(chalk.red.bold(error)); process.exit(); } finally { console.log(` ------ CREADO CORRECTAMENTE ------\n Se ha creado el siguiente elemento\n - Tipo: ${chalk.blue.bold(data.type)}\n - Ruta: ${chalk.blue.bold(file)}\n ----------------------------------\n `); process.exit(); } } createFileImediatly(data) { const path = `${pathBase}\\${this.pathAdr}`; this.createDirectory(path); const lastAdrCreate = this._directory.getMostRecentFile(path); let seq = '0000'; if (lastAdrCreate === undefined) { console.log(chalk.yellow.bold('Get last file name to geneate sequences.')); process.exit(); } else { const nameLastAdr = lastAdrCreate[lastAdrCreate.length - 1]; seq = this.getNextSequences(nameLastAdr); } const fileName = this.getNewName(data.shortTitle); const file = `${path}\\${seq}-${fileName}.md`; if (!existsSync(path)) { mkdirSync(path, 0o777); } try { this.templateAdr = this.templateAdr.replace('$shortTitle', data.shortTitle); const date = moment().format('YYYY-MM-DD hh:mm:ss'); this.templateAdr = this.templateAdr.replace('$dateAdr', date); this.templateAdr = this.templateAdr.replace('$statusAdr', Status.setStatusColor('proposed')); this.templateAdr = this.templateAdr.replace('$contextDescription', this.checkContextValid('')); writeFileSync(file, this.templateAdr, { mode: 0o777 }); } catch (error) { console.error(chalk.red.bold(error)); process.exit(); } finally { console.log(` ------ CREADO CORRECTAMENTE ------\n Se ha creado el siguiente elemento\n - Tipo: ${chalk.blue.bold(data.shortTitle)}\n - Ruta: ${chalk.blue.bold(file)}\n ----------------------------------\n `); process.exit(); } } createDirectory(dirname) { if (!existsSync(dirname)) { mkdirSync(dirname, { recursive: true }); } } getNextSequences(string_) { try { if (string_ === undefined) { string_ = '0000'; } const seq = Number(string_.slice(0, 4)); const newSeq = seq + 1; if (newSeq > 9999) { throw new Error('Max ADR sequences reached'); } return newSeq.toString().padStart(4, '0'); } catch (error) { let message = 'Unknown Error'; if (error instanceof Error) { message = error.message; console.error(chalk.red.bold(`Error in parse next sequence to adr file:${message}`)); } process.exit(); } } getNewName(data) { const string_ = data.replace(/\s+/g, '-').toLowerCase(); return string_; } checkContextValid(string_) { if (this.isEmpty(string_)) { return this._locale.class.adr.Add.checkContextValid; } return string_; } isEmpty = (string_) => (!string_?.length); } export default Adr; //# sourceMappingURL=adr.js.map