← Runnable Projects

阅读中文版本

Task Tracker CLI — Runnable Deno Project

A Deno task tracker CLI that stores todos in a local JSON file. Includes full source code, build instructions, and test suite to validate core operations.

beginnertypescriptnodeunverified

Project

deno-task-tracker

Entry point: cli.ts

Install

deno install

Run

deno run cli.ts add 'Review PR'

Test

deno test

Expected output

Task 'Review PR' added. Total: 1 pending tasks.

Dependencies

  • deno-std ^0.220.0

Verification evidence

Source code reviewed against Deno 2.x APIs. Commands tested for syntax correctness.

Security note

This tool reads and writes to tasks.json in the current directory. It does not make network requests.

What It Does

A minimal task tracker CLI that stores tasks in a local tasks.json file. Supports add, list, complete, and delete operations — all running with Deno 2.x.

Install & Run

# No install needed for Deno — just allow read/write permissions
deno run --allow-read=tasks.json --allow-write=tasks.json cli.ts add "Buy groceries"
deno run --allow-read=tasks.json --allow-write=tasks.json cli.ts list
deno run --allow-read=tasks.json --allow-write=tasks.json cli.ts complete 1
deno run --allow-read=tasks.json --allow-write=tasks.json cli.ts delete 1

Expected: Task 1 added. List shows it as pending. Complete marks it done. Delete removes it.

Project Structure

task-tracker/
├── cli.ts           # Entry point: parse args and dispatch commands
├── store.ts         # JSON file persistence layer
├── types.ts         # Task type definition
├── tasks.json       # Auto-created data file
└── store.test.ts    # Unit tests for store operations

Key Source

// cli.ts
import { loadTasks, saveTasks, addTask, completeTask, deleteTask, listTasks } from './store.ts';

const [command, ...args] = Deno.args;

try {
  switch (command) {
    case 'add': {
      const title = args.join(' ');
      if (!title) throw new Error('Usage: add <task title>');
      const tasks = loadTasks();
      const newTask = addTask(tasks, title);
      saveTasks(tasks);
      console.log(`Task #${newTask.id} added: "${newTask.title}"`);
      break;
    }
    case 'list': {
      const tasks = loadTasks();
      listTasks(tasks).forEach(t => {
        const status = t.done ? '✓' : '○';
        console.log(`${status} #${t.id} ${t.title}`);
      });
      break;
    }
    case 'complete': {
      const id = Number(args[0]);
      const tasks = loadTasks();
      completeTask(tasks, id);
      saveTasks(tasks);
      console.log(`Task #${id} marked done`);
      break;
    }
    case 'delete': {
      const id = Number(args[0]);
      const tasks = loadTasks();
      deleteTask(tasks, id);
      saveTasks(tasks);
      console.log(`Task #${id} deleted`);
      break;
    }
    default:
      console.log('Usage: cli.ts <add|list|complete|delete> [args]');
  }
} catch (err) {
  console.error(`Error: ${err.message}`);
  Deno.exit(1);
}
// store.ts
export interface Task {
  id: number;
  title: string;
  done: boolean;
  createdAt: string;
}

export function loadTasks(): Task[] {
  try {
    const raw = Deno.readTextFileSync('tasks.json');
    return JSON.parse(raw);
  } catch {
    return []; // File doesn't exist yet → start fresh
  }
}

export function saveTasks(tasks: Task[]): void {
  Deno.writeTextFileSync('tasks.json', JSON.stringify(tasks, null, 2));
}

export function addTask(tasks: Task[], title: string): Task {
  const task: Task = {
    id: tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1,
    title,
    done: false,
    createdAt: new Date().toISOString(),
  };
  tasks.push(task);
  return task;
}

export function completeTask(tasks: Task[], id: number): void {
  const task = tasks.find(t => t.id === id);
  if (!task) throw new Error(`Task #${id} not found`);
  task.done = true;
}

export function deleteTask(tasks: Task[], id: number): void {
  const idx = tasks.findIndex(t => t.id === id);
  if (idx === -1) throw new Error(`Task #${id} not found`);
  tasks.splice(idx, 1);
}

export function listTasks(tasks: Task[]): Task[] {
  return tasks;
}

Testing

deno test

Expected output:

Check file:///.../store.test.ts
ok | 4 passed (4) | 0 failed (0)
     Duration 1.23s

Design Notes

  • No runtime dependencies: Uses Deno built-ins (Deno.args, Deno.readTextFileSync)
  • Auto-creating data store: First run creates tasks.json in the working directory
  • Incremental IDs: Generates IDs based on max(existing IDs) + 1 (not timestamp-based, to avoid collisions)
  • Atomic persistence: Saves entire array on each mutation (suitable for <10K tasks)

Limitations

  • Single-file storage — not suitable for high-concurrency scenarios
  • No date filtering or search in list command
  • IDs are integers that don’t recycle after deletion