doingapp 0.4.0__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {doingapp-0.4.0 → doingapp-0.5.1}/PKG-INFO +32 -4
- {doingapp-0.4.0 → doingapp-0.5.1}/README.md +31 -3
- {doingapp-0.4.0 → doingapp-0.5.1}/doing_cli/api.py +14 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/doing_cli/main.py +154 -19
- {doingapp-0.4.0 → doingapp-0.5.1}/doingapp.egg-info/PKG-INFO +32 -4
- {doingapp-0.4.0 → doingapp-0.5.1}/pyproject.toml +1 -1
- {doingapp-0.4.0 → doingapp-0.5.1}/LICENSE +0 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/doing_cli/__init__.py +0 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/doing_cli/auth.py +0 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/doingapp.egg-info/SOURCES.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/doingapp.egg-info/dependency_links.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/doingapp.egg-info/entry_points.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/doingapp.egg-info/requires.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/doingapp.egg-info/top_level.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doingapp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A fast CLI task tracker — track what you're doing from the terminal.
|
|
5
5
|
Author-email: Parker Grimes <parker@altellus.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -62,27 +62,55 @@ doing # See what's on your plate
|
|
|
62
62
|
doing done 1 # Mark task #1 as done
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Or authenticate with an API key (get one from the web UI at Settings → API Keys):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
doing apikey --key doing_xxxxx
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
## Commands
|
|
66
72
|
|
|
67
73
|
| Command | Description |
|
|
68
74
|
|---------|-------------|
|
|
69
75
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
70
76
|
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
77
|
+
| `doing get <id>` | Get a single task by ID |
|
|
71
78
|
| `doing done <id>` | Mark a task as done |
|
|
72
79
|
| `doing pause <id>` | Pause a task |
|
|
73
80
|
| `doing resume <id>` | Resume a paused task |
|
|
74
81
|
| `doing reopen <id>` | Reopen a completed task |
|
|
75
82
|
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
76
83
|
| `doing rm <id>` | Delete a task |
|
|
77
|
-
| `doing ls` | List active tasks |
|
|
78
|
-
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`
|
|
79
|
-
| `doing today` | Show all tasks grouped by status |
|
|
84
|
+
| `doing ls` | List active tasks (`-c` context ID) |
|
|
85
|
+
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
86
|
+
| `doing today` | Show all tasks grouped by status (`-c` context ID) |
|
|
80
87
|
| `doing clear` | Mark all active tasks as done |
|
|
81
88
|
| `doing contexts` | List contexts |
|
|
82
89
|
| `doing context add <name>` | Create a context |
|
|
83
90
|
| `doing context rm <id>` | Delete a context |
|
|
91
|
+
| `doing me` | Show current user info |
|
|
84
92
|
| `doing login` | Log in |
|
|
85
93
|
| `doing logout` | Log out |
|
|
94
|
+
| `doing apikey` | Save an API key (`-k` key) |
|
|
95
|
+
|
|
96
|
+
## JSON output
|
|
97
|
+
|
|
98
|
+
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
doing --json ls # [{"id": 1, "title": "...", ...}]
|
|
102
|
+
doing --json add "New task" # {"id": 2, "title": "New task", ...}
|
|
103
|
+
doing --json done 2 # {"id": 2, "status": "done", ...}
|
|
104
|
+
doing --json contexts # [{"id": 1, "name": "Work", ...}]
|
|
105
|
+
doing --json get 2 # {"id": 2, "title": "...", ...}
|
|
106
|
+
doing --json me # {"id": 1, "email": "...", ...}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Errors also return structured JSON with a non-zero exit code:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
doing --json done 99999 # {"error": true, "status": 404, "detail": "Task not found"}
|
|
113
|
+
```
|
|
86
114
|
|
|
87
115
|
## Configuration
|
|
88
116
|
|
|
@@ -37,27 +37,55 @@ doing # See what's on your plate
|
|
|
37
37
|
doing done 1 # Mark task #1 as done
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Or authenticate with an API key (get one from the web UI at Settings → API Keys):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
doing apikey --key doing_xxxxx
|
|
44
|
+
```
|
|
45
|
+
|
|
40
46
|
## Commands
|
|
41
47
|
|
|
42
48
|
| Command | Description |
|
|
43
49
|
|---------|-------------|
|
|
44
50
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
45
51
|
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
52
|
+
| `doing get <id>` | Get a single task by ID |
|
|
46
53
|
| `doing done <id>` | Mark a task as done |
|
|
47
54
|
| `doing pause <id>` | Pause a task |
|
|
48
55
|
| `doing resume <id>` | Resume a paused task |
|
|
49
56
|
| `doing reopen <id>` | Reopen a completed task |
|
|
50
57
|
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
51
58
|
| `doing rm <id>` | Delete a task |
|
|
52
|
-
| `doing ls` | List active tasks |
|
|
53
|
-
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`
|
|
54
|
-
| `doing today` | Show all tasks grouped by status |
|
|
59
|
+
| `doing ls` | List active tasks (`-c` context ID) |
|
|
60
|
+
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
61
|
+
| `doing today` | Show all tasks grouped by status (`-c` context ID) |
|
|
55
62
|
| `doing clear` | Mark all active tasks as done |
|
|
56
63
|
| `doing contexts` | List contexts |
|
|
57
64
|
| `doing context add <name>` | Create a context |
|
|
58
65
|
| `doing context rm <id>` | Delete a context |
|
|
66
|
+
| `doing me` | Show current user info |
|
|
59
67
|
| `doing login` | Log in |
|
|
60
68
|
| `doing logout` | Log out |
|
|
69
|
+
| `doing apikey` | Save an API key (`-k` key) |
|
|
70
|
+
|
|
71
|
+
## JSON output
|
|
72
|
+
|
|
73
|
+
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
doing --json ls # [{"id": 1, "title": "...", ...}]
|
|
77
|
+
doing --json add "New task" # {"id": 2, "title": "New task", ...}
|
|
78
|
+
doing --json done 2 # {"id": 2, "status": "done", ...}
|
|
79
|
+
doing --json contexts # [{"id": 1, "name": "Work", ...}]
|
|
80
|
+
doing --json get 2 # {"id": 2, "title": "...", ...}
|
|
81
|
+
doing --json me # {"id": 1, "email": "...", ...}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Errors also return structured JSON with a non-zero exit code:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
doing --json done 99999 # {"error": true, "status": 404, "detail": "Task not found"}
|
|
88
|
+
```
|
|
61
89
|
|
|
62
90
|
## Configuration
|
|
63
91
|
|
|
@@ -42,6 +42,13 @@ def login(email: str, password: str) -> dict:
|
|
|
42
42
|
return r.json()
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def get_me() -> dict:
|
|
46
|
+
with _client() as c:
|
|
47
|
+
r = c.get("/auth/me")
|
|
48
|
+
r.raise_for_status()
|
|
49
|
+
return r.json()
|
|
50
|
+
|
|
51
|
+
|
|
45
52
|
def create_task(title: str, notes: str | None = None, context_id: int | None = None) -> dict:
|
|
46
53
|
payload: dict = {"title": title}
|
|
47
54
|
if notes is not None:
|
|
@@ -54,6 +61,13 @@ def create_task(title: str, notes: str | None = None, context_id: int | None = N
|
|
|
54
61
|
return r.json()
|
|
55
62
|
|
|
56
63
|
|
|
64
|
+
def get_task(task_id: int) -> dict:
|
|
65
|
+
with _client() as c:
|
|
66
|
+
r = c.get(f"/tasks/{task_id}")
|
|
67
|
+
r.raise_for_status()
|
|
68
|
+
return r.json()
|
|
69
|
+
|
|
70
|
+
|
|
57
71
|
def list_tasks(status: str | None = None, date: str | None = None,
|
|
58
72
|
context_id: int | None = None) -> list[dict]:
|
|
59
73
|
params: dict = {"tz": _local_tz()}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import sys
|
|
2
3
|
|
|
3
4
|
import httpx
|
|
4
5
|
import typer
|
|
@@ -19,15 +20,41 @@ console = Console()
|
|
|
19
20
|
# Commands that don't require authentication
|
|
20
21
|
AUTH_COMMANDS = {"login", "register", "logout", "apikey"}
|
|
21
22
|
|
|
23
|
+
# Global JSON mode flag, set by --json on any command
|
|
24
|
+
_json_mode = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _output_json(data) -> None:
|
|
28
|
+
"""Write raw JSON to stdout (no Rich formatting) and exit."""
|
|
29
|
+
sys.stdout.write(json.dumps(data) + "\n")
|
|
30
|
+
raise typer.Exit()
|
|
31
|
+
|
|
22
32
|
|
|
23
33
|
def _require_auth():
|
|
24
34
|
"""Exit with a message if the user is not logged in."""
|
|
25
35
|
if load_token() is None:
|
|
36
|
+
if _json_mode:
|
|
37
|
+
_output_json({"error": True, "detail": "Not logged in"})
|
|
26
38
|
console.print("[bold red]Not logged in.[/bold red] Run: [bold]doing login[/bold]")
|
|
27
39
|
raise typer.Exit(code=1)
|
|
28
40
|
|
|
29
41
|
|
|
30
42
|
def _handle_api_error(exc: Exception):
|
|
43
|
+
if _json_mode:
|
|
44
|
+
error: dict = {"error": True}
|
|
45
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
46
|
+
error["status"] = exc.response.status_code
|
|
47
|
+
try:
|
|
48
|
+
error["detail"] = exc.response.json().get("detail", exc.response.text)
|
|
49
|
+
except Exception:
|
|
50
|
+
error["detail"] = exc.response.text
|
|
51
|
+
elif isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
|
|
52
|
+
error["detail"] = f"Could not connect to {api.BASE_URL}"
|
|
53
|
+
else:
|
|
54
|
+
error["detail"] = str(exc)
|
|
55
|
+
sys.stdout.write(json.dumps(error) + "\n")
|
|
56
|
+
raise typer.Exit(code=1)
|
|
57
|
+
|
|
31
58
|
if isinstance(exc, httpx.HTTPStatusError):
|
|
32
59
|
status = exc.response.status_code
|
|
33
60
|
if status == 401:
|
|
@@ -48,6 +75,30 @@ def _handle_api_error(exc: Exception):
|
|
|
48
75
|
raise typer.Exit(code=1)
|
|
49
76
|
|
|
50
77
|
|
|
78
|
+
def _validation_error(detail: str):
|
|
79
|
+
"""Print a validation error (JSON or Rich) and exit with code 1."""
|
|
80
|
+
if _json_mode:
|
|
81
|
+
_output_json({"error": True, "detail": detail})
|
|
82
|
+
console.print(f"[bold red]{detail}[/bold red]")
|
|
83
|
+
raise typer.Exit(code=1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _validate_title(title: str) -> str:
|
|
87
|
+
"""Strip whitespace and newlines from title, validate non-empty."""
|
|
88
|
+
cleaned = title.replace("\n", " ").replace("\r", " ").strip()
|
|
89
|
+
if not cleaned:
|
|
90
|
+
_validation_error("Title cannot be empty")
|
|
91
|
+
return cleaned
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_name(name: str) -> str:
|
|
95
|
+
"""Strip and validate non-empty name for contexts."""
|
|
96
|
+
cleaned = name.strip()
|
|
97
|
+
if not cleaned:
|
|
98
|
+
_validation_error("Name cannot be empty")
|
|
99
|
+
return cleaned
|
|
100
|
+
|
|
101
|
+
|
|
51
102
|
def _print_notes(task: dict) -> None:
|
|
52
103
|
if task.get("notes"):
|
|
53
104
|
console.print(f" [dim]{task['notes']}[/dim]")
|
|
@@ -72,11 +123,16 @@ def _print_done_task(task: dict) -> None:
|
|
|
72
123
|
|
|
73
124
|
|
|
74
125
|
@app.callback()
|
|
75
|
-
def main(
|
|
126
|
+
def main(
|
|
127
|
+
ctx: typer.Context,
|
|
128
|
+
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON (for scripting and AI agents)"),
|
|
129
|
+
):
|
|
76
130
|
"""Doing - track what you're working on."""
|
|
131
|
+
global _json_mode
|
|
132
|
+
_json_mode = json_output
|
|
77
133
|
if ctx.invoked_subcommand is None:
|
|
78
134
|
_require_auth()
|
|
79
|
-
ls(
|
|
135
|
+
ls()
|
|
80
136
|
elif ctx.invoked_subcommand not in AUTH_COMMANDS:
|
|
81
137
|
_require_auth()
|
|
82
138
|
|
|
@@ -91,6 +147,8 @@ def login():
|
|
|
91
147
|
except Exception as exc:
|
|
92
148
|
_handle_api_error(exc)
|
|
93
149
|
save_token(data["access_token"])
|
|
150
|
+
if _json_mode:
|
|
151
|
+
_output_json({"ok": True, "email": email})
|
|
94
152
|
console.print(f"[bold green]Logged in[/bold green] as [bold]{email}[/bold]")
|
|
95
153
|
|
|
96
154
|
|
|
@@ -101,6 +159,8 @@ def register():
|
|
|
101
159
|
password = typer.prompt("Password", hide_input=True)
|
|
102
160
|
password_confirm = typer.prompt("Confirm password", hide_input=True)
|
|
103
161
|
if password != password_confirm:
|
|
162
|
+
if _json_mode:
|
|
163
|
+
_output_json({"error": True, "detail": "Passwords do not match"})
|
|
104
164
|
console.print("[bold red]Passwords do not match.[/bold red]")
|
|
105
165
|
raise typer.Exit(code=1)
|
|
106
166
|
try:
|
|
@@ -108,6 +168,8 @@ def register():
|
|
|
108
168
|
except Exception as exc:
|
|
109
169
|
_handle_api_error(exc)
|
|
110
170
|
save_token(data["access_token"])
|
|
171
|
+
if _json_mode:
|
|
172
|
+
_output_json({"ok": True, "email": email})
|
|
111
173
|
console.print(f"[bold green]Account created![/bold green] Logged in as [bold]{email}[/bold]")
|
|
112
174
|
|
|
113
175
|
|
|
@@ -115,6 +177,8 @@ def register():
|
|
|
115
177
|
def logout():
|
|
116
178
|
"""Log out and clear stored credentials."""
|
|
117
179
|
clear_token()
|
|
180
|
+
if _json_mode:
|
|
181
|
+
_output_json({"ok": True})
|
|
118
182
|
console.print("[dim]Logged out.[/dim]")
|
|
119
183
|
|
|
120
184
|
|
|
@@ -126,12 +190,30 @@ def apikey(
|
|
|
126
190
|
if key is None:
|
|
127
191
|
key = typer.prompt("API key", hide_input=True)
|
|
128
192
|
if not key.startswith("doing_"):
|
|
193
|
+
if _json_mode:
|
|
194
|
+
_output_json({"error": True, "detail": "Invalid API key. Keys start with doing_"})
|
|
129
195
|
console.print("[bold red]Invalid API key.[/bold red] Keys start with [bold]doing_[/bold]")
|
|
130
196
|
raise typer.Exit(code=1)
|
|
131
197
|
save_token(key)
|
|
198
|
+
if _json_mode:
|
|
199
|
+
_output_json({"ok": True})
|
|
132
200
|
console.print("[bold green]API key saved.[/bold green] You're all set.")
|
|
133
201
|
|
|
134
202
|
|
|
203
|
+
@app.command()
|
|
204
|
+
def me():
|
|
205
|
+
"""Show current user info (useful for verifying auth)."""
|
|
206
|
+
try:
|
|
207
|
+
user = api.get_me()
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
_handle_api_error(exc)
|
|
210
|
+
if _json_mode:
|
|
211
|
+
_output_json(user)
|
|
212
|
+
console.print(f" [bold]{user['email']}[/bold] [dim](#{user['id']})[/dim]")
|
|
213
|
+
if user.get("is_admin"):
|
|
214
|
+
console.print(" [yellow]Admin[/yellow]")
|
|
215
|
+
|
|
216
|
+
|
|
135
217
|
@app.command()
|
|
136
218
|
def add(
|
|
137
219
|
title: str = typer.Argument(..., help="What are you working on?"),
|
|
@@ -139,26 +221,47 @@ def add(
|
|
|
139
221
|
context: int = typer.Option(None, "--context", "-c", help="Context ID"),
|
|
140
222
|
):
|
|
141
223
|
"""Add something to your plate."""
|
|
224
|
+
title = _validate_title(title)
|
|
142
225
|
try:
|
|
143
226
|
task = api.create_task(title, notes, context)
|
|
144
227
|
except Exception as exc:
|
|
145
228
|
_handle_api_error(exc)
|
|
229
|
+
if _json_mode:
|
|
230
|
+
_output_json(task)
|
|
146
231
|
console.print(f"[green bold]●[/green bold] Added: [bold]{task['title']}[/bold] [dim](#{task['id']})[/dim]")
|
|
147
232
|
|
|
148
233
|
|
|
234
|
+
@app.command()
|
|
235
|
+
def get(
|
|
236
|
+
task_id: int = typer.Argument(..., help="ID of the task to fetch"),
|
|
237
|
+
):
|
|
238
|
+
"""Get a single task by ID."""
|
|
239
|
+
try:
|
|
240
|
+
task = api.get_task(task_id)
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
_handle_api_error(exc)
|
|
243
|
+
if _json_mode:
|
|
244
|
+
_output_json(task)
|
|
245
|
+
status_icon = {"active": "[green bold]●[/green bold]", "paused": "[yellow]⏸[/yellow]", "done": "[green]✓[/green]"}
|
|
246
|
+
icon = status_icon.get(task["status"], "?")
|
|
247
|
+
ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
|
|
248
|
+
console.print(f" {icon} [dim]#{task['id']}[/dim] {task['title']}{ctx}")
|
|
249
|
+
if task.get("notes"):
|
|
250
|
+
console.print(f" [dim]{task['notes']}[/dim]")
|
|
251
|
+
console.print(f" [dim]Status: {task['status']} Created: {task['created_at']}[/dim]")
|
|
252
|
+
|
|
253
|
+
|
|
149
254
|
@app.command()
|
|
150
255
|
def ls(
|
|
151
256
|
context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
|
|
152
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
153
257
|
):
|
|
154
258
|
"""Show what's on your plate right now."""
|
|
155
259
|
try:
|
|
156
260
|
tasks = api.list_tasks(status="active", context_id=context)
|
|
157
261
|
except Exception as exc:
|
|
158
262
|
_handle_api_error(exc)
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
raise typer.Exit()
|
|
263
|
+
if _json_mode:
|
|
264
|
+
_output_json(tasks)
|
|
162
265
|
if not tasks:
|
|
163
266
|
console.print("Nothing on your plate. Add something: [bold]doing add 'task name'[/bold]")
|
|
164
267
|
raise typer.Exit()
|
|
@@ -170,7 +273,6 @@ def ls(
|
|
|
170
273
|
def log(
|
|
171
274
|
date: str = typer.Option(None, "--date", "-d", help="Date (YYYY-MM-DD), defaults to today"),
|
|
172
275
|
context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
|
|
173
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
174
276
|
):
|
|
175
277
|
"""List tasks marked done today (or on a given date)."""
|
|
176
278
|
from datetime import date as date_cls
|
|
@@ -179,9 +281,8 @@ def log(
|
|
|
179
281
|
tasks = api.list_tasks(status="done", date=query_date, context_id=context)
|
|
180
282
|
except Exception as exc:
|
|
181
283
|
_handle_api_error(exc)
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
raise typer.Exit()
|
|
284
|
+
if _json_mode:
|
|
285
|
+
_output_json(tasks)
|
|
185
286
|
if not tasks:
|
|
186
287
|
label = query_date if date else "today"
|
|
187
288
|
console.print(f"Nothing marked done {label}.")
|
|
@@ -199,6 +300,8 @@ def done(
|
|
|
199
300
|
task = api.mark_done(task_id)
|
|
200
301
|
except Exception as exc:
|
|
201
302
|
_handle_api_error(exc)
|
|
303
|
+
if _json_mode:
|
|
304
|
+
_output_json(task)
|
|
202
305
|
console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
|
|
203
306
|
|
|
204
307
|
|
|
@@ -211,6 +314,8 @@ def reopen(
|
|
|
211
314
|
task = api.reopen_task(task_id)
|
|
212
315
|
except Exception as exc:
|
|
213
316
|
_handle_api_error(exc)
|
|
317
|
+
if _json_mode:
|
|
318
|
+
_output_json(task)
|
|
214
319
|
console.print(f"[green bold]●[/green bold] Reopened: [bold]{task['title']}[/bold]")
|
|
215
320
|
|
|
216
321
|
|
|
@@ -223,6 +328,8 @@ def pause(
|
|
|
223
328
|
task = api.pause_task(task_id)
|
|
224
329
|
except Exception as exc:
|
|
225
330
|
_handle_api_error(exc)
|
|
331
|
+
if _json_mode:
|
|
332
|
+
_output_json(task)
|
|
226
333
|
console.print(f"[yellow]⏸[/yellow] Paused: [bold]{task['title']}[/bold]")
|
|
227
334
|
|
|
228
335
|
|
|
@@ -235,6 +342,8 @@ def resume(
|
|
|
235
342
|
task = api.resume_task(task_id)
|
|
236
343
|
except Exception as exc:
|
|
237
344
|
_handle_api_error(exc)
|
|
345
|
+
if _json_mode:
|
|
346
|
+
_output_json(task)
|
|
238
347
|
console.print(f"[green bold]●[/green bold] Resumed: [bold]{task['title']}[/bold]")
|
|
239
348
|
|
|
240
349
|
|
|
@@ -247,18 +356,29 @@ def edit(
|
|
|
247
356
|
no_context: bool = typer.Option(False, "--no-context", help="Remove context"),
|
|
248
357
|
):
|
|
249
358
|
"""Edit a task's title, notes, or context."""
|
|
359
|
+
if no_context and context is not None:
|
|
360
|
+
_validation_error("--no-context and --context cannot be used together")
|
|
361
|
+
|
|
362
|
+
if title is None and notes is None and context is None and not no_context:
|
|
363
|
+
_validation_error("No changes specified. Use --title, --notes, --context, or --no-context.")
|
|
364
|
+
|
|
365
|
+
# BUG-8: Validate title if provided
|
|
366
|
+
if title is not None:
|
|
367
|
+
title = _validate_title(title)
|
|
368
|
+
|
|
250
369
|
try:
|
|
251
370
|
task = api.update_task(task_id, title=title, notes=notes,
|
|
252
371
|
context_id=context, clear_context=no_context)
|
|
253
372
|
except Exception as exc:
|
|
254
373
|
_handle_api_error(exc)
|
|
374
|
+
if _json_mode:
|
|
375
|
+
_output_json(task)
|
|
255
376
|
console.print(f"[green bold]Updated:[/green bold] {task['title']} [dim](#{task['id']})[/dim]")
|
|
256
377
|
|
|
257
378
|
|
|
258
379
|
@app.command()
|
|
259
380
|
def today(
|
|
260
381
|
context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
|
|
261
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
262
382
|
):
|
|
263
383
|
"""Show everything from today, grouped by status."""
|
|
264
384
|
try:
|
|
@@ -266,9 +386,8 @@ def today(
|
|
|
266
386
|
except Exception as exc:
|
|
267
387
|
_handle_api_error(exc)
|
|
268
388
|
|
|
269
|
-
if
|
|
270
|
-
|
|
271
|
-
raise typer.Exit()
|
|
389
|
+
if _json_mode:
|
|
390
|
+
_output_json(data)
|
|
272
391
|
|
|
273
392
|
active = data.get("active", [])
|
|
274
393
|
paused = data.get("paused", [])
|
|
@@ -305,6 +424,8 @@ def rm(
|
|
|
305
424
|
api.delete_task(task_id)
|
|
306
425
|
except Exception as exc:
|
|
307
426
|
_handle_api_error(exc)
|
|
427
|
+
if _json_mode:
|
|
428
|
+
_output_json({"deleted": True, "id": task_id})
|
|
308
429
|
console.print(f"[dim]Deleted task #{task_id}.[/dim]")
|
|
309
430
|
|
|
310
431
|
|
|
@@ -317,16 +438,25 @@ def clear():
|
|
|
317
438
|
_handle_api_error(exc)
|
|
318
439
|
|
|
319
440
|
if not tasks:
|
|
441
|
+
if _json_mode:
|
|
442
|
+
_output_json([])
|
|
320
443
|
console.print("Nothing to clear.")
|
|
321
444
|
raise typer.Exit()
|
|
322
445
|
|
|
446
|
+
results = []
|
|
323
447
|
for t in tasks:
|
|
324
448
|
try:
|
|
325
|
-
api.mark_done(t["id"])
|
|
449
|
+
result = api.mark_done(t["id"])
|
|
450
|
+
results.append(result)
|
|
326
451
|
except Exception:
|
|
327
|
-
|
|
452
|
+
if not _json_mode:
|
|
453
|
+
console.print(f"[red]Failed to complete task #{t['id']}[/red]")
|
|
328
454
|
continue
|
|
329
|
-
|
|
455
|
+
if not _json_mode:
|
|
456
|
+
console.print(f"[green]✓[/green] Done: [bold]{t['title']}[/bold]")
|
|
457
|
+
|
|
458
|
+
if _json_mode:
|
|
459
|
+
_output_json(results)
|
|
330
460
|
|
|
331
461
|
console.print(f"\n[dim]Cleared {len(tasks)} task(s).[/dim]")
|
|
332
462
|
|
|
@@ -338,6 +468,8 @@ def contexts():
|
|
|
338
468
|
ctx_list = api.list_contexts()
|
|
339
469
|
except Exception as exc:
|
|
340
470
|
_handle_api_error(exc)
|
|
471
|
+
if _json_mode:
|
|
472
|
+
_output_json(ctx_list)
|
|
341
473
|
if not ctx_list:
|
|
342
474
|
console.print("No contexts. Create one: [bold]doing context add 'name'[/bold]")
|
|
343
475
|
raise typer.Exit()
|
|
@@ -350,11 +482,13 @@ def context_add(
|
|
|
350
482
|
name: str = typer.Argument(..., help="Context name"),
|
|
351
483
|
):
|
|
352
484
|
"""Create a new context."""
|
|
353
|
-
|
|
485
|
+
name = _validate_name(name)
|
|
354
486
|
try:
|
|
355
487
|
ctx = api.create_context(name)
|
|
356
488
|
except Exception as exc:
|
|
357
489
|
_handle_api_error(exc)
|
|
490
|
+
if _json_mode:
|
|
491
|
+
_output_json(ctx)
|
|
358
492
|
console.print(f"[green bold]Created context:[/green bold] {ctx['name']} [dim](#{ctx['id']})[/dim]")
|
|
359
493
|
|
|
360
494
|
|
|
@@ -363,11 +497,12 @@ def context_rm(
|
|
|
363
497
|
context_id: int = typer.Argument(..., help="Context ID to delete"),
|
|
364
498
|
):
|
|
365
499
|
"""Delete a context (tasks keep their data, just lose the context)."""
|
|
366
|
-
_require_auth()
|
|
367
500
|
try:
|
|
368
501
|
api.delete_context(context_id)
|
|
369
502
|
except Exception as exc:
|
|
370
503
|
_handle_api_error(exc)
|
|
504
|
+
if _json_mode:
|
|
505
|
+
_output_json({"deleted": True, "id": context_id})
|
|
371
506
|
console.print(f"[dim]Deleted context #{context_id}.[/dim]")
|
|
372
507
|
|
|
373
508
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doingapp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A fast CLI task tracker — track what you're doing from the terminal.
|
|
5
5
|
Author-email: Parker Grimes <parker@altellus.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -62,27 +62,55 @@ doing # See what's on your plate
|
|
|
62
62
|
doing done 1 # Mark task #1 as done
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Or authenticate with an API key (get one from the web UI at Settings → API Keys):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
doing apikey --key doing_xxxxx
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
## Commands
|
|
66
72
|
|
|
67
73
|
| Command | Description |
|
|
68
74
|
|---------|-------------|
|
|
69
75
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
70
76
|
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
77
|
+
| `doing get <id>` | Get a single task by ID |
|
|
71
78
|
| `doing done <id>` | Mark a task as done |
|
|
72
79
|
| `doing pause <id>` | Pause a task |
|
|
73
80
|
| `doing resume <id>` | Resume a paused task |
|
|
74
81
|
| `doing reopen <id>` | Reopen a completed task |
|
|
75
82
|
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
76
83
|
| `doing rm <id>` | Delete a task |
|
|
77
|
-
| `doing ls` | List active tasks |
|
|
78
|
-
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`
|
|
79
|
-
| `doing today` | Show all tasks grouped by status |
|
|
84
|
+
| `doing ls` | List active tasks (`-c` context ID) |
|
|
85
|
+
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
86
|
+
| `doing today` | Show all tasks grouped by status (`-c` context ID) |
|
|
80
87
|
| `doing clear` | Mark all active tasks as done |
|
|
81
88
|
| `doing contexts` | List contexts |
|
|
82
89
|
| `doing context add <name>` | Create a context |
|
|
83
90
|
| `doing context rm <id>` | Delete a context |
|
|
91
|
+
| `doing me` | Show current user info |
|
|
84
92
|
| `doing login` | Log in |
|
|
85
93
|
| `doing logout` | Log out |
|
|
94
|
+
| `doing apikey` | Save an API key (`-k` key) |
|
|
95
|
+
|
|
96
|
+
## JSON output
|
|
97
|
+
|
|
98
|
+
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
doing --json ls # [{"id": 1, "title": "...", ...}]
|
|
102
|
+
doing --json add "New task" # {"id": 2, "title": "New task", ...}
|
|
103
|
+
doing --json done 2 # {"id": 2, "status": "done", ...}
|
|
104
|
+
doing --json contexts # [{"id": 1, "name": "Work", ...}]
|
|
105
|
+
doing --json get 2 # {"id": 2, "title": "...", ...}
|
|
106
|
+
doing --json me # {"id": 1, "email": "...", ...}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Errors also return structured JSON with a non-zero exit code:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
doing --json done 99999 # {"error": true, "status": 404, "detail": "Task not found"}
|
|
113
|
+
```
|
|
86
114
|
|
|
87
115
|
## Configuration
|
|
88
116
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|