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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 51 additions & 32 deletions plan.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
# Feature: NoteAPI v2 — Search, Pagination, and Archiving

## Problem Statement
The API supports basic CRUD but lacks the query features users need for real use: filtering by tag, searching content, paginating large result sets, and archiving old notes without deleting them.

## Approach
Add four features incrementally. Each feature is independent — no ordering dependencies. All use the existing in-memory store (no database changes). Each feature must have tests before it's considered done.

## Phases

### Phase 1: Tag Filtering
- [ ] GET /api/notes?tag=work returns only notes with that tag
- [ ] Tests: single tag, no match, multiple tags on same note
- Integration test: `curl localhost:3000/api/notes?tag=work`

### Phase 2: Full-Text Search
- [ ] GET /api/notes?q=meeting searches title and content (case-insensitive)
- [ ] Tests: match in title, match in content, no match, empty query returns all
- Integration test: `curl localhost:3000/api/notes?q=meeting`

### Phase 3: Pagination
- [ ] GET /api/notes?page=1&limit=10 returns paginated results
- [ ] Response format: `{ data: [...], total: N, page: N, limit: N }`
- [ ] Default: page 1, limit 20
- Integration test: create 25 notes, verify page 1 has 20, page 2 has 5

### Phase 4: Note Archiving
- [ ] Add `archived: boolean` field to Note model (default: false)
- [ ] POST /api/notes/:id/archive and /api/notes/:id/unarchive endpoints
- [ ] GET /api/notes excludes archived by default
- [ ] GET /api/notes?include_archived=true includes them
- Integration test: archive a note, verify it's hidden, unarchive, verify it's back
# fleet-e2e-toy - Implementation Plan

> This plan details the implementation of a Command Line Interface (CLI) wrapper for the `fleet-e2e-toy` project. The CLI will feature help command and flag options, robust validation for empty or whitespace-only inputs, and a version flag.

---

## Tasks

### Phase 1: CLI Foundation and Validation

