Skip to content

Latest commit

 

History

History
167 lines (136 loc) · 7.66 KB

CLI-SPEC.md

File metadata and controls

167 lines (136 loc) · 7.66 KB

Taskly CLI Specification (Contract)

Stable agent-facing interface. Every platform binary is dual-mode: any argument at launch routes to the CLI (the GUI toolkit is never initialized); no arguments opens the GUI. Command output and exit codes are identical across platforms — enforced by golden-output tests in CI (shared/cli-golden/, 61 cases; canonical implementation racket/taskly/cli.rkt, the M6 Racket CLI).

taskly <subcommand> [options] [--json] [--db PATH] [--quiet]
  • --json machine-readable output (stdout only; errors as JSON on stderr)
  • --db PATH overrides DB resolution
  • --quiet / -q minimal output (usually just the id)
  • DB resolution: --db → config last-db-path → ~/.taskly/tasks.db; unopenable → error, exit 4.
  • Response-file expansion is disabled (@10am must never read a file).

Exit codes

Code Meaning Triggers
0 success (including rm of an already-missing id)
1 generic error unknown exceptions; install/uninstall failures
2 validation failure bad --view / --status / --color; unparseable --due; text/name too long or empty
3 not found --list name miss; add with no lists; update/done/undone unknown id
4 database error cannot open; SQLite error

Error JSON on stderr, 2-space indented, always: {"ok": false, "error": "<message>", "exitCode": <code>}

Success JSON on stdout, 2-space indented, null fields omitted (WhenWritingNull), fixed field order.

Task / list JSON shapes

{ "id": 1, "listId": 1, "listName": "工作", "text": "买牛奶", "completed": false,
  "dueDate": "2026-09-16", "dueTime": "10:00", "notes": null, "createdAt": "..." }
{ "id": 1, "name": "工作", "icon": "📋", "color": -16745729, "pendingCount": 3 }

Human-readable (non---json) task row: %5d [x| ] text 🗓 dueDate[ dueTime]; empty result prints (no tasks) (suppressed when --quiet). List row: %5d icon name (pendingCount).

Subcommands

list

Default: all incomplete.

  • --list ID|NAME — numeric → id; otherwise case-sensitive exact name match; miss → exit 3 with List not found by name: "<list>". Takes precedence over --view when both given.
  • --view today|planned|scheduled|all|completed (case-insensitive; scheduled = planned; default all; anything else → exit 2).
  • --status all|incomplete|open|pending|completed|done (default incomplete; all and completed both map to "include completed, completed sinks to bottom"; default/others → incomplete only).
  • --limit N (default 1000).
  • completed view always completed = 1, no ORDER BY.
  • Sorting: today id DESC; planned due_date ASC; all/list id DESC (completed ASC first when included).

lists

All lists with pendingCount (COUNT(*) WHERE list_id=? AND completed=0).

add ""

  • --list ID|NAME (default: first list; none exists → exit 3, No lists exist yet. Create one with taskly mklist first.)
  • --due EXPR (below), --time HH:mm (explicit --time overrides a time carried by --due), --notes "...".
  • createdAt = local ISO-8601 round-trip now. Text validation → exit 2.
  • Output: the stored task (read back after insert, so listName is joined) / human row / quiet → id.

update

Read-modify-write; unspecified fields unchanged. --text --due --clear-due --time --clear-time --list --notes --clear-notes. --due overwrites dueTime only when the expression carries a time (a pure date keeps the existing time); --clear-due clears the date only. Unknown id → exit 3. Outputs the stored task (read back after write, so listName reflects the final list).

done / undone

Idempotent completed=1/0 (affected rows == 0 → exit 3). --json prints the task; --json --quiet prints {"ok":true,"id":N,"completed":bool}.

rm

--json prints {"ok":true,"id":N,"deleted":bool} regardless of --quiet; exit 0 either way. Non-json: Deleted task N / Task N did not exist.

search ""

