peanut-stash
Version:
Collaborative command line cloud Stash, Share, Copy & Paste tool.
924 lines (737 loc) • 31.4 kB
JavaScript
import fetch from 'node-fetch';
import crypto from 'crypto';
import color from 'picocolors';
import figlet from "figlet";
import * as prompts from '@clack/prompts'
import clipboard from 'clipboardy';
import fs from 'fs';
import path from 'path';
import os from 'os';
import {read} from 'read';
import tty from 'tty';
import { execSync } from 'child_process';
import {
ref,
push,
get,
remove,
orderByChild,
equalTo,
query,
limitToFirst
} from 'firebase/database';
import {
PasteClient,
Publicity,
ExpireDate } from "pastebin-api";
import {
loginUser,
logoutUser,
manageUsers,
resetPassword,
registerUser } from './users.js';
import {
stashPeanut,
listPeanuts,
popPeanut } from './stash.js';
// Show console help arguments
export function showArgs() {
console.log(`\n${color.cyan('Peanut Stash 🥜 1.1.1')} - Collaborative command line cloud Stash, Share, Copy & Paste tool.\n`);
console.log("Quickly stash, pop, run, send & receive console commands and text with your coding, IT, devops teams.\n")
console.log(`${color.yellow("Arguments Usage:\n")}`);
console.log(`register (r) <email>\t\t\t ${color.cyan('Register new account')}`);
console.log(`login (i) <email>\t\t\t ${color.cyan('Login (REQUIRED)')}`);
console.log(`logout (o) \t\t\t\t ${color.cyan('Logout')}`);
console.log(`reset (rs)\t\t\t\t ${color.cyan('Reset password')}\n`);
console.log(`stash (s)\t\t\t\t ${color.cyan('Quickly stash terminal text peanuts 🥜 for reuse later')}`);
console.log(`pop (p) \t\t\t\t ${color.cyan('Pop last stashed text peanut 🥜 back to terminal')}`);
console.log(`list (l) \t\t\t\t ${color.cyan('Manage 🥜 stash (add/run/clipboard/print/label/ai/share)')}`);
console.log(`alias (a) \t\t\t\t ${color.cyan('Shortcut name to stashed command to run (optional params)')}`);
console.log(`gemini (ai) \t\t\t\t ${color.cyan('Setup/Infer commands with Gemini v1 API (paid/free)')}\n`);
console.log(`users (u) \t\t\t\t ${color.cyan('Manage connected users to share with')}\n`);
console.log(`categories (c) \t\t\t\t ${color.cyan('Manage label categories')}\n`);
console.log(`server (sv) \t\t\t\t ${color.cyan('Use default or custom firebase server (web app creds)\n')}`);
console.log(`about (a) \t\t\t\t ${color.cyan('About Page\n')}`);
console.log(`${color.yellow("Examples:\n")}`);
console.log(`pnut stash ${color.cyan('or')} pnut s `);
console.log(`pnut list ${color.cyan('or')} pnut l`);
console.log(`pnut pop ${color.cyan('or')} pnut p `);
console.log('\n');
}
// Basic email validator
export function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Helper function to consume API
export async function fetchJsonAPI(url, type = 'GET', body_args = {}) {
const response = await fetch(url, {
method: type,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body_args),
});
const data = await response.json();
return data;
}
// Process what to do based on state (user/action)
export function stateMachine(db, auth, user, action, args) {
// User is signed in
if (user) {
// https://firebase.google.com/docs/reference/js/auth.user
// Check console parameters
switch (action) {
// Run alias shortcut to a stashed command with optional parameters
case 'alias':
case 'a':
runPeanutAlias(user, db, args);
break;
// Configure Server chosen
case 'askGemini':
case 'ai':
askAI(user, db);
break;
// Configure Server chosen
case 'categories':
case 'c':
manageCategories(user, db);
break;
// Configure Server chosen
case 'server':
case 'sv':
manageServer();
break;
// Stash a text peanut in your account
case 'users':
case 'u':
manageUsers(user, db);
break;
// Stash a text peanut in your account
case 'pop':
case 'p':
popPeanut(user, db);
break;
// Stash a text peanut in your account
case 'stash':
case 's':
stashPeanut(user, db, "quit");
break;
// List available peanuts
case 'list':
case 'l':
listPeanuts(user, db);
break;
// register new account and send verification email
case 'register':
case 'r':
registerUser(args[1], auth);
break;
// Login to existing account
case "login":
case "i":
loginUser(args[1], auth, db);
break;
case "logout":
case "o":
logoutUser();
break;
case "about":
case "a":
console.log(figlet.textSync("Peanut Stash 1.1.1 ", { horizontalLayout: "full" }));
console.log(`Quickly stash, pop, run, send & receive console commands and text with your team.\nHelpful tiny tool for coders, IT and devops who work frequently within the terminal.\n\nUnlike pastebin and its 3rd party tools/ecosystem, this tool and project is more focused on quick efficient terminal commands stashing/sharing and not on code sharing.\n${color.cyan("https://github.com/roymasad/peanut-stash")}`);
process.exit(0);
break;
case "reset":
case "rs":
resetPassword(user);
break;
// Show help message
default:
showArgs()
process.exit(0);
break;
}
} else {
// Current user is not signed in with an account
// Only commands available to be processedare register, login and about
switch (action) {
// Configure Server chosen
case 'server':
case 'sv':
manageServer();
break;
// register new account and send verification email
case 'register':
case 'r':
registerUser(args[1], auth);
break;
case "login":
case "i":
loginUser(args[1], auth);
break;
case "about":
case "a":
console.log(figlet.textSync("Peanut Stash 1.0.0", { horizontalLayout: "full" }));
console.log(`Quickly stash, pop, send & receive console commands and text with your team.\nHelpful tiny tool for coders and devops who work frequently within the terminal.\n\nUnlike pastebin and its 3rd party tools/ecosystem, this tool and project is more focused on quick efficient terminal commands stashing/sharing and not on code sharing.\nhttps://www.npmjs.com/package/peanut-stash-cli`);
break;
default:
showArgs()
console.log(`${color.red('Warning:')} User not signed in. An account is required.\n`);
process.exit(0);
break;
}
}
}
// Symetrical encryption/ Function to encrypt the email and password
export function encryptDataSymmetrical(data, encryptionKey) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
let encryptedData = cipher.update(data, 'utf8', 'base64');
encryptedData += cipher.final('base64');
return iv.toString('base64') + ':' + encryptedData;
}
// Function to decrypt the encrypted email and password
export function decryptDataSymmetrical(encryptedData, encryptionKey) {
const [iv, encrypted] = encryptedData.split(':');
const decipher = crypto.createDecipheriv('aes-256-cbc', encryptionKey, Buffer.from(iv, 'base64'));
let decryptedData = decipher.update(encrypted, 'base64', 'utf8');
decryptedData += decipher.final('utf8');
return decryptedData;
}
// Function to encrypt a string using the public key
export function encryptStringWithPublicKey(publicKey, text) {
const buffer = Buffer.from(text, 'utf8');
const encrypted = crypto.publicEncrypt(publicKey, buffer);
return encrypted.toString('base64');
}
// Function to decrypt an encrypted string using the private key
export function decryptStringWithPrivateKey(privateKey, encryptedText) {
const buffer = Buffer.from(encryptedText, 'base64');
const decrypted = crypto.privateDecrypt(privateKey, buffer);
return decrypted.toString('utf8');
}
// Manage default or custom firebase server
async function manageServer(){
try {
// define file locations
const hiddenFolderPath = path.join(os.homedir(), '.peanuts');
const authFilePath = path.join(hiddenFolderPath, 'session.json');
const serverConfFilePath = path.join(hiddenFolderPath, 'server.json');
let answer_action = await prompts.select({
message: color.cyan(`Choose Server`),
options: [ {value: 'default' , label: 'Default Public Server'},
{value: 'custom' , label: 'Custom Private Server'}]
});
if (prompts.isCancel(answer_action)) {
console.log(color.yellow("Cancelled"));
process.exit(0);
}
if (answer_action == 'default') {
// just remove the server config file if it exist, when the app starts it will use default server
if (fs.existsSync(serverConfFilePath)) {
fs.unlinkSync(serverConfFilePath);
}
console.log(color.green("Using default public server"));
process.exit(0);
} else if (answer_action == 'custom') {
console.log("Enter your Firebase Web App Custom Project Keys (free Spark or paid Blaze");
console.log(color.yellow("New/Existing App: https://console.firebase.google.com/"));
try {
var apiKey = await read({ prompt: `${color.cyan('apiKey:')} `});
var authDomain = await read({prompt: `${color.cyan('authDomain:')} `});
var databaseURL = await read({prompt: `${color.cyan('databaseURL:')} `});
var projectId = await read({prompt: `${color.cyan('projectId:')} `});
if (apiKey.length == 0 || authDomain.length == 0 || databaseURL.length == 0 || projectId.length == 0)
{
console.log(`${color.yellow("Error: Empty text")}`);
process.exit(0);
}
} catch(error) {
if (error == "Error: canceled")
console.log(`${color.yellow("Cancelled")}`);
else console.log(`${color.yellow(error)}`);
process.exit(0);
}
let config_json = {
apiKey: apiKey,
authDomain: authDomain,
databaseURL: databaseURL,
projectId: projectId
}
// save the config to a file
fs.writeFileSync(serverConfFilePath, JSON.stringify(config_json));
// log out the user if it is cached
if (fs.existsSync(authFilePath)) {
fs.unlinkSync(authFilePath);
}
console.log(color.green("Using custom private public server. You need to re-login."));
process.exit(0);
}
} catch (error) {
console.log(error);
process.exit(1);
}
process.exit(0);
}
// Manage Categories
async function manageCategories(user, db){
// variables and constants for the loop
const userEmail = user.email;
const firebase_email = userEmail.replace(/\./g, '_');
// load categories we can use to stash under
const categoryRef = ref(db, `users/${firebase_email}/private/categories`);
while(true) {
let categoriesList = []; // for display initially
let categories = []; // to save extra data such as db ref to be able to delete
const snapshot = await get(categoryRef);
try {
if (snapshot.exists()) {
let index = 0;
snapshot.forEach(element => {
categories.push({
name: element.val().name,
databaseRef: element.ref
});
categoriesList.push({label: element.val().name, value: `DAT:${index}:` + element.val().name});
index++;
});
} else
categoriesList = [];
} catch(error) {
console.log(`${color.red('Error Loading Categories:')} ${error}`);
process.exit(1);
}
categoriesList.reverse(); // latest first
categoriesList.push({label: color.cyan("Add"), value: "ADD:add"}); //bottom
categoriesList.push({label: color.yellow("Cancel"), value: "CNL:cancel"}); //bottom
let answer_category = await prompts.select({
message: 'Select a category label',
options: categoriesList
});
if (prompts.isCancel(answer_category)) {
console.log(color.yellow("Cancelled"));
process.exit(0);
}
if (answer_category.substring(0, 4) == "CNL:") {
console.log(`${color.yellow('Cancelled')}`);
process.exit(0);
}
else if (answer_category.substring(0, 4) == "ADD:") {
try {
var answer = await read({prompt: `${color.cyan('\Add a new category:\n')} `});
if (answer.length == 0)
{
console.log(`${color.yellow("Error: Empty text")}`);
continue;
}
} catch(error) {
console.log(`${color.yellow("Cancelled")}`);
process.exit(0);
}
// save it to database
await push(categoryRef,{ name : answer });
console.log(`${color.green('Success: Category added')}`);
continue;
}
else if (answer_category.substring(0, 4) == "DAT:") {
let manage_action = await prompts.select({
message: 'Select Action',
options: [
{label: color.magenta("Export to Pastebin"), value: "exportPastebin"},
{label: color.yellow("Cancel"), value: "cancel"},
{label: `${color.red("# Delete #")}`, value: "delete"},
]
});
if (prompts.isCancel(manage_action)) {
console.log(color.yellow("Cancelled"));
process.exit(0);
}
// export to pastebin entire category
if (manage_action == "exportPastebin") {
answer_category = answer_category.slice(6);
await exportPasteBin(user, db, answer_category, 'category');
continue;
}
else if (manage_action == "cancel") {
console.log(`${color.green('Cancelled')}`);
continue;
}
// delete category label (not content)
else if (manage_action == "delete") {
const shouldDelete = await prompts.confirm({
message: 'This will delete the category label, not the content. Are you Sure?',
});
if (prompts.isCancel(shouldDelete)) {
console.log(color.yellow("Cancelled"));
continue;
}
if (shouldDelete) {
// select and remove prefix
answer_category = answer_category.slice(4);
// get database ref to remove
let [metaDataIndex, category] = answer_category.split(':');
category = answer_category.substring(answer_category.indexOf(':') + 1);
await remove(categories[metaDataIndex].databaseRef);
console.log(`${color.green('Success: Category deleted. Existing peanut labels in stash not affected. Re-categorize them.')}`);
continue;
} else {
console.log(`${color.yellow('Cancelled')}`);
continue;
}
}
}
}
}
// Ask AI(Gemini using API keys) to infer commands for you
// Then select what do do with them (print/execute/clipboard/save/cancel)
async function askAI(user, db) {
// first check if there is a ai.json json file saved in ~/.peanuts
// define file locations
const hiddenFolderPath = path.join(os.homedir(), '.peanuts');
const AIConfFilePath = path.join(hiddenFolderPath, 'ai.json');
if (fs.existsSync(AIConfFilePath)) {
//console.log("AI configuration found");
// load api key
const aiData = fs.readFileSync(AIConfFilePath, 'utf8');
var aiJSON = JSON.parse(aiData);
// check if it is for gemini, for future support for other providers
if (aiJSON.provider != "geminiV1") {
console.log(color.yellow("AI configuration present is not for Gemini V1. Exiting. Delete ~/.peanuts/ai.json and add new api key."));
process.exit(0);
}
let answer_action = await prompts.select({
message: color.cyan(`Choose Action`),
options: [ {value: 'generate' , label: color.cyan('Generate console command')},
{value: 'remove' , label: color.red('# Delete current API Key #')}]
});
if (prompts.isCancel(answer_action)) {
console.log(color.yellow("Cancelled"));
process.exit(0);
}
// Generate or delete actions
if (answer_action == 'generate') {
console.log(color.magenta("Important: Only ask about a console command to create, no other topic, and be concise."));
try {
var geminiQuetion = await read({prompt: `${color.cyan('How do i create a console command to.. ')} `});
if (geminiQuetion.length == 0)
{
console.log(`${color.yellow("Error: Empty text")}`);
process.exit(0);
}
var geminiResponse = await generateGeminiAnswers(geminiQuetion, aiJSON.apiKey, 'generate');
console.log("");
console.log(color.green(geminiResponse));
console.log("");
process.exit(0);
} catch(error) {
if (error == "Error: canceled")
console.log(`${color.yellow("Cancelled")}`);
else console.log(`${color.yellow(error)}`);
process.exit(0);
}
} else if (answer_action == 'remove') {
const shouldDelete = await prompts.confirm({
message: 'Are you Sure?',
});
if (prompts.isCancel(shouldDelete)) {
console.log(color.yellow("Cancelled"));
process.exit(0);
}
if (shouldDelete) {
// remove the config file
fs.unlinkSync(AIConfFilePath);
console.log(`${color.green('Success: gemini API key removed. Relogin with new key.')}`);
process.exit(0);
} else {
console.log(`${color.green('Cancelled')}`);
process.exit(0);
}
}
process.exit(0);
}
else {
console.log(color.cyan("AI configuration not found. One is needed to infer commands."));
console.log(color.yellow("Get a free/paid v1 API key here: https://ai.google.dev/pricing"));
// Ask the user to input their gemini API key so we can save as json to AIConfFilePath
try {
var geminiAPIKey = await read({prompt: `${color.cyan('Enter Gemini API app key: ')} `});
if (geminiAPIKey.length == 0)
{
console.log(`${color.yellow("Error: Empty text")}`);
process.exit(0);
}
let config_json = {
apiKey: geminiAPIKey,
provider : "geminiV1" // this is for version1 of gemini
}
// save the config to a file
fs.writeFileSync(AIConfFilePath, JSON.stringify(config_json));
console.log(`${color.green('Success: gemini API key saved. Rerun command.')}`);
process.exit(0);
} catch(error) {
console.log(`${color.yellow(error)}`);
process.exit(0);
}
process.exit(0);
}
process.exit(0);
}
// Gemini V1 API inference helper function
export async function generateGeminiAnswers(promptQuestion, apiKey, mode = "generate", peanutList = null) {
// add a bit of prompt engineering to limit the discuss to cli command
if (mode == "generate")
{
promptQuestion = "i need to create a specific computer console command, how do i do the following: START OF QUESTION : " + promptQuestion;
promptQuestion += ". END OF QUESTION. IMPORTANT NOTES, don't explain, only output the selected command/parameters to run and choose the most suitable recommended relevant answer for this request"
promptQuestion += ". also if the question's topic is/was not specifically about cli/terminal/console commands generation in the context of computer coding, devops and IT or if you are in doubt about the question's topic, don't reply, just only say 'Invalid prompt'";
}
else if (mode == "explain")
{
promptQuestion = "i need help explaining the following console/terminal computer command: START OF QUESTION:" + promptQuestion;
promptQuestion += ". END OF QUESTION. IMPORTANT NOTES, limit your answer to a few lines";
promptQuestion += ". also if the question's topic is/was not specifically about cli/terminal/console commands in the context of computer coding, devops and IT or if you are in doubt about the question's topic, don't reply, just only say 'Invalid prompt'";
}
else if (mode == "search")
{
let promptQuestion2 = "i need help searching through the following list of terminal commmands: START OF LIST as comma separated array : [ " + peanutList.toString();
promptQuestion2 += ` ] . END OF LIST. Search for one matching terminal command that does the following task best: START OF DESCRIPTION: ${promptQuestion} . END OF DESCRIPTION `;
promptQuestion2 += `. IMPORTANT NOTES, limit your answer to only the command found as-it-is so i can copy paste it into the terminal`;
promptQuestion2 += '. if no command matching that task is found or you are not sure which to select just say "Not found"';
promptQuestion2 += ". also if the question's topic is not specifically my list of cli/terminal/console commands or if you are in doubt about the question's topic, don't reply, just only say 'Not found'";
promptQuestion = promptQuestion2;
}
try {
// Define the request body for the gemini endpoint
const requestBody = {
contents: [
{
parts: [
{
text: promptQuestion
}
]
}
]
};
// Define the URL with the API key
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`;
// Define request options
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
};
// Send the request
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Parse and return the response data
const responseData = await response.json();
const answer = responseData.candidates[0].content.parts[0].text;
// double check if answer exists in peanut list already to ward off AI hallucination
if (mode == "search" && !peanutList.includes(answer)) {
return "Not found";
} else {
return answer;
}
} catch (error) {
console.error('AI Error:', error);
process.exit(1)
}
}
export function getTerminalSize() {
// checks if the standard input stream has a file descriptor = connected to a terminal
if (tty.isatty(process.stdin.fd)) {
const { columns, rows } = process.stdout;
return { console_columns: columns, console_rows: rows };
} else {
console.error('Error: stty command cannot be executed in a non-interactive shell.');
return { columns: null, rows: null };
}
}
// Export data to paste bin either single mode or category
export async function exportPasteBin(user, db, data, mode = "single") {
// variables and constants
const userEmail = user.email;
const firebase_email = userEmail.replace(/\./g, '_');
// location of hidden pastebin config file
const hiddenFolderPath = path.join(os.homedir(), '.peanuts');
const PasteBinConfFilePath = path.join(hiddenFolderPath, 'pastbin.json');
// check if pastebin api key is present
if (fs.existsSync(PasteBinConfFilePath)) {
//console.log("PasteBin configuration found");
// load api key
const pastebinData = fs.readFileSync(PasteBinConfFilePath, 'utf8');
var pastebinJSON = JSON.parse(pastebinData);
// init pastebin client
const pastebinClient = new PasteClient(pastebinJSON.apiKey);
// ask for a title for the paste
try {
var title = await read({prompt: `${color.cyan('Choose a title name for the paste: ')} `});
if (title.length == 0)
{
console.log(`${color.yellow("Error: Empty text")}`);
return;
}
if (title.length > 100) // PasteBin limit is 100 characters
{
console.log(`${color.yellow("Error: Text too long")}`);
}
} catch (error) {
if (error == "Error: canceled")
console.log(`${color.yellow("Cancelled")}`);
else console.log(`${color.yellow(error)}`);
return;
}
// are we exporting a single command or a category?
if (mode == "single"){
const url = await pastebinClient.createPaste({
code: data,
expireDate: ExpireDate.Never,
format: "bash",
name: title,
publicity: Publicity.Unlisted,
});
console.log(color.green("\nSuccess: Command exported to pastebin"));
console.log("\nPasteBin URL: " + color.cyan(url) + "\n");
return;
}
else if (mode == "category"){
// if category is selected, then the data parameter is the category name
// load the text peanuts under that category as a list to save in pastebin
// load peanuts under that category
const peanutRef = ref(db, `users/${firebase_email}/private/peanut-stash`);
let snapshot = await get(peanutRef);
if (snapshot.exists()) {
let peanutList = [];
snapshot.forEach((peanut) => {
// Decrypt data with user private key
let decryptedPeanut = decryptStringWithPrivateKey(user.privateKey, peanut.val().data);
// check if category matches
if (peanut.val().category == data)
{
peanutList.push(decryptedPeanut);
}
});
if (peanutList.length == 0) {
console.log(`${color.cyan('No Peanuts Stashed in that category.')}`);
return;
}
const url = await pastebinClient.createPaste({
code: peanutList.join("\n"),
expireDate: ExpireDate.Never,
format: "bash",
name: title,
publicity: Publicity.Unlisted,
})
console.log(color.green("\nSuccess: Category commands exported to pastebin"));
console.log("\nPasteBin URL: " + color.cyan(url) + "\n");
return;
}
else {
console.log(`${color.cyan('No Peanuts Stashed.')}`);
return;
}
}
}
else {
// no pastebin api key found, ask to save one
console.log(color.cyan("PasteBin configuration not found. One is needed to infer commands."));
console.log(color.yellow("Get a free api key here: https://pastebin.com/doc_api"));
try {
var pastebinAPIKey = await read({prompt: `${color.cyan('Enter Pastebin API key: ')} `});
if (pastebinAPIKey.length == 0)
{
console.log(`${color.yellow("Error: Empty text")}`);
process.exit(0);
}
let config_json = {
apiKey: pastebinAPIKey
}
// save the config to a file
fs.writeFileSync(PasteBinConfFilePath, JSON.stringify(config_json));
console.log(`${color.green('Success: Pastebin API key saved. Rerun command.')}`);
return;
} catch(error) {
console.log(`Error: ${color.red(error)}`);
process.exit(0);
}
}
}
// Run an alias to a stashed command with any optional passed parameters
async function runPeanutAlias(user, db, args) {
const userEmail = user.email;
const firebase_email = userEmail.replace(/\./g, '_');
const parms = [];
// first check if an alias arguments was passed
if (args.length >= 2) {
// loop and add to array the optional arguments after the alias
for (var i = 2; i < args.length; i++) {
parms.push(args[i]);
}
// get/find the alias with this name value in the list, limit to first 1 if there are duplicate alias names
const aliasRef = ref(db, `users/${firebase_email}/private/peanut-alias/`);
// use query since we are filtering, make sure index on name exists
const aliasQuery = query(aliasRef, orderByChild("name"), equalTo(args[1]), limitToFirst(1));
// both alias and peanut stash share save key id (in different location) top optimize retrieval
// get the id of the alias, then use it to retrive the action stashed command
// this approach takes 2 steps, but allows us to link the alias to a command that can be edited
// if we baked/denormalized the command into the alias, we would need to update it also on edit of command
// read speed vs centralization of code update, i am choosing here the later option.
try {
const snapshot = await get(aliasQuery);
if (snapshot.exists()) {
const data = snapshot.val(); // Retrieves the entire result object
const aliasKey = Object.keys(data)[0]; // Then get the first key (should only be one)
// now that we have the alias key, the peanut stash has the same key id, so retrieve the stashed data
const peanutRef = ref(db, `users/${firebase_email}/private/peanut-stash/${aliasKey}`);
const snapshot2 = await get(peanutRef);
let decryptedPeanut = "";
if (snapshot2.exists()) {
decryptedPeanut = decryptStringWithPrivateKey(user.privateKey, snapshot2.val().data);
}
// now parse the decrypted command in case it was a template with variablesand there were passed parameters
const parsedCommand = parseAliasCommand(decryptedPeanut, parms);
// execute the parsed command using await syntac
console.log(color.green(`Success: Command execute\n ${parsedCommand}`));
const output = await execSync(parsedCommand);
console.log(output.toString()); // the command's output
process.exit(0);
} else {
console.log(color.red("Error: Alias not found"));
process.exit(0);
}
} catch (error) {
console.log(color.red(error));
process.exit(0);
}
} else {
console.log(color.red("\nError: No Alias parameter passed"));
}
process.exit(0)
}
// Function to parse any alias template commands with or without built in variables and returns parsed string
// it takes as input args the app cli arguments and the command to parse itself
// argIndex specifies where to start or how many arguments are skipped from the arg array
// this new parsed string can then be executed by the calling function
export function parseAliasCommand(passedCommand, args, argIndex = 0) {
// Regular expression to find all occurrences of `${variableName}`
const variableRegex = /\$\{([^}]+)\}/g;
// Starting index (0 would be 'action' and after that are the rest of the arguments)
//let argIndex = 1;
// Replace each variable with the corresponding argument from args
let parsedCommand = passedCommand.replace(variableRegex, (match) => {
if (argIndex < args.length) {
// Replace with argument and increment the index
return args[argIndex++];
} else {
// No more arguments left, return the original match
return match;
}
});
return parsedCommand;
}