adr-cli
Version:
Architecture Decision Records (ADR) command cli
405 lines • 15.6 kB
JavaScript
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