@x4lt7ab/tab-for-projects 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.
Files changed (44) hide show
  1. package/README.md +266 -0
  2. package/package.json +39 -0
  3. package/src/domain/args.ts +41 -0
  4. package/src/domain/bootstrap.ts +26 -0
  5. package/src/domain/db/connection.ts +21 -0
  6. package/src/domain/db/schema.ts +35 -0
  7. package/src/domain/entities.ts +21 -0
  8. package/src/domain/errors.ts +9 -0
  9. package/src/domain/index.ts +12 -0
  10. package/src/domain/inputs.ts +13 -0
  11. package/src/domain/repositories/projects.ts +75 -0
  12. package/src/domain/repositories/tasks.ts +62 -0
  13. package/src/domain/services/projects.ts +68 -0
  14. package/src/domain/services/tasks.ts +62 -0
  15. package/src/domain/services.ts +22 -0
  16. package/src/domain/statuses.ts +5 -0
  17. package/src/index.ts +4 -0
  18. package/src/mcp/index.ts +3 -0
  19. package/src/mcp/server.ts +140 -0
  20. package/src/mcp/standalone.ts +48 -0
  21. package/src/server/index.ts +102 -0
  22. package/src/server/routes/projects.ts +58 -0
  23. package/src/server/routes/tasks.ts +58 -0
  24. package/src/web/dist/assets/index-Bonqd4_2.js +49 -0
  25. package/src/web/dist/index.html +28 -0
  26. package/src/web/index.html +28 -0
  27. package/src/web/src/App.tsx +829 -0
  28. package/src/web/src/api.ts +1 -0
  29. package/src/web/src/components/Badge.tsx +54 -0
  30. package/src/web/src/components/Button.tsx +58 -0
  31. package/src/web/src/components/Card.tsx +40 -0
  32. package/src/web/src/components/Icon.tsx +22 -0
  33. package/src/web/src/components/IconButton.tsx +50 -0
  34. package/src/web/src/components/Input.tsx +47 -0
  35. package/src/web/src/components/Select.tsx +41 -0
  36. package/src/web/src/components/Stack.tsx +38 -0
  37. package/src/web/src/components/ThemeContext.tsx +53 -0
  38. package/src/web/src/components/ThemeSwitcher.tsx +49 -0
  39. package/src/web/src/components/TopBar.tsx +38 -0
  40. package/src/web/src/components/index.ts +12 -0
  41. package/src/web/src/components/theme.ts +185 -0
  42. package/src/web/src/main.tsx +12 -0
  43. package/src/web/tsconfig.json +17 -0
  44. package/src/web/vite.config.ts +16 -0
