@yinuo-ngm/server 1.0.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 (80) hide show
  1. package/lib/app.d.ts +2 -0
  2. package/lib/app.js +65 -0
  3. package/lib/common/api.d.ts +2 -0
  4. package/lib/common/api.js +13 -0
  5. package/lib/common/editor.d.ts +5 -0
  6. package/lib/common/editor.js +69 -0
  7. package/lib/env.d.ts +7 -0
  8. package/lib/env.js +16 -0
  9. package/lib/index.d.ts +1 -0
  10. package/lib/index.js +20 -0
  11. package/lib/plugins/core.plugin.d.ts +9 -0
  12. package/lib/plugins/core.plugin.js +16 -0
  13. package/lib/plugins/error-handler.plugin.d.ts +6 -0
  14. package/lib/plugins/error-handler.plugin.js +113 -0
  15. package/lib/plugins/request-id.plugin.d.ts +3 -0
  16. package/lib/plugins/request-id.plugin.js +14 -0
  17. package/lib/plugins/routes.plugin.d.ts +3 -0
  18. package/lib/plugins/routes.plugin.js +17 -0
  19. package/lib/plugins/static.plugin.d.ts +2 -0
  20. package/lib/plugins/static.plugin.js +24 -0
  21. package/lib/plugins/success-handle.plugin.d.ts +3 -0
  22. package/lib/plugins/success-handle.plugin.js +30 -0
  23. package/lib/plugins/ws/topics/index.d.ts +1 -0
  24. package/lib/plugins/ws/topics/index.js +17 -0
  25. package/lib/plugins/ws/topics/syslog.ws.d.ts +9 -0
  26. package/lib/plugins/ws/topics/syslog.ws.js +28 -0
  27. package/lib/plugins/ws/topics/task.ws.d.ts +12 -0
  28. package/lib/plugins/ws/topics/task.ws.js +75 -0
  29. package/lib/plugins/ws/ws.context.d.ts +12 -0
  30. package/lib/plugins/ws/ws.context.js +35 -0
  31. package/lib/plugins/ws/ws.plugin.d.ts +3 -0
  32. package/lib/plugins/ws/ws.plugin.js +76 -0
  33. package/lib/plugins/ws/ws.router.d.ts +20 -0
  34. package/lib/plugins/ws/ws.router.js +67 -0
  35. package/lib/routes/config.routes.d.ts +2 -0
  36. package/lib/routes/config.routes.js +65 -0
  37. package/lib/routes/dashboard.routes.d.ts +2 -0
  38. package/lib/routes/dashboard.routes.js +33 -0
  39. package/lib/routes/deps.route.d.ts +2 -0
  40. package/lib/routes/deps.route.js +25 -0
  41. package/lib/routes/fs.routes.d.ts +2 -0
  42. package/lib/routes/fs.routes.js +29 -0
  43. package/lib/routes/index.d.ts +9 -0
  44. package/lib/routes/index.js +22 -0
  45. package/lib/routes/project.routes.d.ts +2 -0
  46. package/lib/routes/project.routes.js +196 -0
  47. package/lib/routes/rss.routes.d.ts +2 -0
  48. package/lib/routes/rss.routes.js +53 -0
  49. package/lib/routes/system.routes.d.ts +2 -0
  50. package/lib/routes/system.routes.js +28 -0
  51. package/lib/routes/task.routes.d.ts +2 -0
  52. package/lib/routes/task.routes.js +48 -0
  53. package/package.json +29 -0
  54. package/src/app.ts +76 -0
  55. package/src/common/api.ts +12 -0
  56. package/src/common/editor.ts +49 -0
  57. package/src/env.ts +12 -0
  58. package/src/index.ts +21 -0
  59. package/src/plugins/core.plugin.ts +34 -0
  60. package/src/plugins/error-handler.plugin.ts +168 -0
  61. package/src/plugins/request-id.plugin.ts +14 -0
  62. package/src/plugins/routes.plugin.ts +24 -0
  63. package/src/plugins/static.plugin.ts +30 -0
  64. package/src/plugins/success-handle.plugin.ts +33 -0
  65. package/src/plugins/ws/topics/index.ts +1 -0
  66. package/src/plugins/ws/topics/syslog.ws.ts +36 -0
  67. package/src/plugins/ws/topics/task.ws.ts +96 -0
  68. package/src/plugins/ws/ws.context.ts +32 -0
  69. package/src/plugins/ws/ws.plugin.ts +103 -0
  70. package/src/plugins/ws/ws.router.ts +79 -0
  71. package/src/routes/config.routes.ts +86 -0
  72. package/src/routes/dashboard.routes.ts +43 -0
  73. package/src/routes/deps.route.ts +51 -0
  74. package/src/routes/fs.routes.ts +31 -0
  75. package/src/routes/index.ts +19 -0
  76. package/src/routes/project.routes.ts +265 -0
  77. package/src/routes/rss.routes.ts +58 -0
  78. package/src/routes/system.routes.ts +32 -0
  79. package/src/routes/task.routes.ts +85 -0
  80. package/tsconfig.json +13 -0
