@andypai/agent-kanban 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/package.json +80 -0
- package/src/__tests__/activity.test.ts +139 -0
- package/src/__tests__/api.test.ts +74 -0
- package/src/__tests__/commands/board.test.ts +51 -0
- package/src/__tests__/commands/bulk.test.ts +51 -0
- package/src/__tests__/commands/column.test.ts +78 -0
- package/src/__tests__/commands/task.test.ts +144 -0
- package/src/__tests__/db.test.ts +327 -0
- package/src/__tests__/id.test.ts +19 -0
- package/src/__tests__/index.test.ts +75 -0
- package/src/__tests__/metrics.test.ts +64 -0
- package/src/__tests__/output.test.ts +39 -0
- package/src/activity.ts +73 -0
- package/src/api.ts +209 -0
- package/src/commands/board.ts +29 -0
- package/src/commands/bulk.ts +19 -0
- package/src/commands/column.ts +60 -0
- package/src/commands/task.ts +117 -0
- package/src/config.ts +29 -0
- package/src/db.ts +587 -0
- package/src/errors.ts +32 -0
- package/src/fixtures.ts +128 -0
- package/src/id.ts +8 -0
- package/src/index.ts +413 -0
- package/src/metrics.ts +98 -0
- package/src/output.ts +105 -0
- package/src/providers/capabilities.ts +25 -0
- package/src/providers/errors.ts +16 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/linear-cache.ts +385 -0
- package/src/providers/linear-client.ts +329 -0
- package/src/providers/linear.ts +305 -0
- package/src/providers/local.ts +135 -0
- package/src/providers/types.ts +65 -0
- package/src/server.ts +91 -0
- package/src/types.ts +123 -0
- package/ui/dist/assets/index-DEnUD0fq.css +1 -0
- package/ui/dist/assets/index-DMRjw1nI.js +40 -0
- package/ui/dist/index.html +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 abpai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# agent-kanban
|
|
2
|
+
|
|
3
|
+
[](https://github.com/abpai/agent-kanban/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Agent-friendly kanban board CLI. Manage tasks via bash commands, parse structured JSON output.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Most project-management tools are built for humans clicking through UIs. `agent-kanban` is built for **CLI-first workflows** — AI agents and scripts get deterministic JSON they can parse, humans get a pretty-printed view and a web dashboard. Runs against a local SQLite file or a Linear backend.
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
- [`docs/README.md`](docs/README.md) for the documentation index
|
|
14
|
+
- [`docs/WORKFLOW.md`](docs/WORKFLOW.md) for a common day-to-day workflow
|
|
15
|
+
- [`docs/providers/linear.md`](docs/providers/linear.md) for Linear provider details
|
|
16
|
+
- [`SKILL.md`](SKILL.md) for agent-specific repo usage instructions
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun install -g @andypai/agent-kanban
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`agent-kanban` targets the Bun runtime. Install Bun first if it is not already available on your machine.
|
|
25
|
+
|
|
26
|
+
### Local development
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/abpai/agent-kanban.git
|
|
30
|
+
cd agent-kanban
|
|
31
|
+
bun install
|
|
32
|
+
cd ui && bun install && cd ..
|
|
33
|
+
bun link
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`bun link` makes `kanban` available globally while you work on the source checkout.
|
|
37
|
+
|
|
38
|
+
## Getting started
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
kanban board init
|
|
42
|
+
kanban task add "Set up CI pipeline" -p high -a alice
|
|
43
|
+
kanban task add "Write integration tests" -c backlog
|
|
44
|
+
kanban board view --pretty
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Running `kanban` with no arguments is equivalent to `kanban board view`.
|
|
48
|
+
|
|
49
|
+
## Providers
|
|
50
|
+
|
|
51
|
+
All operations route through a provider backend. Set `KANBAN_PROVIDER` to choose one.
|
|
52
|
+
|
|
53
|
+
| Variable | Default | Description |
|
|
54
|
+
| ----------------- | ------------- | -------------------------------------- |
|
|
55
|
+
| `KANBAN_PROVIDER` | `local` | `local` or `linear` |
|
|
56
|
+
| `KANBAN_DB_PATH` | auto-resolved | SQLite database path |
|
|
57
|
+
| `LINEAR_API_KEY` | — | Required when `KANBAN_PROVIDER=linear` |
|
|
58
|
+
| `LINEAR_TEAM_ID` | — | Required when `KANBAN_PROVIDER=linear` |
|
|
59
|
+
|
|
60
|
+
Without `KANBAN_DB_PATH`, the local provider resolves the database in this order:
|
|
61
|
+
|
|
62
|
+
1. `./.kanban/board.db` if it exists in the current working directory
|
|
63
|
+
2. `~/.kanban/board.db` if it exists
|
|
64
|
+
3. `./.kanban/board.db` as the path to create
|
|
65
|
+
|
|
66
|
+
### Linear quick start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
export KANBAN_PROVIDER=linear
|
|
70
|
+
export LINEAR_API_KEY=lin_api_...
|
|
71
|
+
export LINEAR_TEAM_ID=<team-id>
|
|
72
|
+
kanban board view
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Capability matrix
|
|
76
|
+
|
|
77
|
+
| Capability | Local | Linear |
|
|
78
|
+
| ----------------------- | ----- | ------ |
|
|
79
|
+
| task create/update/move | yes | yes |
|
|
80
|
+
| task delete | yes | no |
|
|
81
|
+
| activity log | yes | no |
|
|
82
|
+
| metrics | yes | no |
|
|
83
|
+
| column CRUD | yes | no |
|
|
84
|
+
| bulk operations | yes | no |
|
|
85
|
+
| config edit | yes | no |
|
|
86
|
+
|
|
87
|
+
Linear tasks carry an `externalRef` (e.g. `TEAM-123`) and a `url`. Commands accept either the internal ID or the external ref.
|
|
88
|
+
|
|
89
|
+
Unsupported operations return error code `UNSUPPORTED_OPERATION` with exit code 1.
|
|
90
|
+
|
|
91
|
+
## Commands
|
|
92
|
+
|
|
93
|
+
### board
|
|
94
|
+
|
|
95
|
+
| Command | Description |
|
|
96
|
+
| -------------------- | -------------------------------------------------- |
|
|
97
|
+
| `kanban board init` | Initialize a new board with default columns |
|
|
98
|
+
| `kanban board view` | View the full board (default command) |
|
|
99
|
+
| `kanban board reset` | Reset board — drops all data and restores defaults |
|
|
100
|
+
|
|
101
|
+
Default columns: `recurring`, `backlog`, `in-progress`, `review`, `done`.
|
|
102
|
+
|
|
103
|
+
### task
|
|
104
|
+
|
|
105
|
+
| Command | Description |
|
|
106
|
+
| ------------------------------------- | ------------------------------------------------ |
|
|
107
|
+
| `kanban task add <title>` | Add a task |
|
|
108
|
+
| `kanban task list` | List tasks |
|
|
109
|
+
| `kanban task view <id>` | View task details |
|
|
110
|
+
| `kanban task update <id>` | Update task fields |
|
|
111
|
+
| `kanban task delete <id>` | Delete a task |
|
|
112
|
+
| `kanban task move <id> <column>` | Move task to a column |
|
|
113
|
+
| `kanban task assign <id> <user>` | Assign task to a user |
|
|
114
|
+
| `kanban task prioritize <id> <level>` | Set priority (`low`, `medium`, `high`, `urgent`) |
|
|
115
|
+
|
|
116
|
+
**Flags for `task add`:**
|
|
117
|
+
|
|
118
|
+
| Flag | Description |
|
|
119
|
+
| ------------------ | --------------------------------------------------------------- |
|
|
120
|
+
| `-d <text>` | Description |
|
|
121
|
+
| `-c <column>` | Target column (default: `backlog`) |
|
|
122
|
+
| `-p <level>` | Priority: `low`, `medium`, `high`, `urgent` (default: `medium`) |
|
|
123
|
+
| `-a <user>` | Assignee |
|
|
124
|
+
| `--project <name>` | Project tag |
|
|
125
|
+
| `-m <json>` | Arbitrary metadata (must be valid JSON) |
|
|
126
|
+
|
|
127
|
+
**Flags for `task list`:**
|
|
128
|
+
|
|
129
|
+
| Flag | Description |
|
|
130
|
+
| ------------------ | -------------------------------------------------------------- |
|
|
131
|
+
| `-c <column>` | Filter by column |
|
|
132
|
+
| `-p <level>` | Filter by priority |
|
|
133
|
+
| `-a <user>` | Filter by assignee |
|
|
134
|
+
| `--project <name>` | Filter by project |
|
|
135
|
+
| `-l <n>` | Limit results |
|
|
136
|
+
| `--sort <field>` | Sort by: `priority`, `created`, `updated`, `position`, `title` |
|
|
137
|
+
|
|
138
|
+
**Flags for `task update`:**
|
|
139
|
+
|
|
140
|
+
| Flag | Description |
|
|
141
|
+
| ------------------ | --------------- |
|
|
142
|
+
| `--title <text>` | New title |
|
|
143
|
+
| `-d <text>` | New description |
|
|
144
|
+
| `-p <level>` | New priority |
|
|
145
|
+
| `-a <user>` | New assignee |
|
|
146
|
+
| `--project <name>` | New project |
|
|
147
|
+
| `-m <json>` | New metadata |
|
|
148
|
+
|
|
149
|
+
### column
|
|
150
|
+
|
|
151
|
+
| Command | Description |
|
|
152
|
+
| --------------------------------------------- | ----------------------- |
|
|
153
|
+
| `kanban column add <name>` | Add a column |
|
|
154
|
+
| `kanban column list` | List all columns |
|
|
155
|
+
| `kanban column rename <id\|name> <new-name>` | Rename a column |
|
|
156
|
+
| `kanban column reorder <id\|name> <position>` | Move column to position |
|
|
157
|
+
| `kanban column delete <id\|name>` | Delete an empty column |
|
|
158
|
+
|
|
159
|
+
**Flags for `column add`:**
|
|
160
|
+
|
|
161
|
+
| Flag | Description |
|
|
162
|
+
| ---------------- | ------------------------------ |
|
|
163
|
+
| `--position <n>` | Insert at position (0-indexed) |
|
|
164
|
+
| `--color <hex>` | Column color |
|
|
165
|
+
|
|
166
|
+
### bulk
|
|
167
|
+
|
|
168
|
+
| Command | Description |
|
|
169
|
+
| ---------------------------------- | ----------------------------------------- |
|
|
170
|
+
| `kanban bulk move-all <from> <to>` | Move all tasks from one column to another |
|
|
171
|
+
| `kanban bulk clear-done` | Delete all tasks in the `done` column |
|
|
172
|
+
|
|
173
|
+
### config
|
|
174
|
+
|
|
175
|
+
| Command | Description |
|
|
176
|
+
| ------------------------------------- | --------------------------- |
|
|
177
|
+
| `kanban config show` | Show board config (default) |
|
|
178
|
+
| `kanban config set-member <name>` | Add or update a team member |
|
|
179
|
+
| `kanban config remove-member <name>` | Remove a team member |
|
|
180
|
+
| `kanban config add-project <name>` | Register a project |
|
|
181
|
+
| `kanban config remove-project <name>` | Remove a project |
|
|
182
|
+
|
|
183
|
+
**Flags for `config set-member`:**
|
|
184
|
+
|
|
185
|
+
| Flag | Description |
|
|
186
|
+
| ----------------------- | ------------------------------ |
|
|
187
|
+
| `--role <human\|agent>` | Member role (default: `human`) |
|
|
188
|
+
|
|
189
|
+
### serve
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
kanban serve # default port 3000
|
|
193
|
+
kanban serve --port 8080
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Global flags
|
|
197
|
+
|
|
198
|
+
| Flag | Description |
|
|
199
|
+
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
|
200
|
+
| `--pretty` | Human-readable output instead of JSON |
|
|
201
|
+
| `--db <path>` | Database path (default: local first, then `~/.kanban`, else create local; env: `KANBAN_DB_PATH`) |
|
|
202
|
+
| `--project <name>` | Filter or set project context |
|
|
203
|
+
| `-h`, `--help` | Show help text |
|
|
204
|
+
|
|
205
|
+
## Output format
|
|
206
|
+
|
|
207
|
+
Every command returns a JSON envelope on stdout:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{ "ok": true, "data": { ... } }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{ "ok": false, "error": { "code": "TASK_NOT_FOUND", "message": "No task with id 't_abc123'" } }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Exit codes: `0` success, `1` known error, `2` internal error.
|
|
218
|
+
|
|
219
|
+
Pass `--pretty` for human-readable output — board view, task lists, and details are formatted for the terminal.
|
|
220
|
+
|
|
221
|
+
## Web dashboard
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
kanban serve
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
In Linear mode the dashboard hides unsupported actions and shows Linear issue identifiers and links on task cards.
|
|
228
|
+
|
|
229
|
+
Starts a Bun HTTP server with:
|
|
230
|
+
|
|
231
|
+
- **REST API** at `/api/*` — same operations as the CLI (board, tasks, columns, activity, metrics, config)
|
|
232
|
+
- **WebSocket** at `/ws` — push notifications on board mutations (clients receive `{"type":"refresh"}`)
|
|
233
|
+
- **Static UI** served from `ui/dist/` (build with `bun run build:ui` or `bun run ui:build`)
|
|
234
|
+
- **Health check** at `/api/health`
|
|
235
|
+
|
|
236
|
+
## Scripts
|
|
237
|
+
|
|
238
|
+
| Script | Description |
|
|
239
|
+
| -------------------- | ---------------------- |
|
|
240
|
+
| `bun run dev` | Run with watch mode |
|
|
241
|
+
| `bun run start` | Run once |
|
|
242
|
+
| `bun run build` | Bundle to `dist/` |
|
|
243
|
+
| `bun run lint` | ESLint |
|
|
244
|
+
| `bun run format` | Prettier write |
|
|
245
|
+
| `bun run typecheck` | `tsc --noEmit` |
|
|
246
|
+
| `bun run check` | Lint + typecheck |
|
|
247
|
+
| `bun run test` | Bun test runner |
|
|
248
|
+
| `bun run test:watch` | Tests in watch mode |
|
|
249
|
+
| `bun run serve` | Start web dashboard |
|
|
250
|
+
| `bun run ui:dev` | UI dev server |
|
|
251
|
+
| `bun run dev:ui` | API + UI dev servers |
|
|
252
|
+
| `bun run ui:build` | Build UI to `ui/dist/` |
|
|
253
|
+
| `bun run build:ui` | Alias for `ui:build` |
|
|
254
|
+
|
|
255
|
+
## Deployment
|
|
256
|
+
|
|
257
|
+
Build the Docker image:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
docker build -t agent-kanban .
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The same image works for both provider modes — only runtime env/volume config differs.
|
|
264
|
+
|
|
265
|
+
### Local mode (SQLite)
|
|
266
|
+
|
|
267
|
+
Mount a volume for the database directory. WAL mode creates `-wal` and `-shm` sibling files, so the volume must cover the directory, not just the `.db` file.
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
docker run -d \
|
|
271
|
+
-p 3000:3000 \
|
|
272
|
+
-v kanban-data:/data \
|
|
273
|
+
-e KANBAN_DB_PATH=/data/board.db \
|
|
274
|
+
agent-kanban
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Linear mode
|
|
278
|
+
|
|
279
|
+
No volume needed — all state lives in Linear.
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
docker run -d \
|
|
283
|
+
-p 3000:3000 \
|
|
284
|
+
-e KANBAN_PROVIDER=linear \
|
|
285
|
+
-e LINEAR_API_KEY=lin_api_... \
|
|
286
|
+
-e LINEAR_TEAM_ID=team-id \
|
|
287
|
+
agent-kanban
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Dokploy
|
|
291
|
+
|
|
292
|
+
Set the port via `PORT` env var (defaults to `3000`). Port resolution order: `--port` flag → `PORT` env → `3000`. Add provider env vars through Dokploy's environment configuration.
|
|
293
|
+
|
|
294
|
+
## Community
|
|
295
|
+
|
|
296
|
+
If you want to contribute or report an issue, start with these guides:
|
|
297
|
+
|
|
298
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
299
|
+
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
300
|
+
- [SECURITY.md](SECURITY.md)
|
|
301
|
+
|
|
302
|
+
Longer product and workflow docs live under [`docs/`](docs/README.md).
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@andypai/agent-kanban",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent-friendly kanban board CLI. Manage tasks via bash commands, parse structured JSON output.",
|
|
5
|
+
"homepage": "https://github.com/abpai/agent-kanban#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/abpai/agent-kanban.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/abpai/agent-kanban/issues"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent",
|
|
15
|
+
"kanban",
|
|
16
|
+
"cli",
|
|
17
|
+
"bun",
|
|
18
|
+
"sqlite",
|
|
19
|
+
"linear",
|
|
20
|
+
"automation"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"kanban": "src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "src/index.ts",
|
|
27
|
+
"module": "src/index.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"ui/dist"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"bun": ">=1.1.0"
|
|
34
|
+
},
|
|
35
|
+
"packageManager": "bun@1.3.3",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"scripts": {
|
|
38
|
+
"dev": "bun --watch src/index.ts",
|
|
39
|
+
"start": "bun src/index.ts",
|
|
40
|
+
"build": "bun build ./src/index.ts --target bun --outdir ./dist",
|
|
41
|
+
"link": "bun link",
|
|
42
|
+
"lint": "bunx eslint .",
|
|
43
|
+
"format": "bunx prettier --write .",
|
|
44
|
+
"typecheck": "bunx tsc --noEmit",
|
|
45
|
+
"check": "bun run lint && bun run typecheck",
|
|
46
|
+
"test": "bun test",
|
|
47
|
+
"test:watch": "bun test --watch",
|
|
48
|
+
"seed:test": "bun --env-file=.env.test scripts/seed-test-db.ts",
|
|
49
|
+
"serve": "bun src/index.ts serve",
|
|
50
|
+
"ui:dev": "cd ui && bun run dev",
|
|
51
|
+
"ui:build": "cd ui && bun run build",
|
|
52
|
+
"dev:ui": "bun scripts/dev.ts",
|
|
53
|
+
"build:ui": "bun run ui:build",
|
|
54
|
+
"prepare": "if [ \"${HUSKY:-1}\" != \"0\" ] && command -v husky >/dev/null 2>&1; then husky install; fi",
|
|
55
|
+
"prepack": "cd ui && bun install --frozen-lockfile && bun run build"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@eslint/js": "^9.39.1",
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
|
61
|
+
"@typescript-eslint/parser": "^8.18.0",
|
|
62
|
+
"@types/bun": "^1.1.22",
|
|
63
|
+
"eslint": "^9.39.1",
|
|
64
|
+
"eslint-config-prettier": "^10.1.8",
|
|
65
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
66
|
+
"husky": "^8.0.3",
|
|
67
|
+
"lint-staged": "^15.2.0",
|
|
68
|
+
"prettier": "^3.7.4",
|
|
69
|
+
"typescript": "^5.7.2"
|
|
70
|
+
},
|
|
71
|
+
"lint-staged": {
|
|
72
|
+
"*.{js,jsx,ts,tsx}": [
|
|
73
|
+
"eslint --fix",
|
|
74
|
+
"prettier --write"
|
|
75
|
+
],
|
|
76
|
+
"*.{css,html,json,md,scss,yaml,yml}": [
|
|
77
|
+
"prettier --write"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test'
|
|
2
|
+
import { Database } from 'bun:sqlite'
|
|
3
|
+
import {
|
|
4
|
+
initSchema,
|
|
5
|
+
seedDefaultColumns,
|
|
6
|
+
addTask,
|
|
7
|
+
updateTask,
|
|
8
|
+
deleteTask,
|
|
9
|
+
moveTask,
|
|
10
|
+
bulkMoveAll,
|
|
11
|
+
bulkClearDone,
|
|
12
|
+
} from '../db.ts'
|
|
13
|
+
import { listActivity, getTaskActivity, getColumnTimeEntries } from '../activity.ts'
|
|
14
|
+
|
|
15
|
+
let db: Database
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
db = new Database(':memory:')
|
|
19
|
+
db.run('PRAGMA foreign_keys = ON')
|
|
20
|
+
initSchema(db)
|
|
21
|
+
seedDefaultColumns(db)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('activity logging', () => {
|
|
25
|
+
test('addTask logs created activity', () => {
|
|
26
|
+
const task = addTask(db, 'Test task')
|
|
27
|
+
const activities = getTaskActivity(db, task.id)
|
|
28
|
+
expect(activities).toHaveLength(1)
|
|
29
|
+
expect(activities[0]!.action).toBe('created')
|
|
30
|
+
expect(activities[0]!.new_value).toBe('Test task')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('updateTask logs field changes', () => {
|
|
34
|
+
const task = addTask(db, 'Original')
|
|
35
|
+
updateTask(db, task.id, { title: 'Updated', priority: 'high' })
|
|
36
|
+
const activities = getTaskActivity(db, task.id)
|
|
37
|
+
const actions = activities.map((a) => a.action)
|
|
38
|
+
expect(actions).toContain('updated')
|
|
39
|
+
expect(actions).toContain('prioritized')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('updateTask logs assignee change as assigned', () => {
|
|
43
|
+
const task = addTask(db, 'Task')
|
|
44
|
+
updateTask(db, task.id, { assignee: 'alice' })
|
|
45
|
+
const activities = getTaskActivity(db, task.id)
|
|
46
|
+
const assigned = activities.find((a) => a.action === 'assigned')
|
|
47
|
+
expect(assigned).toBeDefined()
|
|
48
|
+
expect(assigned!.new_value).toBe('alice')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('updateTask does not log when value unchanged', () => {
|
|
52
|
+
const task = addTask(db, 'Task', { priority: 'high' })
|
|
53
|
+
updateTask(db, task.id, { priority: 'high' })
|
|
54
|
+
const activities = getTaskActivity(db, task.id)
|
|
55
|
+
expect(activities.filter((a) => a.action === 'prioritized')).toHaveLength(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('deleteTask logs deleted activity', () => {
|
|
59
|
+
const task = addTask(db, 'Doomed')
|
|
60
|
+
const taskId = task.id
|
|
61
|
+
deleteTask(db, taskId)
|
|
62
|
+
const activities = listActivity(db)
|
|
63
|
+
const deleted = activities.find((a) => a.task_id === taskId && a.action === 'deleted')
|
|
64
|
+
expect(deleted).toBeDefined()
|
|
65
|
+
expect(deleted!.old_value).toBe('Doomed')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('moveTask logs moved activity', () => {
|
|
69
|
+
const task = addTask(db, 'Mobile', { column: 'recurring' })
|
|
70
|
+
moveTask(db, task.id, 'in-progress')
|
|
71
|
+
const activities = getTaskActivity(db, task.id)
|
|
72
|
+
const moved = activities.find((a) => a.action === 'moved')
|
|
73
|
+
expect(moved).toBeDefined()
|
|
74
|
+
expect(moved!.old_value).toBe('recurring')
|
|
75
|
+
expect(moved!.new_value).toBe('in-progress')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('bulkMoveAll logs moved activity for each task', () => {
|
|
79
|
+
addTask(db, 'A', { column: 'recurring' })
|
|
80
|
+
addTask(db, 'B', { column: 'recurring' })
|
|
81
|
+
bulkMoveAll(db, 'recurring', 'in-progress')
|
|
82
|
+
const activities = listActivity(db)
|
|
83
|
+
const moves = activities.filter((a) => a.action === 'moved')
|
|
84
|
+
expect(moves).toHaveLength(2)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('bulkClearDone logs deleted activity for each task', () => {
|
|
88
|
+
addTask(db, 'Done A', { column: 'done' })
|
|
89
|
+
addTask(db, 'Done B', { column: 'done' })
|
|
90
|
+
bulkClearDone(db)
|
|
91
|
+
const activities = listActivity(db)
|
|
92
|
+
const deletes = activities.filter((a) => a.action === 'deleted')
|
|
93
|
+
expect(deletes).toHaveLength(2)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('listActivity returns most recent first', () => {
|
|
97
|
+
const task = addTask(db, 'Task')
|
|
98
|
+
updateTask(db, task.id, { title: 'Updated' })
|
|
99
|
+
const activities = getTaskActivity(db, task.id)
|
|
100
|
+
expect(activities[0]!.action).toBe('updated')
|
|
101
|
+
expect(activities[1]!.action).toBe('created')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('listActivity respects limit', () => {
|
|
105
|
+
const task = addTask(db, 'Task')
|
|
106
|
+
updateTask(db, task.id, { title: 'V2' })
|
|
107
|
+
updateTask(db, task.id, { title: 'V3' })
|
|
108
|
+
const activities = listActivity(db, { limit: 2 })
|
|
109
|
+
expect(activities).toHaveLength(2)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('column time tracking', () => {
|
|
114
|
+
test('addTask creates enter record', () => {
|
|
115
|
+
const task = addTask(db, 'Task', { column: 'recurring' })
|
|
116
|
+
const entries = getColumnTimeEntries(db, task.id)
|
|
117
|
+
expect(entries).toHaveLength(1)
|
|
118
|
+
expect(entries[0]!.exited_at).toBeNull()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('moveTask creates exit and enter records', () => {
|
|
122
|
+
const task = addTask(db, 'Task', { column: 'recurring' })
|
|
123
|
+
moveTask(db, task.id, 'in-progress')
|
|
124
|
+
const entries = getColumnTimeEntries(db, task.id)
|
|
125
|
+
expect(entries).toHaveLength(2)
|
|
126
|
+
expect(entries[0]!.exited_at).not.toBeNull()
|
|
127
|
+
expect(entries[1]!.exited_at).toBeNull()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('deleteTask closes open time entry', () => {
|
|
131
|
+
const task = addTask(db, 'Task', { column: 'recurring' })
|
|
132
|
+
const taskId = task.id
|
|
133
|
+
deleteTask(db, taskId)
|
|
134
|
+
const entries = db
|
|
135
|
+
.query('SELECT * FROM column_time_tracking WHERE task_id = $id')
|
|
136
|
+
.all({ $id: taskId }) as { exited_at: string | null }[]
|
|
137
|
+
expect(entries[0]!.exited_at).not.toBeNull()
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Database } from 'bun:sqlite'
|
|
3
|
+
import { initSchema, seedDefaultColumns, addTask } from '../db.ts'
|
|
4
|
+
import { handleRequest } from '../api.ts'
|
|
5
|
+
import { createProvider } from '../providers/index.ts'
|
|
6
|
+
|
|
7
|
+
let db: Database
|
|
8
|
+
let provider: ReturnType<typeof createProvider>
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
process.env['KANBAN_PROVIDER'] = 'local'
|
|
12
|
+
db = new Database(':memory:')
|
|
13
|
+
db.run('PRAGMA foreign_keys = ON')
|
|
14
|
+
initSchema(db)
|
|
15
|
+
seedDefaultColumns(db)
|
|
16
|
+
provider = createProvider(db, ':memory:')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('handleRequest', () => {
|
|
20
|
+
test('returns API 404 envelope for unknown route', async () => {
|
|
21
|
+
const req = new Request('http://localhost/api/not-a-route', { method: 'GET' })
|
|
22
|
+
const result = await handleRequest(provider, req)
|
|
23
|
+
|
|
24
|
+
expect(result.mutated).toBe(false)
|
|
25
|
+
expect(result.response.status).toBe(404)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('marks failed PATCH mutation as not mutated', async () => {
|
|
29
|
+
const req = new Request('http://localhost/api/tasks/t_missing', {
|
|
30
|
+
method: 'PATCH',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ title: 'Nope' }),
|
|
33
|
+
})
|
|
34
|
+
const result = await handleRequest(provider, req)
|
|
35
|
+
|
|
36
|
+
expect(result.response.status).toBe(404)
|
|
37
|
+
expect(result.mutated).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('marks successful task creation as mutated', async () => {
|
|
41
|
+
const req = new Request('http://localhost/api/tasks', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ title: 'Created via API' }),
|
|
45
|
+
})
|
|
46
|
+
const result = await handleRequest(provider, req)
|
|
47
|
+
|
|
48
|
+
expect(result.response.status).toBe(200)
|
|
49
|
+
expect(result.mutated).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('marks successful task delete as mutated', async () => {
|
|
53
|
+
const task = addTask(db, 'Delete me')
|
|
54
|
+
const req = new Request(`http://localhost/api/tasks/${task.id}`, { method: 'DELETE' })
|
|
55
|
+
const result = await handleRequest(provider, req)
|
|
56
|
+
|
|
57
|
+
expect(result.response.status).toBe(200)
|
|
58
|
+
expect(result.mutated).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('returns bootstrap payload', async () => {
|
|
62
|
+
const req = new Request('http://localhost/api/bootstrap', { method: 'GET' })
|
|
63
|
+
const result = await handleRequest(provider, req)
|
|
64
|
+
const body = (await result.response.json()) as {
|
|
65
|
+
ok: boolean
|
|
66
|
+
data: { provider: string; capabilities: { taskDelete: boolean } }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(result.response.status).toBe(200)
|
|
70
|
+
expect(body.ok).toBe(true)
|
|
71
|
+
expect(body.data.provider).toBe('local')
|
|
72
|
+
expect(body.data.capabilities.taskDelete).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test'
|
|
2
|
+
import { Database } from 'bun:sqlite'
|
|
3
|
+
import { initSchema, seedDefaultColumns } from '../../db.ts'
|
|
4
|
+
import { boardInit, boardView, boardReset } from '../../commands/board.ts'
|
|
5
|
+
import { KanbanError } from '../../errors.ts'
|
|
6
|
+
|
|
7
|
+
let db: Database
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
db = new Database(':memory:')
|
|
11
|
+
db.run('PRAGMA foreign_keys = ON')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('boardInit', () => {
|
|
15
|
+
test('initializes a fresh database', () => {
|
|
16
|
+
const result = boardInit(db)
|
|
17
|
+
expect(result.ok).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('throws if already initialized', () => {
|
|
21
|
+
initSchema(db)
|
|
22
|
+
seedDefaultColumns(db)
|
|
23
|
+
expect(() => boardInit(db)).toThrow(KanbanError)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('boardView', () => {
|
|
28
|
+
test('returns board view after init', () => {
|
|
29
|
+
initSchema(db)
|
|
30
|
+
seedDefaultColumns(db)
|
|
31
|
+
const result = boardView(db)
|
|
32
|
+
expect(result.ok).toBe(true)
|
|
33
|
+
if (result.ok) {
|
|
34
|
+
const data = result.data as { columns: unknown[] }
|
|
35
|
+
expect(data.columns).toHaveLength(5)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('throws if not initialized', () => {
|
|
40
|
+
expect(() => boardView(db)).toThrow(KanbanError)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('boardReset', () => {
|
|
45
|
+
test('resets board to defaults', () => {
|
|
46
|
+
initSchema(db)
|
|
47
|
+
seedDefaultColumns(db)
|
|
48
|
+
const result = boardReset(db)
|
|
49
|
+
expect(result.ok).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
})
|