Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <path>` | 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
25 changes: 25 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"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": {
"start": "node src/main.js",
"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"
}
20 changes: 20 additions & 0 deletions src/commands/count.js
Original file line number Diff line number Diff line change
@@ -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}`);
}
17 changes: 17 additions & 0 deletions src/commands/csvToJson.js
Original file line number Diff line number Diff line change
@@ -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);
}
34 changes: 34 additions & 0 deletions src/commands/hash.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
41 changes: 41 additions & 0 deletions src/commands/hashCompare.js
Original file line number Diff line number Diff line change
@@ -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");
}
}
17 changes: 17 additions & 0 deletions src/commands/jsonToCsv.js
Original file line number Diff line number Diff line change
@@ -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);
}
34 changes: 34 additions & 0 deletions src/core/repl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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;
}

const success = await runCommand(input);
if (success) 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);
}
26 changes: 26 additions & 0 deletions src/core/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { parseArgs } from "../utils/argParser.js";

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 false;
}

await handler(parseArgs(args));
return true;
} catch (e) {
if (e.message === "INVALID_INPUT") console.log("Invalid input");
else console.log("Operation failed");
}
}
5 changes: 5 additions & 0 deletions src/core/state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os from "os";

export const state = {
cwd: os.homedir(),
};
28 changes: 28 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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";

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}`);

registerCommand("up", up);
registerCommand("cd", cd);
registerCommand("ls", ls);

registerCommand("count", count);
registerCommand("hash", hash);
registerCommand("hash-compare", hashCompare);
registerCommand("csv-to-json", csvToJson);
registerCommand("json-to-csv", jsonToCsv);

run();
28 changes: 28 additions & 0 deletions src/navigation/cd.js
Original file line number Diff line number Diff line change
@@ -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;
}
37 changes: 37 additions & 0 deletions src/navigation/ls.js
Original file line number Diff line number Diff line change
@@ -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}]`);
}
}
10 changes: 10 additions & 0 deletions src/navigation/up.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading