From a62df8902b64520025a5216709e8956606ec6117 Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 10:43:06 +0100 Subject: [PATCH 01/10] init: start data processing cli --- package.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..db9194c --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "data-processing-cli", + "version": "1.0.0", + "description": "Data Processing Toolkit — an interactive command-line application", + "engines": { + "node": ">=24.10.0", + "npm": ">=10.9.2" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/maiano/data-processing-cli.git" + }, + "keywords": [], + "author": "", + "license": "MIT", + "type": "module", + "bugs": { + "url": "https://github.com/maiano/data-processing-cli/issues" + }, + "homepage": "https://github.com/maiano/data-processing-cli#readme" +} From b48fd3377f4d2449e62a90f3679e5caa838c285f Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 11:16:01 +0100 Subject: [PATCH 02/10] feat: implement core --- package.json | 1 + src/core/repl.js | 35 +++++++++++++++++++++++++++++++++++ src/core/router.js | 22 ++++++++++++++++++++++ src/core/state.js | 5 +++++ src/main.js | 7 +++++++ 5 files changed, 70 insertions(+) create mode 100644 src/core/repl.js create mode 100644 src/core/router.js create mode 100644 src/core/state.js create mode 100644 src/main.js diff --git a/package.json b/package.json index db9194c..d04b2d0 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "npm": ">=10.9.2" }, "scripts": { + "start": "node src/main.js", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { diff --git a/src/core/repl.js b/src/core/repl.js new file mode 100644 index 0000000..ac688dd --- /dev/null +++ b/src/core/repl.js @@ -0,0 +1,35 @@ +import readline from "readline"; +import { runCommand } from "./router.js"; +import { state } from "./state.js"; + +export function run() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: "> ", + }); + + rl.prompt(); + + rl.on("line", async (line) => { + const input = line.trim(); + + if (input === ".exit") { + exit(rl); + return; + } + + await runCommand(input); + + console.log(`You are currently in ${state.cwd}`); + rl.prompt(); + }); + + rl.on("SIGINT", () => exit(rl)); +} + +function exit(rl) { + console.log("Thank you for using Data Processing CLI!"); + rl.close(); + process.exit(0); +} diff --git a/src/core/router.js b/src/core/router.js new file mode 100644 index 0000000..2456899 --- /dev/null +++ b/src/core/router.js @@ -0,0 +1,22 @@ +const commands = {}; + +export function registerCommand(name, handler) { + commands[name] = handler; +} + +export async function runCommand(input) { + try { + const [command, ...args] = input.split(" "); + + const handler = commands[command]; + + if (!handler) { + console.log("Invalid input"); + return; + } + + await handler(args); + } catch { + console.log("Operation failed"); + } +} diff --git a/src/core/state.js b/src/core/state.js new file mode 100644 index 0000000..9812969 --- /dev/null +++ b/src/core/state.js @@ -0,0 +1,5 @@ +import os from "os"; + +export const state = { + cwd: os.homedir(), +}; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..971eaa2 --- /dev/null +++ b/src/main.js @@ -0,0 +1,7 @@ +import { state } from "./core/state.js"; +import { run } from "./core/repl.js"; + +console.log("Welcome to Data Processing CLI!"); +console.log(`You are currently in ${state.cwd}`); + +run(); From 19d6e45d690f3c4e3972f26bfdf67014bfa07da7 Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 15:26:57 +0100 Subject: [PATCH 03/10] feat: add utils --- src/core/repl.js | 5 ++--- src/core/router.js | 12 ++++++++---- src/navigation/cd.js | 0 src/navigation/ls.js | 0 src/navigation/up.js | 0 src/utils/argParser.js | 20 ++++++++++++++++++++ src/utils/pathResolver.js | 7 +++++++ 7 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 src/navigation/cd.js create mode 100644 src/navigation/ls.js create mode 100644 src/navigation/up.js create mode 100644 src/utils/argParser.js create mode 100644 src/utils/pathResolver.js diff --git a/src/core/repl.js b/src/core/repl.js index ac688dd..036f411 100644 --- a/src/core/repl.js +++ b/src/core/repl.js @@ -19,9 +19,8 @@ export function run() { return; } - await runCommand(input); - - console.log(`You are currently in ${state.cwd}`); + const success = await runCommand(input); + if (success) console.log(`You are currently in ${state.cwd}`); rl.prompt(); }); diff --git a/src/core/router.js b/src/core/router.js index 2456899..7afdfdb 100644 --- a/src/core/router.js +++ b/src/core/router.js @@ -1,3 +1,5 @@ +import { parseArgs } from "util"; + const commands = {}; export function registerCommand(name, handler) { @@ -12,11 +14,13 @@ export async function runCommand(input) { if (!handler) { console.log("Invalid input"); - return; + return false; } - await handler(args); - } catch { - console.log("Operation failed"); + await handler(parseArgs(args)); + return true; + } catch (e) { + if (e.message === "INVALID_INPUT") console.log("Invalid input"); + else console.log("Operation failed"); } } diff --git a/src/navigation/cd.js b/src/navigation/cd.js new file mode 100644 index 0000000..e69de29 diff --git a/src/navigation/ls.js b/src/navigation/ls.js new file mode 100644 index 0000000..e69de29 diff --git a/src/navigation/up.js b/src/navigation/up.js new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/argParser.js b/src/utils/argParser.js new file mode 100644 index 0000000..c97edac --- /dev/null +++ b/src/utils/argParser.js @@ -0,0 +1,20 @@ +export function parseArgs(args) { + const result = {}; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (!arg.startsWith("--")) continue; + + const key = arg.slice(2); + const next = args[i + 1]; + + if (next === undefined || next.startsWith("--")) { + result[key] = true; + } else { + result[key] = next; + i++; + } + } + + return result; +} diff --git a/src/utils/pathResolver.js b/src/utils/pathResolver.js new file mode 100644 index 0000000..14b3ce7 --- /dev/null +++ b/src/utils/pathResolver.js @@ -0,0 +1,7 @@ +import path from "path"; + +export function resolvePath(cwd, inputPath) { + if (!inputPath) throw new Error("INVALID_INPUT"); + if (path.isAbsolute(inputPath)) return path.normalize(inputPath); + return path.resolve(cwd, inputPath); +} From f1549705cb93673ab360d5ce0b4f795ab9753f54 Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 15:29:11 +0100 Subject: [PATCH 04/10] feat: add error constants --- src/utils/errors.js | 4 ++++ src/utils/pathResolver.js | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 src/utils/errors.js diff --git a/src/utils/errors.js b/src/utils/errors.js new file mode 100644 index 0000000..b8001dd --- /dev/null +++ b/src/utils/errors.js @@ -0,0 +1,4 @@ +export const ERRORS = { + INVALID_INPUT: "INVALID_INPUT", + OPERATION_FAILED: "OPERATION_FAILED", +}; diff --git a/src/utils/pathResolver.js b/src/utils/pathResolver.js index 14b3ce7..9e79506 100644 --- a/src/utils/pathResolver.js +++ b/src/utils/pathResolver.js @@ -1,7 +1,8 @@ import path from "path"; +import { ERRORS } from "./errors"; export function resolvePath(cwd, inputPath) { - if (!inputPath) throw new Error("INVALID_INPUT"); + if (!inputPath) throw new Error(ERRORS.INVALID_INPUT); if (path.isAbsolute(inputPath)) return path.normalize(inputPath); return path.resolve(cwd, inputPath); } From 5b0fde4f22069517358a1b06c4199b891eff22cf Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 16:19:33 +0100 Subject: [PATCH 05/10] feat: implement navigation commands --- src/core/router.js | 2 +- src/main.js | 9 +++++++++ src/navigation/cd.js | 28 ++++++++++++++++++++++++++++ src/navigation/ls.js | 37 +++++++++++++++++++++++++++++++++++++ src/navigation/up.js | 10 ++++++++++ src/utils/argParser.js | 32 ++++++++++++++++++++------------ src/utils/pathResolver.js | 2 +- 7 files changed, 106 insertions(+), 14 deletions(-) diff --git a/src/core/router.js b/src/core/router.js index 7afdfdb..e0b2e20 100644 --- a/src/core/router.js +++ b/src/core/router.js @@ -1,4 +1,4 @@ -import { parseArgs } from "util"; +import { parseArgs } from "../utils/argParser.js"; const commands = {}; diff --git a/src/main.js b/src/main.js index 971eaa2..809c094 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,16 @@ import { state } from "./core/state.js"; import { run } from "./core/repl.js"; +import { registerCommand } from "./core/router.js"; + +import { up } from "./navigation/up.js"; +import { cd } from "./navigation/cd.js"; +import { ls } from "./navigation/ls.js"; console.log("Welcome to Data Processing CLI!"); console.log(`You are currently in ${state.cwd}`); +registerCommand("up", up); +registerCommand("cd", cd); +registerCommand("ls", ls); + run(); diff --git a/src/navigation/cd.js b/src/navigation/cd.js index e69de29..8b96c97 100644 --- a/src/navigation/cd.js +++ b/src/navigation/cd.js @@ -0,0 +1,28 @@ +import fs from "fs/promises"; +import { state } from "../core/state.js"; +import { resolvePath } from "../utils/pathResolver.js"; +import { ERRORS } from "../utils/errors.js"; + +export async function cd(args) { + const target = args._[0]; + + if (!target) { + throw new Error(ERRORS.INVALID_INPUT); + } + + const resolved = resolvePath(state.cwd, target); + + let stat; + + try { + stat = await fs.stat(resolved); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + + if (!stat.isDirectory()) { + throw new Error(ERRORS.OPERATION_FAILED); + } + + state.cwd = resolved; +} diff --git a/src/navigation/ls.js b/src/navigation/ls.js index e69de29..92656f3 100644 --- a/src/navigation/ls.js +++ b/src/navigation/ls.js @@ -0,0 +1,37 @@ +import fs from "fs/promises"; +import path from "path"; +import { state } from "../core/state.js"; +import { ERRORS } from "../utils/errors.js"; + +export async function ls() { + let entries; + try { + entries = await fs.readdir(state.cwd); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + + const items = await Promise.all( + entries.map(async (entry) => { + const fullPath = path.join(state.cwd, entry); + const stat = await fs.stat(fullPath); + + return { + name: entry, + type: stat.isDirectory() ? "folder" : "file", + }; + }), + ); + + items.sort((a, b) => { + if (a.type !== b.type) { + return a.type === "folder" ? -1 : 1; + } + + return a.name.localeCompare(b.name); + }); + + for (const item of items) { + console.log(`${item.name} [${item.type}]`); + } +} diff --git a/src/navigation/up.js b/src/navigation/up.js index e69de29..8bd2f5b 100644 --- a/src/navigation/up.js +++ b/src/navigation/up.js @@ -0,0 +1,10 @@ +import path from "path"; +import { state } from "../core/state.js"; + +export async function up() { + const parent = path.dirname(state.cwd); + + if (parent !== state.cwd) { + state.cwd = parent; + } +} diff --git a/src/utils/argParser.js b/src/utils/argParser.js index c97edac..5fb4a53 100644 --- a/src/utils/argParser.js +++ b/src/utils/argParser.js @@ -1,19 +1,27 @@ -export function parseArgs(args) { - const result = {}; +export function parseArgs(tokens) { + const result = { _: [] }; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (!arg.startsWith("--")) continue; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; - const key = arg.slice(2); - const next = args[i + 1]; + if (token.startsWith("--")) { + const key = token.slice(2); - if (next === undefined || next.startsWith("--")) { - result[key] = true; - } else { - result[key] = next; - i++; + if (!key) continue; + + const next = tokens[i + 1]; + + if (next === undefined || next.startsWith("--")) { + result[key] = true; + } else { + result[key] = next; + i++; + } + + continue; } + + result._.push(token); } return result; diff --git a/src/utils/pathResolver.js b/src/utils/pathResolver.js index 9e79506..2ae8a7e 100644 --- a/src/utils/pathResolver.js +++ b/src/utils/pathResolver.js @@ -1,5 +1,5 @@ import path from "path"; -import { ERRORS } from "./errors"; +import { ERRORS } from "./errors.js"; export function resolvePath(cwd, inputPath) { if (!inputPath) throw new Error(ERRORS.INVALID_INPUT); From 104d10b51c4dfc2d3b79e799a04ce17e1b465555 Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 18:46:13 +0100 Subject: [PATCH 06/10] feat: implement count --- src/commands/count.js | 20 ++++++++++++ src/main.js | 4 +++ src/services/countService.js | 61 ++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 src/commands/count.js create mode 100644 src/services/countService.js diff --git a/src/commands/count.js b/src/commands/count.js new file mode 100644 index 0000000..3e273d1 --- /dev/null +++ b/src/commands/count.js @@ -0,0 +1,20 @@ +import { resolvePath } from "../utils/pathResolver.js"; +import { ERRORS } from "../utils/errors.js"; +import { state } from "../core/state.js"; +import { countFileStats } from "../services/countService.js"; + +export async function count(args) { + const input = args.input; + + if (!input) { + throw new Error(ERRORS.INVALID_INPUT); + } + + const filePath = resolvePath(state.cwd, input); + + const stats = await countFileStats(filePath); + + console.log(`Lines: ${stats.lines}`); + console.log(`Words: ${stats.words}`); + console.log(`Characters: ${stats.characters}`); +} diff --git a/src/main.js b/src/main.js index 809c094..56947e4 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,8 @@ import { up } from "./navigation/up.js"; import { cd } from "./navigation/cd.js"; import { ls } from "./navigation/ls.js"; +import { count } from "./commands/count.js"; + console.log("Welcome to Data Processing CLI!"); console.log(`You are currently in ${state.cwd}`); @@ -13,4 +15,6 @@ registerCommand("up", up); registerCommand("cd", cd); registerCommand("ls", ls); +registerCommand("count", count); + run(); diff --git a/src/services/countService.js b/src/services/countService.js new file mode 100644 index 0000000..7c14418 --- /dev/null +++ b/src/services/countService.js @@ -0,0 +1,61 @@ +import { pipeline } from "stream/promises"; +import { Transform } from "stream"; +import fs from "fs"; +import { ERRORS } from "../utils/errors.js"; + +const WHITESPACE = /\s/; + +export async function countFileStats(filePath) { + let inWord = false; + let lastChar = null; + const stats = { + lines: 0, + words: 0, + characters: 0, + }; + + const counter = new Transform({ + transform(chunk, _enc, done) { + const str = chunk.toString("utf8"); + + for (const char of str) { + stats.characters++; + lastChar = char; + + if (char === "\n") { + stats.lines++; + } + + if (WHITESPACE.test(char)) { + inWord = false; + } else { + if (!inWord) { + stats.words++; + inWord = true; + } + } + } + + done(); + }, + }); + + try { + await pipeline( + fs.createReadStream(filePath, { encoding: "utf8" }), + counter, + ); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + + if (stats.characters > 0 && lastChar !== "\n") { + stats.lines++; + } + + return { + lines: stats.lines, + words: stats.words, + characters: stats.characters, + }; +} From 99f01725e8e9f59b4aa42eb302c3636fba80986a Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 21:15:39 +0100 Subject: [PATCH 07/10] feat: implement hash --- src/commands/hash.js | 34 ++++++++++++++++++++++++++++++++++ src/main.js | 2 ++ src/services/hashService.js | 23 +++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/commands/hash.js create mode 100644 src/services/hashService.js diff --git a/src/commands/hash.js b/src/commands/hash.js new file mode 100644 index 0000000..2244ed4 --- /dev/null +++ b/src/commands/hash.js @@ -0,0 +1,34 @@ +import fs from "fs/promises"; +import path from "path"; +import { resolvePath } from "../utils/pathResolver.js"; +import { ERRORS } from "../utils/errors.js"; +import { state } from "../core/state.js"; +import { hashFile } from "../services/hashService.js"; + +export async function hash(args) { + const input = args.input; + const algorithm = args.algorithm || "sha256"; + + if (!input) { + throw new Error(ERRORS.INVALID_INPUT); + } + + const filePath = resolvePath(state.cwd, input); + + const digest = await hashFile(filePath, algorithm); + + console.log(`${algorithm}: ${digest}`); + + if (args.save) { + const fileName = path.basename(filePath); + const dir = path.dirname(filePath); + + const hashPath = path.join(dir, `${fileName}.${algorithm}`); + + try { + await fs.writeFile(hashPath, digest); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + } +} diff --git a/src/main.js b/src/main.js index 56947e4..91e8a26 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import { cd } from "./navigation/cd.js"; import { ls } from "./navigation/ls.js"; import { count } from "./commands/count.js"; +import { hash } from "./commands/hash.js"; console.log("Welcome to Data Processing CLI!"); console.log(`You are currently in ${state.cwd}`); @@ -16,5 +17,6 @@ registerCommand("cd", cd); registerCommand("ls", ls); registerCommand("count", count); +registerCommand("hash", hash); run(); diff --git a/src/services/hashService.js b/src/services/hashService.js new file mode 100644 index 0000000..0af366a --- /dev/null +++ b/src/services/hashService.js @@ -0,0 +1,23 @@ +import fs from "fs"; +import { pipeline } from "stream/promises"; +import crypto from "crypto"; +import { Transform } from "stream"; +import { ERRORS } from "../utils/errors.js"; + +const SUPPORTED = new Set(["sha256", "md5", "sha512"]); + +export async function hashFile(filePath, algorithm = "sha256") { + if (!SUPPORTED.has(algorithm)) { + throw new Error(ERRORS.OPERATION_FAILED); + } + + const hash = crypto.createHash(algorithm); + + try { + await pipeline(fs.createReadStream(filePath), hash); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + + return hash.digest("hex"); +} From 6f6be29dde4478088583ce43fa3c32d2f4cbe724 Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 21:29:28 +0100 Subject: [PATCH 08/10] feat: implement hash-compare --- src/commands/hashCompare.js | 41 +++++++++++++++++++++++++++++++++++++ src/main.js | 2 ++ 2 files changed, 43 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..9056b56 --- /dev/null +++ b/src/commands/hashCompare.js @@ -0,0 +1,41 @@ +import fs from "fs/promises"; +import { resolvePath } from "../utils/pathResolver.js"; +import { ERRORS } from "../utils/errors.js"; +import { state } from "../core/state.js"; +import { hashFile } from "../services/hashService.js"; + +export async function hashCompare(args) { + const input = args.input; + const hashPathArg = args.hash; + const algorithm = args.algorithm || "sha256"; + + if (!input || !hashPathArg) { + throw new Error(ERRORS.INVALID_INPUT); + } + + const filePath = resolvePath(state.cwd, input); + const hashFilePath = resolvePath(state.cwd, hashPathArg); + + let actualHash; + try { + actualHash = await hashFile(filePath, algorithm); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + + let expectedHash; + + try { + expectedHash = await fs.readFile(hashFilePath, "utf8"); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + + expectedHash = expectedHash.trim().toLowerCase(); + + if (actualHash.toLowerCase() === expectedHash) { + console.log("OK"); + } else { + console.log("MISMATCH"); + } +} diff --git a/src/main.js b/src/main.js index 91e8a26..6ab8d5c 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ import { ls } from "./navigation/ls.js"; import { count } from "./commands/count.js"; import { hash } from "./commands/hash.js"; +import { hashCompare } from "./commands/hashCompare.js"; console.log("Welcome to Data Processing CLI!"); console.log(`You are currently in ${state.cwd}`); @@ -18,5 +19,6 @@ registerCommand("ls", ls); registerCommand("count", count); registerCommand("hash", hash); +registerCommand("hash-compare", hashCompare); run(); From a5a12b3a828739fb3a37cec7e8172f0f449e58cd Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 23:06:56 +0100 Subject: [PATCH 09/10] feat: implement csv/json --- src/commands/csvToJson.js | 17 ++++++ src/commands/jsonToCsv.js | 17 ++++++ src/main.js | 4 ++ src/services/csvService.js | 107 +++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 src/commands/csvToJson.js create mode 100644 src/commands/jsonToCsv.js create mode 100644 src/services/csvService.js diff --git a/src/commands/csvToJson.js b/src/commands/csvToJson.js new file mode 100644 index 0000000..31467df --- /dev/null +++ b/src/commands/csvToJson.js @@ -0,0 +1,17 @@ +import { resolvePath } from "../utils/pathResolver.js"; +import { ERRORS } from "../utils/errors.js"; +import { state } from "../core/state.js"; +import { csvToJsonStream } from "../services/csvService.js"; + +export async function csvToJson(args) { + const { input, output } = args; + + if (!input || !output) { + throw new Error(ERRORS.INVALID_INPUT); + } + + const inputPath = resolvePath(state.cwd, input); + const outputPath = resolvePath(state.cwd, output); + + await csvToJsonStream(inputPath, outputPath); +} diff --git a/src/commands/jsonToCsv.js b/src/commands/jsonToCsv.js new file mode 100644 index 0000000..d184b57 --- /dev/null +++ b/src/commands/jsonToCsv.js @@ -0,0 +1,17 @@ +import { resolvePath } from "../utils/pathResolver.js"; +import { ERRORS } from "../utils/errors.js"; +import { state } from "../core/state.js"; +import { jsonToCsvStream } from "../services/csvService.js"; + +export async function jsonToCsv(args) { + const { input, output } = args; + + if (!input || !output) { + throw new Error(ERRORS.INVALID_INPUT); + } + + const inputPath = resolvePath(state.cwd, input); + const outputPath = resolvePath(state.cwd, output); + + await jsonToCsvStream(inputPath, outputPath); +} diff --git a/src/main.js b/src/main.js index 6ab8d5c..5b03d98 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,8 @@ import { ls } from "./navigation/ls.js"; import { count } from "./commands/count.js"; import { hash } from "./commands/hash.js"; import { hashCompare } from "./commands/hashCompare.js"; +import { csvToJson } from "./commands/csvToJson.js"; +import { jsonToCsv } from "./commands/jsonToCsv.js"; console.log("Welcome to Data Processing CLI!"); console.log(`You are currently in ${state.cwd}`); @@ -20,5 +22,7 @@ registerCommand("ls", ls); registerCommand("count", count); registerCommand("hash", hash); registerCommand("hash-compare", hashCompare); +registerCommand("csv-to-json", csvToJson); +registerCommand("json-to-csv", jsonToCsv); run(); diff --git a/src/services/csvService.js b/src/services/csvService.js new file mode 100644 index 0000000..e2d712a --- /dev/null +++ b/src/services/csvService.js @@ -0,0 +1,107 @@ +import fs from "fs"; +import fsp from "fs/promises"; +import { Readable, Transform } from "stream"; +import { pipeline } from "stream/promises"; +import { ERRORS } from "../utils/errors.js"; + +function buildObject(headers, line) { + const values = line.split(","); + const obj = {}; + headers.forEach((h, i) => { + obj[h] = values[i] ?? ""; + }); + return obj; +} + +export async function csvToJsonStream(inputPath, outputPath) { + let headers = null; + let isFirst = true; + let buffer = ""; + + const transform = new Transform({ + transform(chunk, _enc, callback) { + buffer += chunk.toString(); + + const lines = buffer.split("\n"); + buffer = lines.pop(); + + for (const line of lines) { + if (!line.trim()) continue; + + if (!headers) { + headers = line.trimEnd().split(","); + continue; + } + + const json = JSON.stringify(buildObject(headers, line.trimEnd())); + this.push(isFirst ? "[\n" + json : ",\n" + json); + isFirst = false; + } + + callback(); + }, + + flush(callback) { + if (buffer.trim() && headers) { + const json = JSON.stringify(buildObject(headers, buffer.trimEnd())); + this.push(isFirst ? "[\n" + json : ",\n" + json); + isFirst = false; + } + + this.push(isFirst ? "[]" : "\n]"); + callback(); + }, + }); + + try { + await pipeline( + fs.createReadStream(inputPath), + transform, + fs.createWriteStream(outputPath), + ); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } +} + +function* generateLines(headers, data) { + yield headers.join(",") + "\n"; + for (const row of data) { + yield headers.map((h) => String(row[h] ?? "")).join(",") + "\n"; + } +} + +export async function jsonToCsvStream(inputPath, outputPath) { + let data; + + try { + const content = await fsp.readFile(inputPath, "utf8"); + data = JSON.parse(content); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + + if (!Array.isArray(data)) { + throw new Error(ERRORS.OPERATION_FAILED); + } + + if (data.length === 0) { + try { + await fsp.writeFile(outputPath, ""); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } + return; + } + + const headers = Object.keys(data[0]); + + try { + await pipeline( + Readable.from(generateLines(headers, data)), + fs.createWriteStream(outputPath), + ); + } catch { + throw new Error(ERRORS.OPERATION_FAILED); + } +} From 4260d85a7b650a7ed7d3da948280f7810a0f5caa Mon Sep 17 00:00:00 2001 From: maiano Date: Sun, 15 Mar 2026 23:27:40 +0100 Subject: [PATCH 10/10] docs: add readme --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6acedfb..b3b0d30 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ -# data-processing-cli -Data Processing Toolkit — an interactive command-line application +# Data Processing CLI + +Interactive command-line tool for file navigation and data processing. + +## Requirements + +- Node.js 24.10.0+ + +## Setup +``` +npm run start +``` + +## Commands + +### Navigation + +| Command | Description | +|--------|-------------| +| `up` | Move up one directory level | +| `cd ` | Navigate to directory (relative or absolute) | +| `ls` | List files and folders in current directory | +| `.exit` | Exit the application | + +### Data Processing + +#### Count lines, words and characters +``` +count --input file.txt +``` + +#### Calculate file hash +``` +hash --input file.txt +hash --input file.txt --algorithm md5 +hash --input file.txt --algorithm sha512 +hash --input file.txt --save +``` +Supported algorithms: `sha256` (default), `md5`, `sha512` + +#### Compare file hash +``` +hash-compare --input file.txt --hash file.txt.sha256 +hash-compare --input file.txt --hash file.txt.md5 --algorithm md5 +``` + +#### Convert CSV to JSON +``` +csv-to-json --input data.csv --output data.json +``` + +#### Convert JSON to CSV +``` +json-to-csv --input data.json --output data.csv +``` + +## Notes + +- All file paths can be relative (to current working directory) or absolute +- All file operations use Streams API +- Working directory starts at user home directory \ No newline at end of file