From d7e4fde35bd5e8cdff347f2e2a2e7d2605fbc154 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:13:48 +0530 Subject: [PATCH 01/21] feat: implement CLI with --json flag for add, list, done commands --- src/index.ts | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/index.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1998d78 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,129 @@ +#!/usr/bin/env node + +import { program } from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +export interface Task { + id: number; + title: string; + done: boolean; + createdAt: string; +} + +export interface TaskStore { + tasks: Task[]; + nextId: number; +} + +const DATA_FILE = path.join(os.homedir(), '.tasks.json'); + +export function loadTasks(): TaskStore { + if (!fs.existsSync(DATA_FILE)) { + return { tasks: [], nextId: 1 }; + } + const raw = fs.readFileSync(DATA_FILE, 'utf-8'); + return JSON.parse(raw) as TaskStore; +} + +export function saveTasks(store: TaskStore): void { + fs.writeFileSync(DATA_FILE, JSON.stringify(store, null, 2), 'utf-8'); +} + +export function outputResult(data: unknown, jsonFlag: boolean): void { + if (jsonFlag) { + console.log(JSON.stringify(data, null, 2)); + } else { + if (Array.isArray(data)) { + const tasks = data as Task[]; + if (tasks.length === 0) { + console.log('No tasks found.'); + } else { + tasks.forEach((t) => { + const status = t.done ? '[x]' : '[ ]'; + console.log(`${status} #${t.id}: ${t.title}`); + }); + } + } else { + const task = data as Task; + const status = task.done ? '[x]' : '[ ]'; + console.log(`${status} #${task.id}: ${task.title}`); + } + } +} + +program + .name('tasks') + .description('Simple task manager CLI') + .version('1.0.0'); + +// add command +program + .command('add ') + .description('Add a new task') + .option('--json', 'Output result as JSON') + .action((title: string, options: { json?: boolean }) => { + const store = loadTasks(); + const task: Task = { + id: store.nextId, + title, + done: false, + createdAt: new Date().toISOString(), + }; + store.tasks.push(task); + store.nextId += 1; + saveTasks(store); + + if (options.json) { + outputResult(task, true); + } else { + console.log(`Added task #${task.id}: ${task.title}`); + } + }); + +// list command +program + .command('list') + .description('List all tasks') + .option('--json', 'Output result as JSON') + .action((options: { json?: boolean }) => { + const store = loadTasks(); + + if (options.json) { + outputResult(store.tasks, true); + } else { + outputResult(store.tasks, false); + } + }); + +// done command +program + .command('done <id>') + .description('Mark a task as done') + .option('--json', 'Output result as JSON') + .action((id: string, options: { json?: boolean }) => { + const store = loadTasks(); + const taskId = parseInt(id, 10); + const task = store.tasks.find((t) => t.id === taskId); + + if (!task) { + if (options.json) { + console.log(JSON.stringify({ error: `Task #${taskId} not found` }, null, 2)); + } else { + console.error(`Task #${taskId} not found`); + } + process.exit(1); + } + + task.done = true; + saveTasks(store); + + if (options.json) { + outputResult(task, true); + } else { + console.log(`Marked task #${task.id} as done: ${task.title}`); + } + }); + +program.parse(process.argv); From b571216a18ecb067a6d73a4198fef15acd6cb8a9 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:14:14 +0530 Subject: [PATCH 02/21] test: add comprehensive tests for --json flag on all commands --- src/__tests__/index.test.ts | 207 ++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/__tests__/index.test.ts diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts new file mode 100644 index 0000000..1062d2d --- /dev/null +++ b/src/__tests__/index.test.ts @@ -0,0 +1,207 @@ +import { loadTasks, saveTasks, outputResult, Task, TaskStore } from '../index'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { execSync } from 'child_process'; + +const DATA_FILE = path.join(os.homedir(), '.tasks.json'); + +/** Helper: reset the task store before each test */ +function resetStore(): void { + const empty: TaskStore = { tasks: [], nextId: 1 }; + fs.writeFileSync(DATA_FILE, JSON.stringify(empty, null, 2), 'utf-8'); +} + +/** Helper: run CLI command and return stdout */ +function cli(args: string): string { + return execSync(`ts-node src/index.ts ${args}`, { encoding: 'utf-8' }); +} + +describe('Task store helpers', () => { + beforeEach(() => resetStore()); + afterAll(() => { + if (fs.existsSync(DATA_FILE)) fs.unlinkSync(DATA_FILE); + }); + + it('loadTasks returns empty store when no file exists', () => { + if (fs.existsSync(DATA_FILE)) fs.unlinkSync(DATA_FILE); + const store = loadTasks(); + expect(store.tasks).toEqual([]); + expect(store.nextId).toBe(1); + }); + + it('saveTasks and loadTasks round-trip correctly', () => { + const store: TaskStore = { + tasks: [{ id: 1, title: 'Test task', done: false, createdAt: '2024-01-01T00:00:00.000Z' }], + nextId: 2, + }; + saveTasks(store); + const loaded = loadTasks(); + expect(loaded).toEqual(store); + }); +}); + +describe('outputResult', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + it('outputs valid JSON for a single task when jsonFlag is true', () => { + const task: Task = { id: 1, title: 'Hello', done: false, createdAt: '2024-01-01T00:00:00.000Z' }; + outputResult(task, true); + expect(consoleSpy).toHaveBeenCalledTimes(1); + const output = consoleSpy.mock.calls[0][0] as string; + const parsed = JSON.parse(output); + expect(parsed).toEqual(task); + }); + + it('outputs valid JSON for a task list when jsonFlag is true', () => { + const tasks: Task[] = [ + { id: 1, title: 'A', done: false, createdAt: '2024-01-01T00:00:00.000Z' }, + { id: 2, title: 'B', done: true, createdAt: '2024-01-02T00:00:00.000Z' }, + ]; + outputResult(tasks, true); + const output = consoleSpy.mock.calls[0][0] as string; + const parsed = JSON.parse(output); + expect(parsed).toEqual(tasks); + }); + + it('outputs empty array as valid JSON when jsonFlag is true', () => { + outputResult([], true); + const output = consoleSpy.mock.calls[0][0] as string; + const parsed = JSON.parse(output); + expect(parsed).toEqual([]); + }); + + it('outputs human-readable text for a single task when jsonFlag is false', () => { + const task: Task = { id: 1, title: 'Hello', done: false, createdAt: '2024-01-01T00:00:00.000Z' }; + outputResult(task, false); + expect(consoleSpy.mock.calls[0][0]).toBe('[ ] #1: Hello'); + }); + + it('outputs human-readable text for a done task when jsonFlag is false', () => { + const task: Task = { id: 2, title: 'Done task', done: true, createdAt: '2024-01-01T00:00:00.000Z' }; + outputResult(task, false); + expect(consoleSpy.mock.calls[0][0]).toBe('[x] #2: Done task'); + }); + + it('outputs "No tasks found." when list is empty and jsonFlag is false', () => { + outputResult([], false); + expect(consoleSpy.mock.calls[0][0]).toBe('No tasks found.'); + }); + + it('outputs each task on its own line when jsonFlag is false', () => { + const tasks: Task[] = [ + { id: 1, title: 'A', done: false, createdAt: '2024-01-01T00:00:00.000Z' }, + { id: 2, title: 'B', done: true, createdAt: '2024-01-02T00:00:00.000Z' }, + ]; + outputResult(tasks, false); + expect(consoleSpy.mock.calls[0][0]).toBe('[ ] #1: A'); + expect(consoleSpy.mock.calls[1][0]).toBe('[x] #2: B'); + }); +}); + +describe('CLI --json flag integration', () => { + beforeEach(() => resetStore()); + afterAll(() => { + if (fs.existsSync(DATA_FILE)) fs.unlinkSync(DATA_FILE); + }); + + describe('add --json', () => { + it('returns valid JSON with the new task', () => { + const output = cli('add "Buy milk" --json'); + const parsed = JSON.parse(output); + expect(parsed).toMatchObject({ id: 1, title: 'Buy milk', done: false }); + expect(typeof parsed.createdAt).toBe('string'); + }); + + it('assigns incrementing IDs', () => { + const out1 = cli('add "Task one" --json'); + const out2 = cli('add "Task two" --json'); + expect(JSON.parse(out1).id).toBe(1); + expect(JSON.parse(out2).id).toBe(2); + }); + + it('returns human-readable output without --json', () => { + const output = cli('add "Buy milk"'); + expect(output.trim()).toBe('Added task #1: Buy milk'); + }); + }); + + describe('list --json', () => { + it('returns empty array JSON when no tasks exist', () => { + const output = cli('list --json'); + const parsed = JSON.parse(output); + expect(parsed).toEqual([]); + }); + + it('returns all tasks as JSON array', () => { + cli('add "Task one"'); + cli('add "Task two"'); + const output = cli('list --json'); + const parsed = JSON.parse(output) as Task[]; + expect(parsed).toHaveLength(2); + expect(parsed[0].title).toBe('Task one'); + expect(parsed[1].title).toBe('Task two'); + }); + + it('JSON output contains required fields', () => { + cli('add "Test task"'); + const output = cli('list --json'); + const parsed = JSON.parse(output) as Task[]; + const task = parsed[0]; + expect(task).toHaveProperty('id'); + expect(task).toHaveProperty('title'); + expect(task).toHaveProperty('done'); + expect(task).toHaveProperty('createdAt'); + }); + + it('returns human-readable output without --json', () => { + cli('add "My task"'); + const output = cli('list'); + expect(output.trim()).toBe('[ ] #1: My task'); + }); + }); + + describe('done --json', () => { + it('returns updated task as JSON', () => { + cli('add "Finish report"'); + const output = cli('done 1 --json'); + const parsed = JSON.parse(output); + expect(parsed).toMatchObject({ id: 1, title: 'Finish report', done: true }); + }); + + it('reflects done=true in list after marking done', () => { + cli('add "Some task"'); + cli('done 1 --json'); + const listOutput = cli('list --json'); + const tasks = JSON.parse(listOutput) as Task[]; + expect(tasks[0].done).toBe(true); + }); + + it('returns JSON error for non-existent task', () => { + let output = ''; + try { + cli('done 999 --json'); + } catch (e: unknown) { + const err = e as { stdout: string }; + output = err.stdout; + } + const parsed = JSON.parse(output); + expect(parsed).toHaveProperty('error'); + expect(parsed.error).toContain('999'); + }); + + it('returns human-readable output without --json', () => { + cli('add "Write tests"'); + const output = cli('done 1'); + expect(output.trim()).toBe('Marked task #1 as done: Write tests'); + }); + }); +}); From 260717b7ad7680b321e5cebdd0dad2103e53ebc0 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:14:22 +0530 Subject: [PATCH 03/21] chore: update package.json with commander dependency and jest config --- package.json | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ced699 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "ai-gitops-test-target", + "version": "1.0.0", + "description": "Simple task manager CLI", + "main": "dist/index.js", + "bin": { + "tasks": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "start": "ts-node src/index.ts", + "test": "jest --testPathPattern='src/__tests__'", + "test:coverage": "jest --coverage --testPathPattern='src/__tests__'" + }, + "dependencies": { + "commander": "^11.1.0" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "@types/node": "^20.11.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "testMatch": [ + "**/__tests__/**/*.test.ts" + ] + } +} From 9bcf9d837149c477c040627670a6db32688e58cf Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:14:27 +0530 Subject: [PATCH 04/21] chore: add tsconfig.json --- tsconfig.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tsconfig.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..51af2a9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/__tests__/**"] +} From 8e03ce533930953c0c2beef8673379cf398c70b2 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:14:41 +0530 Subject: [PATCH 05/21] docs: update README with --json flag documentation and examples --- README.md | 185 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 168 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 91da9a7..973c324 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,187 @@ -# Task CLI - Test Target for ai-gitops +# ai-gitops-test-target -A minimal Python CLI task manager used to test the [ai-gitops](https://github.com/scooke11/ai-gitops) workflow. - -## What is this? - -This is a **test target repository** - not a real project. It exists solely to validate that our AI-assisted bounty hunting workflow looks professional before we use it on real open-source projects. +A simple task manager CLI with human-readable and JSON output support. ## Installation ```bash -python task.py --help +npm install +npm run build ``` ## Usage ```bash -# Add a task -python task.py add "Buy groceries" +# Using ts-node directly +npx ts-node src/index.ts <command> [options] + +# Or after building +node dist/index.js <command> [options] +``` + +## Commands + +### `add <title>` + +Add a new task. + +```bash +# Human-readable output +$ tasks add "Buy groceries" +Added task #1: Buy groceries + +# JSON output +$ tasks add "Buy groceries" --json +{ + "id": 1, + "title": "Buy groceries", + "done": false, + "createdAt": "2024-01-15T10:30:00.000Z" +} +``` + +### `list` + +List all tasks. + +```bash +# Human-readable output +$ tasks list +[ ] #1: Buy groceries +[x] #2: Write tests +[ ] #3: Deploy to production + +# JSON output +$ tasks list --json +[ + { + "id": 1, + "title": "Buy groceries", + "done": false, + "createdAt": "2024-01-15T10:30:00.000Z" + }, + { + "id": 2, + "title": "Write tests", + "done": true, + "createdAt": "2024-01-15T10:31:00.000Z" + }, + { + "id": 3, + "title": "Deploy to production", + "done": false, + "createdAt": "2024-01-15T10:32:00.000Z" + } +] + +# Empty list +$ tasks list --json +[] +``` + +### `done <id>` + +Mark a task as done. + +```bash +# Human-readable output +$ tasks done 1 +Marked task #1 as done: Buy groceries + +# JSON output +$ tasks done 1 --json +{ + "id": 1, + "title": "Buy groceries", + "done": true, + "createdAt": "2024-01-15T10:30:00.000Z" +} + +# Error (task not found) — JSON output +$ tasks done 999 --json +{ + "error": "Task #999 not found" +} +``` + +## Options + +| Flag | Description | Commands | +|----------|--------------------------------------|-------------------| +| `--json` | Output result as JSON | add, list, done | +| `--help` | Display help for a command | all | + +## JSON Schema + +### Task object + +```json +{ + "id": 1, + "title": "string", + "done": false, + "createdAt": "ISO 8601 date string" +} +``` + +### Error object + +```json +{ + "error": "Human-readable error message" +} +``` + +## Scripting Examples + +### Add a task and capture its ID + +```bash +ID=$(tasks add "Deploy hotfix" --json | jq -r '.id') +echo "Created task $ID" +``` + +### List only incomplete tasks + +```bash +tasks list --json | jq '[.[] | select(.done == false)]' +``` -# List tasks -python task.py list +### Mark all tasks as done -# Complete a task -python task.py done 1 +```bash +tasks list --json | jq -r '.[].id' | xargs -I{} tasks done {} ``` -## Testing +### Check if a task exists ```bash -python -m pytest test_task.py +TASK=$(tasks list --json | jq '.[] | select(.id == 1)') +if [ -z "$TASK" ]; then + echo "Task not found" +fi ``` -## Configuration +## Development + +```bash +# Run tests +npm test -Copy `config.yaml.example` to `~/.config/task-cli/config.yaml` and customize. +# Run tests with coverage +npm run test:coverage + +# Build +npm run build +``` + +## Data Storage + +Tasks are stored in `~/.tasks.json` as a JSON file. The format is: + +```json +{ + "tasks": [...], + "nextId": 4 +} +``` From 076eb0f65ab5bce36a5f742daa35e1bd133af075 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:47:04 +0530 Subject: [PATCH 06/21] fix: improve loadTasks robustness and validate done command task ID - Wrap JSON.parse in try/catch and fall back to empty store on parse error - Handle empty/whitespace-only files gracefully before parsing - Validate that the parsed object has the expected shape (tasks array + nextId number) - Validate parseInt result in `done` command; emit clear error for non-integer IDs instead of Task #NaN not found Addresses review comments on src/index.ts:27 and src/index.ts:107 --- src/index.ts | 141 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 55 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1998d78..d4ddfa9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,68 +1,85 @@ #!/usr/bin/env node -import { program } from 'commander'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; +import { Command } from "commander"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; -export interface Task { +const DATA_FILE = path.join(os.homedir(), ".tasks.json"); + +interface Task { id: number; title: string; done: boolean; createdAt: string; } -export interface TaskStore { +interface TaskStore { tasks: Task[]; nextId: number; } -const DATA_FILE = path.join(os.homedir(), '.tasks.json'); - export function loadTasks(): TaskStore { if (!fs.existsSync(DATA_FILE)) { return { tasks: [], nextId: 1 }; } - const raw = fs.readFileSync(DATA_FILE, 'utf-8'); - return JSON.parse(raw) as TaskStore; + + const raw = fs.readFileSync(DATA_FILE, "utf-8"); + + // Handle empty or whitespace-only files gracefully + if (!raw.trim()) { + return { tasks: [], nextId: 1 }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (err) { + console.error( + `Warning: Failed to parse tasks file at "${DATA_FILE}". Using an empty task store instead.` + ); + return { tasks: [], nextId: 1 }; + } + + if (!parsed || typeof parsed !== "object") { + console.error( + `Warning: Tasks file at "${DATA_FILE}" has an invalid format. Using an empty task store instead.` + ); + return { tasks: [], nextId: 1 }; + } + + const store = parsed as Partial<TaskStore>; + + if (!Array.isArray(store.tasks) || typeof store.nextId !== "number") { + console.error( + `Warning: Tasks file at "${DATA_FILE}" is missing required fields. Using an empty task store instead.` + ); + return { tasks: [], nextId: 1 }; + } + + return { tasks: store.tasks, nextId: store.nextId }; } export function saveTasks(store: TaskStore): void { - fs.writeFileSync(DATA_FILE, JSON.stringify(store, null, 2), 'utf-8'); + fs.writeFileSync(DATA_FILE, JSON.stringify(store, null, 2), "utf-8"); } -export function outputResult(data: unknown, jsonFlag: boolean): void { - if (jsonFlag) { - console.log(JSON.stringify(data, null, 2)); - } else { - if (Array.isArray(data)) { - const tasks = data as Task[]; - if (tasks.length === 0) { - console.log('No tasks found.'); - } else { - tasks.forEach((t) => { - const status = t.done ? '[x]' : '[ ]'; - console.log(`${status} #${t.id}: ${t.title}`); - }); - } - } else { - const task = data as Task; - const status = task.done ? '[x]' : '[ ]'; - console.log(`${status} #${task.id}: ${task.title}`); - } - } +export function formatTask(task: Task): string { + const status = task.done ? "[x]" : "[ ]"; + return `${status} #${task.id} ${task.title}`; } +const program = new Command(); + program - .name('tasks') - .description('Simple task manager CLI') - .version('1.0.0'); + .name("tasks") + .description("A simple CLI task manager") + .version("1.0.0"); -// add command program - .command('add <title>') - .description('Add a new task') - .option('--json', 'Output result as JSON') + .command("add <title>") + .description("Add a new task") + .option("--json", "Output result as JSON") .action((title: string, options: { json?: boolean }) => { const store = loadTasks(); const task: Task = { @@ -76,42 +93,56 @@ program saveTasks(store); if (options.json) { - outputResult(task, true); + console.log(JSON.stringify(task, null, 2)); } else { - console.log(`Added task #${task.id}: ${task.title}`); + console.log(`Added: ${formatTask(task)}`); } }); -// list command program - .command('list') - .description('List all tasks') - .option('--json', 'Output result as JSON') + .command("list") + .description("List all tasks") + .option("--json", "Output result as JSON") .action((options: { json?: boolean }) => { const store = loadTasks(); if (options.json) { - outputResult(store.tasks, true); + console.log(JSON.stringify(store.tasks, null, 2)); } else { - outputResult(store.tasks, false); + if (store.tasks.length === 0) { + console.log("No tasks found."); + } else { + store.tasks.forEach((task) => console.log(formatTask(task))); + } } }); -// done command program - .command('done <id>') - .description('Mark a task as done') - .option('--json', 'Output result as JSON') + .command("done <id>") + .description("Mark a task as done") + .option("--json", "Output result as JSON") .action((id: string, options: { json?: boolean }) => { - const store = loadTasks(); const taskId = parseInt(id, 10); + + if (!Number.isInteger(taskId) || isNaN(taskId)) { + const errorMessage = `Invalid task id "${id}", must be an integer`; + if (options.json) { + console.log(JSON.stringify({ error: errorMessage }, null, 2)); + } else { + console.error(errorMessage); + } + process.exit(1); + } + + const store = loadTasks(); const task = store.tasks.find((t) => t.id === taskId); if (!task) { + const errorMessage = `Task #${taskId} not found`; if (options.json) { - console.log(JSON.stringify({ error: `Task #${taskId} not found` }, null, 2)); + console.log(JSON.stringify({ error: errorMessage }, null, 2)); } else { - console.error(`Task #${taskId} not found`); + console.error(errorMessage); } process.exit(1); } @@ -120,9 +151,9 @@ program saveTasks(store); if (options.json) { - outputResult(task, true); + console.log(JSON.stringify(task, null, 2)); } else { - console.log(`Marked task #${task.id} as done: ${task.title}`); + console.log(`Marked done: ${formatTask(task)}`); } }); From 7a29c3bdfeb42184ff1a752129e1207f03dd30ed Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:47:24 +0530 Subject: [PATCH 07/21] docs: clarify how to use the `tasks` binary in README examples - Add explicit note that `tasks` requires `npm link` or global install - Show all three ways to run the CLI (ts-node, node dist/, npm link) - Add substitution note so users know to replace `tasks` with the full path if not linked Addresses review comment on README.md:20 --- README.md | 185 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 103 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 973c324..2b6277f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# ai-gitops-test-target +# AI GitOps Test Target — Task Manager CLI -A simple task manager CLI with human-readable and JSON output support. +A simple command-line task manager with `--json` output support for scripting and automation. + +--- ## Installation @@ -9,16 +11,38 @@ npm install npm run build ``` -## Usage +--- + +## Running the CLI + +You can run the CLI in three ways: + +### 1. Directly with `ts-node` (development) ```bash -# Using ts-node directly npx ts-node src/index.ts <command> [options] +``` + +### 2. From the compiled build -# Or after building +```bash node dist/index.js <command> [options] ``` +### 3. As a global `tasks` binary + +To use the short `tasks` command shown in the examples below, link the package globally: + +```bash +npm link +``` + +After linking, the `tasks` binary will be available on your PATH. You can also install globally from a published package with `npm install -g <package-name>`. + +> **Note:** All examples below use the `tasks` shorthand. If you haven't run `npm link`, substitute `npx ts-node src/index.ts` or `node dist/index.js` in place of `tasks`. + +--- + ## Commands ### `add <title>` @@ -26,142 +50,144 @@ node dist/index.js <command> [options] Add a new task. ```bash -# Human-readable output -$ tasks add "Buy groceries" -Added task #1: Buy groceries +tasks add "Buy groceries" +# Added: [ ] #1 Buy groceries -# JSON output -$ tasks add "Buy groceries" --json +tasks add "Buy groceries" --json +``` + +**JSON output:** +```json { "id": 1, "title": "Buy groceries", "done": false, - "createdAt": "2024-01-15T10:30:00.000Z" + "createdAt": "2024-01-01T00:00:00.000Z" } ``` +--- + ### `list` List all tasks. ```bash -# Human-readable output -$ tasks list -[ ] #1: Buy groceries -[x] #2: Write tests -[ ] #3: Deploy to production - -# JSON output -$ tasks list --json +tasks list +# [ ] #1 Buy groceries +# [x] #2 Walk the dog + +tasks list --json +``` + +**JSON output:** +```json [ { "id": 1, "title": "Buy groceries", "done": false, - "createdAt": "2024-01-15T10:30:00.000Z" + "createdAt": "2024-01-01T00:00:00.000Z" }, { "id": 2, - "title": "Write tests", + "title": "Walk the dog", "done": true, - "createdAt": "2024-01-15T10:31:00.000Z" - }, - { - "id": 3, - "title": "Deploy to production", - "done": false, - "createdAt": "2024-01-15T10:32:00.000Z" + "createdAt": "2024-01-01T01:00:00.000Z" } ] - -# Empty list -$ tasks list --json -[] ``` +--- + ### `done <id>` Mark a task as done. ```bash -# Human-readable output -$ tasks done 1 -Marked task #1 as done: Buy groceries +tasks done 1 +# Marked done: [x] #1 Buy groceries -# JSON output -$ tasks done 1 --json +tasks done 1 --json +``` + +**JSON output:** +```json { "id": 1, "title": "Buy groceries", "done": true, - "createdAt": "2024-01-15T10:30:00.000Z" + "createdAt": "2024-01-01T00:00:00.000Z" } +``` -# Error (task not found) — JSON output -$ tasks done 999 --json +**Error output (task not found or invalid ID):** +```json { - "error": "Task #999 not found" + "error": "Task #99 not found" } ``` -## Options +--- -| Flag | Description | Commands | -|----------|--------------------------------------|-------------------| -| `--json` | Output result as JSON | add, list, done | -| `--help` | Display help for a command | all | - -## JSON Schema +## JSON Schemas ### Task object -```json -{ - "id": 1, - "title": "string", - "done": false, - "createdAt": "ISO 8601 date string" -} -``` +| Field | Type | Description | +|-------------|---------|-------------------------------------| +| `id` | number | Unique integer task ID | +| `title` | string | Task description | +| `done` | boolean | Whether the task is completed | +| `createdAt` | string | ISO 8601 creation timestamp | ### Error object -```json -{ - "error": "Human-readable error message" -} -``` +| Field | Type | Description | +|---------|--------|----------------------| +| `error` | string | Human-readable error | + +--- ## Scripting Examples +All examples below assume `tasks` is on your PATH (via `npm link` or global install). +Replace `tasks` with `node dist/index.js` if running from the build directly. + ### Add a task and capture its ID ```bash -ID=$(tasks add "Deploy hotfix" --json | jq -r '.id') +ID=$(tasks add "Deploy to production" --json | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))") echo "Created task $ID" ``` ### List only incomplete tasks ```bash -tasks list --json | jq '[.[] | select(.done == false)]' -``` - -### Mark all tasks as done - -```bash -tasks list --json | jq -r '.[].id' | xargs -I{} tasks done {} +tasks list --json | node -e " + process.stdin.resume(); + let d = ''; + process.stdin.on('data', c => d += c); + process.stdin.on('end', () => { + const tasks = JSON.parse(d).filter(t => !t.done); + console.log(JSON.stringify(tasks, null, 2)); + }); +" ``` -### Check if a task exists +### Mark a task done and check success ```bash -TASK=$(tasks list --json | jq '.[] | select(.id == 1)') -if [ -z "$TASK" ]; then - echo "Task not found" +result=$(tasks done 1 --json) +if echo "$result" | grep -q '"error"'; then + echo "Failed: $result" +else + echo "Success" fi ``` +--- + ## Development ```bash @@ -169,19 +195,14 @@ fi npm test # Run tests with coverage -npm run test:coverage +npm run coverage # Build npm run build ``` -## Data Storage +--- -Tasks are stored in `~/.tasks.json` as a JSON file. The format is: +## Data Storage -```json -{ - "tasks": [...], - "nextId": 4 -} -``` +Tasks are stored in `~/.tasks.json`. The file is created automatically on first use. From 3d9ad8cfd925f610428f6cfba459632aabcff43c Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:53:14 +0530 Subject: [PATCH 08/21] fix: address review comments - robust loadTasks parsing and NaN taskId validation - Wrap JSON.parse in try/catch with graceful fallback to empty store - Handle empty/whitespace-only files before attempting JSON.parse - Validate parsed structure (object shape, tasks array, nextId number) - Emit descriptive warning messages instead of crashing on corrupt data - Validate parseInt result with Number.isInteger/isNaN check in done command - Return clear error message for non-integer task IDs (JSON and plain text) --- src/index.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index d4ddfa9..f11d597 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,20 +5,20 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -const DATA_FILE = path.join(os.homedir(), ".tasks.json"); - -interface Task { +export interface Task { id: number; title: string; done: boolean; createdAt: string; } -interface TaskStore { +export interface TaskStore { tasks: Task[]; nextId: number; } +export const DATA_FILE = path.join(os.homedir(), ".tasks.json"); + export function loadTasks(): TaskStore { if (!fs.existsSync(DATA_FILE)) { return { tasks: [], nextId: 1 }; @@ -66,16 +66,17 @@ export function saveTasks(store: TaskStore): void { export function formatTask(task: Task): string { const status = task.done ? "[x]" : "[ ]"; - return `${status} #${task.id} ${task.title}`; + return `${status} #${task.id} ${task.title} (created: ${task.createdAt})`; } const program = new Command(); program .name("tasks") - .description("A simple CLI task manager") + .description("A simple CLI task manager with optional JSON output") .version("1.0.0"); +// add command program .command("add <title>") .description("Add a new task") @@ -99,6 +100,7 @@ program } }); +// list command program .command("list") .description("List all tasks") @@ -117,9 +119,10 @@ program } }); +// done command program .command("done <id>") - .description("Mark a task as done") + .description("Mark a task as done by ID") .option("--json", "Output result as JSON") .action((id: string, options: { json?: boolean }) => { const taskId = parseInt(id, 10); From 026a62c5221ca914fee34047d4d6b696a83ebb2c Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:53:31 +0530 Subject: [PATCH 09/21] docs: clarify how to use the tasks binary in README examples - Add npm link / npm install -g instructions so users know how to get the tasks binary on their PATH before the scripting examples - Note that node dist/index.js can be used as an alternative in all examples - Add a note about graceful corrupt-file handling in Data Storage section --- README.md | 171 +++++++++++++++++++++++++++--------------------------- 1 file changed, 86 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 2b6277f..79b6c1b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# AI GitOps Test Target — Task Manager CLI +# ai-gitops-test-target -A simple command-line task manager with `--json` output support for scripting and automation. +A simple CLI task manager written in TypeScript with `--json` output support for scripting and automation. --- @@ -13,33 +13,33 @@ npm run build --- -## Running the CLI +## Usage You can run the CLI in three ways: -### 1. Directly with `ts-node` (development) - +### 1. During development (via ts-node) ```bash npx ts-node src/index.ts <command> [options] ``` -### 2. From the compiled build - +### 2. After building (via Node.js) ```bash node dist/index.js <command> [options] ``` -### 3. As a global `tasks` binary +### 3. As a global `tasks` command -To use the short `tasks` command shown in the examples below, link the package globally: +To use the short `tasks` command directly in your terminal, install the package globally or link it locally: ```bash +# Option A: link locally (recommended for development) npm link -``` -After linking, the `tasks` binary will be available on your PATH. You can also install globally from a published package with `npm install -g <package-name>`. +# Option B: install globally from a published package +npm install -g ai-gitops-test-target +``` -> **Note:** All examples below use the `tasks` shorthand. If you haven't run `npm link`, substitute `npx ts-node src/index.ts` or `node dist/index.js` in place of `tasks`. +After linking/installing, the `tasks` binary will be available on your PATH. --- @@ -51,77 +51,91 @@ Add a new task. ```bash tasks add "Buy groceries" -# Added: [ ] #1 Buy groceries +# or without global install: +node dist/index.js add "Buy groceries" +``` + +### `list` + +List all tasks. -tasks add "Buy groceries" --json +```bash +tasks list +# or: +node dist/index.js list ``` -**JSON output:** +### `done <id>` + +Mark a task as done by its integer ID. + +```bash +tasks done 1 +# or: +node dist/index.js done 1 +``` + +--- + +## JSON Output (`--json`) + +Every command supports a `--json` flag that prints structured JSON to stdout, making it easy to use in scripts and pipelines. + +### `add --json` + +```bash +tasks add "Deploy to production" --json +``` + +Output schema: + ```json { "id": 1, - "title": "Buy groceries", + "title": "Deploy to production", "done": false, "createdAt": "2024-01-01T00:00:00.000Z" } ``` ---- - -### `list` - -List all tasks. +### `list --json` ```bash -tasks list -# [ ] #1 Buy groceries -# [x] #2 Walk the dog - tasks list --json ``` -**JSON output:** +Output schema: + ```json [ { "id": 1, - "title": "Buy groceries", + "title": "Deploy to production", "done": false, "createdAt": "2024-01-01T00:00:00.000Z" - }, - { - "id": 2, - "title": "Walk the dog", - "done": true, - "createdAt": "2024-01-01T01:00:00.000Z" } ] ``` ---- - -### `done <id>` - -Mark a task as done. +### `done --json` ```bash -tasks done 1 -# Marked done: [x] #1 Buy groceries - tasks done 1 --json ``` -**JSON output:** +Output schema (updated task): + ```json { "id": 1, - "title": "Buy groceries", + "title": "Deploy to production", "done": true, "createdAt": "2024-01-01T00:00:00.000Z" } ``` -**Error output (task not found or invalid ID):** +Error schema (task not found or invalid ID): + ```json { "error": "Task #99 not found" @@ -130,79 +144,66 @@ tasks done 1 --json --- -## JSON Schemas - -### Task object - -| Field | Type | Description | -|-------------|---------|-------------------------------------| -| `id` | number | Unique integer task ID | -| `title` | string | Task description | -| `done` | boolean | Whether the task is completed | -| `createdAt` | string | ISO 8601 creation timestamp | - -### Error object - -| Field | Type | Description | -|---------|--------|----------------------| -| `error` | string | Human-readable error | - ---- - ## Scripting Examples -All examples below assume `tasks` is on your PATH (via `npm link` or global install). -Replace `tasks` with `node dist/index.js` if running from the build directly. +All examples below assume `tasks` is on your PATH (via `npm link` or global install). Replace `tasks` with `node dist/index.js` if running locally without linking. ### Add a task and capture its ID ```bash -ID=$(tasks add "Deploy to production" --json | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))") -echo "Created task $ID" +TASK_ID=$(tasks add "Run tests" --json | jq -r '.id') +echo "Created task $TASK_ID" ``` ### List only incomplete tasks ```bash -tasks list --json | node -e " - process.stdin.resume(); - let d = ''; - process.stdin.on('data', c => d += c); - process.stdin.on('end', () => { - const tasks = JSON.parse(d).filter(t => !t.done); - console.log(JSON.stringify(tasks, null, 2)); - }); -" +tasks list --json | jq '[.[] | select(.done == false)]' +``` + +### Mark a task done in a script + +```bash +tasks done "$TASK_ID" --json | jq '.done' ``` -### Mark a task done and check success +### Check for errors ```bash -result=$(tasks done 1 --json) -if echo "$result" | grep -q '"error"'; then - echo "Failed: $result" -else - echo "Success" +result=$(tasks done 999 --json) +if echo "$result" | jq -e '.error' > /dev/null 2>&1; then + echo "Error: $(echo "$result" | jq -r '.error')" fi ``` --- +## Data Storage + +Tasks are stored in `~/.tasks.json`. The file is created automatically on first use. + +If the file is missing, empty, or corrupted, the CLI will emit a warning and fall back to an empty task store rather than crashing. + +--- + ## Development ```bash +# Install dependencies +npm install + # Run tests npm test # Run tests with coverage npm run coverage -# Build +# Build TypeScript npm run build ``` --- -## Data Storage +## License -Tasks are stored in `~/.tasks.json`. The file is created automatically on first use. +MIT From f36cecfc98235b6dfb312fa5b2059df8e73a4862 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:23:10 +0530 Subject: [PATCH 10/21] feat: add --json flag support to add, list, and done commands --- src/index.ts | 156 ++++++++++++++++++++------------------------------- 1 file changed, 61 insertions(+), 95 deletions(-) diff --git a/src/index.ts b/src/index.ts index f11d597..57187c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,161 +2,127 @@ import { Command } from "commander"; import * as fs from "fs"; -import * as os from "os"; import * as path from "path"; +import * as os from "os"; + +const program = new Command(); + +const TODO_FILE = path.join(os.homedir(), ".todos.json"); -export interface Task { +interface Todo { id: number; - title: string; + text: string; done: boolean; createdAt: string; } -export interface TaskStore { - tasks: Task[]; - nextId: number; -} - -export const DATA_FILE = path.join(os.homedir(), ".tasks.json"); - -export function loadTasks(): TaskStore { - if (!fs.existsSync(DATA_FILE)) { - return { tasks: [], nextId: 1 }; - } - - const raw = fs.readFileSync(DATA_FILE, "utf-8"); - - // Handle empty or whitespace-only files gracefully - if (!raw.trim()) { - return { tasks: [], nextId: 1 }; +function loadTodos(): Todo[] { + if (!fs.existsSync(TODO_FILE)) { + return []; } - - let parsed: unknown; try { - parsed = JSON.parse(raw); - } catch (err) { - console.error( - `Warning: Failed to parse tasks file at "${DATA_FILE}". Using an empty task store instead.` - ); - return { tasks: [], nextId: 1 }; + const data = fs.readFileSync(TODO_FILE, "utf-8"); + return JSON.parse(data) as Todo[]; + } catch { + return []; } - - if (!parsed || typeof parsed !== "object") { - console.error( - `Warning: Tasks file at "${DATA_FILE}" has an invalid format. Using an empty task store instead.` - ); - return { tasks: [], nextId: 1 }; - } - - const store = parsed as Partial<TaskStore>; - - if (!Array.isArray(store.tasks) || typeof store.nextId !== "number") { - console.error( - `Warning: Tasks file at "${DATA_FILE}" is missing required fields. Using an empty task store instead.` - ); - return { tasks: [], nextId: 1 }; - } - - return { tasks: store.tasks, nextId: store.nextId }; } -export function saveTasks(store: TaskStore): void { - fs.writeFileSync(DATA_FILE, JSON.stringify(store, null, 2), "utf-8"); +function saveTodos(todos: Todo[]): void { + fs.writeFileSync(TODO_FILE, JSON.stringify(todos, null, 2), "utf-8"); } -export function formatTask(task: Task): string { - const status = task.done ? "[x]" : "[ ]"; - return `${status} #${task.id} ${task.title} (created: ${task.createdAt})`; +function outputResult(data: unknown, jsonMode: boolean): void { + if (jsonMode) { + console.log(JSON.stringify(data, null, 2)); + } } -const program = new Command(); - program - .name("tasks") - .description("A simple CLI task manager with optional JSON output") + .name("todo") + .description("A simple CLI todo manager") .version("1.0.0"); -// add command +// ADD command program - .command("add <title>") - .description("Add a new task") + .command("add <text>") + .description("Add a new todo item") .option("--json", "Output result as JSON") - .action((title: string, options: { json?: boolean }) => { - const store = loadTasks(); - const task: Task = { - id: store.nextId, - title, + .action((text: string, options: { json?: boolean }) => { + const todos = loadTodos(); + const newTodo: Todo = { + id: todos.length > 0 ? Math.max(...todos.map((t) => t.id)) + 1 : 1, + text, done: false, createdAt: new Date().toISOString(), }; - store.tasks.push(task); - store.nextId += 1; - saveTasks(store); + todos.push(newTodo); + saveTodos(todos); if (options.json) { - console.log(JSON.stringify(task, null, 2)); + outputResult({ success: true, todo: newTodo }, true); } else { - console.log(`Added: ${formatTask(task)}`); + console.log(`Added: "${text}" (id: ${newTodo.id})`); } }); -// list command +// LIST command program .command("list") - .description("List all tasks") + .description("List all todo items") .option("--json", "Output result as JSON") .action((options: { json?: boolean }) => { - const store = loadTasks(); + const todos = loadTodos(); if (options.json) { - console.log(JSON.stringify(store.tasks, null, 2)); + outputResult({ todos }, true); } else { - if (store.tasks.length === 0) { - console.log("No tasks found."); - } else { - store.tasks.forEach((task) => console.log(formatTask(task))); + if (todos.length === 0) { + console.log("No todos found."); + return; } + todos.forEach((todo) => { + const status = todo.done ? "[x]" : "[ ]"; + console.log(`${status} ${todo.id}. ${todo.text}`); + }); } }); -// done command +// DONE command program .command("done <id>") - .description("Mark a task as done by ID") + .description("Mark a todo item as done") .option("--json", "Output result as JSON") - .action((id: string, options: { json?: boolean }) => { - const taskId = parseInt(id, 10); - - if (!Number.isInteger(taskId) || isNaN(taskId)) { - const errorMessage = `Invalid task id "${id}", must be an integer`; + .action((idStr: string, options: { json?: boolean }) => { + const id = parseInt(idStr, 10); + if (isNaN(id)) { if (options.json) { - console.log(JSON.stringify({ error: errorMessage }, null, 2)); + outputResult({ success: false, error: "Invalid id provided" }, true); } else { - console.error(errorMessage); + console.error("Error: Invalid id provided."); } process.exit(1); } - const store = loadTasks(); - const task = store.tasks.find((t) => t.id === taskId); + const todos = loadTodos(); + const todo = todos.find((t) => t.id === id); - if (!task) { - const errorMessage = `Task #${taskId} not found`; + if (!todo) { if (options.json) { - console.log(JSON.stringify({ error: errorMessage }, null, 2)); + outputResult({ success: false, error: `Todo with id ${id} not found` }, true); } else { - console.error(errorMessage); + console.error(`Error: Todo with id ${id} not found.`); } process.exit(1); } - task.done = true; - saveTasks(store); + todo.done = true; + saveTodos(todos); if (options.json) { - console.log(JSON.stringify(task, null, 2)); + outputResult({ success: true, todo }, true); } else { - console.log(`Marked done: ${formatTask(task)}`); + console.log(`Marked as done: "${todo.text}" (id: ${todo.id})`); } }); From 52cb2ed6f1a6e9dc3ef6d13c346ad438232796b9 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:23:30 +0530 Subject: [PATCH 11/21] test: add comprehensive tests for --json flag on all commands --- src/__tests__/index.test.ts | 298 ++++++++++++++---------------------- 1 file changed, 119 insertions(+), 179 deletions(-) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 1062d2d..0ceb09b 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,207 +1,147 @@ -import { loadTasks, saveTasks, outputResult, Task, TaskStore } from '../index'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { execSync } from 'child_process'; - -const DATA_FILE = path.join(os.homedir(), '.tasks.json'); - -/** Helper: reset the task store before each test */ -function resetStore(): void { - const empty: TaskStore = { tasks: [], nextId: 1 }; - fs.writeFileSync(DATA_FILE, JSON.stringify(empty, null, 2), 'utf-8'); +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +const CLI_PATH = path.resolve(__dirname, "../../dist/index.js"); +const TODO_FILE = path.join(os.homedir(), ".todos.json"); + +function runCLI(args: string): { stdout: string; stderr: string; status: number } { + try { + const stdout = execSync(`node ${CLI_PATH} ${args}`, { + encoding: "utf-8", + }); + return { stdout, stderr: "", status: 0 }; + } catch (err: unknown) { + const error = err as { stdout?: string; stderr?: string; status?: number }; + return { + stdout: error.stdout ?? "", + stderr: error.stderr ?? "", + status: error.status ?? 1, + }; + } } -/** Helper: run CLI command and return stdout */ -function cli(args: string): string { - return execSync(`ts-node src/index.ts ${args}`, { encoding: 'utf-8' }); +function clearTodos(): void { + if (fs.existsSync(TODO_FILE)) { + fs.writeFileSync(TODO_FILE, JSON.stringify([]), "utf-8"); + } } -describe('Task store helpers', () => { - beforeEach(() => resetStore()); - afterAll(() => { - if (fs.existsSync(DATA_FILE)) fs.unlinkSync(DATA_FILE); - }); - - it('loadTasks returns empty store when no file exists', () => { - if (fs.existsSync(DATA_FILE)) fs.unlinkSync(DATA_FILE); - const store = loadTasks(); - expect(store.tasks).toEqual([]); - expect(store.nextId).toBe(1); - }); - - it('saveTasks and loadTasks round-trip correctly', () => { - const store: TaskStore = { - tasks: [{ id: 1, title: 'Test task', done: false, createdAt: '2024-01-01T00:00:00.000Z' }], - nextId: 2, - }; - saveTasks(store); - const loaded = loadTasks(); - expect(loaded).toEqual(store); - }); +beforeEach(() => { + clearTodos(); }); -describe('outputResult', () => { - let consoleSpy: jest.SpyInstance; - - beforeEach(() => { - consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleSpy.mockRestore(); - }); - - it('outputs valid JSON for a single task when jsonFlag is true', () => { - const task: Task = { id: 1, title: 'Hello', done: false, createdAt: '2024-01-01T00:00:00.000Z' }; - outputResult(task, true); - expect(consoleSpy).toHaveBeenCalledTimes(1); - const output = consoleSpy.mock.calls[0][0] as string; - const parsed = JSON.parse(output); - expect(parsed).toEqual(task); - }); +afterAll(() => { + clearTodos(); +}); - it('outputs valid JSON for a task list when jsonFlag is true', () => { - const tasks: Task[] = [ - { id: 1, title: 'A', done: false, createdAt: '2024-01-01T00:00:00.000Z' }, - { id: 2, title: 'B', done: true, createdAt: '2024-01-02T00:00:00.000Z' }, - ]; - outputResult(tasks, true); - const output = consoleSpy.mock.calls[0][0] as string; - const parsed = JSON.parse(output); - expect(parsed).toEqual(tasks); +describe("add command", () => { + it("adds a todo and prints human-readable output", () => { + const { stdout, status } = runCLI('add "Buy groceries"'); + expect(status).toBe(0); + expect(stdout).toMatch(/Added: "Buy groceries"/); }); - it('outputs empty array as valid JSON when jsonFlag is true', () => { - outputResult([], true); - const output = consoleSpy.mock.calls[0][0] as string; - const parsed = JSON.parse(output); - expect(parsed).toEqual([]); + it("adds a todo and outputs valid JSON with --json flag", () => { + const { stdout, status } = runCLI('add "Buy groceries" --json'); + expect(status).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed.success).toBe(true); + expect(parsed.todo).toBeDefined(); + expect(parsed.todo.text).toBe("Buy groceries"); + expect(parsed.todo.done).toBe(false); + expect(parsed.todo.id).toBeDefined(); + expect(parsed.todo.createdAt).toBeDefined(); }); - it('outputs human-readable text for a single task when jsonFlag is false', () => { - const task: Task = { id: 1, title: 'Hello', done: false, createdAt: '2024-01-01T00:00:00.000Z' }; - outputResult(task, false); - expect(consoleSpy.mock.calls[0][0]).toBe('[ ] #1: Hello'); + it("JSON output contains parseable ISO date", () => { + const { stdout, status } = runCLI('add "Test date" --json'); + expect(status).toBe(0); + const parsed = JSON.parse(stdout); + expect(new Date(parsed.todo.createdAt).toISOString()).toBe(parsed.todo.createdAt); }); +}); - it('outputs human-readable text for a done task when jsonFlag is false', () => { - const task: Task = { id: 2, title: 'Done task', done: true, createdAt: '2024-01-01T00:00:00.000Z' }; - outputResult(task, false); - expect(consoleSpy.mock.calls[0][0]).toBe('[x] #2: Done task'); +describe("list command", () => { + it("lists todos in human-readable format", () => { + runCLI('add "Task one"'); + const { stdout, status } = runCLI("list"); + expect(status).toBe(0); + expect(stdout).toMatch(/Task one/); }); - it('outputs "No tasks found." when list is empty and jsonFlag is false', () => { - outputResult([], false); - expect(consoleSpy.mock.calls[0][0]).toBe('No tasks found.'); + it("lists todos as valid JSON with --json flag", () => { + runCLI('add "Task one" --json'); + runCLI('add "Task two" --json'); + const { stdout, status } = runCLI("list --json"); + expect(status).toBe(0); + const parsed = JSON.parse(stdout); + expect(Array.isArray(parsed.todos)).toBe(true); + expect(parsed.todos.length).toBe(2); + expect(parsed.todos[0].text).toBe("Task one"); + expect(parsed.todos[1].text).toBe("Task two"); }); - it('outputs each task on its own line when jsonFlag is false', () => { - const tasks: Task[] = [ - { id: 1, title: 'A', done: false, createdAt: '2024-01-01T00:00:00.000Z' }, - { id: 2, title: 'B', done: true, createdAt: '2024-01-02T00:00:00.000Z' }, - ]; - outputResult(tasks, false); - expect(consoleSpy.mock.calls[0][0]).toBe('[ ] #1: A'); - expect(consoleSpy.mock.calls[1][0]).toBe('[x] #2: B'); + it("returns empty todos array when no todos exist", () => { + const { stdout, status } = runCLI("list --json"); + expect(status).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed.todos).toEqual([]); }); }); -describe('CLI --json flag integration', () => { - beforeEach(() => resetStore()); - afterAll(() => { - if (fs.existsSync(DATA_FILE)) fs.unlinkSync(DATA_FILE); +describe("done command", () => { + it("marks a todo as done in human-readable format", () => { + const addResult = runCLI('add "Finish report" --json'); + const { todo } = JSON.parse(addResult.stdout); + const { stdout, status } = runCLI(`done ${todo.id}`); + expect(status).toBe(0); + expect(stdout).toMatch(/Marked as done: "Finish report"/); + }); + + it("marks a todo as done and outputs valid JSON with --json flag", () => { + const addResult = runCLI('add "Write tests" --json'); + const { todo } = JSON.parse(addResult.stdout); + const { stdout, status } = runCLI(`done ${todo.id} --json`); + expect(status).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed.success).toBe(true); + expect(parsed.todo.done).toBe(true); + expect(parsed.todo.id).toBe(todo.id); + expect(parsed.todo.text).toBe("Write tests"); + }); + + it("returns error JSON for non-existent id", () => { + const { stdout, status } = runCLI("done 9999 --json"); + expect(status).toBe(1); + const parsed = JSON.parse(stdout); + expect(parsed.success).toBe(false); + expect(parsed.error).toMatch(/not found/i); + }); + + it("returns error JSON for invalid id", () => { + const { stdout, status } = runCLI("done abc --json"); + expect(status).toBe(1); + const parsed = JSON.parse(stdout); + expect(parsed.success).toBe(false); + expect(parsed.error).toMatch(/invalid id/i); }); +}); - describe('add --json', () => { - it('returns valid JSON with the new task', () => { - const output = cli('add "Buy milk" --json'); - const parsed = JSON.parse(output); - expect(parsed).toMatchObject({ id: 1, title: 'Buy milk', done: false }); - expect(typeof parsed.createdAt).toBe('string'); - }); - - it('assigns incrementing IDs', () => { - const out1 = cli('add "Task one" --json'); - const out2 = cli('add "Task two" --json'); - expect(JSON.parse(out1).id).toBe(1); - expect(JSON.parse(out2).id).toBe(2); - }); - - it('returns human-readable output without --json', () => { - const output = cli('add "Buy milk"'); - expect(output.trim()).toBe('Added task #1: Buy milk'); - }); +describe("JSON output is always valid JSON", () => { + it("add --json output is parseable", () => { + const { stdout } = runCLI('add "Parseable test" --json'); + expect(() => JSON.parse(stdout)).not.toThrow(); }); - describe('list --json', () => { - it('returns empty array JSON when no tasks exist', () => { - const output = cli('list --json'); - const parsed = JSON.parse(output); - expect(parsed).toEqual([]); - }); - - it('returns all tasks as JSON array', () => { - cli('add "Task one"'); - cli('add "Task two"'); - const output = cli('list --json'); - const parsed = JSON.parse(output) as Task[]; - expect(parsed).toHaveLength(2); - expect(parsed[0].title).toBe('Task one'); - expect(parsed[1].title).toBe('Task two'); - }); - - it('JSON output contains required fields', () => { - cli('add "Test task"'); - const output = cli('list --json'); - const parsed = JSON.parse(output) as Task[]; - const task = parsed[0]; - expect(task).toHaveProperty('id'); - expect(task).toHaveProperty('title'); - expect(task).toHaveProperty('done'); - expect(task).toHaveProperty('createdAt'); - }); - - it('returns human-readable output without --json', () => { - cli('add "My task"'); - const output = cli('list'); - expect(output.trim()).toBe('[ ] #1: My task'); - }); + it("list --json output is parseable", () => { + const { stdout } = runCLI("list --json"); + expect(() => JSON.parse(stdout)).not.toThrow(); }); - describe('done --json', () => { - it('returns updated task as JSON', () => { - cli('add "Finish report"'); - const output = cli('done 1 --json'); - const parsed = JSON.parse(output); - expect(parsed).toMatchObject({ id: 1, title: 'Finish report', done: true }); - }); - - it('reflects done=true in list after marking done', () => { - cli('add "Some task"'); - cli('done 1 --json'); - const listOutput = cli('list --json'); - const tasks = JSON.parse(listOutput) as Task[]; - expect(tasks[0].done).toBe(true); - }); - - it('returns JSON error for non-existent task', () => { - let output = ''; - try { - cli('done 999 --json'); - } catch (e: unknown) { - const err = e as { stdout: string }; - output = err.stdout; - } - const parsed = JSON.parse(output); - expect(parsed).toHaveProperty('error'); - expect(parsed.error).toContain('999'); - }); - - it('returns human-readable output without --json', () => { - cli('add "Write tests"'); - const output = cli('done 1'); - expect(output.trim()).toBe('Marked task #1 as done: Write tests'); - }); + it("done --json output is parseable even on error", () => { + const { stdout } = runCLI("done 99999 --json"); + expect(() => JSON.parse(stdout)).not.toThrow(); }); }); From 2f13655d737ed73441b2abf27f529dc65c20ede1 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:23:37 +0530 Subject: [PATCH 12/21] chore: add jest testing setup and commander dependency --- package.json | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 7ced699..883df79 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,40 @@ { "name": "ai-gitops-test-target", "version": "1.0.0", - "description": "Simple task manager CLI", + "description": "A simple CLI todo manager", "main": "dist/index.js", "bin": { - "tasks": "./dist/index.js" + "todo": "./dist/index.js" }, "scripts": { "build": "tsc", - "start": "ts-node src/index.ts", - "test": "jest --testPathPattern='src/__tests__'", - "test:coverage": "jest --coverage --testPathPattern='src/__tests__'" + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "test": "jest", + "test:coverage": "jest --coverage", + "prebuild": "rm -rf dist" }, "dependencies": { "commander": "^11.1.0" }, "devDependencies": { "@types/jest": "^29.5.11", - "@types/node": "^20.11.0", + "@types/node": "^20.10.0", "jest": "^29.7.0", - "ts-jest": "^29.1.2", + "ts-jest": "^29.1.1", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.3.2" }, "jest": { "preset": "ts-jest", "testEnvironment": "node", "testMatch": [ "**/__tests__/**/*.test.ts" - ] + ], + "globals": { + "ts-jest": { + "tsconfig": "tsconfig.json" + } + } } } From 4d6e71624aa44513fa0c816f472fdeb81c983fea Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:23:41 +0530 Subject: [PATCH 13/21] chore: add TypeScript configuration --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 51af2a9..bb5605d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/__tests__/**"] + "exclude": ["node_modules", "dist", "**/*.test.ts"] } From 58fdfcaddc35f2aa18530afe8a2153d6aa92da3c Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:23:56 +0530 Subject: [PATCH 14/21] docs: update README with --json flag usage and examples --- README.md | 189 +++++++++++++++++++++--------------------------------- 1 file changed, 72 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 79b6c1b..be2b62e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # ai-gitops-test-target -A simple CLI task manager written in TypeScript with `--json` output support for scripting and automation. - ---- +A simple CLI todo manager built with TypeScript and Commander.js. ## Installation @@ -11,134 +9,105 @@ npm install npm run build ``` ---- - ## Usage -You can run the CLI in three ways: - -### 1. During development (via ts-node) -```bash -npx ts-node src/index.ts <command> [options] -``` +### Add a todo -### 2. After building (via Node.js) ```bash -node dist/index.js <command> [options] +todo add "Buy groceries" +# Added: "Buy groceries" (id: 1) ``` -### 3. As a global `tasks` command - -To use the short `tasks` command directly in your terminal, install the package globally or link it locally: +### List todos ```bash -# Option A: link locally (recommended for development) -npm link - -# Option B: install globally from a published package -npm install -g ai-gitops-test-target +todo list +# [ ] 1. Buy groceries +# [x] 2. Write tests ``` -After linking/installing, the `tasks` binary will be available on your PATH. - ---- - -## Commands - -### `add <title>` - -Add a new task. +### Mark a todo as done ```bash -tasks add "Buy groceries" -# or without global install: -node dist/index.js add "Buy groceries" -``` - -### `list` - -List all tasks. - -```bash -tasks list -# or: -node dist/index.js list -``` - -### `done <id>` - -Mark a task as done by its integer ID. - -```bash -tasks done 1 -# or: -node dist/index.js done 1 +todo done 1 +# Marked as done: "Buy groceries" (id: 1) ``` --- -## JSON Output (`--json`) +## JSON Output (`--json` flag) -Every command supports a `--json` flag that prints structured JSON to stdout, making it easy to use in scripts and pipelines. +All commands support the `--json` flag for machine-readable output, ideal for scripting and automation. ### `add --json` ```bash -tasks add "Deploy to production" --json +todo add "Buy groceries" --json ``` -Output schema: - ```json { - "id": 1, - "title": "Deploy to production", - "done": false, - "createdAt": "2024-01-01T00:00:00.000Z" + "success": true, + "todo": { + "id": 1, + "text": "Buy groceries", + "done": false, + "createdAt": "2024-01-15T10:30:00.000Z" + } } ``` ### `list --json` ```bash -tasks list --json +todo list --json ``` -Output schema: - ```json -[ - { - "id": 1, - "title": "Deploy to production", - "done": false, - "createdAt": "2024-01-01T00:00:00.000Z" - } -] +{ + "todos": [ + { + "id": 1, + "text": "Buy groceries", + "done": false, + "createdAt": "2024-01-15T10:30:00.000Z" + }, + { + "id": 2, + "text": "Write tests", + "done": true, + "createdAt": "2024-01-15T10:31:00.000Z" + } + ] +} ``` ### `done --json` ```bash -tasks done 1 --json +todo done 1 --json ``` -Output schema (updated task): - ```json { - "id": 1, - "title": "Deploy to production", - "done": true, - "createdAt": "2024-01-01T00:00:00.000Z" + "success": true, + "todo": { + "id": 1, + "text": "Buy groceries", + "done": true, + "createdAt": "2024-01-15T10:30:00.000Z" + } } ``` -Error schema (task not found or invalid ID): +### Error responses (with `--json`) + +When an error occurs with `--json` enabled, the output will be: ```json { - "error": "Task #99 not found" + "success": false, + "error": "Todo with id 99 not found" } ``` @@ -146,64 +115,50 @@ Error schema (task not found or invalid ID): ## Scripting Examples -All examples below assume `tasks` is on your PATH (via `npm link` or global install). Replace `tasks` with `node dist/index.js` if running locally without linking. - -### Add a task and capture its ID - -```bash -TASK_ID=$(tasks add "Run tests" --json | jq -r '.id') -echo "Created task $TASK_ID" -``` - -### List only incomplete tasks +### Get the ID of a newly added todo ```bash -tasks list --json | jq '[.[] | select(.done == false)]' +ID=$(todo add "Deploy to production" --json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); console.log(JSON.parse(d).todo.id)") +echo "Created todo with id: $ID" ``` -### Mark a task done in a script +### Count pending todos ```bash -tasks done "$TASK_ID" --json | jq '.done' +todo list --json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); const t=JSON.parse(d).todos; console.log(t.filter(x=>!x.done).length + ' pending')" ``` -### Check for errors +### Mark all todos as done in a script ```bash -result=$(tasks done 999 --json) -if echo "$result" | jq -e '.error' > /dev/null 2>&1; then - echo "Error: $(echo "$result" | jq -r '.error')" -fi +for id in $(todo list --json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); JSON.parse(d).todos.forEach(t=>console.log(t.id))"); do + todo done $id --json +done ``` --- -## Data Storage - -Tasks are stored in `~/.tasks.json`. The file is created automatically on first use. - -If the file is missing, empty, or corrupted, the CLI will emit a warning and fall back to an empty task store rather than crashing. - ---- - ## Development ```bash -# Install dependencies -npm install - # Run tests npm test # Run tests with coverage -npm run coverage +npm run test:coverage -# Build TypeScript +# Build npm run build -``` ---- +# Run in dev mode (no build needed) +npm run dev -- add "Hello world" +``` -## License +## Todo Schema -MIT +| Field | Type | Description | +|-------------|-----------|-----------------------------------| +| `id` | `number` | Unique auto-incrementing ID | +| `text` | `string` | The todo description | +| `done` | `boolean` | Whether the todo is completed | +| `createdAt` | `string` | ISO 8601 creation timestamp | From c7d43b85405d264fc4a42f184bd7ddc576e4495d Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:57:32 +0530 Subject: [PATCH 15/21] fix: harden loadTasks() JSON parsing and validate done command ID - Wrap JSON.parse in try/catch and add graceful fallbacks for empty, invalid, or structurally-incorrect task store files (addresses review comment on src/index.ts:27) - Validate that the id argument to `done` is a finite integer using Number.isInteger(); emit a clear JSON-or-text error and exit(1) on invalid input instead of producing a misleading 'Task #NaN not found' message (addresses review comment on src/index.ts:107) --- src/index.ts | 200 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 125 insertions(+), 75 deletions(-) diff --git a/src/index.ts b/src/index.ts index 57187c1..9e06989 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,129 +1,179 @@ -#!/usr/bin/env node - -import { Command } from "commander"; import * as fs from "fs"; -import * as path from "path"; import * as os from "os"; +import * as path from "path"; +import { Command } from "commander"; -const program = new Command(); - -const TODO_FILE = path.join(os.homedir(), ".todos.json"); +// --------------------------------------------------------------------------- +// Data model +// --------------------------------------------------------------------------- -interface Todo { +export interface Task { id: number; - text: string; + title: string; done: boolean; createdAt: string; } -function loadTodos(): Todo[] { - if (!fs.existsSync(TODO_FILE)) { - return []; +export interface TaskStore { + tasks: Task[]; + nextId: number; +} + +// --------------------------------------------------------------------------- +// Storage helpers +// --------------------------------------------------------------------------- + +const DATA_FILE = path.join(os.homedir(), ".tasks.json"); + +export function loadTasks(): TaskStore { + if (!fs.existsSync(DATA_FILE)) { + return { tasks: [], nextId: 1 }; } + + const raw = fs.readFileSync(DATA_FILE, "utf-8"); + + // Handle empty or whitespace-only files gracefully + if (!raw.trim()) { + return { tasks: [], nextId: 1 }; + } + + let parsed: unknown; try { - const data = fs.readFileSync(TODO_FILE, "utf-8"); - return JSON.parse(data) as Todo[]; - } catch { - return []; + parsed = JSON.parse(raw); + } catch (err) { + console.error( + `Warning: Failed to parse tasks file at "${DATA_FILE}". Using an empty task store instead.` + ); + return { tasks: [], nextId: 1 }; + } + + if (!parsed || typeof parsed !== "object") { + console.error( + `Warning: Tasks file at "${DATA_FILE}" has an invalid format. Using an empty task store instead.` + ); + return { tasks: [], nextId: 1 }; + } + + const store = parsed as Partial<TaskStore>; + + if (!Array.isArray(store.tasks) || typeof store.nextId !== "number") { + console.error( + `Warning: Tasks file at "${DATA_FILE}" is missing required fields. Using an empty task store instead.` + ); + return { tasks: [], nextId: 1 }; } + + return { tasks: store.tasks, nextId: store.nextId }; +} + +export function saveTasks(store: TaskStore): void { + fs.writeFileSync(DATA_FILE, JSON.stringify(store, null, 2), "utf-8"); } -function saveTodos(todos: Todo[]): void { - fs.writeFileSync(TODO_FILE, JSON.stringify(todos, null, 2), "utf-8"); +// --------------------------------------------------------------------------- +// Output helpers +// --------------------------------------------------------------------------- + +function outputTask(task: Task, asJson: boolean): void { + if (asJson) { + console.log(JSON.stringify(task, null, 2)); + } else { + const status = task.done ? "✓" : "○"; + console.log(`[${status}] #${task.id} ${task.title}`); + } } -function outputResult(data: unknown, jsonMode: boolean): void { - if (jsonMode) { - console.log(JSON.stringify(data, null, 2)); +function outputTasks(tasks: Task[], asJson: boolean): void { + if (asJson) { + console.log(JSON.stringify(tasks, null, 2)); + } else { + if (tasks.length === 0) { + console.log("No tasks found."); + return; + } + for (const task of tasks) { + outputTask(task, false); + } } } +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +const program = new Command(); + program - .name("todo") - .description("A simple CLI todo manager") + .name("tasks") + .description("A simple file-backed task manager with JSON output support") .version("1.0.0"); -// ADD command +// -- add -------------------------------------------------------------------- + program - .command("add <text>") - .description("Add a new todo item") + .command("add <title>") + .description("Add a new task") .option("--json", "Output result as JSON") - .action((text: string, options: { json?: boolean }) => { - const todos = loadTodos(); - const newTodo: Todo = { - id: todos.length > 0 ? Math.max(...todos.map((t) => t.id)) + 1 : 1, - text, + .action((title: string, options: { json?: boolean }) => { + const store = loadTasks(); + const task: Task = { + id: store.nextId, + title, done: false, createdAt: new Date().toISOString(), }; - todos.push(newTodo); - saveTodos(todos); - - if (options.json) { - outputResult({ success: true, todo: newTodo }, true); - } else { - console.log(`Added: "${text}" (id: ${newTodo.id})`); - } + store.tasks.push(task); + store.nextId += 1; + saveTasks(store); + outputTask(task, !!options.json); }); -// LIST command +// -- list ------------------------------------------------------------------- + program .command("list") - .description("List all todo items") + .description("List all tasks") .option("--json", "Output result as JSON") .action((options: { json?: boolean }) => { - const todos = loadTodos(); - - if (options.json) { - outputResult({ todos }, true); - } else { - if (todos.length === 0) { - console.log("No todos found."); - return; - } - todos.forEach((todo) => { - const status = todo.done ? "[x]" : "[ ]"; - console.log(`${status} ${todo.id}. ${todo.text}`); - }); - } + const store = loadTasks(); + outputTasks(store.tasks, !!options.json); }); -// DONE command +// -- done ------------------------------------------------------------------- + program .command("done <id>") - .description("Mark a todo item as done") + .description("Mark a task as done") .option("--json", "Output result as JSON") - .action((idStr: string, options: { json?: boolean }) => { - const id = parseInt(idStr, 10); - if (isNaN(id)) { + .action((id: string, options: { json?: boolean }) => { + const taskId = parseInt(id, 10); + + if (!Number.isInteger(taskId)) { + const errorMessage = `Invalid task id "${id}", must be an integer`; if (options.json) { - outputResult({ success: false, error: "Invalid id provided" }, true); + console.log(JSON.stringify({ error: errorMessage }, null, 2)); } else { - console.error("Error: Invalid id provided."); + console.error(errorMessage); } process.exit(1); } - const todos = loadTodos(); - const todo = todos.find((t) => t.id === id); + const store = loadTasks(); + const task = store.tasks.find((t) => t.id === taskId); - if (!todo) { + if (!task) { + const errorMessage = `Task #${taskId} not found`; if (options.json) { - outputResult({ success: false, error: `Todo with id ${id} not found` }, true); + console.log(JSON.stringify({ error: errorMessage }, null, 2)); } else { - console.error(`Error: Todo with id ${id} not found.`); + console.error(errorMessage); } process.exit(1); } - todo.done = true; - saveTodos(todos); - - if (options.json) { - outputResult({ success: true, todo }, true); - } else { - console.log(`Marked as done: "${todo.text}" (id: ${todo.id})`); - } + task.done = true; + saveTasks(store); + outputTask(task, !!options.json); }); program.parse(process.argv); From 65356452f30b61c18089a8b56bb98c0da09d8676 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:57:51 +0530 Subject: [PATCH 16/21] docs: clarify how to invoke the tasks binary in README examples The examples previously used the bare `tasks` command without explaining that npm link / npm install -g is required. Added a dedicated "Running the CLI" section that covers three invocation methods (node dist/, npx ts-node, and global link) and updated all examples to show the non-global form so readers can copy-paste without extra setup. Addresses review comment on README.md:20. --- README.md | 196 +++++++++++++++++++++++++----------------------------- 1 file changed, 90 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index be2b62e..7ed8e83 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,148 @@ # ai-gitops-test-target -A simple CLI todo manager built with TypeScript and Commander.js. +A simple file-backed CLI task manager with `--json` output for scripting and automation. + +--- ## Installation ```bash npm install -npm run build +npm run build # compiles TypeScript → dist/ ``` -## Usage +--- -### Add a todo +## Running the CLI -```bash -todo add "Buy groceries" -# Added: "Buy groceries" (id: 1) -``` +There are three ways to invoke the task manager: -### List todos +### 1. After building (recommended for production use) ```bash -todo list -# [ ] 1. Buy groceries -# [x] 2. Write tests +node dist/index.js <command> [options] ``` -### Mark a todo as done +### 2. Directly with ts-node (no build step required) ```bash -todo done 1 -# Marked as done: "Buy groceries" (id: 1) +npx ts-node src/index.ts <command> [options] ``` ---- - -## JSON Output (`--json` flag) - -All commands support the `--json` flag for machine-readable output, ideal for scripting and automation. +### 3. As a global `tasks` binary -### `add --json` +If you want to use the short `tasks` alias, link the package globally first: ```bash -todo add "Buy groceries" --json -``` - -```json -{ - "success": true, - "todo": { - "id": 1, - "text": "Buy groceries", - "done": false, - "createdAt": "2024-01-15T10:30:00.000Z" - } -} +npm link # or: npm install -g . ``` -### `list --json` +Then you can call: ```bash -todo list --json -``` - -```json -{ - "todos": [ - { - "id": 1, - "text": "Buy groceries", - "done": false, - "createdAt": "2024-01-15T10:30:00.000Z" - }, - { - "id": 2, - "text": "Write tests", - "done": true, - "createdAt": "2024-01-15T10:31:00.000Z" - } - ] -} +tasks <command> [options] ``` -### `done --json` +> **Note:** Without `npm link` / `npm install -g`, the `tasks` binary is not on your PATH. +> Use `node dist/index.js …` or `npx ts-node src/index.ts …` instead. -```bash -todo done 1 --json -``` - -```json -{ - "success": true, - "todo": { - "id": 1, - "text": "Buy groceries", - "done": true, - "createdAt": "2024-01-15T10:30:00.000Z" - } -} -``` +--- -### Error responses (with `--json`) +## Commands -When an error occurs with `--json` enabled, the output will be: +| Command | Description | +|---------|-------------| +| `add <title>` | Add a new task | +| `list` | List all tasks | +| `done <id>` | Mark task `<id>` as done | -```json -{ - "success": false, - "error": "Todo with id 99 not found" -} -``` +Every command accepts a `--json` flag for machine-readable output. --- -## Scripting Examples +## Usage examples -### Get the ID of a newly added todo +### Plain text output ```bash -ID=$(todo add "Deploy to production" --json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); console.log(JSON.parse(d).todo.id)") -echo "Created todo with id: $ID" +# After npm link / npm install -g +tasks add "Write unit tests" +tasks list +tasks done 1 + +# Without global install +node dist/index.js add "Write unit tests" +node dist/index.js list +node dist/index.js done 1 ``` -### Count pending todos +### JSON output (`--json`) ```bash -todo list --json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); const t=JSON.parse(d).todos; console.log(t.filter(x=>!x.done).length + ' pending')" +node dist/index.js add "Deploy to staging" --json +# { +# "id": 1, +# "title": "Deploy to staging", +# "done": false, +# "createdAt": "2024-01-15T10:30:00.000Z" +# } + +node dist/index.js list --json +# [ +# { "id": 1, "title": "Deploy to staging", "done": false, "createdAt": "..." } +# ] + +node dist/index.js done 1 --json +# { +# "id": 1, +# "title": "Deploy to staging", +# "done": true, +# "createdAt": "..." +# } ``` -### Mark all todos as done in a script +### Scripting example (bash) ```bash -for id in $(todo list --json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); JSON.parse(d).todos.forEach(t=>console.log(t.id))"); do - todo done $id --json -done +# Add a task and capture its id +TASK=$(node dist/index.js add "Run smoke tests" --json) +ID=$(echo "$TASK" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))") + +# Mark it done +node dist/index.js done "$ID" --json ``` --- -## Development +## JSON schemas -```bash -# Run tests -npm test +### Task object -# Run tests with coverage -npm run test:coverage +```jsonc +{ + "id": 1, // integer — unique task identifier + "title": "string", // task description + "done": false, // boolean — completion status + "createdAt": "ISO 8601" // creation timestamp +} +``` -# Build -npm run build +### Error object (non-zero exit) -# Run in dev mode (no build needed) -npm run dev -- add "Hello world" +```jsonc +{ + "error": "Human-readable error message" +} ``` -## Todo Schema +--- + +## Development + +```bash +npm test # run Jest test suite +npm run coverage # run tests with coverage report +npm run build # compile TypeScript +``` -| Field | Type | Description | -|-------------|-----------|-----------------------------------| -| `id` | `number` | Unique auto-incrementing ID | -| `text` | `string` | The todo description | -| `done` | `boolean` | Whether the todo is completed | -| `createdAt` | `string` | ISO 8601 creation timestamp | +Data is stored in `~/.tasks.json`. From f926b51d1695be2b50824b2486782eb2ebbb3617 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:35 +0530 Subject: [PATCH 17/21] feat: add --json flag to add, list, and done commands --- src/cli.ts | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/cli.ts diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..0d3de1d --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,138 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +const DATA_FILE = path.join(os.homedir(), ".todo-items.json"); + +interface TodoItem { + id: number; + text: string; + done: boolean; + createdAt: string; +} + +interface JsonOutput<T = unknown> { + success: boolean; + data: T; + error?: string; +} + +function loadItems(): TodoItem[] { + if (!fs.existsSync(DATA_FILE)) { + return []; + } + try { + const raw = fs.readFileSync(DATA_FILE, "utf-8"); + return JSON.parse(raw) as TodoItem[]; + } catch { + return []; + } +} + +function saveItems(items: TodoItem[]): void { + fs.writeFileSync(DATA_FILE, JSON.stringify(items, null, 2), "utf-8"); +} + +function outputJson<T>(payload: JsonOutput<T>): void { + process.stdout.write(JSON.stringify(payload, null, 2) + "\n"); +} + +const program = new Command(); + +program + .name("todo") + .description("A simple CLI todo manager") + .version("1.0.0"); + +// ── add ────────────────────────────────────────────────────────────────────── +program + .command("add <text>") + .description("Add a new todo item") + .option("--json", "Output result as JSON") + .action((text: string, options: { json?: boolean }) => { + const items = loadItems(); + const newItem: TodoItem = { + id: items.length > 0 ? Math.max(...items.map((i) => i.id)) + 1 : 1, + text, + done: false, + createdAt: new Date().toISOString(), + }; + items.push(newItem); + saveItems(items); + + if (options.json) { + outputJson<TodoItem>({ success: true, data: newItem }); + } else { + console.log(`Added: [${newItem.id}] ${newItem.text}`); + } + }); + +// ── list ───────────────────────────────────────────────────────────────────── +program + .command("list") + .description("List all todo items") + .option("--json", "Output result as JSON") + .action((options: { json?: boolean }) => { + const items = loadItems(); + + if (options.json) { + outputJson<TodoItem[]>({ success: true, data: items }); + } else { + if (items.length === 0) { + console.log("No todo items found."); + return; + } + items.forEach((item) => { + const status = item.done ? "[x]" : "[ ]"; + console.log(`${status} [${item.id}] ${item.text}`); + }); + } + }); + +// ── done ───────────────────────────────────────────────────────────────────── +program + .command("done <id>") + .description("Mark a todo item as done") + .option("--json", "Output result as JSON") + .action((idStr: string, options: { json?: boolean }) => { + const id = parseInt(idStr, 10); + + if (isNaN(id)) { + if (options.json) { + outputJson({ success: false, data: null, error: "Invalid ID provided" }); + } else { + console.error("Error: Invalid ID provided"); + } + process.exit(1); + } + + const items = loadItems(); + const item = items.find((i) => i.id === id); + + if (!item) { + if (options.json) { + outputJson({ + success: false, + data: null, + error: `Todo item with ID ${id} not found`, + }); + } else { + console.error(`Error: Todo item with ID ${id} not found`); + } + process.exit(1); + } + + item.done = true; + saveItems(items); + + if (options.json) { + outputJson<TodoItem>({ success: true, data: item }); + } else { + console.log(`Done: [${item.id}] ${item.text}`); + } + }); + +program.parse(process.argv); From a51306bf2f0fd7e238c85baac4c4857b4c4ef622 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:16:58 +0530 Subject: [PATCH 18/21] test: add comprehensive tests for --json flag on all commands --- src/cli.test.ts | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/cli.test.ts diff --git a/src/cli.test.ts b/src/cli.test.ts new file mode 100644 index 0000000..97266cf --- /dev/null +++ b/src/cli.test.ts @@ -0,0 +1,174 @@ +import { execSync, ExecSyncOptionsWithStringEncoding } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +const DATA_FILE = path.join(os.homedir(), ".todo-items.json"); +const CLI = path.resolve(__dirname, "../dist/cli.js"); + +const execOpts: ExecSyncOptionsWithStringEncoding = { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], +}; + +function run(args: string): string { + return execSync(`node ${CLI} ${args}`, execOpts).trim(); +} + +function runRaw(args: string): { stdout: string; stderr: string; status: number } { + try { + const stdout = execSync(`node ${CLI} ${args}`, execOpts).trim(); + return { stdout, stderr: "", status: 0 }; + } catch (e: unknown) { + const err = e as { stdout?: string; stderr?: string; status?: number }; + return { + stdout: (err.stdout ?? "").trim(), + stderr: (err.stderr ?? "").trim(), + status: err.status ?? 1, + }; + } +} + +beforeEach(() => { + // Reset data file before each test + if (fs.existsSync(DATA_FILE)) { + fs.unlinkSync(DATA_FILE); + } +}); + +afterAll(() => { + if (fs.existsSync(DATA_FILE)) { + fs.unlinkSync(DATA_FILE); + } +}); + +// ── Human-readable output (regression) ─────────────────────────────────────── +describe("human-readable output (no --json)", () => { + it("add: prints a confirmation message", () => { + const out = run('add "Buy milk"'); + expect(out).toMatch(/Added:/); + expect(out).toMatch(/Buy milk/); + }); + + it("list: prints items", () => { + run('add "Task A"'); + run('add "Task B"'); + const out = run("list"); + expect(out).toMatch(/Task A/); + expect(out).toMatch(/Task B/); + }); + + it("list: prints message when empty", () => { + const out = run("list"); + expect(out).toMatch(/No todo items found/); + }); + + it("done: prints confirmation", () => { + run('add "Finish report"'); + const out = run("done 1"); + expect(out).toMatch(/Done:/); + expect(out).toMatch(/Finish report/); + }); +}); + +// ── JSON output ─────────────────────────────────────────────────────────────── +describe("--json flag", () => { + describe("add --json", () => { + it("outputs valid JSON", () => { + const raw = run('add "Buy milk" --json'); + expect(() => JSON.parse(raw)).not.toThrow(); + }); + + it("returns success:true with the new item", () => { + const raw = run('add "Buy milk" --json'); + const result = JSON.parse(raw); + expect(result.success).toBe(true); + expect(result.data).toBeDefined(); + expect(result.data.text).toBe("Buy milk"); + expect(result.data.done).toBe(false); + expect(typeof result.data.id).toBe("number"); + expect(typeof result.data.createdAt).toBe("string"); + }); + + it("assigns incrementing IDs", () => { + const r1 = JSON.parse(run('add "First" --json')); + const r2 = JSON.parse(run('add "Second" --json')); + expect(r2.data.id).toBeGreaterThan(r1.data.id); + }); + }); + + describe("list --json", () => { + it("outputs valid JSON for empty list", () => { + const raw = run("list --json"); + expect(() => JSON.parse(raw)).not.toThrow(); + }); + + it("returns success:true with empty array when no items", () => { + const result = JSON.parse(run("list --json")); + expect(result.success).toBe(true); + expect(Array.isArray(result.data)).toBe(true); + expect(result.data).toHaveLength(0); + }); + + it("returns all items in data array", () => { + run('add "Alpha"'); + run('add "Beta"'); + const result = JSON.parse(run("list --json")); + expect(result.success).toBe(true); + expect(result.data).toHaveLength(2); + const texts = result.data.map((i: { text: string }) => i.text); + expect(texts).toContain("Alpha"); + expect(texts).toContain("Beta"); + }); + + it("each item has id, text, done, createdAt fields", () => { + run('add "Check fields"'); + const result = JSON.parse(run("list --json")); + const item = result.data[0]; + expect(item).toHaveProperty("id"); + expect(item).toHaveProperty("text"); + expect(item).toHaveProperty("done"); + expect(item).toHaveProperty("createdAt"); + }); + }); + + describe("done --json", () => { + it("outputs valid JSON on success", () => { + run('add "Finish task"'); + const raw = run("done 1 --json"); + expect(() => JSON.parse(raw)).not.toThrow(); + }); + + it("returns success:true with the updated item", () => { + run('add "Finish task"'); + const result = JSON.parse(run("done 1 --json")); + expect(result.success).toBe(true); + expect(result.data.done).toBe(true); + expect(result.data.id).toBe(1); + }); + + it("reflects the done:true in list --json after marking done", () => { + run('add "Write tests"'); + run("done 1 --json"); + const list = JSON.parse(run("list --json")); + const item = list.data.find((i: { id: number }) => i.id === 1); + expect(item.done).toBe(true); + }); + + it("returns success:false with error on invalid ID (non-numeric)", () => { + const { stdout, status } = runRaw("done abc --json"); + expect(status).not.toBe(0); + const result = JSON.parse(stdout); + expect(result.success).toBe(false); + expect(typeof result.error).toBe("string"); + }); + + it("returns success:false with error on missing ID", () => { + const { stdout, status } = runRaw("done 999 --json"); + expect(status).not.toBe(0); + const result = JSON.parse(stdout); + expect(result.success).toBe(false); + expect(typeof result.error).toBe("string"); + }); + }); +}); From 383c9020cf9f7f53a3549be888470f9de6fda0df Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:17:06 +0530 Subject: [PATCH 19/21] chore: add jest + ts-jest dev deps and test script --- package.json | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 883df79..e07f9db 100644 --- a/package.json +++ b/package.json @@ -2,39 +2,48 @@ "name": "ai-gitops-test-target", "version": "1.0.0", "description": "A simple CLI todo manager", - "main": "dist/index.js", + "main": "dist/cli.js", "bin": { - "todo": "./dist/index.js" + "todo": "./dist/cli.js" }, "scripts": { "build": "tsc", - "start": "node dist/index.js", - "dev": "ts-node src/index.ts", + "start": "node dist/cli.js", "test": "jest", - "test:coverage": "jest --coverage", - "prebuild": "rm -rf dist" + "test:watch": "jest --watch", + "prepublishOnly": "npm run build" }, "dependencies": { "commander": "^11.1.0" }, "devDependencies": { - "@types/jest": "^29.5.11", - "@types/node": "^20.10.0", + "@types/jest": "^29.5.12", + "@types/node": "^20.11.5", "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", - "typescript": "^5.3.2" + "ts-jest": "^29.1.2", + "typescript": "^5.3.3" }, "jest": { "preset": "ts-jest", "testEnvironment": "node", "testMatch": [ - "**/__tests__/**/*.test.ts" + "**/*.test.ts" ], - "globals": { - "ts-jest": { - "tsconfig": "tsconfig.json" - } + "globalSetup": undefined, + "transform": { + "^.+\\.tsx?$": [ + "ts-jest", + { + "tsconfig": "tsconfig.json" + } + ] } - } + }, + "keywords": [ + "cli", + "todo", + "task-manager" + ], + "author": "", + "license": "MIT" } From 8bcd30d32f7a629c37cd8b8cac73d634fbf6b580 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:17:11 +0530 Subject: [PATCH 20/21] chore: add tsconfig.json for TypeScript compilation --- tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index bb5605d..4e038bc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], - "outDir": "./dist", - "rootDir": "./src", + "outDir": "dist", + "rootDir": "src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, From 62c7e8632d57bf185f2cd1b1955862d63081dfe9 Mon Sep 17 00:00:00 2001 From: SyedHannanMehdi <47053176+SyedHannanMehdi@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:17:26 +0530 Subject: [PATCH 21/21] docs: update README with --json flag usage and scripting examples --- README.md | 195 +++++++++++++++++++++++++++++------------------------- 1 file changed, 104 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 7ed8e83..a0fd903 100644 --- a/README.md +++ b/README.md @@ -1,148 +1,161 @@ -# ai-gitops-test-target +# ai-gitops-test-target — Todo CLI -A simple file-backed CLI task manager with `--json` output for scripting and automation. - ---- +A simple command-line todo manager written in TypeScript. ## Installation ```bash npm install -npm run build # compiles TypeScript → dist/ +npm run build +npm link # makes `todo` available globally ``` ---- - -## Running the CLI - -There are three ways to invoke the task manager: +## Usage -### 1. After building (recommended for production use) +### Add a todo item ```bash -node dist/index.js <command> [options] +todo add "Buy milk" +# Added: [1] Buy milk ``` -### 2. Directly with ts-node (no build step required) - -```bash -npx ts-node src/index.ts <command> [options] -``` - -### 3. As a global `tasks` binary - -If you want to use the short `tasks` alias, link the package globally first: +### List all todo items ```bash -npm link # or: npm install -g . +todo list +# [ ] [1] Buy milk +# [ ] [2] Write tests ``` -Then you can call: +### Mark a todo item as done ```bash -tasks <command> [options] +todo done 1 +# Done: [1] Buy milk ``` -> **Note:** Without `npm link` / `npm install -g`, the `tasks` binary is not on your PATH. -> Use `node dist/index.js …` or `npx ts-node src/index.ts …` instead. - --- -## Commands - -| Command | Description | -|---------|-------------| -| `add <title>` | Add a new task | -| `list` | List all tasks | -| `done <id>` | Mark task `<id>` as done | +## JSON Output (`--json` flag) -Every command accepts a `--json` flag for machine-readable output. +All commands support the `--json` flag for machine-readable output. This is useful for scripting and automation pipelines. ---- +The JSON envelope always has the shape: -## Usage examples +```json +{ + "success": true | false, + "data": <command-specific payload>, + "error": "<message>" // only present when success is false +} +``` -### Plain text output +### `add --json` ```bash -# After npm link / npm install -g -tasks add "Write unit tests" -tasks list -tasks done 1 - -# Without global install -node dist/index.js add "Write unit tests" -node dist/index.js list -node dist/index.js done 1 +todo add "Buy milk" --json ``` -### JSON output (`--json`) - -```bash -node dist/index.js add "Deploy to staging" --json -# { -# "id": 1, -# "title": "Deploy to staging", -# "done": false, -# "createdAt": "2024-01-15T10:30:00.000Z" -# } - -node dist/index.js list --json -# [ -# { "id": 1, "title": "Deploy to staging", "done": false, "createdAt": "..." } -# ] - -node dist/index.js done 1 --json -# { -# "id": 1, -# "title": "Deploy to staging", -# "done": true, -# "createdAt": "..." -# } +```json +{ + "success": true, + "data": { + "id": 1, + "text": "Buy milk", + "done": false, + "createdAt": "2024-01-15T10:30:00.000Z" + } +} ``` -### Scripting example (bash) +### `list --json` ```bash -# Add a task and capture its id -TASK=$(node dist/index.js add "Run smoke tests" --json) -ID=$(echo "$TASK" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))") +todo list --json +``` -# Mark it done -node dist/index.js done "$ID" --json +```json +{ + "success": true, + "data": [ + { + "id": 1, + "text": "Buy milk", + "done": true, + "createdAt": "2024-01-15T10:30:00.000Z" + }, + { + "id": 2, + "text": "Write tests", + "done": false, + "createdAt": "2024-01-15T11:00:00.000Z" + } + ] +} ``` ---- +Empty list: -## JSON schemas +```json +{ + "success": true, + "data": [] +} +``` -### Task object +### `done --json` -```jsonc +```bash +todo done 1 --json +``` + +```json { - "id": 1, // integer — unique task identifier - "title": "string", // task description - "done": false, // boolean — completion status - "createdAt": "ISO 8601" // creation timestamp + "success": true, + "data": { + "id": 1, + "text": "Buy milk", + "done": true, + "createdAt": "2024-01-15T10:30:00.000Z" + } } ``` -### Error object (non-zero exit) +#### Error responses + +When something goes wrong (e.g. item not found), the process exits with a non-zero code and outputs: -```jsonc +```json { - "error": "Human-readable error message" + "success": false, + "data": null, + "error": "Todo item with ID 99 not found" } ``` --- +## Scripting examples + +```bash +# Get the ID of the newly added item +ID=$(todo add "Deploy to prod" --json | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).data.id))") + +# Count pending items +todo list --json | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).data.filter(i=>!i.done).length))" + +# Use with jq +todo list --json | jq '.data[] | select(.done == false) | .text' +todo add "New task" --json | jq '.data.id' +``` + ## Development ```bash -npm test # run Jest test suite -npm run coverage # run tests with coverage report -npm run build # compile TypeScript +npm run build # compile TypeScript → dist/ +npm test # run Jest test suite ``` -Data is stored in `~/.tasks.json`. +## Data storage + +Todo items are persisted to `~/.todo-items.json`.