From 83f6387d446ee4261ad2fb14857fae4bed841fc8 Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Wed, 11 Mar 2026 23:33:31 +0200 Subject: [PATCH 01/11] feat: add basic scope part --- src/main.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/main.js diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..c6937cf --- /dev/null +++ b/src/main.js @@ -0,0 +1,53 @@ +import readline from "readline"; +import os from "os"; +import process from "process"; + +process.chdir(os.homedir()); + +console.log("Welcome to Data Processing CLI!"); +console.log(`You are currently in ${process.cwd()}`); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: "> ", +}); + +rl.prompt(); + +rl.on("line", (input) => { + const commandInput = input.trim(); + + if (!commandInput) { + rl.prompt(); + return; + } + + if (commandInput === ".exit") { + exitProgram(); + return; + } + + try { + const [command, ...args] = commandInput.split(" "); + + switch (command) { + default: + console.log("Invalid input."); + } + console.log(`You are currently in ${process.cwd()}`); + } catch (err) { + console.log("Operation failed"); + } + + rl.prompt(); +}); + +rl.on("SIGINT", () => { + exitProgram(); +}); + +function exitProgram() { + console.log("Thank you for using Data Processing CLI!"); + process.exit(0); +} From 773806609ea270fdf40a89a275977f48709622a9 Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sat, 14 Mar 2026 21:46:43 +0200 Subject: [PATCH 02/11] feat: implement navigation & working directory commands --- src/main.js | 13 +++++++-- src/navigation.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/navigation.js diff --git a/src/main.js b/src/main.js index c6937cf..15a9885 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ import readline from "readline"; import os from "os"; import process from "process"; +import {changeDir, listDir, up} from "./navigation.js"; process.chdir(os.homedir()); @@ -29,9 +30,17 @@ rl.on("line", (input) => { } try { - const [command, ...args] = commandInput.split(" "); - + const [command, ...args] = commandInput.trim().split(/\s+/); switch (command) { + case "up": + up(); + break; + case "cd": + changeDir(args.join(" ")); + break; + case "ls": + listDir(); + break; default: console.log("Invalid input."); } diff --git a/src/navigation.js b/src/navigation.js new file mode 100644 index 0000000..54dcd6f --- /dev/null +++ b/src/navigation.js @@ -0,0 +1,70 @@ +import path from "path"; +import fs from "fs"; + +export const up = () => { + try { + const currentDir = process.cwd(); + const parentDir = path.resolve(currentDir, ".."); + + if (parentDir !== currentDir) { + process.chdir(parentDir); + } + + console.log(`You are already in the root directory: ${process.cwd()}`); + } catch { + console.log("Operation failed"); + } +}; + +export const changeDir = (providedPath) => { + try { + if (!providedPath) { + console.log("Operation failed: Missing path"); + return; + } + + const resolvedPath = path.isAbsolute(providedPath) + ? providedPath + : path.resolve(process.cwd(), providedPath); + + if ( + !fs.existsSync(resolvedPath) || + !fs.statSync(resolvedPath).isDirectory() + ) { + console.log("Operation failed: Invalid directory"); + return; + } + + process.chdir(resolvedPath); + console.log(`You are now in: ${process.cwd()}`); + } catch { + console.log("Operation failed"); + } +}; + +export const listDir = () => { + try { + const currentDir = process.cwd(); + + const entries = fs.readdirSync(currentDir, {withFileTypes: true}); + + const formatted = entries.map((entry) => ({ + name: entry.name, + type: entry.isDirectory() ? "folder" : "file", + })); + + formatted.sort((a, b) => { + if (a.type !== b.type) { + return a.type === "folder" ? -1 : 1; + } + + return a.name.localeCompare(b.name); + }); + + formatted.forEach((item) => { + console.log(`${item.name} [${item.type}]`); + }); + } catch { + console.log("Operation failed"); + } +}; From e2c21f219e5f1e0063724b3dc7bc09d1b793778e Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sat, 14 Mar 2026 23:33:55 +0200 Subject: [PATCH 03/11] feat: add csv-to-json and json-to-csv commands --- src/commands/csvToJson.js | 65 +++++++++++++++++++++++++++++++++++++++ src/commands/data.csv | 5 +++ src/commands/jsonToCsv.js | 60 ++++++++++++++++++++++++++++++++++++ src/main.js | 12 ++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/commands/csvToJson.js create mode 100644 src/commands/data.csv create mode 100644 src/commands/jsonToCsv.js diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js new file mode 100644 index 0000000..a9858a2 --- /dev/null +++ b/src/commands/csvToJson.js @@ -0,0 +1,65 @@ +import fs from "fs"; +import path from "path"; +import {Transform} from "stream"; +import {pipeline} from "stream/promises"; + +export const csvToJson = async (inputPath, outputPath) => { + try { + const resolvedInput = path.resolve(process.cwd(), inputPath); + const resolvedOutput = path.resolve(process.cwd(), outputPath); + + if (!fs.existsSync(resolvedInput)) { + console.log("Operation failed"); + return; + } + + fs.closeSync(fs.openSync(resolvedOutput, "a")); + + const readStream = fs.createReadStream(resolvedInput, "utf-8"); + const writeStream = fs.createWriteStream(resolvedOutput); + + let headers = []; + let isFirstLine = true; + let isFirstObject = true; + + const transform = new Transform({ + readableObjectMode: false, + writableObjectMode: false, + + transform(chunk, _, callback) { + const inputLines = chunk.toString().split("\n").filter(Boolean); + let output = ""; + + for (const line of inputLines) { + const values = line.split(","); + + if (isFirstLine) { + headers = values; + isFirstLine = false; + output += "["; + continue; + } + + const outPutEntity = {}; + headers.forEach((h, i) => { + outPutEntity[h.trim()] = values[i]?.trim(); + }); + + if (!isFirstObject) output += ","; + output += JSON.stringify(outPutEntity); + isFirstObject = false; + } + + callback(null, output); + }, + + flush(callback) { + callback(null, "]"); + }, + }); + + await pipeline(readStream, transform, writeStream); + } catch { + console.log("Operation failed"); + } +}; diff --git a/src/commands/data.csv b/src/commands/data.csv new file mode 100644 index 0000000..9a8b8a3 --- /dev/null +++ b/src/commands/data.csv @@ -0,0 +1,5 @@ +name,age,city +Alice,30,New York +Bob,25,London +Bob,32,Paris +Mark,27,Munich \ No newline at end of file diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js new file mode 100644 index 0000000..6bedc55 --- /dev/null +++ b/src/commands/jsonToCsv.js @@ -0,0 +1,60 @@ +import fs from "fs"; +import path from "path"; +import {Transform} from "stream"; +import {pipeline} from "stream/promises"; + +export const jsonToCsv = async (inputPath, outputPath) => { + try { + const resolvedInput = path.resolve(process.cwd(), inputPath); + const resolvedOutput = path.resolve(process.cwd(), outputPath); + + if (!fs.existsSync(resolvedInput)) { + console.log("Operation failed: input file does not exist"); + return; + } + + fs.closeSync(fs.openSync(resolvedOutput, "a")); + + const readStream = fs.createReadStream(resolvedInput, "utf-8"); + const writeStream = fs.createWriteStream(resolvedOutput); + + let headersWritten = false; + let headers = []; + let leftData = ""; + + const transform = new Transform({ + readableObjectMode: false, + writableObjectMode: false, + + transform(chunk, _, callback) { + leftData += chunk.toString(); + + try { + const jsonArray = JSON.parse(leftData); + + if (!headersWritten && jsonArray.length > 0) { + headers = Object.keys(jsonArray[0]); + writeStream.write(headers.join(",") + "\n"); + headersWritten = true; + } + + for (const obj of jsonArray) { + const line = headers.map((header) => obj[header] ?? "").join(","); + writeStream.write(line + "\n"); + } + + leftData = ""; + callback(); + } catch (err) { + callback(); + } + }, + }); + + await pipeline(readStream, transform, writeStream); + + console.log(`CSV saved to ${resolvedOutput}`); + } catch (err) { + console.log("Operation failed:", err.message); + } +}; diff --git a/src/main.js b/src/main.js index 15a9885..2ea5935 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,9 @@ import readline from "readline"; import os from "os"; import process from "process"; import {changeDir, listDir, up} from "./navigation.js"; +import {csvToJson} from "./commands/csvToJson.js"; +import {argParser} from "./utils/argParser.js"; +import {jsonToCsv} from "./commands/jsonToCsv.js"; process.chdir(os.homedir()); @@ -31,6 +34,9 @@ rl.on("line", (input) => { try { const [command, ...args] = commandInput.trim().split(/\s+/); + const input = argParser("--input", args); + const output = argParser("--output", args); + switch (command) { case "up": up(); @@ -41,6 +47,12 @@ rl.on("line", (input) => { case "ls": listDir(); break; + case "csv-to-json": + csvToJson(input, output); + break; + case "json-to-csv": + jsonToCsv(input, output); + break; default: console.log("Invalid input."); } From 9393bdb6f268d3f6bbbd6236616764d307ccafc4 Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sun, 15 Mar 2026 00:00:51 +0200 Subject: [PATCH 04/11] feat: implement count command --- src/commands/count.js | 39 +++++++++++++++++++++++++++++++++++++++ src/commands/file.txt | 18 ++++++++++++++++++ src/main.js | 6 +++++- 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/commands/count.js create mode 100644 src/commands/file.txt diff --git a/src/commands/count.js b/src/commands/count.js new file mode 100644 index 0000000..2286a60 --- /dev/null +++ b/src/commands/count.js @@ -0,0 +1,39 @@ +import fs from "fs"; +import path from "path"; +import readline from "readline"; + +export const countFile = async (inputPath) => { + try { + const resolvedInput = path.resolve(process.cwd(), inputPath); + + if (!fs.existsSync(resolvedInput)) { + console.log("Operation failed: input file does not exist"); + return; + } + + let lines = 0; + let words = 0; + let characters = 0; + + const readStream = fs.createReadStream(resolvedInput, "utf-8"); + + const rl = readline.createInterface({ + input: readStream, + crlfDelay: Infinity, + }); + + rl.on("line", (line) => { + lines += 1; + characters += line.length + 1; + words += line.trim().split(/\s+/).filter(Boolean).length; + }); + + await new Promise((resolve) => rl.on("close", resolve)); + + console.log(`Lines: ${lines}`); + console.log(`Words: ${words}`); + console.log(`Characters: ${characters}`); + } catch (err) { + console.log("Operation failed:", err.message); + } +}; diff --git a/src/commands/file.txt b/src/commands/file.txt new file mode 100644 index 0000000..9313ed5 --- /dev/null +++ b/src/commands/file.txt @@ -0,0 +1,18 @@ +Random text and words: +apple +banana +cloud +river +mountain +sky +computer +keyboard +mouse +window +door +light +shadow +book +pen +paper +pineapple diff --git a/src/main.js b/src/main.js index 2ea5935..a915f4a 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import {changeDir, listDir, up} from "./navigation.js"; import {csvToJson} from "./commands/csvToJson.js"; import {argParser} from "./utils/argParser.js"; import {jsonToCsv} from "./commands/jsonToCsv.js"; +import {countFile} from "./commands/count.js"; process.chdir(os.homedir()); @@ -36,7 +37,7 @@ rl.on("line", (input) => { const [command, ...args] = commandInput.trim().split(/\s+/); const input = argParser("--input", args); const output = argParser("--output", args); - + switch (command) { case "up": up(); @@ -53,6 +54,9 @@ rl.on("line", (input) => { case "json-to-csv": jsonToCsv(input, output); break; + case "count": + countFile(input); + break; default: console.log("Invalid input."); } From c9c727d1d79150c9f68668dd83ef84f47dec1872 Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sun, 15 Mar 2026 00:14:11 +0200 Subject: [PATCH 05/11] refactor: substitute process.cwd() for working directory variable --- src/commands/count.js | 3 ++- src/commands/csvToJson.js | 5 +++-- src/commands/jsonToCsv.js | 5 +++-- src/main.js | 2 +- src/utils/argParser.js | 4 ++++ 5 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 src/utils/argParser.js diff --git a/src/commands/count.js b/src/commands/count.js index 2286a60..ac8c997 100644 --- a/src/commands/count.js +++ b/src/commands/count.js @@ -1,10 +1,11 @@ import fs from "fs"; import path from "path"; import readline from "readline"; +import { CURRENT_DIR } from "../main"; export const countFile = async (inputPath) => { try { - const resolvedInput = path.resolve(process.cwd(), inputPath); + const resolvedInput = path.resolve(CURRENT_DIR, inputPath); if (!fs.existsSync(resolvedInput)) { console.log("Operation failed: input file does not exist"); diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index a9858a2..d81964a 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -2,11 +2,12 @@ import fs from "fs"; import path from "path"; import {Transform} from "stream"; import {pipeline} from "stream/promises"; +import {CURRENT_DIR} from "../main"; export const csvToJson = async (inputPath, outputPath) => { try { - const resolvedInput = path.resolve(process.cwd(), inputPath); - const resolvedOutput = path.resolve(process.cwd(), outputPath); + const resolvedInput = path.resolve(CURRENT_DIR, inputPath); + const resolvedOutput = path.resolve(CURRENT_DIR, outputPath); if (!fs.existsSync(resolvedInput)) { console.log("Operation failed"); diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index 6bedc55..525651b 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -2,11 +2,12 @@ import fs from "fs"; import path from "path"; import {Transform} from "stream"; import {pipeline} from "stream/promises"; +import { CURRENT_DIR } from "../main"; export const jsonToCsv = async (inputPath, outputPath) => { try { - const resolvedInput = path.resolve(process.cwd(), inputPath); - const resolvedOutput = path.resolve(process.cwd(), outputPath); + const resolvedInput = path.resolve(CURRENT_DIR, inputPath); + const resolvedOutput = path.resolve(CURRENT_DIR, outputPath); if (!fs.existsSync(resolvedInput)) { console.log("Operation failed: input file does not exist"); diff --git a/src/main.js b/src/main.js index a915f4a..ce1eed3 100644 --- a/src/main.js +++ b/src/main.js @@ -7,7 +7,7 @@ import {argParser} from "./utils/argParser.js"; import {jsonToCsv} from "./commands/jsonToCsv.js"; import {countFile} from "./commands/count.js"; -process.chdir(os.homedir()); +export const CURRENT_DIR = os.homedir(); console.log("Welcome to Data Processing CLI!"); console.log(`You are currently in ${process.cwd()}`); diff --git a/src/utils/argParser.js b/src/utils/argParser.js new file mode 100644 index 0000000..ba7fc73 --- /dev/null +++ b/src/utils/argParser.js @@ -0,0 +1,4 @@ +export const argParser = (argName, args) => { + const argIndex = args.indexOf(argName); + return argIndex !== -1 ? args[argIndex + 1] : undefined; +}; From c7f7bc2d0e3852da59f6c896b7106ccfbe172388 Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sun, 15 Mar 2026 12:28:00 +0200 Subject: [PATCH 06/11] feat: add hash command --- src/commands/count.js | 2 +- src/commands/csvToJson.js | 2 +- src/commands/hash.js | 50 +++++++++++++++++++++++++++++++++++++++ src/commands/jsonToCsv.js | 2 +- src/main.js | 8 +++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/commands/hash.js diff --git a/src/commands/count.js b/src/commands/count.js index ac8c997..22c4c41 100644 --- a/src/commands/count.js +++ b/src/commands/count.js @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import readline from "readline"; -import { CURRENT_DIR } from "../main"; +import { CURRENT_DIR } from "../main.js"; export const countFile = async (inputPath) => { try { diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index d81964a..da2dba9 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -2,7 +2,7 @@ import fs from "fs"; import path from "path"; import {Transform} from "stream"; import {pipeline} from "stream/promises"; -import {CURRENT_DIR} from "../main"; +import {CURRENT_DIR} from "../main.js"; export const csvToJson = async (inputPath, outputPath) => { try { diff --git a/src/commands/hash.js b/src/commands/hash.js new file mode 100644 index 0000000..d37ce0d --- /dev/null +++ b/src/commands/hash.js @@ -0,0 +1,50 @@ +import fs from "fs"; +import path from "path"; +import crypto from "crypto"; + +const SUPPORTED_ALGORITHMS = ["sha256", "md5", "sha512"]; + +export const hashFile = ({input, algorithm = "sha256", save = false}) => { + if (!input) { + console.log("Operation failed: Missing input file"); + return; + } + + if (!SUPPORTED_ALGORITHMS.includes(algorithm)) { + console.log("Operation failed: Unsupported algorithm"); + return; + } + + const resolvedPath = path.resolve(process.cwd(), input); + + if (!fs.existsSync(resolvedPath)) { + console.log("Operation failed: Input file does not exist"); + return; + } + + try { + const hash = crypto.createHash(algorithm); + const stream = fs.createReadStream(resolvedPath); + + stream.on("data", (chunk) => hash.update(chunk)); + + stream.on("end", () => { + const digest = hash.digest("hex"); + console.log(`${algorithm}: ${digest}`); + console.log(save, 1111); + if (save) { + const outputFile = `${resolvedPath}.${algorithm}`; + try { + fs.writeFileSync(outputFile, digest); + console.log(`Saved hash to ${outputFile}`); + } catch { + console.log("Operation failed"); + } + } + }); + + stream.on("error", () => console.log("Operation failed")); + } catch { + console.log("Operation failed"); + } +}; diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index 525651b..57bf0f8 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -2,7 +2,7 @@ import fs from "fs"; import path from "path"; import {Transform} from "stream"; import {pipeline} from "stream/promises"; -import { CURRENT_DIR } from "../main"; +import { CURRENT_DIR } from "../main.js"; export const jsonToCsv = async (inputPath, outputPath) => { try { diff --git a/src/main.js b/src/main.js index ce1eed3..7c06fe4 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import {csvToJson} from "./commands/csvToJson.js"; import {argParser} from "./utils/argParser.js"; import {jsonToCsv} from "./commands/jsonToCsv.js"; import {countFile} from "./commands/count.js"; +import {hashFile} from "./commands/hash.js"; export const CURRENT_DIR = os.homedir(); @@ -57,6 +58,13 @@ rl.on("line", (input) => { case "count": countFile(input); break; + case "hash": + hashFile({ + input, + algorithm: argParser("--algorithm", args), + save: args.indexOf("--save") !== -1, + }); + break; default: console.log("Invalid input."); } From 4f5a86e07fda0bbab593498e993165a1ab7bf2a8 Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sun, 15 Mar 2026 12:47:44 +0200 Subject: [PATCH 07/11] feat: implement hash-compare command --- src/commands/hashCompare.js | 50 +++++++++++++++++++++++++++++++++++++ src/main.js | 7 ++++++ 2 files changed, 57 insertions(+) create mode 100644 src/commands/hashCompare.js diff --git a/src/commands/hashCompare.js b/src/commands/hashCompare.js new file mode 100644 index 0000000..38f7dcc --- /dev/null +++ b/src/commands/hashCompare.js @@ -0,0 +1,50 @@ +import fs from "fs"; +import path from "path"; +import crypto from "crypto"; + +const SUPPORTED_ALGORITHMS = ["sha256", "md5", "sha512"]; + +export const hashCompare = (input, hashProvided, algorithm = "sha256") => { + try { + if (!input || !hashProvided) { + console.log("Operation failed"); + return; + } + + if (!SUPPORTED_ALGORITHMS.includes(algorithm)) { + console.log("Operation failed"); + return; + } + + const inputPath = path.resolve(process.cwd(), input); + const hashPath = path.resolve(process.cwd(), hashProvided); + + if (!fs.existsSync(inputPath) || !fs.existsSync(hashPath)) { + console.log("Operation failed"); + return; + } + + const expectedHash = fs.readFileSync(hashPath, "utf8").trim().toLowerCase(); + console.log(expectedHash, 1111); + const hash = crypto.createHash(algorithm); + const stream = fs.createReadStream(inputPath); + + stream.on("data", (chunk) => hash.update(chunk)); + + stream.on("end", () => { + const calculatedHash = hash.digest("hex").toLowerCase(); + + if (calculatedHash === expectedHash) { + console.log("OK"); + } else { + console.log("MISMATCH"); + } + }); + + stream.on("error", () => { + console.log("Operation failed"); + }); + } catch { + console.log("Operation failed"); + } +}; diff --git a/src/main.js b/src/main.js index 7c06fe4..cd07023 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import {argParser} from "./utils/argParser.js"; import {jsonToCsv} from "./commands/jsonToCsv.js"; import {countFile} from "./commands/count.js"; import {hashFile} from "./commands/hash.js"; +import {hashCompare} from "./commands/hashCompare.js"; export const CURRENT_DIR = os.homedir(); @@ -64,6 +65,12 @@ rl.on("line", (input) => { algorithm: argParser("--algorithm", args), save: args.indexOf("--save") !== -1, }); + case "hash-compare": + hashCompare( + input, + argParser("--hash", args), + argParser("--algorithm", args), + ); break; default: console.log("Invalid input."); From 83116afbc51a85572876416305577aaf62512f06 Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sun, 15 Mar 2026 23:00:09 +0200 Subject: [PATCH 08/11] feat: add encrypt command --- src/commands/encrypt.js | 49 +++++++++++++++++++++++++++++++++++++++++ src/main.js | 8 +++++++ 2 files changed, 57 insertions(+) create mode 100644 src/commands/encrypt.js diff --git a/src/commands/encrypt.js b/src/commands/encrypt.js new file mode 100644 index 0000000..43679ee --- /dev/null +++ b/src/commands/encrypt.js @@ -0,0 +1,49 @@ +import fs from "fs"; +import path from "path"; +import crypto from "crypto"; +import {pipeline} from "stream"; + +export const encrypt = (input, output, password) => { + try { + if (!input || !output || !password) { + console.log("Operation failed: Missing required parameters"); + return; + } + + const inputPath = path.resolve(process.cwd(), input); + const outputPath = path.resolve(process.cwd(), output); + + if (!fs.existsSync(inputPath)) { + console.log("Operation failed: Input file not found"); + return; + } + + fs.closeSync(fs.openSync(outputPath, "a")); + + const salt = crypto.randomBytes(16); + const iv = crypto.randomBytes(12); + const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha256"); + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); + + const inputStream = fs.createReadStream(inputPath); + const outputStream = fs.createWriteStream(outputPath); + + outputStream.write(Buffer.concat([salt, iv])); + + pipeline(inputStream, cipher, outputStream, (err) => { + if (err) { + console.log("Operation failed"); + return; + } + + const authTag = cipher.getAuthTag(); + fs.appendFile(outputPath, authTag, (err) => { + if (err) { + console.log("Operation failed"); + } + }); + }); + } catch { + console.log("Operation failed: An error occurred during encryption"); + } +}; diff --git a/src/main.js b/src/main.js index cd07023..27ed31f 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ import {jsonToCsv} from "./commands/jsonToCsv.js"; import {countFile} from "./commands/count.js"; import {hashFile} from "./commands/hash.js"; import {hashCompare} from "./commands/hashCompare.js"; +import { encrypt } from "./commands/encrypt.js"; export const CURRENT_DIR = os.homedir(); @@ -72,6 +73,13 @@ rl.on("line", (input) => { argParser("--algorithm", args), ); break; + case "encrypt": + encrypt( + input, + output, + argParser("--password", args) + ); + break; default: console.log("Invalid input."); } From c9ce9e43332e7aca89d5e5c1e9ef7f3bacd594be Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sun, 15 Mar 2026 23:20:26 +0200 Subject: [PATCH 09/11] feat: implement decrypt command --- src/commands/decrypt.js | 65 +++++++++++++++++++++++++++++++++++++++++ src/main.js | 11 ++++--- 2 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/commands/decrypt.js diff --git a/src/commands/decrypt.js b/src/commands/decrypt.js new file mode 100644 index 0000000..ab0a04b --- /dev/null +++ b/src/commands/decrypt.js @@ -0,0 +1,65 @@ +import fs from "fs"; +import path from "path"; +import crypto from "crypto"; +import {pipeline} from "stream"; + +const HEADER_SIZE = 28; +const AUTH_TAG_SIZE = 16; +const TOTAL_BUFFER_SIZE = HEADER_SIZE + AUTH_TAG_SIZE; + +export const decrypt = (input, output, password) => { + try { + if (!input || !output || !password) { + console.log("Operation failed: Missing required parameters"); + return; + } + + const inputPath = path.resolve(process.cwd(), input); + const outputPath = path.resolve(process.cwd(), output); + + if (!fs.existsSync(inputPath)) { + console.log("Operation failed: Input file not found"); + return; + } + + const stat = fs.statSync(inputPath); + + if (stat.size < TOTAL_BUFFER_SIZE) { + console.log("Operation failed: File is too small to be valid"); + return; + } + + const fd = fs.openSync(inputPath, "r"); + + const header = Buffer.alloc(HEADER_SIZE); + fs.readSync(fd, header, 0, HEADER_SIZE, 0); + + const salt = header.subarray(0, AUTH_TAG_SIZE); + const iv = header.subarray(AUTH_TAG_SIZE, HEADER_SIZE); + + const authTag = Buffer.alloc(AUTH_TAG_SIZE); + fs.readSync(fd, authTag, 0, AUTH_TAG_SIZE, stat.size - AUTH_TAG_SIZE); + + fs.closeSync(fd); + + const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha256"); + + const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); + decipher.setAuthTag(authTag); + + const inputStream = fs.createReadStream(inputPath, { + start: HEADER_SIZE, + end: stat.size - AUTH_TAG_SIZE - 1, + }); + + const outputStream = fs.createWriteStream(outputPath); + + pipeline(inputStream, decipher, outputStream, (err) => { + if (err) { + console.log("Operation failed"); + } + }); + } catch { + console.log("Operation failed"); + } +}; diff --git a/src/main.js b/src/main.js index 27ed31f..8a06a7a 100644 --- a/src/main.js +++ b/src/main.js @@ -8,7 +8,8 @@ import {jsonToCsv} from "./commands/jsonToCsv.js"; import {countFile} from "./commands/count.js"; import {hashFile} from "./commands/hash.js"; import {hashCompare} from "./commands/hashCompare.js"; -import { encrypt } from "./commands/encrypt.js"; +import {encrypt} from "./commands/encrypt.js"; +import {decrypt} from "./commands/decrypt.js"; export const CURRENT_DIR = os.homedir(); @@ -74,11 +75,9 @@ rl.on("line", (input) => { ); break; case "encrypt": - encrypt( - input, - output, - argParser("--password", args) - ); + encrypt(input, output, argParser("--password", args)); + case "decrypt": + decrypt(input, output, argParser("--password", args)); break; default: console.log("Invalid input."); From 38a7cc9edf1ad079cc5e164ad6d75f7d8c68b47a Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Sun, 15 Mar 2026 23:36:46 +0200 Subject: [PATCH 10/11] refactor: move commands logic to repl.js file --- src/commands/count.js | 3 +-- src/commands/csvToJson.js | 5 ++-- src/commands/jsonToCsv.js | 5 ++-- src/main.js | 57 +++------------------------------------ src/repl.js | 56 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 62 deletions(-) create mode 100644 src/repl.js diff --git a/src/commands/count.js b/src/commands/count.js index 22c4c41..2286a60 100644 --- a/src/commands/count.js +++ b/src/commands/count.js @@ -1,11 +1,10 @@ import fs from "fs"; import path from "path"; import readline from "readline"; -import { CURRENT_DIR } from "../main.js"; export const countFile = async (inputPath) => { try { - const resolvedInput = path.resolve(CURRENT_DIR, inputPath); + const resolvedInput = path.resolve(process.cwd(), inputPath); if (!fs.existsSync(resolvedInput)) { console.log("Operation failed: input file does not exist"); diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js index da2dba9..a9858a2 100644 --- a/src/commands/csvToJson.js +++ b/src/commands/csvToJson.js @@ -2,12 +2,11 @@ import fs from "fs"; import path from "path"; import {Transform} from "stream"; import {pipeline} from "stream/promises"; -import {CURRENT_DIR} from "../main.js"; export const csvToJson = async (inputPath, outputPath) => { try { - const resolvedInput = path.resolve(CURRENT_DIR, inputPath); - const resolvedOutput = path.resolve(CURRENT_DIR, outputPath); + const resolvedInput = path.resolve(process.cwd(), inputPath); + const resolvedOutput = path.resolve(process.cwd(), outputPath); if (!fs.existsSync(resolvedInput)) { console.log("Operation failed"); diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js index 57bf0f8..6bedc55 100644 --- a/src/commands/jsonToCsv.js +++ b/src/commands/jsonToCsv.js @@ -2,12 +2,11 @@ import fs from "fs"; import path from "path"; import {Transform} from "stream"; import {pipeline} from "stream/promises"; -import { CURRENT_DIR } from "../main.js"; export const jsonToCsv = async (inputPath, outputPath) => { try { - const resolvedInput = path.resolve(CURRENT_DIR, inputPath); - const resolvedOutput = path.resolve(CURRENT_DIR, outputPath); + const resolvedInput = path.resolve(process.cwd(), inputPath); + const resolvedOutput = path.resolve(process.cwd(), outputPath); if (!fs.existsSync(resolvedInput)) { console.log("Operation failed: input file does not exist"); diff --git a/src/main.js b/src/main.js index 8a06a7a..e49846d 100644 --- a/src/main.js +++ b/src/main.js @@ -1,17 +1,9 @@ import readline from "readline"; import os from "os"; import process from "process"; -import {changeDir, listDir, up} from "./navigation.js"; -import {csvToJson} from "./commands/csvToJson.js"; -import {argParser} from "./utils/argParser.js"; -import {jsonToCsv} from "./commands/jsonToCsv.js"; -import {countFile} from "./commands/count.js"; -import {hashFile} from "./commands/hash.js"; -import {hashCompare} from "./commands/hashCompare.js"; -import {encrypt} from "./commands/encrypt.js"; -import {decrypt} from "./commands/decrypt.js"; +import {processCommand} from "./repl.js"; -export const CURRENT_DIR = os.homedir(); +process.chdir(os.homedir()); console.log("Welcome to Data Processing CLI!"); console.log(`You are currently in ${process.cwd()}`); @@ -38,50 +30,7 @@ rl.on("line", (input) => { } try { - const [command, ...args] = commandInput.trim().split(/\s+/); - const input = argParser("--input", args); - const output = argParser("--output", args); - - switch (command) { - case "up": - up(); - break; - case "cd": - changeDir(args.join(" ")); - break; - case "ls": - listDir(); - break; - case "csv-to-json": - csvToJson(input, output); - break; - case "json-to-csv": - jsonToCsv(input, output); - break; - case "count": - countFile(input); - break; - case "hash": - hashFile({ - input, - algorithm: argParser("--algorithm", args), - save: args.indexOf("--save") !== -1, - }); - case "hash-compare": - hashCompare( - input, - argParser("--hash", args), - argParser("--algorithm", args), - ); - break; - case "encrypt": - encrypt(input, output, argParser("--password", args)); - case "decrypt": - decrypt(input, output, argParser("--password", args)); - break; - default: - console.log("Invalid input."); - } + processCommand(commandInput); console.log(`You are currently in ${process.cwd()}`); } catch (err) { console.log("Operation failed"); diff --git a/src/repl.js b/src/repl.js new file mode 100644 index 0000000..350d449 --- /dev/null +++ b/src/repl.js @@ -0,0 +1,56 @@ +import {changeDir, listDir, up} from "./navigation.js"; +import {csvToJson} from "./commands/csvToJson.js"; +import {argParser} from "./utils/argParser.js"; +import {jsonToCsv} from "./commands/jsonToCsv.js"; +import {countFile} from "./commands/count.js"; +import {hashFile} from "./commands/hash.js"; +import {hashCompare} from "./commands/hashCompare.js"; +import {encrypt} from "./commands/encrypt.js"; +import {decrypt} from "./commands/decrypt.js"; + +export const processCommand = (commandInput) => { + const [command, ...args] = commandInput.trim().split(/\s+/); + const input = argParser("--input", args); + const output = argParser("--output", args); + + switch (command) { + case "up": + up(); + break; + case "cd": + changeDir(args.join(" ")); + break; + case "ls": + listDir(); + break; + case "csv-to-json": + csvToJson(input, output); + break; + case "json-to-csv": + jsonToCsv(input, output); + break; + case "count": + countFile(input); + break; + case "hash": + hashFile({ + input, + algorithm: argParser("--algorithm", args), + save: args.indexOf("--save") !== -1, + }); + case "hash-compare": + hashCompare( + input, + argParser("--hash", args), + argParser("--algorithm", args), + ); + break; + case "encrypt": + encrypt(input, output, argParser("--password", args)); + case "decrypt": + decrypt(input, output, argParser("--password", args)); + break; + default: + console.log("Invalid input."); + } +}; From f4c9cc74e5309d76da1264dcf5efad56a56d390d Mon Sep 17 00:00:00 2001 From: Zhanna Chaikovska Date: Mon, 16 Mar 2026 00:16:29 +0200 Subject: [PATCH 11/11] feat: add log-stats commands --- package.json | 3 +- scripts/generate-logs.js | 99 +++++++++++++++++++++++++++++++++++ src/commands/logStats.js | 108 +++++++++++++++++++++++++++++++++++++++ src/repl.js | 4 ++ src/workers/logWorker.js | 55 ++++++++++++++++++++ 5 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-logs.js create mode 100644 src/commands/logStats.js create mode 100644 src/workers/logWorker.js diff --git a/package.json b/package.json index baa0129..d2502e7 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ }, "type": "module", "scripts": { - "start": "node src/main.js" + "start": "node src/main.js", + "generate-logs": "node scripts/generate-logs.js --output src/commands/logs.txt --lines 500000" }, "repository": { "type": "git", diff --git a/scripts/generate-logs.js b/scripts/generate-logs.js new file mode 100644 index 0000000..2a3f5df --- /dev/null +++ b/scripts/generate-logs.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +"use strict"; +import fs from "fs"; +import path from "path"; +import {parseArgs} from "util"; + +const {values} = parseArgs({ + options: { + output: {type: "string"}, + lines: {type: "string", default: "100000"}, + seed: {type: "string", default: "123456"}, + }, +}); + +const output = values.output; +const lines = Number(values.lines); +const seed = Number(values.seed); + +if (!output || !Number.isFinite(lines) || lines <= 0) { + process.stderr.write( + "Usage: node scripts/generate-logs.js --output --lines [--seed ]\n", + ); + process.exit(1); +} + +const levels = ["INFO", "WARN", "ERROR"]; +const services = [ + "user-service", + "order-service", + "payment-service", + "search-service", + "email-service", +]; +const methods = ["GET", "POST", "PUT", "DELETE"]; +const paths = [ + "/api/users", + "/api/users/:id", + "/api/orders", + "/api/orders/:id", + "/api/payments", + "/api/search", + "/api/login", + "/api/logout", + "/api/health", +]; + +let state = seed >>> 0; +const rand = () => { + // LCG: deterministic pseudo-random generator + state = (1664525 * state + 1013904223) >>> 0; + return state / 0xffffffff; +}; + +const pick = (arr) => arr[Math.floor(rand() * arr.length)]; + +const start = Date.parse("2026-01-01T00:00:00.000Z"); +let current = start; + +const outPath = path.resolve(process.cwd(), output); +fs.mkdirSync(path.dirname(outPath), {recursive: true}); + +const stream = fs.createWriteStream(outPath, {encoding: "utf8"}); + +let written = 0; +const writeBatch = () => { + let ok = true; + while (written < lines && ok) { + const dt = Math.floor(rand() * 5000); // up to 5s + current += dt; + const iso = new Date(current).toISOString(); + const level = pick(levels); + const service = pick(services); + const method = pick(methods); + const pathVal = pick(paths); + const statusBase = level === "ERROR" ? 500 : level === "WARN" ? 400 : 200; + const status = statusBase + Math.floor(rand() * 50); + const responseTime = 5 + Math.floor(rand() * 2000); + const line = `${iso} ${level} ${service} ${status} ${responseTime} ${method} ${pathVal}\n`; + ok = stream.write(line); + written += 1; + } + + if (written < lines) { + stream.once("drain", writeBatch); + } else { + stream.end(); + } +}; + +stream.on("finish", () => { + process.stdout.write(`Generated ${lines} lines at ${outPath}\n`); +}); + +stream.on("error", (err) => { + process.stderr.write(`Failed to write logs: ${err.message}\n`); + process.exit(1); +}); + +writeBatch(); diff --git a/src/commands/logStats.js b/src/commands/logStats.js new file mode 100644 index 0000000..7319ba2 --- /dev/null +++ b/src/commands/logStats.js @@ -0,0 +1,108 @@ +import fs from "fs"; +import path from "path"; +import os from "os"; +import {Worker} from "worker_threads"; + +export const logStats = (input, output) => { + try { + if (!input || !output) { + console.log("Operation failed: missing --input or --output"); + return; + } + + const inputPath = path.resolve(process.cwd(), input); + const outputPath = path.resolve(process.cwd(), output); + + if (!fs.existsSync(inputPath)) { + console.log("Operation failed: input file does not exist"); + return; + } + + const fileSize = fs.statSync(inputPath).size; + const cpuCores = os.cpus().length; + + const chunkSize = Math.floor(fileSize / cpuCores); + + const workers = []; + const promises = []; + + for (let i = 0; i < cpuCores; i++) { + const start = i * chunkSize; + const end = i === cpuCores - 1 ? fileSize : (i + 1) * chunkSize; + + const worker = new Worker(new URL("../workers/logWorker.js", import.meta.url), { + workerData: { + file: inputPath, + start, + end, + }, + }); + + promises.push( + new Promise((resolve, reject) => { + worker.on("message", resolve); + worker.on("error", reject); + worker.on("exit", (code) => { + if (code !== 0) reject(new Error()); + }); + }), + ); + + workers.push(worker); + } + + Promise.all(promises) + .then((results) => { + const finalStats = mergeStats(results); + + fs.writeFileSync(outputPath, JSON.stringify(finalStats, null, 2)); + }) + .catch(() => { + console.log("Operation failed 4"); + }); + } catch { + console.log("Operation failed 5"); + } +}; + +function mergeStats(results) { + const final = { + total: 0, + levels: {}, + status: {"2xx": 0, "3xx": 0, "4xx": 0, "5xx": 0}, + paths: {}, + responseSum: 0, + }; + + for (const result of results) { + final.total += result.total; + final.responseSum += result.responseSum; + + for (const [level, count] of Object.entries(result.levels)) { + final.levels[level] = (final.levels[level] || 0) + count; + } + + for (const [status, count] of Object.entries(result.status)) { + final.status[status] += count; + } + + for (const [path, count] of Object.entries(result.paths)) { + final.paths[path] = (final.paths[path] || 0) + count; + } + } + + const topPaths = Object.entries(final.paths) + .map(([path, count]) => ({path, count})) + .sort((a, b) => b.count - a.count) + .slice(0, 5); + + return { + total: final.total, + levels: final.levels, + status: final.status, + topPaths, + avgResponseTimeMs: final.total + ? Number((final.responseSum / final.total).toFixed(2)) + : 0, + }; +} diff --git a/src/repl.js b/src/repl.js index 350d449..ab66f49 100644 --- a/src/repl.js +++ b/src/repl.js @@ -7,6 +7,7 @@ import {hashFile} from "./commands/hash.js"; import {hashCompare} from "./commands/hashCompare.js"; import {encrypt} from "./commands/encrypt.js"; import {decrypt} from "./commands/decrypt.js"; +import {logStats} from "./commands/logStats.js"; export const processCommand = (commandInput) => { const [command, ...args] = commandInput.trim().split(/\s+/); @@ -50,6 +51,9 @@ export const processCommand = (commandInput) => { case "decrypt": decrypt(input, output, argParser("--password", args)); break; + case "log-stats": + logStats(input, output); + break; default: console.log("Invalid input."); } diff --git a/src/workers/logWorker.js b/src/workers/logWorker.js new file mode 100644 index 0000000..a7acb63 --- /dev/null +++ b/src/workers/logWorker.js @@ -0,0 +1,55 @@ +import fs from "fs"; +import {parentPort, workerData} from "worker_threads"; + +const {file, start, end} = workerData; + +const stream = fs.createReadStream(file, { + start, + end, + encoding: "utf8", +}); + +let buffer = ""; + +const stats = { + total: 0, + levels: {}, + status: {"2xx": 0, "3xx": 0, "4xx": 0, "5xx": 0}, + paths: {}, + responseSum: 0, +}; + +stream.on("data", (chunk) => { + buffer += chunk; + + const lines = buffer.split("\n"); + buffer = lines.pop(); + + for (const line of lines) { + if (!line.trim()) continue; + + const parts = line.split(" "); + if (parts.length < 7) continue; + + const level = parts[1]; + const statusCode = parseInt(parts[3]); + const responseTime = parseFloat(parts[4]); + const path = parts[6]; + + stats.total++; + stats.responseSum += responseTime; + + stats.levels[level] = (stats.levels[level] || 0) + 1; + + const statusClass = Math.floor(statusCode / 100) + "xx"; + if (stats.status[statusClass] !== undefined) { + stats.status[statusClass]++; + } + + stats.paths[path] = (stats.paths[path] || 0) + 1; + } +}); + +stream.on("end", () => { + parentPort.postMessage(stats); +});