#!/usr/bin/env node const path = require('path'); const { Command } = require('commander'); const shell = require('shelljs'); class This { constructor() { this.version = '0.0.1'; // Default version, can be overridden in subclasses this.description = 'This is the parent class all other scripts should extend.'; this.scriptName = path.parse(process.argv[1]).base; this.workingDir = path.parse(process.argv[1]).dir; } init() { // implement commander abilities this.program = new Command() .name(this.scriptName) .version(this.version) .description(this.description) .configureOutput({ // Visibly override write routines as example! //writeOut: (str) => process.stdout.write(`[OUT] ${str}`), //writeErr: (str) => process.stdout.write(`[ERR] ${str}`), // Highlight errors in color. outputError: (str, write) => write(this._echoInRed(str)), }); // dynamically add all methods with appropriate docstring to commander this.listMethods().forEach((method) => { this._parseDocStringToCmd(this[method]); }); } getClassName() { return this.constructor.name; } listMethods() { const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter( (prop) => typeof this[prop] === 'function' && prop !== 'constructor' && !prop.startsWith('_') ); return methods; } listProperties() { const properties = Object.keys(this).filter((prop) => typeof this[prop] !== 'function'); return properties; } discovery() { console.log(`My name is '${this.getClassName()}' and I have the version: ${this.version}`); console.log(`My methods are:`, this.listMethods()); console.log(`My properties are:`, this.listProperties()); } // Method to create a context manager for this instance withContext(callback) { callback(this); } start() { // Not implemented, so it needs to be overridden! console.warn(this._echoInYellow(`Method ${this._getCurrentFunctionName()} not implemented!`)); } execCmd(cmd) { // Run external tool synchronously if (shell.exec(cmd).code !== 0) { shell.echo('[ERR] ' + this._echoInRed(`Command '${cmd}' failed`)); shell.exit(1); } } _echoInRed(str) { // Add ANSI escape codes to display text in red. return `\x1b[31m${str}\x1b[0m`; } _echoInYellow(str) { // Add ANSI escape codes to display text in yellow. return `\x1b[33m${str}\x1b[0m`; } _echoInGreen(str) { // Add ANSI escape codes to display text in green. return `\x1b[32m${str}\x1b[0m`; } _getCurrentFunctionName() { // Create an Error object (but don't throw it) const err = new Error(); // Extract the current stack trace Error.captureStackTrace(err, this._getCurrentFunctionName); // Extract the function name from the stack trace const callerName = err.stack.split('\n')[1].trim().split(' ')[1]; return callerName; } // Utility function to extract docstring from a function, e.g. // // echo(str) { // /** // @command // @argument('') // */ // console.log(str); // } // // Attention: // In JavaScript, when a script is executed as a bash script with a shebang (#!/usr/bin/env node), // the toString method on functions does not include comments preceding the function definition. _getFunctionDocString(fn) { const fnStr = fn.toString(); const docStringMatch = fnStr.match(/\/\*[\s\S]*?\*\//); return docStringMatch ? docStringMatch[0] : null; } // Parser function to convert docstring to commander statement _parseDocStringToCmd(fn) { const docString = this._getFunctionDocString(fn); if (!docString) { // No docstring found return; } // Extract command and arguments const commandMatch = docString.match(/@command(?:\('([^']*)'\))?/); const aliasMatch = docString.match(/@alias\('(.+?)'\)/); const argumentsMatch = /@argument\('(.+?)'\)/g; const optionsMatch = /@option\('(.+?)'\)/g; const defaultMatch = docString.match(/@default/); const executableMatch = docString.match(/@executable\('(.+?)'\)/); if (!commandMatch) { // No @command tag found return; } const commandName = fn.name; const commandDescription = commandMatch ? commandMatch[1] : ''; const alias = aliasMatch ? aliasMatch[1] : ''; const defaultCommand = defaultMatch ? true : false; const executable = executableMatch ? executableMatch[1] : ''; // Generate the commander statement // Get the function reference const func = this[commandName].bind(this); // Create a command // When `.command()` is invoked with a description argument, // this tells Commander that you're going to use a stand-alone executable for the subcommand. let command; if (commandDescription) { command = this.program.command(commandName, commandDescription, { isDefault: defaultCommand, executableFile: executable, }); if (alias) { command.alias(alias); } } else { command = this.program.command(commandName, { isDefault: defaultCommand }); // Set the action for the command command.action((arg) => { func(arg); }); } // If the commandName expects arguments, we add them here let argMatch; while ((argMatch = argumentsMatch.exec(docString)) !== null) { command.argument(argMatch[1]); } // If the commandName expects options, we add them here let optMatch; while ((optMatch = optionsMatch.exec(docString)) !== null) { command.option(optMatch[1]); } } _addMethodsToClass(cls, metadata) { if (cls) { cls.commands.push(metadata); } } // Higher-order function to decorate other functions and provide logging _logDecorator(fn) { return function (...args) { console.log(`Calling ${fn.name} with arguments:`, args); const result = fn.apply(this, args); // Use apply to maintain context console.log(`Result of ${fn.name}:`, result); return result; }; } } module.exports = This;