@locusai/server 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/package.json +24 -0
- package/src/controllers/artifact.controller.ts +45 -0
- package/src/controllers/ci.controller.ts +16 -0
- package/src/controllers/doc.controller.ts +116 -0
- package/src/controllers/event.controller.ts +20 -0
- package/src/controllers/sprint.controller.ts +34 -0
- package/src/controllers/task.controller.ts +98 -0
- package/src/db.ts +107 -0
- package/src/index.ts +133 -0
- package/src/middleware/error.middleware.ts +21 -0
- package/src/repositories/artifact.repository.ts +44 -0
- package/src/repositories/base.repository.ts +5 -0
- package/src/repositories/comment.repository.ts +32 -0
- package/src/repositories/event.repository.ts +34 -0
- package/src/repositories/sprint.repository.ts +60 -0
- package/src/repositories/task.repository.ts +159 -0
- package/src/routes/artifacts.routes.ts +11 -0
- package/src/routes/ci.routes.ts +10 -0
- package/src/routes/docs.routes.ts +13 -0
- package/src/routes/events.routes.ts +10 -0
- package/src/routes/sprints.routes.ts +12 -0
- package/src/routes/tasks.routes.ts +18 -0
- package/src/services/ci.service.ts +65 -0
- package/src/services/sprint.service.ts +28 -0
- package/src/services/task.service.ts +182 -0
- package/src/task-processor.ts +304 -0
- package/tsconfig.json +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Locus AI
|
|
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/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@locusai/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "bun run src/index.ts",
|
|
8
|
+
"lint": "bun lint"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"express": "^4.19.2",
|
|
12
|
+
"better-sqlite3": "^11.5.0",
|
|
13
|
+
"zod": "^3.23.8",
|
|
14
|
+
"@locusai/shared": "workspace:*",
|
|
15
|
+
"cors": "^2.8.5",
|
|
16
|
+
"dockerode": "^4.0.2"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
20
|
+
"@types/cors": "^2.8.19",
|
|
21
|
+
"@types/dockerode": "^3.3.33",
|
|
22
|
+
"@types/express": "^5.0.6"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { NextFunction, Request, Response } from "express";
|
|
4
|
+
import type { ArtifactRepository } from "../repositories/artifact.repository.js";
|
|
5
|
+
|
|
6
|
+
export class ArtifactController {
|
|
7
|
+
constructor(
|
|
8
|
+
private artifactRepo: ArtifactRepository,
|
|
9
|
+
private workspaceDir: string
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
getArtifactFile = (req: Request, res: Response, next: NextFunction) => {
|
|
13
|
+
try {
|
|
14
|
+
const { taskId, type, filename } = req.params;
|
|
15
|
+
const filePath = join(
|
|
16
|
+
this.workspaceDir,
|
|
17
|
+
"artifacts",
|
|
18
|
+
taskId as string,
|
|
19
|
+
type as string,
|
|
20
|
+
filename as string
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (!existsSync(filePath)) {
|
|
24
|
+
return res
|
|
25
|
+
.status(404)
|
|
26
|
+
.json({ error: { message: "Artifact not found" } });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
res.sendFile(filePath);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
next(err);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
getByTaskId = (req: Request, res: Response, next: NextFunction) => {
|
|
36
|
+
try {
|
|
37
|
+
const artifacts = this.artifactRepo.findByTaskId(
|
|
38
|
+
req.params.taskId as string
|
|
39
|
+
);
|
|
40
|
+
res.json(artifacts);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
next(err);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import type { CiService } from "../services/ci.service.js";
|
|
3
|
+
|
|
4
|
+
export class CiController {
|
|
5
|
+
constructor(private ciService: CiService) {}
|
|
6
|
+
|
|
7
|
+
run = async (req: Request, res: Response, next: NextFunction) => {
|
|
8
|
+
try {
|
|
9
|
+
const { taskId, preset } = req.body;
|
|
10
|
+
const result = await this.ciService.runCi(taskId, preset);
|
|
11
|
+
res.json(result);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
next(err);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
statSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import type { NextFunction, Request, Response } from "express";
|
|
10
|
+
|
|
11
|
+
export interface DocNode {
|
|
12
|
+
type: "file" | "directory";
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
children?: DocNode[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class DocController {
|
|
19
|
+
constructor(private config: { repoPath: string; docsPath: string }) {}
|
|
20
|
+
|
|
21
|
+
getAll = (_req: Request, res: Response, next: NextFunction) => {
|
|
22
|
+
try {
|
|
23
|
+
const locusDir = join(this.config.repoPath, ".locus");
|
|
24
|
+
const docsFile = join(locusDir, "docs.json");
|
|
25
|
+
|
|
26
|
+
if (!existsSync(docsFile)) {
|
|
27
|
+
return res.json([]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const docs = JSON.parse(readFileSync(docsFile, "utf-8"));
|
|
31
|
+
res.json(docs);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
next(err);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
getTree = (_req: Request, res: Response, next: NextFunction) => {
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(this.config.docsPath)) {
|
|
40
|
+
return res.json([]);
|
|
41
|
+
}
|
|
42
|
+
const tree = this.buildTree(this.config.docsPath);
|
|
43
|
+
res.json(tree);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
next(err);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
read = (req: Request, res: Response, next: NextFunction) => {
|
|
50
|
+
try {
|
|
51
|
+
const filePath = req.query.path as string;
|
|
52
|
+
if (!filePath) {
|
|
53
|
+
return res.status(400).json({ error: { message: "Path is required" } });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const fullPath = join(this.config.docsPath, filePath);
|
|
57
|
+
if (!fullPath.startsWith(this.config.docsPath)) {
|
|
58
|
+
return res.status(403).json({ error: { message: "Invalid path" } });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
62
|
+
res.json({ content });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
next(err);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
write = (req: Request, res: Response, next: NextFunction) => {
|
|
69
|
+
try {
|
|
70
|
+
const { path: filePath, content } = req.body;
|
|
71
|
+
if (!filePath) {
|
|
72
|
+
return res.status(400).json({ error: { message: "Path is required" } });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fullPath = join(this.config.docsPath, filePath);
|
|
76
|
+
if (!fullPath.startsWith(this.config.docsPath)) {
|
|
77
|
+
return res.status(403).json({ error: { message: "Invalid path" } });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
81
|
+
res.json({ ok: true });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
next(err);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private buildTree(dir: string, basePath: string = ""): DocNode[] {
|
|
88
|
+
const entries = readdirSync(dir);
|
|
89
|
+
const nodes: DocNode[] = [];
|
|
90
|
+
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.startsWith(".")) continue;
|
|
93
|
+
|
|
94
|
+
const fullPath = join(dir, entry);
|
|
95
|
+
const relativePath = join(basePath, entry);
|
|
96
|
+
const stats = statSync(fullPath);
|
|
97
|
+
|
|
98
|
+
if (stats.isDirectory()) {
|
|
99
|
+
nodes.push({
|
|
100
|
+
type: "directory",
|
|
101
|
+
name: entry,
|
|
102
|
+
path: relativePath,
|
|
103
|
+
children: this.buildTree(fullPath, relativePath),
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
nodes.push({
|
|
107
|
+
type: "file",
|
|
108
|
+
name: entry,
|
|
109
|
+
path: relativePath,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return nodes;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import type { EventRepository } from "../repositories/event.repository.js";
|
|
3
|
+
|
|
4
|
+
export class EventController {
|
|
5
|
+
constructor(private eventRepo: EventRepository) {}
|
|
6
|
+
|
|
7
|
+
getByTaskId = (req: Request, res: Response, next: NextFunction) => {
|
|
8
|
+
try {
|
|
9
|
+
const taskId = req.query.taskId as string;
|
|
10
|
+
const events = this.eventRepo.findByTaskId(taskId);
|
|
11
|
+
const formattedEvents = events.map((e) => ({
|
|
12
|
+
...e,
|
|
13
|
+
payload: JSON.parse(e.payload || "{}"),
|
|
14
|
+
}));
|
|
15
|
+
res.json(formattedEvents);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
next(err);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import type { SprintService } from "../services/sprint.service.js";
|
|
3
|
+
|
|
4
|
+
export class SprintController {
|
|
5
|
+
constructor(private sprintService: SprintService) {}
|
|
6
|
+
|
|
7
|
+
getAll = (_req: Request, res: Response, next: NextFunction) => {
|
|
8
|
+
try {
|
|
9
|
+
const sprints = this.sprintService.getAllSprints();
|
|
10
|
+
res.json(sprints);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
next(err);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
create = (req: Request, res: Response, next: NextFunction) => {
|
|
17
|
+
try {
|
|
18
|
+
const { name } = req.body;
|
|
19
|
+
const id = this.sprintService.createSprint(name);
|
|
20
|
+
res.json({ id });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
next(err);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
updateStatus = (req: Request, res: Response, next: NextFunction) => {
|
|
27
|
+
try {
|
|
28
|
+
this.sprintService.updateSprint(req.params.id as string, req.body);
|
|
29
|
+
res.json({ ok: true });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
next(err);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { TaskSchema } from "@locusai/shared";
|
|
2
|
+
import type { NextFunction, Request, Response } from "express";
|
|
3
|
+
import type { TaskService } from "../services/task.service.js";
|
|
4
|
+
|
|
5
|
+
export class TaskController {
|
|
6
|
+
constructor(private taskService: TaskService) {}
|
|
7
|
+
|
|
8
|
+
getAll = (_req: Request, res: Response, next: NextFunction) => {
|
|
9
|
+
try {
|
|
10
|
+
const tasks = this.taskService.getAllTasks();
|
|
11
|
+
res.json(tasks);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
next(err);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
getById = (req: Request, res: Response, next: NextFunction) => {
|
|
18
|
+
try {
|
|
19
|
+
const task = this.taskService.getTaskById(req.params.id as string);
|
|
20
|
+
res.json(task);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
next(err);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
create = (req: Request, res: Response, next: NextFunction) => {
|
|
27
|
+
try {
|
|
28
|
+
const data = TaskSchema.parse(req.body);
|
|
29
|
+
const id = this.taskService.createTask(data);
|
|
30
|
+
res.json({ id });
|
|
31
|
+
} catch (err) {
|
|
32
|
+
next(err);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
update = (req: Request, res: Response, next: NextFunction) => {
|
|
37
|
+
try {
|
|
38
|
+
this.taskService.updateTask(req.params.id as string, req.body);
|
|
39
|
+
res.json({ ok: true });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
next(err);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
delete = (req: Request, res: Response, next: NextFunction) => {
|
|
46
|
+
try {
|
|
47
|
+
this.taskService.deleteTask(req.params.id as string);
|
|
48
|
+
res.json({ ok: true });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
next(err);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
addComment = (req: Request, res: Response, next: NextFunction) => {
|
|
55
|
+
try {
|
|
56
|
+
const { author, text } = req.body;
|
|
57
|
+
this.taskService.addComment(req.params.id as string, author, text);
|
|
58
|
+
res.json({ ok: true });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
next(err);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
dispatch = (req: Request, res: Response, next: NextFunction) => {
|
|
65
|
+
try {
|
|
66
|
+
const { workerId, sprintId } = req.body;
|
|
67
|
+
if (!sprintId) {
|
|
68
|
+
return res
|
|
69
|
+
.status(400)
|
|
70
|
+
.json({ error: { message: "sprintId is required" } });
|
|
71
|
+
}
|
|
72
|
+
const task = this.taskService.dispatchTask(workerId, Number(sprintId));
|
|
73
|
+
res.json(task);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
next(err);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
lock = (req: Request, res: Response, next: NextFunction) => {
|
|
80
|
+
try {
|
|
81
|
+
const { agentId, ttlSeconds } = req.body;
|
|
82
|
+
this.taskService.lockTask(req.params.id as string, agentId, ttlSeconds);
|
|
83
|
+
res.json({ ok: true });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
next(err);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
unlock = (req: Request, res: Response, next: NextFunction) => {
|
|
90
|
+
try {
|
|
91
|
+
const { agentId } = req.body;
|
|
92
|
+
this.taskService.unlockTask(req.params.id as string, agentId);
|
|
93
|
+
res.json({ ok: true });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
next(err);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function initDb(workspaceDir: string) {
|
|
5
|
+
const db = new Database(join(workspaceDir, "db.sqlite"));
|
|
6
|
+
|
|
7
|
+
// Create tables if they don't exist
|
|
8
|
+
db.run(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
title TEXT NOT NULL,
|
|
12
|
+
description TEXT,
|
|
13
|
+
status TEXT NOT NULL,
|
|
14
|
+
priority TEXT NOT NULL DEFAULT 'MEDIUM',
|
|
15
|
+
labels TEXT,
|
|
16
|
+
assigneeRole TEXT,
|
|
17
|
+
parentId INTEGER,
|
|
18
|
+
lockedBy TEXT,
|
|
19
|
+
lockExpiresAt INTEGER,
|
|
20
|
+
acceptanceChecklist TEXT,
|
|
21
|
+
createdAt INTEGER NOT NULL,
|
|
22
|
+
updatedAt INTEGER NOT NULL,
|
|
23
|
+
FOREIGN KEY(parentId) REFERENCES tasks(id)
|
|
24
|
+
);`);
|
|
25
|
+
|
|
26
|
+
db.run(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS comments (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
taskId INTEGER NOT NULL,
|
|
30
|
+
author TEXT NOT NULL,
|
|
31
|
+
text TEXT NOT NULL,
|
|
32
|
+
createdAt INTEGER NOT NULL,
|
|
33
|
+
FOREIGN KEY(taskId) REFERENCES tasks(id)
|
|
34
|
+
);`);
|
|
35
|
+
|
|
36
|
+
db.run(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
taskId INTEGER NOT NULL,
|
|
40
|
+
type TEXT NOT NULL,
|
|
41
|
+
title TEXT NOT NULL,
|
|
42
|
+
contentText TEXT,
|
|
43
|
+
filePath TEXT,
|
|
44
|
+
createdBy TEXT NOT NULL,
|
|
45
|
+
createdAt INTEGER NOT NULL,
|
|
46
|
+
FOREIGN KEY(taskId) REFERENCES tasks(id)
|
|
47
|
+
);`);
|
|
48
|
+
|
|
49
|
+
db.run(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
taskId INTEGER NOT NULL,
|
|
53
|
+
type TEXT NOT NULL,
|
|
54
|
+
payload TEXT,
|
|
55
|
+
createdAt INTEGER NOT NULL,
|
|
56
|
+
FOREIGN KEY(taskId) REFERENCES tasks(id)
|
|
57
|
+
);`);
|
|
58
|
+
|
|
59
|
+
// Run migrations for existing tasks table columns
|
|
60
|
+
try {
|
|
61
|
+
const tableInfo = db.prepare("PRAGMA table_info(tasks)").all() as {
|
|
62
|
+
name: string;
|
|
63
|
+
}[];
|
|
64
|
+
const columns = tableInfo.map((col) => col.name);
|
|
65
|
+
|
|
66
|
+
if (!columns.includes("priority")) {
|
|
67
|
+
db.run(
|
|
68
|
+
"ALTER TABLE tasks ADD COLUMN priority TEXT NOT NULL DEFAULT 'MEDIUM'"
|
|
69
|
+
);
|
|
70
|
+
console.log("Migration: Added priority column to tasks table");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!columns.includes("parentId")) {
|
|
74
|
+
db.run("ALTER TABLE tasks ADD COLUMN parentId INTEGER");
|
|
75
|
+
console.log("Migration: Added parentId column to tasks table");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!columns.includes("lockedBy")) {
|
|
79
|
+
db.run("ALTER TABLE tasks ADD COLUMN lockedBy TEXT");
|
|
80
|
+
console.log("Migration: Added lockedBy column to tasks table");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!columns.includes("lockExpiresAt")) {
|
|
84
|
+
db.run("ALTER TABLE tasks ADD COLUMN lockExpiresAt INTEGER");
|
|
85
|
+
console.log("Migration: Added lockExpiresAt column to tasks table");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!columns.includes("sprintId")) {
|
|
89
|
+
db.run("ALTER TABLE tasks ADD COLUMN sprintId INTEGER");
|
|
90
|
+
console.log("Migration: Added sprintId column to tasks table");
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error("Migration error:", err);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
db.run(`
|
|
97
|
+
CREATE TABLE IF NOT EXISTS sprints (
|
|
98
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
99
|
+
name TEXT NOT NULL,
|
|
100
|
+
status TEXT NOT NULL DEFAULT 'PLANNED',
|
|
101
|
+
startDate INTEGER,
|
|
102
|
+
endDate INTEGER,
|
|
103
|
+
createdAt INTEGER NOT NULL
|
|
104
|
+
);`);
|
|
105
|
+
|
|
106
|
+
return db;
|
|
107
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import cors from "cors";
|
|
5
|
+
import express from "express";
|
|
6
|
+
import { ArtifactController } from "./controllers/artifact.controller.js";
|
|
7
|
+
import { CiController } from "./controllers/ci.controller.js";
|
|
8
|
+
import { DocController } from "./controllers/doc.controller.js";
|
|
9
|
+
import { EventController } from "./controllers/event.controller.js";
|
|
10
|
+
import { SprintController } from "./controllers/sprint.controller.js";
|
|
11
|
+
// Controllers
|
|
12
|
+
import { TaskController } from "./controllers/task.controller.js";
|
|
13
|
+
import { initDb } from "./db.js";
|
|
14
|
+
// Middleware
|
|
15
|
+
import { errorHandler } from "./middleware/error.middleware.js";
|
|
16
|
+
import { ArtifactRepository } from "./repositories/artifact.repository.js";
|
|
17
|
+
import { CommentRepository } from "./repositories/comment.repository.js";
|
|
18
|
+
import { EventRepository } from "./repositories/event.repository.js";
|
|
19
|
+
import { SprintRepository } from "./repositories/sprint.repository.js";
|
|
20
|
+
// Repositories
|
|
21
|
+
import { TaskRepository } from "./repositories/task.repository.js";
|
|
22
|
+
import { createArtifactsRouter } from "./routes/artifacts.routes.js";
|
|
23
|
+
import { createCiRouter } from "./routes/ci.routes.js";
|
|
24
|
+
import { createDocsRouter } from "./routes/docs.routes.js";
|
|
25
|
+
import { createEventsRouter } from "./routes/events.routes.js";
|
|
26
|
+
import { createSprintsRouter } from "./routes/sprints.routes.js";
|
|
27
|
+
// Routes
|
|
28
|
+
import { createTaskRouter } from "./routes/tasks.routes.js";
|
|
29
|
+
import { CiService } from "./services/ci.service.js";
|
|
30
|
+
import { SprintService } from "./services/sprint.service.js";
|
|
31
|
+
// Services
|
|
32
|
+
import { TaskService } from "./services/task.service.js";
|
|
33
|
+
import { TaskProcessor } from "./task-processor.js";
|
|
34
|
+
|
|
35
|
+
const { values } = parseArgs({
|
|
36
|
+
args: Bun.argv,
|
|
37
|
+
options: {
|
|
38
|
+
project: { type: "string" },
|
|
39
|
+
},
|
|
40
|
+
strict: true,
|
|
41
|
+
allowPositionals: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!values.project) {
|
|
45
|
+
console.error("Usage: bun run dev -- --project <workspaceDir>");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const workspaceDir = isAbsolute(values.project)
|
|
50
|
+
? values.project
|
|
51
|
+
: join(process.cwd(), values.project);
|
|
52
|
+
const configPath = join(workspaceDir, "workspace.config.json");
|
|
53
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
54
|
+
|
|
55
|
+
const db = initDb(workspaceDir);
|
|
56
|
+
|
|
57
|
+
// Initialize Repositories
|
|
58
|
+
const taskRepo = new TaskRepository(db);
|
|
59
|
+
const eventRepo = new EventRepository(db);
|
|
60
|
+
const commentRepo = new CommentRepository(db);
|
|
61
|
+
const artifactRepo = new ArtifactRepository(db);
|
|
62
|
+
const sprintRepo = new SprintRepository(db);
|
|
63
|
+
|
|
64
|
+
const processor = new TaskProcessor(db, {
|
|
65
|
+
ciPresetsPath: config.ciPresetsPath,
|
|
66
|
+
repoPath: workspaceDir,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Initialize Services
|
|
70
|
+
const taskService = new TaskService(
|
|
71
|
+
taskRepo,
|
|
72
|
+
eventRepo,
|
|
73
|
+
commentRepo,
|
|
74
|
+
artifactRepo,
|
|
75
|
+
processor
|
|
76
|
+
);
|
|
77
|
+
const sprintService = new SprintService(sprintRepo);
|
|
78
|
+
const ciService = new CiService(artifactRepo, eventRepo, {
|
|
79
|
+
ciPresetsPath: config.ciPresetsPath,
|
|
80
|
+
repoPath: workspaceDir,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Initialize Controllers
|
|
84
|
+
const taskController = new TaskController(taskService);
|
|
85
|
+
const sprintController = new SprintController(sprintService);
|
|
86
|
+
const ciController = new CiController(ciService);
|
|
87
|
+
const artifactController = new ArtifactController(artifactRepo, workspaceDir);
|
|
88
|
+
const docController = new DocController({
|
|
89
|
+
repoPath: config.repoPath,
|
|
90
|
+
docsPath: config.docsPath,
|
|
91
|
+
});
|
|
92
|
+
const eventController = new EventController(eventRepo);
|
|
93
|
+
|
|
94
|
+
const app = express();
|
|
95
|
+
app.use(cors());
|
|
96
|
+
app.use(express.json({ limit: "12mb" }));
|
|
97
|
+
|
|
98
|
+
// Routes Implementation
|
|
99
|
+
app.use("/api/tasks", createTaskRouter(taskController));
|
|
100
|
+
app.use("/api/docs", createDocsRouter(docController));
|
|
101
|
+
app.use("/api/sprints", createSprintsRouter(sprintController));
|
|
102
|
+
app.use("/api/ci", createCiRouter(ciController));
|
|
103
|
+
app.use("/api/events", createEventsRouter(eventController));
|
|
104
|
+
app.use("/api", createArtifactsRouter(artifactController));
|
|
105
|
+
|
|
106
|
+
// Health Check
|
|
107
|
+
app.get("/health", (_req, res) => res.json({ status: "ok" }));
|
|
108
|
+
|
|
109
|
+
// Error Handling
|
|
110
|
+
app.use(errorHandler);
|
|
111
|
+
|
|
112
|
+
const PORT = 3080;
|
|
113
|
+
|
|
114
|
+
// Serve Dashboard UI if it exists
|
|
115
|
+
const dashboardPaths = [
|
|
116
|
+
join(import.meta.dir, "../public/dashboard"), // Production (bundled)
|
|
117
|
+
join(process.cwd(), "packages/cli/public/dashboard"), // Dev (root)
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const dashboardPath = dashboardPaths.find((p) => existsSync(p));
|
|
121
|
+
|
|
122
|
+
if (dashboardPath) {
|
|
123
|
+
console.log(`Serving dashboard from ${dashboardPath}`);
|
|
124
|
+
app.use(express.static(dashboardPath));
|
|
125
|
+
// Client-side routing fallback
|
|
126
|
+
app.get("/[^api]*", (_req, res) => {
|
|
127
|
+
res.sendFile(join(dashboardPath, "index.html"));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
app.listen(PORT, () =>
|
|
132
|
+
console.log(`Server running on http://localhost:${PORT}`)
|
|
133
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { ServiceError } from "../services/task.service.js";
|
|
3
|
+
|
|
4
|
+
export function errorHandler(
|
|
5
|
+
err: Error,
|
|
6
|
+
_req: Request,
|
|
7
|
+
res: Response,
|
|
8
|
+
_next: NextFunction
|
|
9
|
+
) {
|
|
10
|
+
console.error("Server Error:", err);
|
|
11
|
+
|
|
12
|
+
const statusCode = err instanceof ServiceError ? err.statusCode : 500;
|
|
13
|
+
const message = err.message || "Internal Server Error";
|
|
14
|
+
|
|
15
|
+
res.status(statusCode).json({
|
|
16
|
+
error: {
|
|
17
|
+
message,
|
|
18
|
+
stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { BaseRepository } from "./base.repository.js";
|
|
2
|
+
|
|
3
|
+
export interface DBArtifact {
|
|
4
|
+
id: number;
|
|
5
|
+
taskId: number;
|
|
6
|
+
type: string;
|
|
7
|
+
title: string;
|
|
8
|
+
contentText?: string;
|
|
9
|
+
filePath?: string;
|
|
10
|
+
createdBy: string;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ArtifactRepository extends BaseRepository {
|
|
15
|
+
findByTaskId(taskId: number | string): DBArtifact[] {
|
|
16
|
+
return this.db
|
|
17
|
+
.prepare(
|
|
18
|
+
"SELECT * FROM artifacts WHERE taskId = ? ORDER BY createdAt DESC"
|
|
19
|
+
)
|
|
20
|
+
.all(taskId) as DBArtifact[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
create(data: Omit<DBArtifact, "id" | "createdAt">): void {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
this.db
|
|
26
|
+
.prepare(`
|
|
27
|
+
INSERT INTO artifacts (taskId, type, title, contentText, filePath, createdBy, createdAt)
|
|
28
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
29
|
+
`)
|
|
30
|
+
.run(
|
|
31
|
+
data.taskId,
|
|
32
|
+
data.type,
|
|
33
|
+
data.title,
|
|
34
|
+
data.contentText ?? null,
|
|
35
|
+
data.filePath ?? null,
|
|
36
|
+
data.createdBy,
|
|
37
|
+
now
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deleteByTaskId(taskId: number | string): void {
|
|
42
|
+
this.db.prepare("DELETE FROM artifacts WHERE taskId = ?").run(taskId);
|
|
43
|
+
}
|
|
44
|
+
}
|