@@ -0,0 +1,96 @@
1
+ import type { WsClientMsg, WsServerMsg, TaskEventPayloadMap, TaskEventType, TaskOutputPayload, TaskOutputMsg, TaskEventMsg, LogLine, TaskRuntime } from "@yinuo-ngm/core";
2
+ import { WsContext } from "../ws.context";
3
+ import type { TopicHandler } from "../ws.router";
4
+
5
+ const keyOfTask = (taskId: string) => `task:${taskId}`;
6
+
7
+ export type TaskWsDeps = {
8
+ getTaskSnapshotByTaskId?: (taskId: string) => Promise<TaskRuntime | null>;
9
+ getTaskTailLogsByRun?: (runId: string, tail: number) => Promise<LogLine[]>;
10
+ resizeRun?: (taskId: string, cols: number, rows: number) => void;
11
+ };
12
+
13
+ export function createTaskTopicHandler(
14
+ deps: TaskWsDeps,
15
+ getAllClients: () => Iterable<WsContext>
16
+ ): TopicHandler & {
17
+ pushOutput(payload: TaskOutputPayload): void;
18
+ pushEvent<K extends TaskEventType>(type: K, payload: TaskEventPayloadMap[K]): void;
19
+ } {
20
+ return {
21
+ topic: "task",
22
+ async sub(ctx, msg: Extract<WsClientMsg, { op: "sub"; topic: "task" }>) {
23
+ const taskId = String(msg?.taskId ?? "").trim();
24
+ const tail = Number(msg?.tail ?? 0);
25
+
26
+ if (!taskId) {
27
+ ctx.send({ op: "error", code: "TASK_ID_REQUIRED", message: "taskId is required", ts: Date.now() });
28
+ return;
29
+ }
30
+ ctx.addSub("task", keyOfTask(taskId));
31
+
32
+ // snapshot:按 taskId
33
+ let snap: TaskRuntime | undefined
34
+ if (deps.getTaskSnapshotByTaskId) {
35
+ snap = await deps.getTaskSnapshotByTaskId(taskId) ?? undefined;
36
+ if (snap) {
37
+ const m: WsServerMsg = {
38
+ op: "task.event",
39
+ type: "snapshot",
40
+ payload: snap,
41
+ ts: Date.now(),
42
+ };
43
+ ctx.send(m);
44
+ }
45
+ }
46
+
47
+ // tail logs : 按 runId 去拉
48
+ if (snap?.runId && deps.getTaskTailLogsByRun && tail > 0) {
49
+ const entries = await deps.getTaskTailLogsByRun(snap.runId, tail);
50
+ for (const e of entries ?? []) {
51
+ const m: TaskOutputMsg = {
52
+ op: "task.output",
53
+ payload: {
54
+ taskId: taskId,
55
+ runId: snap.runId,
56
+ stream: (e?.level === "warn" || e?.level === "error") ? "stderr" : "stdout",
57
+ text: String(e?.text ?? ""),
58
+ },
59
+ ts: e?.ts ?? Date.now(),
60
+ };
61
+ ctx.send(m);
62
+ }
63
+ }
64
+ },
65
+ // resize
66
+ resize(ctx, msg: Extract<WsClientMsg, { op: "resize" }>) {
67
+ const taskId = (msg as any).taskId as string;
68
+ const cols = Number((msg as any).cols);
69
+ const rows = Number((msg as any).rows);
70
+ if (!taskId || !Number.isFinite(cols) || !Number.isFinite(rows)) return;
71
+
72
+ // 最小值
73
+ const c = Math.max(2, Math.floor(cols));
74
+ const r = Math.max(1, Math.floor(rows));
75
+
76
+ deps.resizeRun?.(taskId, c, r);
77
+ },
78
+
79
+ unsub(ctx, msg: Extract<WsClientMsg, { op: "unsub"; topic: "task" }>) {
80
+ const taskId = String(msg?.taskId ?? "").trim();
81
+ if (!taskId) return;
82
+ ctx.delSub("task", keyOfTask(taskId));
83
+ },
84
+
85
+ pushOutput(payload: TaskOutputPayload) {
86
+ const m: TaskOutputMsg = { op: "task.output", payload, ts: Date.now() };
87
+ for (const c of getAllClients()) if (c.hasSub("task", keyOfTask(payload.taskId))) c.send(m);
88
+ },
89
+
90
+ pushEvent<K extends TaskEventType>(type: K, payload: TaskEventPayloadMap[K]) {
91
+ const m: WsServerMsg = { op: "task.event", type, payload, ts: Date.now() } as TaskEventMsg;
92
+ for (const c of getAllClients()) if (c.hasSub("task", keyOfTask(payload.taskId))) c.send(m);
93
+
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,32 @@
1
+ import type { WsServerMsg, WsTopic } from "@yinuo-ngm/core";
2
+ export class WsContext {
3
+ readonly connId: string;
4
+ private subs = new Map<WsTopic, Set<string>>();
5
+
6
+ constructor(connId: string, private socket: WebSocket) {
7
+ this.connId = connId;
8
+ }
9
+
10
+ send(msg: WsServerMsg) {
11
+ if (this.socket.readyState !== this.socket.OPEN) return;
12
+ this.socket.send(JSON.stringify(msg));
13
+ }
14
+
15
+ addSub(topic: WsTopic, key: string) {
16
+ const set = this.subs.get(topic) ?? new Set<string>();
17
+ set.add(key);
18
+ this.subs.set(topic, set);
19
+ }
20
+ delSub(topic: WsTopic, key: string) {
21
+ const set = this.subs.get(topic);
22
+ if (!set) return;
23
+ set.delete(key);
24
+ if (set.size === 0) this.subs.delete(topic);
25
+ }
26
+ hasSub(topic: WsTopic, key: string) {
27
+ return this.subs.get(topic)?.has(key) ?? false;
28
+ }
29
+ clearAll() {
30
+ this.subs.clear();
31
+ }
32
+ }
@@ -0,0 +1,103 @@
1
+ // server/src/plugins/ws/ws.plugin.ts
2
+ import { Events } from "@yinuo-ngm/core";
3
+ import websocket from "@fastify/websocket";
4
+ import type { FastifyInstance } from "fastify";
5
+ import fp from "fastify-plugin";
6
+ import { createTaskTopicHandler } from "./topics";
7
+ import { createSyslogTopicHandler } from "./topics/syslog.ws";
8
+ import { WsContext } from "./ws.context";
9
+ import { WsRouter } from "./ws.router";
10
+
11
+ function uid() {
12
+ return Math.random().toString(16).slice(2) + Date.now().toString(16);
13
+ }
14
+
15
+ export default fp(async function wsPlugin(fastify: FastifyInstance) {
16
+
17
+ await fastify.register(websocket);
18
+
19
+ const clients = new Map<string, WsContext>();
20
+
21
+ const router = new WsRouter();
22
+
23
+ // 注册 topics: core.task 的能力注入进来
24
+ const taskHandler = createTaskTopicHandler(
25
+ {
26
+ getTaskSnapshotByTaskId: (taskId) => fastify.core.task.getSnapshotByTaskId(taskId),
27
+ getTaskTailLogsByRun: (runId, tail) => fastify.core.task.getTailLogsByRun(runId, tail),
28
+ resizeRun: (taskId, cols, rows) => fastify.core.task.resizeRun(taskId, cols, rows),
29
+ },
30
+ () => clients.values()
31
+ );
32
+ router.register(taskHandler);
33
+
34
+ // syslog
35
+ const syslogHandler = createSyslogTopicHandler(
36
+ { getSyslogTail: (tail) => fastify.core.sysLog.tail(tail) },
37
+ () => clients.values()
38
+ );
39
+ router.register(syslogHandler);
40
+
41
+
42
+ // 事件订阅:拿到 disposer,onClose 释放
43
+ const offs: Array<() => void> = [];
44
+
45
+ offs.push(fastify.core.events.on(Events.TASK_OUTPUT, (e) => {
46
+ taskHandler.pushOutput(e);
47
+ }));
48
+
49
+ offs.push(fastify.core.events.on(Events.TASK_STARTED, (e) => {
50
+ taskHandler.pushEvent("started", e);
51
+ }));
52
+
53
+ offs.push(fastify.core.events.on(Events.TASK_STOP_REQUESTED, (e) => {
54
+ taskHandler.pushEvent("stopRequested", e);
55
+ }));
56
+
57
+ offs.push(fastify.core.events.on(Events.TASK_EXITED, (e) => {
58
+ taskHandler.pushEvent("exited", e);
59
+ }));
60
+
61
+ offs.push(fastify.core.events.on(Events.TASK_FAILED, (e) => {
62
+ taskHandler.pushEvent("failed", e);
63
+ }));
64
+
65
+ offs.push(fastify.core.events.on(Events.SYSLOG_APPENDED, (e) => {
66
+ syslogHandler.push(e.entry);
67
+ }));
68
+
69
+ offs.push(fastify.core.events.on(Events.PROJECT_BOOTSTRAP_DONE, (e) => {
70
+ taskHandler.pushEvent("bootstrapDone", e);
71
+ }))
72
+
73
+ offs.push(fastify.core.events.on(Events.PROJECT_BOOTSTRAP_FAILED, (e) => {
74
+ taskHandler.pushEvent("bootstrapFailed", e);
75
+ }))
76
+
77
+ offs.push(fastify.core.events.on(Events.PROJECT_BOOTSTRAP_NEED_PICK_ROOT, (e) => {
78
+ taskHandler.pushEvent("bootstrapNeedPickRoot", e);
79
+ }))
80
+
81
+
82
+ fastify.addHook("onClose", async () => {
83
+ // 防止 dev 热重载 / 反复 register 导致重复推送
84
+ offs.forEach((off) => {
85
+ try { off(); } catch { }
86
+ });
87
+ clients.clear();
88
+ });
89
+
90
+ fastify.get("/ws", { websocket: true }, (ws) => {
91
+ const connId = uid();
92
+ const ctx = new WsContext(connId, ws);
93
+ // console.log("[ws] open", connId, "clients=", clients.size + 1);
94
+ clients.set(connId, ctx);
95
+ ctx.send({ op: "hello", connId, ts: Date.now() });
96
+ // 接收client消息
97
+ ws.on("message", (data: any) => router.handleRaw(ctx, data));
98
+ ws.on("close", () => {
99
+ ctx.clearAll();
100
+ clients.delete(connId);
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,79 @@
1
+ import { WsClientMsg, WsServerMsg, WsTopic, ErrorCode } from "@yinuo-ngm/core";
2
+ import { WsContext } from "./ws.context";
3
+
4
+ export type TopicHandler = {
5
+ topic: WsTopic;
6
+ sub(ctx: WsContext, msg: Extract<WsClientMsg, { op: "sub" }>): Promise<void> | void;
7
+ unsub(ctx: WsContext, msg: Extract<WsClientMsg, { op: "unsub" }>): Promise<void> | void;
8
+
9
+ resize?(ctx: WsContext, msg: Extract<WsClientMsg, { op: "resize" }>): Promise<void> | void;
10
+ };
11
+
12
+ export class WsRouter {
13
+ private handlers = new Map<WsTopic, TopicHandler>();
14
+
15
+ register(handler: TopicHandler) {
16
+ this.handlers.set(handler.topic, handler);
17
+ }
18
+
19
+ handleRaw(ctx: WsContext, raw: any) {
20
+ let msg: WsClientMsg | null = null;
21
+ // console.log("[ws] recv", ctx.connId, raw);
22
+ try {
23
+ const text = typeof raw === "string" ? raw : raw?.toString?.() ?? "";
24
+ msg = JSON.parse(text);
25
+ } catch {
26
+ ctx.send(this.err("BAD_JSON", "Invalid JSON"));
27
+ return;
28
+ }
29
+
30
+ if (!msg || typeof msg !== "object" || !("op" in msg)) {
31
+ ctx.send(this.err("BAD_MSG", "Invalid message"));
32
+ return;
33
+ }
34
+
35
+ if (msg.op === "ping") {
36
+ ctx.send({ op: "pong", ts: Date.now() });
37
+ return;
38
+ }
39
+
40
+ // resize
41
+ if (msg.op === "resize") {
42
+ const topic = (msg as Extract<WsClientMsg, { op: "resize" }>).topic as WsTopic;
43
+ const h = this.handlers.get(topic);
44
+ if (!h || !h.resize) {
45
+ ctx.send(this.err("OP_NOT_SUPPORTED", `resize not supported on topic: ${String(topic)}`, { topic }));
46
+ return;
47
+ }
48
+ try {
49
+ return void h.resize(ctx, msg as any);
50
+ } catch (e: any) {
51
+ ctx.send(this.err("HANDLER_FAILED", e?.message ?? "handler failed", { topic, op: msg.op }));
52
+ return;
53
+ }
54
+ }
55
+
56
+
57
+ if (msg.op === "sub" || msg.op === "unsub") {
58
+ const topic = (msg as Extract<WsClientMsg, { op: "sub" | "unsub" }>).topic as WsTopic;
59
+ const h = this.handlers.get(topic);
60
+ if (!h) {
61
+ ctx.send(this.err("TOPIC_NOT_FOUND", `Unknown topic: ${String(topic)}`, { topic }));
62
+ return;
63
+ }
64
+ try {
65
+ if (msg.op === "sub") return void h.sub(ctx, msg as any);
66
+ return void h.unsub(ctx, msg as any);
67
+ } catch (e: any) {
68
+ ctx.send(this.err("HANDLER_FAILED", e?.message ?? "handler failed", { topic, op: msg.op }));
69
+ }
70
+ return;
71
+ }
72
+
73
+ ctx.send(this.err("OP_NOT_FOUND", `Unknown op: ${(msg as any).op}`));
74
+ }
75
+
76
+ private err(code: ErrorCode, message: string, details?: any): WsServerMsg {
77
+ return { op: "error", code, message, details, ts: Date.now() };
78
+ }
79
+ }
@@ -0,0 +1,86 @@
1
+ import { AppError } from "@yinuo-ngm/core";
2
+ import type { FastifyInstance } from "fastify";
3
+ import { openFolder } from "../common/editor";
4
+
5
+ export default async function configRoutes(fastify: FastifyInstance) {
6
+
7
+ // GET catalog
8
+ fastify.get("/catalog/:projectId", async (req) => {
9
+ const { projectId } = req.params as { projectId: string };
10
+ return await fastify.core.config.getCatalog(projectId);
11
+ });
12
+
13
+ // GET read doc
14
+ fastify.get("/readDoc/:projectId/:docId", async (req) => {
15
+ const { projectId, docId } = req.params as { projectId: string; docId: string };
16
+ return await fastify.core.config.readDoc(projectId, docId);
17
+ });
18
+
19
+ // POST write doc
20
+ fastify.post("/writeDoc/:projectId/:docId", async (req) => {
21
+ const { projectId, docId } = req.params as { projectId: string; docId: string };
22
+ const body = req.body as any;
23
+
24
+ // 约定:raw 优先,其次 data
25
+ const next = body?.raw ?? body?.data;
26
+ if (next === undefined) {
27
+ throw new AppError("CONFIG_WRITE_FAILED", "missing body.raw or body.data", { projectId, docId });
28
+ }
29
+ return await fastify.core.config.writeDoc(projectId, docId, next);
30
+ });
31
+
32
+ /**
33
+ * 在编辑器打开项目
34
+ * POST /projects/openInEditor/:projectId
35
+ * body: { editor?: "code" | "system" }
36
+ */
37
+ fastify.post("/openInEditor/:projectId/:docId", async (req) => {
38
+ try {
39
+ const { projectId, docId } = req.params as { projectId: string; docId: string };
40
+ const { filePath } = await fastify.core.config.openDoc(projectId, docId);
41
+ await openFolder(filePath, { editor: 'code' });
42
+ return { ok: true };
43
+ } catch (e: any) {
44
+ throw new AppError("EDITOR_LAUNCH_FAILED", e?.message || "openInEditor failed");
45
+ }
46
+ });
47
+
48
+ // GET read schema
49
+ fastify.get("/readSchema/:projectId/:domainId", async (req) => {
50
+ const { projectId, domainId } = req.params as { projectId: string; domainId: string };
51
+ return await fastify.core.config.readDomainSchema(projectId, domainId);
52
+ });
53
+
54
+ // POST write schema
55
+ fastify.post("/writeSchema/:projectId/:domainId", async (req) => {
56
+ const { projectId, domainId } = req.params as { projectId: string; domainId: string };
57
+ const body = req.body as any;
58
+ const vm = body?.vm;
59
+ if (vm === undefined) {
60
+ throw new AppError("BAD_REQUEST", "missing body.vm");
61
+ }
62
+ await fastify.core.config.writeDomainSchema(projectId, domainId, vm);
63
+ return {
64
+ projectId,
65
+ domainId,
66
+ };
67
+ });
68
+
69
+ // GET domain schema doc
70
+ fastify.get("/getDomainSchema/:projectId/:domainId", async (req) => {
71
+ const { projectId, domainId } = req.params as { projectId: string; domainId: string };
72
+ return await fastify.core.config.getDomainSchemaDoc(projectId, domainId);
73
+ });
74
+
75
+ // POST diff domain schema
76
+ fastify.post("/diffSchema/:projectId/:domainId", async (req) => {
77
+ const { projectId, domainId } = req.params as { projectId: string; domainId: string };
78
+ const body = req.body as any;
79
+ const vm = body?.vm;
80
+ if (vm === undefined) {
81
+ throw new AppError("BAD_REQUEST", "missing body.vm");
82
+ }
83
+ return await fastify.core.config.diffDomainSchema(projectId, domainId, vm);
84
+ });
85
+
86
+ }
@@ -0,0 +1,43 @@
1
+ import type { DashboardDocV1 } from "@yinuo-ngm/core";
2
+ import type { FastifyInstance } from "fastify";
3
+
4
+ export default async function dashboardRoutes(fastify: FastifyInstance) {
5
+ // GET /dashboard/:projectId
6
+ fastify.get("/getInfo/:projectId", async (req,) => {
7
+ const { projectId } = req.params as any;
8
+ return await fastify.core.dashboard.getOrCreate(projectId);
9
+ });
10
+
11
+ // post /dashboard/update/:projectId
12
+ fastify.post("/update/:projectId", async (req,) => {
13
+ const { projectId } = req.params as any;
14
+ const body = req.body as DashboardDocV1;
15
+ return await fastify.core.dashboard.saveWithConflictCheck(projectId, body);
16
+ });
17
+
18
+ // post /dashboard/widgets
19
+ fastify.get("/widgets/:projectId", async (req,) => {
20
+ const { projectId } = req.params as any;
21
+ return await fastify.core.dashboard.getAvailableWidgets(projectId);
22
+ });
23
+
24
+ // post /dashboard/widgets/:projectId
25
+ fastify.get("/addWidget/:projectId/:widgetKey/:x/:y", async (req,) => {
26
+ let { projectId, widgetKey, x, y } = req.params as any;
27
+ x = Number(x) || 0;
28
+ y = Number(y) || 0;
29
+ return await fastify.core.dashboard.addWidget(projectId, widgetKey, x, y);
30
+ });
31
+
32
+ // delete /dashboard/widgets/:projectId/:widgetKey
33
+ fastify.get("/removeWidget/:projectId/:widgetId", async (req,) => {
34
+ const { projectId, widgetId } = req.params as any;
35
+ return await fastify.core.dashboard.removeWidget(projectId, widgetId);
36
+ });
37
+
38
+ fastify.post("/updateItemConfig/:projectId/:widgetId", async (req,) => {
39
+ const { projectId, widgetId } = req.params as any;
40
+ const config = req.body as any;
41
+ return await fastify.core.dashboard.updateItemConfig(projectId, widgetId, config);
42
+ });
43
+ }
@@ -0,0 +1,51 @@
1
+ import type { FastifyInstance } from "fastify";
2
+
3
+ export default async function depsRoutes(fastify: FastifyInstance) {
4
+
5
+ fastify.get("/list/:projectId", async (req) => {
6
+ const { projectId } = req.params as any;
7
+ const data = await fastify.core.deps.list(projectId);
8
+ return data;
9
+ });
10
+
11
+ fastify.post("/install/:projectId", async (req) => {
12
+ const { projectId } = req.params as any;
13
+ const body = req.body as {
14
+ name: string;
15
+ group: "dependencies" | "devDependencies";
16
+ target: "required" | "latest" | "custom";
17
+ version?: string;
18
+ };
19
+
20
+ await fastify.core.deps.install(projectId, body);
21
+ return { ok: true };
22
+ });
23
+
24
+ fastify.post("/uninstall/:projectId", async (req) => {
25
+ const { projectId } = req.params as any;
26
+ const body = req.body as {
27
+ name: string;
28
+ group: "dependencies" | "devDependencies";
29
+ };
30
+
31
+ await fastify.core.deps.uninstall(projectId, body);
32
+ return { ok: true };
33
+ });
34
+
35
+ fastify.post("/devtools/install/:projectId", async (req) => {
36
+ // // 先给最小实现:安装一组推荐包(可按需要改)
37
+ // const { projectId } = req.params as any;
38
+ // const project = await fastify.core.project.get(projectId);
39
+
40
+ // // 举例:eslint/prettier(也可以换成你自己的 devtools 集合)
41
+ // const pkgs = ["eslint", "prettier"];
42
+ // for (const name of pkgs) {
43
+ // await fastify.core.deps.install(projectId, {
44
+ // name,
45
+ // group: "devDependencies",
46
+ // target: "latest",
47
+ // });
48
+ // }
49
+ return { ok: true };
50
+ });
51
+ }
@@ -0,0 +1,31 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import path from "node:path";
3
+
4
+ export default async function fsRoutes(fastify: FastifyInstance) {
5
+
6
+ fastify.get("/ls", async (req) => {
7
+ const q = req.query as { path?: string; showSystem?: "0" | "1" | boolean };
8
+
9
+ const showSystem =
10
+ q.showSystem === "1" || q.showSystem === true || q.showSystem === ("true" as any);
11
+
12
+ return fastify.core.fs.ls(q.path || "", { showSystem, detectProject: true, detectConcurrency: 8 });
13
+ });
14
+
15
+ fastify.post("/mkdir", async (req) => {
16
+ const body = req.body as { path?: string; name?: string };
17
+ return fastify.core.fs.mkdir(body?.path || "", body?.name || "", {
18
+ recursive: true, // 默认支持多级创建
19
+ });
20
+ });
21
+
22
+ fastify.get("/path-exists", async (req) => {
23
+ const q = req.query as { path?: string };
24
+ const raw = String(q?.path ?? "").trim();
25
+ if (!raw) return { exists: false };
26
+ // 规范化一下,避免奇怪的相对路径
27
+ const p = path.resolve(raw);
28
+ const exists = await fastify.core.fs.exists(p);
29
+ return { exists };
30
+ });
31
+ }
@@ -0,0 +1,19 @@
1
+ import configRoutes from './config.routes';
2
+ import dashboardRoutes from './dashboard.routes';
3
+ import depsRoutes from './deps.route';
4
+ import fsRoutes from './fs.routes';
5
+ import projectRoutes from './project.routes';
6
+ import rssRoutes from './rss.routes';
7
+ import systemRoutes from './system.routes';
8
+ import taskRoutes from './task.routes';
9
+
10
+ export {
11
+ configRoutes,
12
+ depsRoutes,
13
+ fsRoutes,
14
+ projectRoutes,
15
+ systemRoutes,
16
+ taskRoutes,
17
+ dashboardRoutes,
18
+ rssRoutes,
19
+ };