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 ')
+ .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 [options]
+
+# Or after building
+node dist/index.js [options]
+```
+
+## Commands
+
+### `add `
+
+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 `
+
+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;
+
+ 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 ')
- .description('Add a new task')
- .option('--json', 'Output result as JSON')
+ .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 = {
@@ -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 ')
- .description('Mark a task as done')
- .option('--json', 'Output result as JSON')
+ .command("done ")
+ .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 [options]
+```
+
+### 2. From the compiled build
-# Or after building
+```bash
node dist/index.js [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 `.
+
+> **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 `
@@ -26,142 +50,144 @@ node dist/index.js [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 `
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 ")
.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 ")
- .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 [options]
```
-### 2. From the compiled build
-
+### 2. After building (via Node.js)
```bash
node dist/index.js [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 `.
+# 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 `
+
+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 `
-
-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;
-
- 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 ")
- .description("Add a new task")
+ .command("add ")
+ .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 ")
- .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 [options]
-```
+### Add a todo
-### 2. After building (via Node.js)
```bash
-node dist/index.js [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 `
-
-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 `
-
-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;
+
+ 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 ")
- .description("Add a new todo item")
+ .command("add ")
+ .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 ")
- .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 [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 [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 [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 ` | Add a new task |
+| `list` | List all tasks |
+| `done ` | Mark task `` 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 {
+ 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(payload: JsonOutput): 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 ")
+ .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({ 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({ 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 ")
+ .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({ 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 [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 [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 [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 ` | Add a new task |
-| `list` | List all tasks |
-| `done ` | Mark task `` 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": ,
+ "error": "" // 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`.