@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.
- package/README.md +266 -0
- package/package.json +39 -0
- package/src/domain/args.ts +41 -0
- package/src/domain/bootstrap.ts +26 -0
- package/src/domain/db/connection.ts +21 -0
- package/src/domain/db/schema.ts +35 -0
- package/src/domain/entities.ts +21 -0
- package/src/domain/errors.ts +9 -0
- package/src/domain/index.ts +12 -0
- package/src/domain/inputs.ts +13 -0
- package/src/domain/repositories/projects.ts +75 -0
- package/src/domain/repositories/tasks.ts +62 -0
- package/src/domain/services/projects.ts +68 -0
- package/src/domain/services/tasks.ts +62 -0
- package/src/domain/services.ts +22 -0
- package/src/domain/statuses.ts +5 -0
- package/src/index.ts +4 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +140 -0
- package/src/mcp/standalone.ts +48 -0
- package/src/server/index.ts +102 -0
- package/src/server/routes/projects.ts +58 -0
- package/src/server/routes/tasks.ts +58 -0
- package/src/web/dist/assets/index-Bonqd4_2.js +49 -0
- package/src/web/dist/index.html +28 -0
- package/src/web/index.html +28 -0
- package/src/web/src/App.tsx +829 -0
- package/src/web/src/api.ts +1 -0
- package/src/web/src/components/Badge.tsx +54 -0
- package/src/web/src/components/Button.tsx +58 -0
- package/src/web/src/components/Card.tsx +40 -0
- package/src/web/src/components/Icon.tsx +22 -0
- package/src/web/src/components/IconButton.tsx +50 -0
- package/src/web/src/components/Input.tsx +47 -0
- package/src/web/src/components/Select.tsx +41 -0
- package/src/web/src/components/Stack.tsx +38 -0
- package/src/web/src/components/ThemeContext.tsx +53 -0
- package/src/web/src/components/ThemeSwitcher.tsx +49 -0
- package/src/web/src/components/TopBar.tsx +38 -0
- package/src/web/src/components/index.ts +12 -0
- package/src/web/src/components/theme.ts +185 -0
- package/src/web/src/main.tsx +12 -0
- package/src/web/tsconfig.json +17 -0
- 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,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
|
+
}
|