text LIKE '%kw%' — task text only, completed included, unsorted, --limit default 100. Empty keyword → empty array.

mklist ""

--icon <emoji> --color #RRGGBB|AARRGGBB|<int>. Name validation → exit 2. Color parsing: plain int → as-is; #RRGGBB → 0xFF000000|RGB; 8-digit hex → as-is; anything else → exit 2. Null icon/color get defaults (📋 / system blue 0xFF007AFF). Output: list JSON / list row / quiet → id.

rmlist

Cascade-delete tasks then the list; output like rm (Deleted list N (and its tasks)); exit 0 either way.

install-cli / uninstall-cli

  • macOS/Linux: shell wrapper #!/bin/sh\nexec "<exe>" "$@" at ~/.local/bin/taskly (chmod +x); if ~/.local/bin is not on PATH, append an idempotent # Added by Taskly block to ~/.zshrc (macOS) / ~/.bashrc (Linux) and report NeedsShellRestart.
  • Windows: copy self to %LOCALAPPDATA%\Programs\Taskly\taskly.exe, write taskly.cmd shim, add the dir to HKCU\Environment\Path (idempotent, REG_EXPAND_SZ) and broadcast WM_SETTINGCHANGE.
  • Uninstall removes the artifacts; when nothing is installed, print taskly command was not installed (nothing to remove). to stderr and exit 1.

--due expression grammar (ParseDue)

Pre-map bare words (case-insensitive): today → +0d, tomorrow/tmw → +1d, tonight → @20:00.

Then DateParser:

  1. Relative +N{m|h|d|w|M} — regex ^(\d*)([mhdwM])$, digits optional (default 1), case-sensitive (m minutes, M month = 30 days), from now.
  2. @time: now | H | H:MM | Ham | Hpm | H:MMam | H:MMpm (am/pm any case; 12am→0, pm<12→+12; hour 0–23, min 0–59). Optional trailing modifier: tomorrow/tmw, or sun..sat = next such weekday (if today or past → next week). No modifier + time already past → tomorrow.
  3. Absolute: yyyy-MM-dd, yyyy/MM/dd, MM/dd/yyyy, dd/MM/yyyy (tried in this order), year in [1900, 2100]; normalized to yyyy-MM-dd.
  4. Relative results are full yyyy-MM-dd HH:mm:ss; absolute are yyyy-MM-dd.

Pure-date intent (today/tomorrow/tmw, +Nd/+Nw/+NM, a 10-char absolute result) clears dueTime; +Nm/+Nh/@… keep it.

Unparseable → exit 2: Cannot parse date/time: "<due>". Supported: +10m, +2h, +1d, +1w, @10am, @10:30pm, today, tomorrow, yyyy-MM-dd

Golden CLI suite (shared fixture)

shared/cli-golden/ pins identical argv → identical stdout/stderr/exit code across platforms. cases.json declares the inputs; each case runs against a fresh database seeded from seed.sql (or the case's seed override, e.g. seed-empty.sql), so cases are independent. Expected bytes live in golden/<case>.{out,err,exit}.

Comparison normalizes dynamic values in actual output first:

  • ISO-8601 local timestamps → {{now}}; the OS-specific detail after Cannot open database: <path> is dropped — the contract pins the message prefix and exit code only
  • today / tomorrow (yyyy-MM-dd) → {{today}} / {{tomorrow}}, but only for cases flagged "dynamicDates": true (those using relative --due expressions) — a static fixture date must never be rewritten when the calendar catches up with it

Each platform wraps the shared runner.py with its own binary path; macOS: apps/macos/scripts/golden-cli.sh (wired into native.yml). Cases with pre commands build dynamic state (e.g. a task due today) before the asserted command; pre output is discarded but must exit 0. install-cli / uninstall-cli are excluded (they mutate PATH/shell files). After an intentional behavior change: fix the spec first, run scripts/golden-cli.sh --record, review the diff against this spec, and commit the new goldens in the same change.