Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d7e4fde
feat: implement CLI with --json flag for add, list, done commands
SyedHannanMehdi Mar 29, 2026
b571216
test: add comprehensive tests for --json flag on all commands
SyedHannanMehdi Mar 29, 2026
260717b
chore: update package.json with commander dependency and jest config
SyedHannanMehdi Mar 29, 2026
9bcf9d8
chore: add tsconfig.json
SyedHannanMehdi Mar 29, 2026
8e03ce5
docs: update README with --json flag documentation and examples
SyedHannanMehdi Mar 29, 2026
076eb0f
fix: improve loadTasks robustness and validate done command task ID
SyedHannanMehdi Mar 29, 2026
7a29c3b
docs: clarify how to use the `tasks` binary in README examples
SyedHannanMehdi Mar 29, 2026
3d9ad8c
fix: address review comments - robust loadTasks parsing and NaN taskI…
SyedHannanMehdi Mar 29, 2026
026a62c
docs: clarify how to use the tasks binary in README examples
SyedHannanMehdi Mar 29, 2026
f36cecf
feat: add --json flag support to add, list, and done commands
SyedHannanMehdi Mar 29, 2026
52cb2ed
test: add comprehensive tests for --json flag on all commands
SyedHannanMehdi Mar 29, 2026
2f13655
chore: add jest testing setup and commander dependency
SyedHannanMehdi Mar 29, 2026
4d6e716
chore: add TypeScript configuration
SyedHannanMehdi Mar 29, 2026
58fdfca
docs: update README with --json flag usage and examples
SyedHannanMehdi Mar 29, 2026
c7d43b8
fix: harden loadTasks() JSON parsing and validate done command ID
SyedHannanMehdi Mar 29, 2026
6535645
docs: clarify how to invoke the tasks binary in README examples
SyedHannanMehdi Mar 29, 2026
f926b51
feat: add --json flag to add, list, and done commands
SyedHannanMehdi Mar 30, 2026
a51306b
test: add comprehensive tests for --json flag on all commands
SyedHannanMehdi Mar 30, 2026
383c902
chore: add jest + ts-jest dev deps and test script
SyedHannanMehdi Mar 30, 2026
8bcd30d
chore: add tsconfig.json for TypeScript compilation
SyedHannanMehdi Mar 30, 2026
62c7e86
docs: update README with --json flag usage and scripting examples
SyedHannanMehdi Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 142 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,161 @@
# Task CLI - Test Target for ai-gitops
# ai-gitops-test-target — Todo CLI

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 command-line todo manager written in TypeScript.

## Installation

```bash
python task.py --help
npm install
npm run build
npm link # makes `todo` available globally
```

## Usage

### Add a todo item

```bash
todo add "Buy milk"
# Added: [1] Buy milk
```

### List all todo items

```bash
todo list
# [ ] [1] Buy milk
# [ ] [2] Write tests
```

### Mark a todo item as done

```bash
todo done 1
# Done: [1] Buy milk
```

---

## JSON Output (`--json` flag)

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:

```json
{
"success": true | false,
"data": <command-specific payload>,
"error": "<message>" // only present when success is false
}
```

### `add --json`

```bash
todo add "Buy milk" --json
```

```json
{
"success": true,
"data": {
"id": 1,
"text": "Buy milk",
"done": false,
"createdAt": "2024-01-15T10:30:00.000Z"
}
}
```

### `list --json`

```bash
todo list --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
{
"success": true,
"data": []
}
```

### `done --json`

```bash
todo done 1 --json
```

```json
{
"success": true,
"data": {
"id": 1,
"text": "Buy milk",
"done": true,
"createdAt": "2024-01-15T10:30:00.000Z"
}
}
```

#### Error responses

When something goes wrong (e.g. item not found), the process exits with a non-zero code and outputs:

```json
{
"success": false,
"data": null,
"error": "Todo item with ID 99 not found"
}
```

---

## Scripting examples

```bash
# Add a task
python task.py add "Buy groceries"
# 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))")

# List tasks
python task.py list
# 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))"

# Complete a task
python task.py done 1
# Use with jq
todo list --json | jq '.data[] | select(.done == false) | .text'
todo add "New task" --json | jq '.data.id'
```

## Testing
## Development

```bash
python -m pytest test_task.py
npm run build # compile TypeScript → dist/
npm test # run Jest test suite
```

## Configuration
## Data storage

Copy `config.yaml.example` to `~/.config/task-cli/config.yaml` and customize.
Todo items are persisted to `~/.todo-items.json`.
49 changes: 49 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "ai-gitops-test-target",
"version": "1.0.0",
"description": "A simple CLI todo manager",
"main": "dist/cli.js",
"bin": {
"todo": "./dist/cli.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/cli.js",
"test": "jest",
"test:watch": "jest --watch",
"prepublishOnly": "npm run build"
},
"dependencies": {
"commander": "^11.1.0"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.11.5",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"testMatch": [
"**/*.test.ts"
],
"globalSetup": undefined,
"transform": {
"^.+\\.tsx?$": [
"ts-jest",
{
"tsconfig": "tsconfig.json"
}
]
}
},
"keywords": [
"cli",
"todo",
"task-manager"
],
"author": "",
"license": "MIT"
}
147 changes: 147 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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,
};
}
}

function clearTodos(): void {
if (fs.existsSync(TODO_FILE)) {
fs.writeFileSync(TODO_FILE, JSON.stringify([]), "utf-8");
}
}

beforeEach(() => {
clearTodos();
});

afterAll(() => {
clearTodos();
});

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("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("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);
});
});

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("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("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("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("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();
});

it("list --json output is parseable", () => {
const { stdout } = runCLI("list --json");
expect(() => JSON.parse(stdout)).not.toThrow();
});

it("done --json output is parseable even on error", () => {
const { stdout } = runCLI("done 99999 --json");
expect(() => JSON.parse(stdout)).not.toThrow();
});
});
Loading