idea-manager 0.2.0 → 0.3.1
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 +33 -41
- package/next.config.ts +0 -1
- package/package.json +2 -2
- package/{src/app/icon.svg → public/favicon.svg} +2 -2
- package/src/app/api/filesystem/route.ts +49 -0
- package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
- package/src/app/api/projects/[id]/items/route.ts +51 -1
- package/src/app/api/projects/[id]/scan/route.ts +73 -0
- package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
- package/src/app/api/projects/[id]/structure/route.ts +34 -3
- package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
- package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/globals.css +465 -5
- package/src/app/layout.tsx +3 -0
- package/src/app/page.tsx +260 -88
- package/src/app/projects/[id]/page.tsx +366 -183
- package/src/cli.ts +10 -10
- package/src/components/DirectoryPicker.tsx +137 -0
- package/src/components/ScanPanel.tsx +743 -0
- package/src/components/brainstorm/Editor.tsx +20 -4
- package/src/components/brainstorm/MemoPin.tsx +91 -5
- package/src/components/dashboard/SubProjectCard.tsx +76 -0
- package/src/components/dashboard/TabBar.tsx +42 -0
- package/src/components/task/ProjectTree.tsx +223 -0
- package/src/components/task/PromptEditor.tsx +107 -0
- package/src/components/task/StatusFlow.tsx +43 -0
- package/src/components/task/TaskChat.tsx +134 -0
- package/src/components/task/TaskDetail.tsx +205 -0
- package/src/components/task/TaskList.tsx +119 -0
- package/src/components/tree/CardView.tsx +206 -0
- package/src/components/tree/RefinePopover.tsx +157 -0
- package/src/components/tree/TreeNode.tsx +147 -38
- package/src/components/tree/TreeView.tsx +270 -26
- package/src/components/ui/ConfirmDialog.tsx +88 -0
- package/src/lib/ai/chat-responder.ts +4 -2
- package/src/lib/ai/cleanup.ts +87 -0
- package/src/lib/ai/client.ts +175 -58
- package/src/lib/ai/prompter.ts +19 -24
- package/src/lib/ai/refiner.ts +128 -0
- package/src/lib/ai/structurer.ts +340 -11
- package/src/lib/db/queries/context.ts +76 -0
- package/src/lib/db/queries/items.ts +133 -12
- package/src/lib/db/queries/projects.ts +12 -8
- package/src/lib/db/queries/sub-projects.ts +122 -0
- package/src/lib/db/queries/task-conversations.ts +27 -0
- package/src/lib/db/queries/task-prompts.ts +32 -0
- package/src/lib/db/queries/tasks.ts +133 -0
- package/src/lib/db/schema.ts +75 -0
- package/src/lib/mcp/server.ts +38 -39
- package/src/lib/mcp/tools.ts +47 -45
- package/src/lib/scanner.ts +573 -0
- package/src/lib/task-store.ts +97 -0
- package/src/types/index.ts +65 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { getProject } from '@/lib/db/queries/projects';
|
|
3
|
+
import { getBrainstorm } from '@/lib/db/queries/brainstorms';
|
|
4
|
+
import { getProjectContextSummary } from '@/lib/db/queries/context';
|
|
5
|
+
import { structureWithChatDirect } from '@/lib/ai/structurer';
|
|
6
|
+
import {
|
|
7
|
+
getTask, startTask, addTaskEvent, finishTask, failTask,
|
|
8
|
+
addTaskListener, cleanupTasks,
|
|
9
|
+
} from '@/lib/task-store';
|
|
10
|
+
|
|
11
|
+
export async function GET(
|
|
12
|
+
request: NextRequest,
|
|
13
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
14
|
+
) {
|
|
15
|
+
const { id } = await params;
|
|
16
|
+
const project = getProject(id);
|
|
17
|
+
if (!project) {
|
|
18
|
+
return new Response('Project not found', { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
cleanupTasks();
|
|
22
|
+
|
|
23
|
+
const existingTask = getTask(id);
|
|
24
|
+
|
|
25
|
+
// If there's an active task, attach to it (reconnect scenario)
|
|
26
|
+
if (existingTask && existingTask.status === 'running') {
|
|
27
|
+
return createReconnectStream(id, existingTask);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If recently finished task exists, replay final result
|
|
31
|
+
if (existingTask && existingTask.status === 'done' && existingTask.result) {
|
|
32
|
+
return createReplayStream(existingTask);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Start new task
|
|
36
|
+
const brainstorm = getBrainstorm(id);
|
|
37
|
+
if (!brainstorm) {
|
|
38
|
+
return new Response('Project not initialized', { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hasContent = brainstorm.content.trim();
|
|
42
|
+
const hasContext = !!getProjectContextSummary(id);
|
|
43
|
+
|
|
44
|
+
if (!hasContent && !hasContext) {
|
|
45
|
+
return new Response('No content to structure', { status: 400 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// User-provided project description from scan panel
|
|
49
|
+
const userDescription = request.nextUrl.searchParams.get('desc') || '';
|
|
50
|
+
|
|
51
|
+
let content = hasContent
|
|
52
|
+
? brainstorm.content
|
|
53
|
+
: '프로젝트 스캔 결과를 분석하여 현재 프로젝트의 구조, 진행 상황, TODO 항목을 파악해주세요.';
|
|
54
|
+
|
|
55
|
+
if (userDescription) {
|
|
56
|
+
content = `[사용자가 제공한 프로젝트 설명]\n${userDescription}\n\n${content}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const brainstormId = brainstorm.id;
|
|
60
|
+
|
|
61
|
+
// Start background task
|
|
62
|
+
startTask(id);
|
|
63
|
+
|
|
64
|
+
const send = async (event: string, data: unknown) => {
|
|
65
|
+
addTaskEvent(id, event, data);
|
|
66
|
+
if (event === 'done') {
|
|
67
|
+
finishTask(id, data);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Run structuring in background (detached from stream)
|
|
72
|
+
(async () => {
|
|
73
|
+
try {
|
|
74
|
+
await structureWithChatDirect(id, brainstormId, content, send);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const msg = error instanceof Error ? error.message : 'Structure failed';
|
|
77
|
+
addTaskEvent(id, 'error', { error: msg });
|
|
78
|
+
failTask(id, msg);
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
// Stream events to this client
|
|
83
|
+
return createReconnectStream(id, getTask(id)!);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createReconnectStream(projectId: string, task: ReturnType<typeof getTask>) {
|
|
87
|
+
const encoder = new TextEncoder();
|
|
88
|
+
let unsubscribe: (() => void) | null = null;
|
|
89
|
+
|
|
90
|
+
const stream = new ReadableStream({
|
|
91
|
+
start(controller) {
|
|
92
|
+
const send = (event: string, data: unknown) => {
|
|
93
|
+
try {
|
|
94
|
+
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
95
|
+
} catch {
|
|
96
|
+
// controller may be closed
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Replay past events
|
|
101
|
+
if (task) {
|
|
102
|
+
for (const ev of task.events) {
|
|
103
|
+
send(ev.event, ev.data);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If already finished, close immediately
|
|
107
|
+
if (task.status !== 'running') {
|
|
108
|
+
controller.close();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Listen for new events
|
|
114
|
+
unsubscribe = addTaskListener(projectId, (event, data) => {
|
|
115
|
+
send(event, data);
|
|
116
|
+
if (event === 'done' || event === 'error') {
|
|
117
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
118
|
+
unsubscribe?.();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
cancel() {
|
|
123
|
+
unsubscribe?.();
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return new Response(stream, {
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'text/event-stream',
|
|
130
|
+
'Cache-Control': 'no-cache',
|
|
131
|
+
'Connection': 'keep-alive',
|
|
132
|
+
'X-Accel-Buffering': 'no',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createReplayStream(task: NonNullable<ReturnType<typeof getTask>>) {
|
|
138
|
+
const encoder = new TextEncoder();
|
|
139
|
+
const stream = new ReadableStream({
|
|
140
|
+
start(controller) {
|
|
141
|
+
for (const ev of task.events) {
|
|
142
|
+
try {
|
|
143
|
+
controller.enqueue(encoder.encode(`event: ${ev.event}\ndata: ${JSON.stringify(ev.data)}\n\n`));
|
|
144
|
+
} catch { break; }
|
|
145
|
+
}
|
|
146
|
+
controller.close();
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return new Response(stream, {
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'text/event-stream',
|
|
153
|
+
'Cache-Control': 'no-cache',
|
|
154
|
+
'Connection': 'keep-alive',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getSubProject, updateSubProject, deleteSubProject } from '@/lib/db/queries/sub-projects';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
_request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
7
|
+
) {
|
|
8
|
+
const { subId } = await params;
|
|
9
|
+
const sp = getSubProject(subId);
|
|
10
|
+
if (!sp) {
|
|
11
|
+
return NextResponse.json({ error: 'Sub-project not found' }, { status: 404 });
|
|
12
|
+
}
|
|
13
|
+
return NextResponse.json(sp);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function PUT(
|
|
17
|
+
request: NextRequest,
|
|
18
|
+
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
19
|
+
) {
|
|
20
|
+
const { subId } = await params;
|
|
21
|
+
const body = await request.json();
|
|
22
|
+
const sp = updateSubProject(subId, body);
|
|
23
|
+
if (!sp) {
|
|
24
|
+
return NextResponse.json({ error: 'Sub-project not found' }, { status: 404 });
|
|
25
|
+
}
|
|
26
|
+
return NextResponse.json(sp);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function DELETE(
|
|
30
|
+
_request: NextRequest,
|
|
31
|
+
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
32
|
+
) {
|
|
33
|
+
const { subId } = await params;
|
|
34
|
+
const deleted = deleteSubProject(subId);
|
|
35
|
+
if (!deleted) {
|
|
36
|
+
return NextResponse.json({ error: 'Sub-project not found' }, { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
return NextResponse.json({ success: true });
|
|
39
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getTaskConversations, addTaskConversation } from '@/lib/db/queries/task-conversations';
|
|
3
|
+
import { getTask } from '@/lib/db/queries/tasks';
|
|
4
|
+
import { getTaskPrompt } from '@/lib/db/queries/task-prompts';
|
|
5
|
+
import { getBrainstorm } from '@/lib/db/queries/brainstorms';
|
|
6
|
+
import { runClaude } from '@/lib/ai/client';
|
|
7
|
+
|
|
8
|
+
export async function GET(
|
|
9
|
+
_request: NextRequest,
|
|
10
|
+
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
11
|
+
) {
|
|
12
|
+
const { taskId } = await params;
|
|
13
|
+
const conversations = getTaskConversations(taskId);
|
|
14
|
+
return NextResponse.json(conversations);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function POST(
|
|
18
|
+
request: NextRequest,
|
|
19
|
+
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
20
|
+
) {
|
|
21
|
+
const { id: projectId, taskId } = await params;
|
|
22
|
+
const body = await request.json();
|
|
23
|
+
|
|
24
|
+
if (!body.message || typeof body.message !== 'string') {
|
|
25
|
+
return NextResponse.json({ error: 'message is required' }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const task = getTask(taskId);
|
|
29
|
+
if (!task) {
|
|
30
|
+
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Save user message
|
|
34
|
+
const userMsg = addTaskConversation(taskId, 'user', body.message);
|
|
35
|
+
|
|
36
|
+
// Build context for AI
|
|
37
|
+
const history = getTaskConversations(taskId);
|
|
38
|
+
const prompt = getTaskPrompt(taskId);
|
|
39
|
+
const brainstorm = getBrainstorm(projectId);
|
|
40
|
+
|
|
41
|
+
const systemPrompt = `You are a helpful assistant helping refine a development task. Respond in Korean. Be concise.
|
|
42
|
+
|
|
43
|
+
Task: ${task.title}
|
|
44
|
+
Description: ${task.description}
|
|
45
|
+
Status: ${task.status}
|
|
46
|
+
${prompt?.content ? `Current prompt:\n${prompt.content}` : ''}
|
|
47
|
+
${brainstorm?.content ? `\nBrainstorming context:\n${brainstorm.content.slice(0, 3000)}` : ''}`;
|
|
48
|
+
|
|
49
|
+
const conversationText = history
|
|
50
|
+
.map(m => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
|
|
51
|
+
.join('\n');
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const aiResponse = await runClaude(`${systemPrompt}\n\nConversation:\n${conversationText}`);
|
|
55
|
+
const aiMsg = addTaskConversation(taskId, 'assistant', aiResponse.trim());
|
|
56
|
+
return NextResponse.json({ userMessage: userMsg, aiMessage: aiMsg });
|
|
57
|
+
} catch {
|
|
58
|
+
return NextResponse.json({ error: 'AI response failed' }, { status: 500 });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getTaskPrompt, upsertTaskPrompt } from '@/lib/db/queries/task-prompts';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
_request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
7
|
+
) {
|
|
8
|
+
const { taskId } = await params;
|
|
9
|
+
const prompt = getTaskPrompt(taskId);
|
|
10
|
+
return NextResponse.json(prompt ?? { content: '', prompt_type: 'manual' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function PUT(
|
|
14
|
+
request: NextRequest,
|
|
15
|
+
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
16
|
+
) {
|
|
17
|
+
const { taskId } = await params;
|
|
18
|
+
const body = await request.json();
|
|
19
|
+
|
|
20
|
+
if (typeof body.content !== 'string') {
|
|
21
|
+
return NextResponse.json({ error: 'content is required' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const prompt = upsertTaskPrompt(taskId, body.content, body.prompt_type);
|
|
25
|
+
return NextResponse.json(prompt);
|
|
26
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getTask, updateTask, deleteTask } from '@/lib/db/queries/tasks';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
_request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
7
|
+
) {
|
|
8
|
+
const { taskId } = await params;
|
|
9
|
+
const task = getTask(taskId);
|
|
10
|
+
if (!task) {
|
|
11
|
+
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
|
12
|
+
}
|
|
13
|
+
return NextResponse.json(task);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function PUT(
|
|
17
|
+
request: NextRequest,
|
|
18
|
+
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
19
|
+
) {
|
|
20
|
+
const { taskId } = await params;
|
|
21
|
+
const body = await request.json();
|
|
22
|
+
const task = updateTask(taskId, body);
|
|
23
|
+
if (!task) {
|
|
24
|
+
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
|
25
|
+
}
|
|
26
|
+
return NextResponse.json(task);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function DELETE(
|
|
30
|
+
_request: NextRequest,
|
|
31
|
+
{ params }: { params: Promise<{ id: string; subId: string; taskId: string }> },
|
|
32
|
+
) {
|
|
33
|
+
const { taskId } = await params;
|
|
34
|
+
const deleted = deleteTask(taskId);
|
|
35
|
+
if (!deleted) {
|
|
36
|
+
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
return NextResponse.json({ success: true });
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getTasks, createTask } from '@/lib/db/queries/tasks';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
_request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
7
|
+
) {
|
|
8
|
+
const { subId } = await params;
|
|
9
|
+
const tasks = getTasks(subId);
|
|
10
|
+
return NextResponse.json(tasks);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function POST(
|
|
14
|
+
request: NextRequest,
|
|
15
|
+
{ params }: { params: Promise<{ id: string; subId: string }> },
|
|
16
|
+
) {
|
|
17
|
+
const { id, subId } = await params;
|
|
18
|
+
const body = await request.json();
|
|
19
|
+
|
|
20
|
+
if (!body.title || typeof body.title !== 'string') {
|
|
21
|
+
return NextResponse.json({ error: 'title is required' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const task = createTask({
|
|
25
|
+
project_id: id,
|
|
26
|
+
sub_project_id: subId,
|
|
27
|
+
title: body.title,
|
|
28
|
+
description: body.description,
|
|
29
|
+
status: body.status,
|
|
30
|
+
priority: body.priority,
|
|
31
|
+
});
|
|
32
|
+
return NextResponse.json(task, { status: 201 });
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getSubProjectsWithStats, createSubProject } from '@/lib/db/queries/sub-projects';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
_request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
7
|
+
) {
|
|
8
|
+
const { id } = await params;
|
|
9
|
+
const subProjects = getSubProjectsWithStats(id);
|
|
10
|
+
return NextResponse.json(subProjects);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function POST(
|
|
14
|
+
request: NextRequest,
|
|
15
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
16
|
+
) {
|
|
17
|
+
const { id } = await params;
|
|
18
|
+
const body = await request.json();
|
|
19
|
+
|
|
20
|
+
if (!body.name || typeof body.name !== 'string') {
|
|
21
|
+
return NextResponse.json({ error: 'name is required' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sp = createSubProject({
|
|
25
|
+
project_id: id,
|
|
26
|
+
name: body.name,
|
|
27
|
+
description: body.description,
|
|
28
|
+
folder_path: body.folder_path,
|
|
29
|
+
});
|
|
30
|
+
return NextResponse.json(sp, { status: 201 });
|
|
31
|
+
}
|
|
@@ -14,6 +14,6 @@ export async function POST(request: NextRequest) {
|
|
|
14
14
|
return NextResponse.json({ error: 'name is required' }, { status: 400 });
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const project = createProject(name, description || '');
|
|
17
|
+
const project = createProject(name, description || '', body.project_path || undefined);
|
|
18
18
|
return NextResponse.json(project, { status: 201 });
|
|
19
19
|
}
|