#### Task 1: Create CLI Launcher Scripts
- **Change:** Create three launcher scripts (`tool`, `tool.ps1`, `tool.cmd`) in the root directory to run `src/cli.ts` via `ts-node`. The bash script `tool` will execute `npx ts-node src/cli.ts "$@"`, `tool.ps1` will run `..\node_modules\.bin\ts-node.cmd src/cli.ts $args`, and `tool.cmd` will run `npx ts-node src/cli.ts %*`.
- **Files:** [NEW] [tool](file:///C:/Users/akhil/git/apra-fleet-e2e-doer/tool), [NEW] [tool.ps1](file:///C:/Users/akhil/git/apra-fleet-e2e-doer/tool.ps1), [NEW] [tool.cmd](file:///C:/Users/akhil/git/apra-fleet-e2e-doer/tool.cmd)
- **Tier:** cheap
- **Done when:** The files exist in the project root with the correct execution format and permissions.
- **Blockers:** File permissions and execution policies for PowerShell scripts on Windows environments.

#### Task 2: Implement CLI Entry Point and Logic
- **Change:** Create the CLI entry point `src/cli.ts`. Parse the command line arguments from `process.argv.slice(2)`. Support `--version` and `-v` flags that print `fleet-e2e-toy v1.0.0` and exit 0. Support `help` command, `--help` and `-h` flags that print command usage details and exit 0. Validate that the first argument and the argument for the `add` command are not empty or whitespace-only (e.g. `trim().length === 0`), rejecting with a user-friendly error and exiting with a non-zero exit code. Stub `add` and `serve` commands.
- **Files:** [NEW] [cli.ts](file:///C:/Users/akhil/git/apra-fleet-e2e-doer/src/cli.ts)
- **Tier:** standard
- **Done when:** `npm run build` compiles the TypeScript files successfully, and running the tool with `--version` and `help` commands outputs the correct version and usage details.
- **Blockers:** None.

#### Task 3: Add CLI Unit Tests
- **Change:** Add a Jest test suite `tests/cli.test.ts` to test CLI behavior. Test the `--version` and `-v` flag output. Test the `help` command, `--help` and `-h` flags. Test empty string or whitespace argument validation on the first argument and the `add` subcommand. Verify that `add <title>` logs `Note added: <title>` and `serve` logs `Starting server...`.
- **Files:** [NEW] [cli.test.ts](file:///C:/Users/akhil/git/apra-fleet-e2e-doer/tests/cli.test.ts)
- **Tier:** standard
- **Done when:** Running `npm test` runs all CLI test suites and they pass successfully.
- **Blockers:** Command line execution using `execSync` might have shell differences between Windows and Unix.

#### VERIFY: CLI Foundation and Validation
- Run full test suite using `npm test`
- Confirm all Phase 1 changes work together by manually testing version, help, validation, and commands using `./tool`
- Report: tests passing, any regressions, any issues found

---

## Risk Register

| Risk | Impact | Mitigation |
|------|--------|------------|
| Script execution failure on Windows due to PowerShell execution policies or path issues | med | Provide fallback CMD (`tool.cmd`) and Bash (`tool`) launcher scripts and specify exact execution paths for tests. |
| Incomplete validation for whitespace-only strings (e.g. carriage returns or unicode spaces) | low | Use standard `String.prototype.trim()` to filter out all whitespace before performing length checks. |
| Executing CLI tests hangs if the process does not terminate | med | Ensure all execution paths in `src/cli.ts` call `process.exit(code)` explicitly. |

## Notes
- Each task should result in a git commit
- Verify tasks are checkpoints – stop and report after each one
- Base branch: main
- Implementation branch: e2e-s8.1-26319774000
54 changes: 54 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const args = process.argv.slice(2);

const HELP_TEXT = `Usage:
tool <command> [arguments]

Commands:
add <title> Add a new note with the specified title
serve Start the NoteAPI server
help Show this help message

Flags:
-v, --version Show version details
-h, --help Show help details`;

if (args.length === 0) {
console.log(HELP_TEXT);
process.exit(0);
}

const firstArg = args[0];

// Check version flags as first argument
if (firstArg === "--version" || firstArg === "-v") {
console.log("fleet-e2e-toy v1.0.0");
process.exit(0);
}

// Check help flags/command as first argument
if (firstArg === "--help" || firstArg === "-h" || firstArg === "help") {
console.log(HELP_TEXT);
process.exit(0);
}

// Validate first argument is not empty or whitespace-only
if (firstArg.trim().length === 0) {
console.error("Error: Command or argument cannot be empty or whitespace-only.");
process.exit(1);
}

if (firstArg === "add") {
const title = args[1];
if (title === undefined || title.trim().length === 0) {
console.error("Error: Note title cannot be empty or whitespace-only.");
process.exit(1);
}
console.log(`Note added: ${title}`);
process.exit(0);
} else if (firstArg === "serve") {
console.log("Starting server...");
process.exit(0);
} else {
console.error(`Error: Unknown command: ${firstArg}`);
process.exit(1);
}
131 changes: 131 additions & 0 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { execSync } from "child_process";

function runCLI(argsString: string): { stdout: string; stderr: string; status: number } {
try {
const stdout = execSync(`npx ts-node src/cli.ts ${argsString}`, { stdio: "pipe" });
return {
stdout: stdout.toString(),
stderr: "",
status: 0,
};
} catch (error) {
const err = error as { stdout?: Buffer; stderr?: Buffer; status?: number };
return {
stdout: err.stdout ? err.stdout.toString() : "",
stderr: err.stderr ? err.stderr.toString() : "",
status: err.status ?? 1,
};
}
}

describe("CLI Tests", () => {
it("prints version for --version flag", () => {
const result = runCLI("--version");
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("fleet-e2e-toy v1.0.0");
});

it("prints version for -v flag", () => {
const result = runCLI("-v");
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("fleet-e2e-toy v1.0.0");
});

it("prints help for --help flag", () => {
const result = runCLI("--help");
expect(result.status).toBe(0);
expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("Commands:");
expect(result.stdout).toContain("Flags:");
});

it("prints help for -h flag", () => {
const result = runCLI("-h");
expect(result.status).toBe(0);
expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("Commands:");
expect(result.stdout).toContain("Flags:");
});

it("prints help for help command", () => {
const result = runCLI("help");
expect(result.status).toBe(0);
expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("Commands:");
expect(result.stdout).toContain("Flags:");
});

it("rejects empty first argument", () => {
const result = runCLI('""');
expect(result.status).toBe(1);
expect(result.stderr).toContain("Error: Command or argument cannot be empty or whitespace-only.");
});

it("rejects whitespace-only first argument", () => {
const result = runCLI('" "');
expect(result.status).toBe(1);
expect(result.stderr).toContain("Error: Command or argument cannot be empty or whitespace-only.");
});

it("rejects empty note title on add command", () => {
const result = runCLI('add ""');
expect(result.status).toBe(1);
expect(result.stderr).toContain("Error: Note title cannot be empty or whitespace-only.");
});

it("rejects whitespace-only note title on add command", () => {
const result = runCLI('add " "');
expect(result.status).toBe(1);
expect(result.stderr).toContain("Error: Note title cannot be empty or whitespace-only.");
});

it("logs success message on valid add command", () => {
const result = runCLI('add "Test Note"');
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("Note added: Test Note");
});

it("logs success message on valid serve command", () => {
const result = runCLI("serve");
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("Starting server...");
});

it("displays help and exits 0 for no arguments", () => {
const result = runCLI("");
expect(result.status).toBe(0);
expect(result.stdout).toContain("Usage:");
expect(result.stdout).toContain("Commands:");
});

it("adds note with title mimicking version short flag", () => {
const result = runCLI('add "-v"');
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("Note added: -v");
});

it("adds note with title mimicking version long flag", () => {
const result = runCLI('add "--version"');
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("Note added: --version");
});

it("adds note with title mimicking help command", () => {
const result = runCLI('add "help"');
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("Note added: help");
});

it("adds note with title mimicking help short flag", () => {
const result = runCLI('add "-h"');
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("Note added: -h");
});

it("adds note with title mimicking help long flag", () => {
const result = runCLI('add "--help"');
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("Note added: --help");
});
});

2 changes: 2 additions & 0 deletions tool
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
npx ts-node src/cli.ts "$@"
1 change: 1 addition & 0 deletions tool.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@npx ts-node src/cli.ts %*
1 change: 1 addition & 0 deletions tool.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx ts-node src/cli.ts $args
Loading