package/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # tab-for-projects
2
+
3
+ A self-contained project management tool. Install it, run it, and get a web UI and REST API with zero configuration.
4
+
5
+ All your data stays local in a single SQLite file — no external databases, no cloud accounts, no setup wizards.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ # install bun (the only prerequisite)
11
+ curl -fsSL https://bun.sh/install | bash
12
+
13
+ # install tab-for-projects
14
+ bun install -g @x4lt7ab/tab-for-projects
15
+
16
+ # run it
17
+ tab-for-projects
18
+ ```
19
+
20
+ Open `http://localhost:3000` and you're ready to go.
21
+
22
+ If `tab-for-projects` is not found, add bun's global bin directory to your PATH:
23
+
24
+ ```bash
25
+ echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc
26
+ source ~/.zshrc
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ Everything works out of the box. If you need to customize, use environment variables or CLI flags:
32
+
33
+ | Environment variable | CLI flag | Default | Description |
34
+ |----------------------|-----------------|---------------------------------------|------------------------------|
35
+ | `PM_PORT` | `--port` | `3000` | HTTP port |
36
+ | `PM_HOST` | `--host` | `127.0.0.1` | Bind address |
37
+ | `SQLITE_PATH` | `--sqlite-path` | `~/.tab/project-management/sqlite.db` | Full path to SQLite database |
38
+
39
+ CLI flags take precedence over environment variables.
40
+
41
+ ```bash
42
+ tab-for-projects --port 8080 --sqlite-path /opt/tab-for-projects/data/sqlite.db
43
+ ```
44
+
45
+ ## Your data
46
+
47
+ All data is stored in a single file: `~/.tab/project-management/sqlite.db` by default.
48
+
49
+ **Back up** your data at any time:
50
+
51
+ ```bash
52
+ sqlite3 ~/.tab/project-management/sqlite.db ".backup /path/to/backup.db"
53
+ ```
54
+
55
+ **Start fresh** by deleting the database file and restarting. A new one is created automatically.
56
+
57
+ ## API
58
+
59
+ tab-for-projects exposes a REST API alongside the web UI. All endpoints return JSON.
60
+
61
+ ### Projects
62
+
63
+ ```
64
+ GET /api/projects List all projects
65
+ POST /api/projects Create a project
66
+ GET /api/projects/:slug Get a project
67
+ PATCH /api/projects/:slug Update a project
68
+ DELETE /api/projects/:slug Delete a project
69
+ ```
70
+
71
+ **Create a project:**
72
+
73
+ ```bash
74
+ curl -X POST http://localhost:3000/api/projects \
75
+ -H "Content-Type: application/json" \
76
+ -d '{"name": "Website Redesign", "slug": "website-redesign", "description": "Q2 refresh"}'
77
+ ```
78
+
79
+ **Response:**
80
+
81
+ ```json
82
+ {
83
+ "id": "01JABBCD1234EFGH5678IJKL",
84
+ "name": "Website Redesign",
85
+ "slug": "website-redesign",
86
+ "description": "Q2 refresh",
87
+ "status": "active",
88
+ "created_at": "2026-03-23T12:00:00.000Z",
89
+ "updated_at": "2026-03-23T12:00:00.000Z"
90
+ }
91
+ ```
92
+
93
+ Project status can be `active`, `paused`, `completed`, or `archived`.
94
+
95
+ ### Health check
96
+
97
+ ```
98
+ GET /api/health Returns {"status": "ok"}
99
+ ```
100
+
101
+ ## MCP server
102
+
103
+ tab-for-projects includes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/mcp`, letting AI assistants manage your projects and tasks directly.
104
+
105
+ The MCP endpoint is served on the same port as the API and web UI — no separate process needed.
106
+
107
+ ### Claude Code
108
+
109
+ ```bash
110
+ claude mcp add tab-for-projects --transport http http://localhost:3000/mcp
111
+ ```
112
+
113
+ Or add it to your project's `.mcp.json`:
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "tab-for-projects": {
119
+ "url": "http://localhost:3000/mcp"
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### Claude Desktop
126
+
127
+ Open **Settings > Developer > Edit Config** and add to `claude_desktop_config.json`:
128
+
129
+ ```json
130
+ {
131
+ "mcpServers": {
132
+ "tab-for-projects": {
133
+ "url": "http://localhost:3000/mcp"
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ Restart Claude Desktop after saving.
140
+
141
+ ### Cursor
142
+
143
+ Open **Settings > MCP Servers > Add new MCP server** and use:
144
+
145
+ - **Name:** `tab-for-projects`
146
+ - **Type:** `url`
147
+ - **URL:** `http://localhost:3000/mcp`
148
+
149
+ ### Remote access
150
+
151
+ To make tab-for-projects accessible from the local network, bind to all interfaces:
152
+
153
+ ```bash
154
+ tab-for-projects --host 0.0.0.0
155
+ ```
156
+
157
+ Then from another machine, point your MCP client at the server:
158
+
159
+ ```bash
160
+ claude mcp add tab-for-projects --transport http http://192.168.1.100:3000/mcp
161
+ ```
162
+
163
+ Replace `192.168.1.100` with the host's actual IP address.
164
+
165
+ ### Custom database path
166
+
167
+ ```json
168
+ {
169
+ "mcpServers": {
170
+ "tab-for-projects": {
171
+ "url": "http://localhost:3000/mcp"
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ Start the server with a custom database path:
178
+
179
+ ```bash
180
+ tab-for-projects --sqlite-path /path/to/your/sqlite.db
181
+ ```
182
+
183
+ ### Available tools
184
+
185
+ | Tool | Description |
186
+ |---|---|
187
+ | `list_projects` | List all projects |
188
+ | `get_project` | Get a project by its slug |
189
+ | `create_project` | Create a new project (name, slug, optional description/status) |
190
+ | `update_project` | Update a project's name, description, or status |
191
+ | `delete_project` | Delete a project by slug |
192
+ | `list_tasks` | List all tasks in a project |
193
+ | `create_task` | Create a task in a project (title, optional description/status) |
194
+ | `update_task` | Update a task's title, description, or status |
195
+ | `delete_task` | Delete a task by ID |
196
+
197
+ Task status values: `todo`, `in_progress`, `done`. Project status values: `active`, `paused`, `completed`, `archived`.
198
+
199
+ ## Deploying
200
+
201
+ ### systemd (Linux)
202
+
203
+ ```ini
204
+ # /etc/systemd/system/tab-for-projects.service
205
+ [Unit]
206
+ Description=tab-for-projects
207
+ After=network.target
208
+
209
+ [Service]
210
+ Type=simple
211
+ ExecStart=/home/pm/.bun/bin/tab-for-projects
212
+ Environment=PM_HOST=0.0.0.0
213
+ Environment=SQLITE_PATH=/var/lib/project-management/sqlite.db
214
+ Restart=on-failure
215
+ RestartSec=5
216
+
217
+ [Install]
218
+ WantedBy=multi-user.target
219
+ ```
220
+
221
+ ```bash
222
+ sudo systemctl enable --now tab-for-projects
223
+ ```
224
+
225
+ ### launchd (macOS)
226
+
227
+ ```xml
228
+ <!-- ~/Library/LaunchAgents/com.alttab.tab-for-projects.plist -->
229
+ <?xml version="1.0" encoding="UTF-8"?>
230
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
231
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
232
+ <plist version="1.0">
233
+ <dict>
234
+ <key>Label</key>
235
+ <string>com.alttab.tab-for-projects</string>
236
+ <key>ProgramArguments</key>
237
+ <array>
238
+ <string>/Users/you/.bun/bin/tab-for-projects</string>
239
+ </array>
240
+ <key>RunAtLoad</key>
241
+ <true/>
242
+ <key>KeepAlive</key>
243
+ <true/>
244
+ </dict>
245
+ </plist>
246
+ ```
247
+
248
+ ```bash
249
+ launchctl load ~/Library/LaunchAgents/com.alttab.tab-for-projects.plist
250
+ ```
251
+
252
+ ## Upgrading
253
+
254
+ ```bash
255
+ bun install -g @x4lt7ab/tab-for-projects@latest
256
+ ```
257
+
258
+ Restart the server after upgrading. Database migrations run automatically — your data is preserved.
259
+
260
+ ## Contributing
261
+
262
+ tab-for-projects is built with TypeScript, Bun, Hono, and React. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and architecture details.
263
+
264
+ ## License
265
+
266
+ MIT
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@x4lt7ab/tab-for-projects",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "tab-for-projects": "./src/index.ts"
7
+ },
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "engines": {
12
+ "bun": ">=1.3"
13
+ },
14
+ "scripts": {
15
+ "dev": "bun run --hot src/index.ts & cd src/web && vite",
16
+ "build": "cd src/web && vite build",
17
+ "start": "bun run src/index.ts",
18
+ "serve": "bun run build && bun run start",
19
+ "test": "bun test",
20
+ "prepublishOnly": "bun run build"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.27.1",
24
+ "@x4lt7ab/tab-for-projects": "/Users/alttab/AltT4b/project-management",
25
+ "hono": "^4",
26
+ "ulid": "^2",
27
+ "zod": "^4.3.6"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "^1.2",
31
+ "@types/react": "^19.1.2",
32
+ "@types/react-dom": "^19.1.2",
33
+ "@vitejs/plugin-react": "^4.4.1",
34
+ "react": "^19.1.0",
35
+ "react-dom": "^19.1.0",
36
+ "typescript": "^5.8.3",
37
+ "vite": "^6.3.5"
38
+ }
39
+ }
@@ -0,0 +1,41 @@
1
+ import { networkInterfaces } from "os";
2
+
3
+ export interface ServerOptions {
4
+ port: number;
5
+ host: string;
6
+ dbPath?: string;
7
+ }
8
+
9
+ export function parseArgs(defaults: { port: number; portEnv: string }): ServerOptions {
10
+ const args = process.argv.slice(2);
11
+ let port = Number(process.env[defaults.portEnv]) || defaults.port;
12
+ let host = process.env.PM_HOST ?? "127.0.0.1";
13
+ let dbPath: string | undefined = process.env.SQLITE_PATH;
14
+
15
+ for (let i = 0; i < args.length; i++) {
16
+ if (args[i] === "--port" && args[i + 1]) port = Number(args[++i]);
17
+ if (args[i] === "--host" && args[i + 1]) host = args[++i];
18
+ if (args[i] === "--sqlite-path" && args[i + 1]) dbPath = args[++i];
19
+ }
20
+
21
+ return { port, host, dbPath };
22
+ }
23
+
24
+ export function getNetworkAddress(): string | undefined {
25
+ for (const addrs of Object.values(networkInterfaces())) {
26
+ for (const addr of addrs ?? []) {
27
+ if (addr.family === "IPv4" && !addr.internal) return addr.address;
28
+ }
29
+ }
30
+ }
31
+
32
+ export function logListening(name: string, host: string, port: number): void {
33
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
34
+ console.error(`${name} listening on http://${displayHost}:${port}`);
35
+ if (host === "0.0.0.0") {
36
+ const networkAddr = getNetworkAddress();
37
+ if (networkAddr) {
38
+ console.error(`${name} network: http://${networkAddr}:${port}`);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,26 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { createDatabase } from "./db/connection";
3
+ import { runMigrations } from "./db/schema";
4
+ import { ProjectRepository } from "./repositories/projects";
5
+ import { TaskRepository } from "./repositories/tasks";
6
+ import { ProjectService } from "./services/projects";
7
+ import { TaskService } from "./services/tasks";
8
+ import type { IProjectService, ITaskService } from "./services";
9
+
10
+ export interface AppContext {
11
+ db: Database;
12
+ projectService: IProjectService;
13
+ taskService: ITaskService;
14
+ }
15
+
16
+ export function bootstrap(dbPath?: string): AppContext {
17
+ const db = createDatabase(dbPath);
18
+ runMigrations(db);
19
+
20
+ const projectRepo = new ProjectRepository(db);
21
+ const taskRepo = new TaskRepository(db);
22
+ const projectService = new ProjectService(projectRepo);
23
+ const taskService = new TaskService(taskRepo, projectRepo);
24
+
25
+ return { db, projectService, taskService };
26
+ }
@@ -0,0 +1,21 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ const DEFAULT_DB_PATH = join(homedir(), ".tab", "project-management", "sqlite.db");
7
+
8
+ export function getDbPath(): string {
9
+ return process.env.SQLITE_PATH ?? DEFAULT_DB_PATH;
10
+ }
11
+
12
+ export function createDatabase(dbPath?: string): Database {
13
+ const resolvedPath = dbPath ?? getDbPath();
14
+ mkdirSync(join(resolvedPath, ".."), { recursive: true });
15
+
16
+ const db = new Database(resolvedPath);
17
+ db.run("PRAGMA journal_mode = WAL");
18
+ db.run("PRAGMA foreign_keys = ON");
19
+
20
+ return db;
21
+ }
@@ -0,0 +1,35 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { PROJECT_STATUSES, TASK_STATUSES } from "../statuses";
3
+
4
+ const projectStatusCheck = PROJECT_STATUSES.map((s) => `'${s}'`).join(", ");
5
+ const taskStatusCheck = TASK_STATUSES.map((s) => `'${s}'`).join(", ");
6
+
7
+ export function runMigrations(db: Database): void {
8
+ db.run(`
9
+ CREATE TABLE IF NOT EXISTS projects (
10
+ id TEXT PRIMARY KEY,
11
+ slug TEXT NOT NULL UNIQUE,
12
+ name TEXT NOT NULL,
13
+ description TEXT NOT NULL DEFAULT '',
14
+ status TEXT NOT NULL DEFAULT 'active'
15
+ CHECK (status IN (${projectStatusCheck})),
16
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
17
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
18
+ )
19
+ `);
20
+
21
+ db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug)");
22
+
23
+ db.run(`
24
+ CREATE TABLE IF NOT EXISTS tasks (
25
+ id TEXT PRIMARY KEY,
26
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
27
+ title TEXT NOT NULL,
28
+ description TEXT NOT NULL DEFAULT '',
29
+ status TEXT NOT NULL DEFAULT 'todo'
30
+ CHECK (status IN (${taskStatusCheck})),
31
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
32
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
33
+ )
34
+ `);
35
+ }
@@ -0,0 +1,21 @@
1
+ import type { ProjectStatus, TaskStatus } from "./statuses";
2
+
3
+ export interface Project {
4
+ id: string;
5
+ slug: string;
6
+ name: string;
7
+ description: string;
8
+ status: ProjectStatus;
9
+ created_at: string;
10
+ updated_at: string;
11
+ }
12
+
13
+ export interface Task {
14
+ id: string;
15
+ project_id: string;
16
+ title: string;
17
+ description: string;
18
+ status: TaskStatus;
19
+ created_at: string;
20
+ updated_at: string;
21
+ }
@@ -0,0 +1,9 @@
1
+ export class ServiceError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public statusCode: number
5
+ ) {
6
+ super(message);
7
+ this.name = "ServiceError";
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./statuses";
2
+ export * from "./entities";
3
+ export * from "./inputs";
4
+ export * from "./errors";
5
+ export * from "./services";
6
+ export * from "./bootstrap";
7
+ export { createDatabase, getDbPath } from "./db/connection";
8
+ export { runMigrations } from "./db/schema";
9
+ export { ProjectRepository } from "./repositories/projects";
10
+ export { TaskRepository } from "./repositories/tasks";
11
+ export { ProjectService } from "./services/projects";
12
+ export { TaskService } from "./services/tasks";
@@ -0,0 +1,13 @@
1
+ import type { Project, Task } from "./entities";
2
+
3
+ export type CreateProjectInput = Pick<Project, "name" | "slug"> &
4
+ Partial<Pick<Project, "description" | "status">>;
5
+
6
+ export type UpdateProjectInput = Partial<
7
+ Pick<Project, "name" | "description" | "status">
8
+ >;
9
+
10
+ export type CreateTaskInput = Pick<Task, "title"> &
11
+ Partial<Pick<Task, "description" | "status">>;
12
+
13
+ export type UpdateTaskInput = Partial<Pick<Task, "title" | "description" | "status">>;
@@ -0,0 +1,75 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { ulid } from "ulid";
3
+ import type { Project } from "../entities";
4
+ import type { CreateProjectInput, UpdateProjectInput } from "../inputs";
5
+
6
+ export class ProjectRepository {
7
+ constructor(private db: Database) {}
8
+
9
+ findAll(): Project[] {
10
+ return this.db
11
+ .query("SELECT * FROM projects ORDER BY created_at DESC")
12
+ .all() as Project[];
13
+ }
14
+
15
+ findById(id: string): Project | null {
16
+ return (
17
+ this.db.query("SELECT * FROM projects WHERE id = ?").get(id) as Project | null
18
+ );
19
+ }
20
+
21
+ findBySlug(slug: string): Project | null {
22
+ return (
23
+ this.db.query("SELECT * FROM projects WHERE slug = ?").get(slug) as Project | null
24
+ );
25
+ }
26
+
27
+ create(input: CreateProjectInput): Project {
28
+ const id = ulid();
29
+ const now = new Date().toISOString();
30
+
31
+ this.db
32
+ .query(
33
+ `INSERT INTO projects (id, slug, name, description, status, created_at, updated_at)
34
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
35
+ )
36
+ .run(
37
+ id,
38
+ input.slug,
39
+ input.name,
40
+ input.description ?? "",
41
+ input.status ?? "active",
42
+ now,
43
+ now
44
+ );
45
+
46
+ return this.findById(id)!;
47
+ }
48
+
49
+ update(slug: string, input: UpdateProjectInput): Project | null {
50
+ const existing = this.findBySlug(slug);
51
+ if (!existing) return null;
52
+
53
+ const name = input.name ?? existing.name;
54
+ const description = input.description ?? existing.description;
55
+ const status = input.status ?? existing.status;
56
+ const now = new Date().toISOString();
57
+
58
+ this.db
59
+ .query(
60
+ `UPDATE projects
61
+ SET name = ?, description = ?, status = ?, updated_at = ?
62
+ WHERE slug = ?`
63
+ )
64
+ .run(name, description, status, now, slug);
65
+
66
+ return this.findBySlug(slug)!;
67
+ }
68
+
69
+ delete(slug: string): boolean {
70
+ const result = this.db
71
+ .query("DELETE FROM projects WHERE slug = ?")
72
+ .run(slug);
73
+ return result.changes > 0;
74
+ }
75
+ }
@@ -0,0 +1,62 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { ulid } from "ulid";
3
+ import type { Task } from "../entities";
4
+ import type { CreateTaskInput } from "../inputs";
5
+ import type { UpdateTaskInput } from "../inputs";
6
+
7
+ export class TaskRepository {
8
+ constructor(private db: Database) {}
9
+
10
+ findByProject(projectId: string): Task[] {
11
+ return this.db
12
+ .query("SELECT * FROM tasks WHERE project_id = ? ORDER BY created_at ASC")
13
+ .all(projectId) as Task[];
14
+ }
15
+
16
+ create(projectId: string, input: CreateTaskInput): Task {
17
+ const id = ulid();
18
+ const now = new Date().toISOString();
19
+
20
+ this.db
21
+ .query(
22
+ `INSERT INTO tasks (id, project_id, title, description, status, created_at, updated_at)
23
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
24
+ )
25
+ .run(
26
+ id,
27
+ projectId,
28
+ input.title,
29
+ input.description ?? "",
30
+ input.status ?? "todo",
31
+ now,
32
+ now
33
+ );
34
+
35
+ return this.db.query("SELECT * FROM tasks WHERE id = ?").get(id) as Task;
36
+ }
37
+
38
+ update(id: string, input: UpdateTaskInput): Task | null {
39
+ const existing = this.db
40
+ .query("SELECT * FROM tasks WHERE id = ?")
41
+ .get(id) as Task | null;
42
+ if (!existing) return null;
43
+
44
+ const title = input.title ?? existing.title;
45
+ const description = input.description ?? existing.description;
46
+ const status = input.status ?? existing.status;
47
+ const now = new Date().toISOString();
48
+
49
+ this.db
50
+ .query(
51
+ `UPDATE tasks SET title = ?, description = ?, status = ?, updated_at = ? WHERE id = ?`
52
+ )
53
+ .run(title, description, status, now, id);
54
+
55
+ return this.db.query("SELECT * FROM tasks WHERE id = ?").get(id) as Task;
56
+ }
57
+
58
+ delete(id: string): boolean {
59
+ const result = this.db.query("DELETE FROM tasks WHERE id = ?").run(id);
60
+ return result.changes > 0;
61
+ }
62
+ }