@aion0/forge 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/CLAUDE.md +4 -0
- package/README.md +264 -0
- package/app/api/auth/[...nextauth]/route.ts +3 -0
- package/app/api/claude/[id]/route.ts +31 -0
- package/app/api/claude/[id]/stream/route.ts +63 -0
- package/app/api/claude/route.ts +28 -0
- package/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/app/api/claude-sessions/sync/route.ts +17 -0
- package/app/api/flows/route.ts +6 -0
- package/app/api/flows/run/route.ts +19 -0
- package/app/api/notify/test/route.ts +33 -0
- package/app/api/projects/route.ts +7 -0
- package/app/api/sessions/[id]/chat/route.ts +64 -0
- package/app/api/sessions/[id]/messages/route.ts +9 -0
- package/app/api/sessions/[id]/route.ts +17 -0
- package/app/api/sessions/route.ts +20 -0
- package/app/api/settings/route.ts +15 -0
- package/app/api/status/route.ts +12 -0
- package/app/api/tasks/[id]/route.ts +36 -0
- package/app/api/tasks/[id]/stream/route.ts +77 -0
- package/app/api/tasks/link/route.ts +37 -0
- package/app/api/tasks/route.ts +43 -0
- package/app/api/tasks/session/route.ts +14 -0
- package/app/api/templates/route.ts +6 -0
- package/app/api/tunnel/route.ts +20 -0
- package/app/api/watchers/route.ts +33 -0
- package/app/globals.css +26 -0
- package/app/icon.svg +26 -0
- package/app/layout.tsx +17 -0
- package/app/login/page.tsx +61 -0
- package/app/page.tsx +9 -0
- package/cli/mw.ts +377 -0
- package/components/ChatPanel.tsx +191 -0
- package/components/ClaudeTerminal.tsx +267 -0
- package/components/Dashboard.tsx +270 -0
- package/components/MarkdownContent.tsx +57 -0
- package/components/NewSessionModal.tsx +93 -0
- package/components/NewTaskModal.tsx +456 -0
- package/components/ProjectList.tsx +108 -0
- package/components/SessionList.tsx +74 -0
- package/components/SessionView.tsx +655 -0
- package/components/SettingsModal.tsx +366 -0
- package/components/StatusBar.tsx +99 -0
- package/components/TaskBoard.tsx +110 -0
- package/components/TaskDetail.tsx +351 -0
- package/components/TunnelToggle.tsx +163 -0
- package/components/WebTerminal.tsx +1069 -0
- package/docs/LOCAL-DEPLOY.md +144 -0
- package/docs/roadmap-multi-agent-workflow.md +330 -0
- package/instrumentation.ts +14 -0
- package/lib/auth.ts +47 -0
- package/lib/claude-process.ts +352 -0
- package/lib/claude-sessions.ts +267 -0
- package/lib/cloudflared.ts +218 -0
- package/lib/flows.ts +86 -0
- package/lib/init.ts +82 -0
- package/lib/notify.ts +75 -0
- package/lib/password.ts +77 -0
- package/lib/projects.ts +86 -0
- package/lib/session-manager.ts +156 -0
- package/lib/session-watcher.ts +345 -0
- package/lib/settings.ts +44 -0
- package/lib/task-manager.ts +668 -0
- package/lib/telegram-bot.ts +912 -0
- package/lib/terminal-server.ts +70 -0
- package/lib/terminal-standalone.ts +363 -0
- package/middleware.ts +33 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +16 -0
- package/package.json +66 -0
- package/postcss.config.mjs +7 -0
- package/src/config/index.ts +119 -0
- package/src/core/db/database.ts +133 -0
- package/src/core/memory/strategy.ts +32 -0
- package/src/core/providers/chat.ts +65 -0
- package/src/core/providers/registry.ts +60 -0
- package/src/core/session/manager.ts +190 -0
- package/src/types/index.ts +128 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getSessionManager } from '@/lib/session-manager';
|
|
3
|
+
|
|
4
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
5
|
+
const { id } = await params;
|
|
6
|
+
const manager = getSessionManager();
|
|
7
|
+
const session = manager.get(id) || manager.getByName(id);
|
|
8
|
+
if (!session) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
9
|
+
return NextResponse.json(session);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
13
|
+
const { id } = await params;
|
|
14
|
+
const manager = getSessionManager();
|
|
15
|
+
manager.delete(id);
|
|
16
|
+
return NextResponse.json({ ok: true });
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getSessionManager } from '@/lib/session-manager';
|
|
3
|
+
import { loadAllTemplates } from '@/src/config';
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
const manager = getSessionManager();
|
|
7
|
+
const sessions = manager.list();
|
|
8
|
+
return NextResponse.json(sessions);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function POST(req: Request) {
|
|
12
|
+
const body = await req.json();
|
|
13
|
+
const manager = getSessionManager();
|
|
14
|
+
try {
|
|
15
|
+
const session = manager.create(body);
|
|
16
|
+
return NextResponse.json(session);
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
return NextResponse.json({ error: err.message }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { loadSettings, saveSettings, type Settings } from '@/lib/settings';
|
|
3
|
+
import { restartTelegramBot } from '@/lib/init';
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
return NextResponse.json(loadSettings());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function PUT(req: Request) {
|
|
10
|
+
const body = await req.json() as Settings;
|
|
11
|
+
saveSettings(body);
|
|
12
|
+
// Restart Telegram bot in case token/chatId changed
|
|
13
|
+
restartTelegramBot();
|
|
14
|
+
return NextResponse.json({ ok: true });
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getSessionManager } from '@/lib/session-manager';
|
|
3
|
+
import { listAvailableProviders } from '@/src/core/providers/registry';
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
const manager = getSessionManager();
|
|
7
|
+
return NextResponse.json({
|
|
8
|
+
sessions: manager.list(),
|
|
9
|
+
providers: listAvailableProviders(),
|
|
10
|
+
usage: manager.getUsageSummary(),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getTask, cancelTask, deleteTask, retryTask } from '@/lib/task-manager';
|
|
3
|
+
|
|
4
|
+
// Get task details (including full log)
|
|
5
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
|
+
const { id } = await params;
|
|
7
|
+
const task = getTask(id);
|
|
8
|
+
if (!task) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
9
|
+
return NextResponse.json(task);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Actions: cancel, retry
|
|
13
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
14
|
+
const { id } = await params;
|
|
15
|
+
const { action } = await req.json();
|
|
16
|
+
|
|
17
|
+
if (action === 'cancel') {
|
|
18
|
+
const ok = cancelTask(id);
|
|
19
|
+
return NextResponse.json({ ok });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (action === 'retry') {
|
|
23
|
+
const newTask = retryTask(id);
|
|
24
|
+
if (!newTask) return NextResponse.json({ error: 'Cannot retry this task' }, { status: 400 });
|
|
25
|
+
return NextResponse.json(newTask);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Delete a task
|
|
32
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
33
|
+
const { id } = await params;
|
|
34
|
+
deleteTask(id);
|
|
35
|
+
return NextResponse.json({ ok: true });
|
|
36
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getTask } from '@/lib/task-manager';
|
|
2
|
+
import { onTaskEvent } from '@/lib/task-manager';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
export const runtime = 'nodejs';
|
|
6
|
+
|
|
7
|
+
// SSE stream for real-time task log updates
|
|
8
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
|
|
11
|
+
const task = getTask(id);
|
|
12
|
+
if (!task) {
|
|
13
|
+
return new Response('Task not found', { status: 404 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
let unsubscribe: (() => void) | null = null;
|
|
18
|
+
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
19
|
+
let closed = false;
|
|
20
|
+
|
|
21
|
+
const stream = new ReadableStream({
|
|
22
|
+
start(controller) {
|
|
23
|
+
// Send existing log entries
|
|
24
|
+
for (const entry of task.log) {
|
|
25
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'log', entry })}\n\n`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Send current status
|
|
29
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'status', status: task.status })}\n\n`));
|
|
30
|
+
|
|
31
|
+
// Heartbeat
|
|
32
|
+
heartbeat = setInterval(() => {
|
|
33
|
+
if (!closed) {
|
|
34
|
+
try { controller.enqueue(encoder.encode(': heartbeat\n\n')); } catch { cleanup(); }
|
|
35
|
+
}
|
|
36
|
+
}, 15000);
|
|
37
|
+
|
|
38
|
+
// Listen for new events
|
|
39
|
+
unsubscribe = onTaskEvent((taskId, event, data) => {
|
|
40
|
+
if (taskId !== id || closed) return;
|
|
41
|
+
try {
|
|
42
|
+
if (event === 'log') {
|
|
43
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'log', entry: data })}\n\n`));
|
|
44
|
+
} else if (event === 'status') {
|
|
45
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'status', status: data })}\n\n`));
|
|
46
|
+
// Close stream when task is done
|
|
47
|
+
if (data === 'done' || data === 'failed' || data === 'cancelled') {
|
|
48
|
+
// Send final task data
|
|
49
|
+
const finalTask = getTask(id);
|
|
50
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'complete', task: finalTask })}\n\n`));
|
|
51
|
+
cleanup();
|
|
52
|
+
controller.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch { cleanup(); }
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
cancel() {
|
|
59
|
+
cleanup();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function cleanup() {
|
|
64
|
+
closed = true;
|
|
65
|
+
if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
|
|
66
|
+
if (unsubscribe) { unsubscribe(); unsubscribe = null; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return new Response(stream, {
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'text/event-stream',
|
|
72
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
73
|
+
Connection: 'keep-alive',
|
|
74
|
+
'X-Accel-Buffering': 'no',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { createTask } from '@/lib/task-manager';
|
|
3
|
+
import { getProjectInfo } from '@/lib/projects';
|
|
4
|
+
import { getDb } from '@/src/core/db/database';
|
|
5
|
+
import { getDbPath } from '@/src/config';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Link an existing local Claude Code session to a project.
|
|
9
|
+
* Creates a placeholder task with the conversation_id so future tasks
|
|
10
|
+
* for this project automatically continue that session.
|
|
11
|
+
*/
|
|
12
|
+
export async function POST(req: Request) {
|
|
13
|
+
const { projectName, conversationId } = await req.json();
|
|
14
|
+
|
|
15
|
+
if (!projectName || !conversationId) {
|
|
16
|
+
return NextResponse.json({ error: 'projectName and conversationId required' }, { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const project = getProjectInfo(projectName);
|
|
20
|
+
if (!project) {
|
|
21
|
+
return NextResponse.json({ error: `Project not found: ${projectName}` }, { status: 404 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create a placeholder "done" task that carries the conversation_id
|
|
25
|
+
const db = getDb(getDbPath());
|
|
26
|
+
const id = `link-${Date.now().toString(36)}`;
|
|
27
|
+
db.prepare(`
|
|
28
|
+
INSERT INTO tasks (id, project_name, project_path, prompt, status, priority, conversation_id, log, result_summary, completed_at)
|
|
29
|
+
VALUES (?, ?, ?, ?, 'done', 0, ?, '[]', ?, datetime('now'))
|
|
30
|
+
`).run(id, project.name, project.path, '(linked from local CLI)', conversationId, `Session ${conversationId} linked from local CLI`);
|
|
31
|
+
|
|
32
|
+
return NextResponse.json({
|
|
33
|
+
id,
|
|
34
|
+
projectName: project.name,
|
|
35
|
+
conversationId,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { createTask, listTasks } from '@/lib/task-manager';
|
|
3
|
+
import { ensureInitialized } from '@/lib/init';
|
|
4
|
+
import { getProjectInfo } from '@/lib/projects';
|
|
5
|
+
import type { TaskStatus } from '@/src/types';
|
|
6
|
+
|
|
7
|
+
// List tasks — optionally filter by status
|
|
8
|
+
export async function GET(req: Request) {
|
|
9
|
+
ensureInitialized();
|
|
10
|
+
const url = new URL(req.url);
|
|
11
|
+
const status = url.searchParams.get('status') as TaskStatus | null;
|
|
12
|
+
return NextResponse.json(listTasks(status || undefined));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Create a new task
|
|
16
|
+
export async function POST(req: Request) {
|
|
17
|
+
const { projectName, prompt, priority, newSession, conversationId, scheduledAt, mode, watchConfig } = await req.json();
|
|
18
|
+
|
|
19
|
+
if (!projectName || !prompt) {
|
|
20
|
+
return NextResponse.json({ error: 'projectName and prompt are required' }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const project = getProjectInfo(projectName);
|
|
24
|
+
if (!project) {
|
|
25
|
+
return NextResponse.json({ error: `Project not found: ${projectName}` }, { status: 404 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// conversationId: explicit value → use it; newSession → empty string (force new); otherwise → auto-inherit
|
|
29
|
+
const convId = conversationId || (newSession ? '' : undefined);
|
|
30
|
+
|
|
31
|
+
const task = createTask({
|
|
32
|
+
projectName: project.name,
|
|
33
|
+
projectPath: project.path,
|
|
34
|
+
prompt,
|
|
35
|
+
priority: priority || 0,
|
|
36
|
+
conversationId: convId,
|
|
37
|
+
scheduledAt: scheduledAt || undefined,
|
|
38
|
+
mode: mode || 'prompt',
|
|
39
|
+
watchConfig: watchConfig || undefined,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return NextResponse.json(task);
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getProjectConversationId } from '@/lib/task-manager';
|
|
3
|
+
|
|
4
|
+
export async function GET(req: Request) {
|
|
5
|
+
const url = new URL(req.url);
|
|
6
|
+
const project = url.searchParams.get('project');
|
|
7
|
+
|
|
8
|
+
if (!project) {
|
|
9
|
+
return NextResponse.json({ error: 'project parameter required' }, { status: 400 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const conversationId = getProjectConversationId(project);
|
|
13
|
+
return NextResponse.json({ conversationId });
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { startTunnel, stopTunnel, getTunnelStatus } from '@/lib/cloudflared';
|
|
3
|
+
|
|
4
|
+
/** GET /api/tunnel — current tunnel status */
|
|
5
|
+
export async function GET() {
|
|
6
|
+
return NextResponse.json(getTunnelStatus());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** POST /api/tunnel — start or stop tunnel */
|
|
10
|
+
export async function POST(req: Request) {
|
|
11
|
+
const { action } = await req.json() as { action: 'start' | 'stop' };
|
|
12
|
+
|
|
13
|
+
if (action === 'stop') {
|
|
14
|
+
stopTunnel();
|
|
15
|
+
return NextResponse.json({ ok: true, ...getTunnelStatus() });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = await startTunnel();
|
|
19
|
+
return NextResponse.json({ ok: !result.error, ...getTunnelStatus() });
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ensureInitialized } from '@/lib/init';
|
|
3
|
+
import { listWatchers, createWatcher, deleteWatcher, toggleWatcher } from '@/lib/session-watcher';
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
ensureInitialized();
|
|
7
|
+
return NextResponse.json(listWatchers());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function POST(req: Request) {
|
|
11
|
+
ensureInitialized();
|
|
12
|
+
const body = await req.json();
|
|
13
|
+
|
|
14
|
+
if (body.action === 'delete') {
|
|
15
|
+
deleteWatcher(body.id);
|
|
16
|
+
return NextResponse.json({ ok: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (body.action === 'toggle') {
|
|
20
|
+
toggleWatcher(body.id, body.active);
|
|
21
|
+
return NextResponse.json({ ok: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create new watcher
|
|
25
|
+
const watcher = createWatcher({
|
|
26
|
+
projectName: body.projectName,
|
|
27
|
+
sessionId: body.sessionId,
|
|
28
|
+
label: body.label,
|
|
29
|
+
checkInterval: body.checkInterval || 60,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return NextResponse.json(watcher);
|
|
33
|
+
}
|
package/app/globals.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--bg-primary: #0a0a0a;
|
|
5
|
+
--bg-secondary: #141414;
|
|
6
|
+
--bg-tertiary: #1e1e1e;
|
|
7
|
+
--border: #2a2a2a;
|
|
8
|
+
--text-primary: #e5e5e5;
|
|
9
|
+
--text-secondary: #999;
|
|
10
|
+
--accent: #3b82f6;
|
|
11
|
+
--green: #22c55e;
|
|
12
|
+
--yellow: #eab308;
|
|
13
|
+
--red: #ef4444;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
background: var(--bg-primary);
|
|
18
|
+
color: var(--text-primary);
|
|
19
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Scrollbar */
|
|
23
|
+
::-webkit-scrollbar { width: 6px; }
|
|
24
|
+
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
|
25
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
26
|
+
|
package/app/icon.svg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#60a5fa"/>
|
|
5
|
+
<stop offset="100%" stop-color="#3b82f6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<!-- Background -->
|
|
9
|
+
<rect width="32" height="32" rx="7" fill="#1a1a2e"/>
|
|
10
|
+
<!-- Anvil base -->
|
|
11
|
+
<rect x="6" y="22" width="20" height="3" rx="1" fill="url(#g)"/>
|
|
12
|
+
<!-- Anvil middle -->
|
|
13
|
+
<rect x="9" y="18" width="14" height="4" fill="url(#g)" opacity="0.9"/>
|
|
14
|
+
<!-- Anvil top face -->
|
|
15
|
+
<polygon points="9,15 23,15 23,18 9,18" fill="url(#g)" opacity="0.85"/>
|
|
16
|
+
<!-- Anvil horn (left) -->
|
|
17
|
+
<polygon points="9,15 4,17 4,18 9,18" fill="#60a5fa" opacity="0.8"/>
|
|
18
|
+
<!-- Hammer handle -->
|
|
19
|
+
<rect x="19" y="4" width="2.5" height="10" rx="1" fill="#f59e0b" transform="rotate(-35 20.25 9)"/>
|
|
20
|
+
<!-- Hammer head -->
|
|
21
|
+
<rect x="17" y="2.5" width="7" height="3.5" rx="1" fill="#f59e0b" transform="rotate(-35 20.5 4.25)"/>
|
|
22
|
+
<!-- Sparks -->
|
|
23
|
+
<circle cx="14" cy="12.5" r="1.2" fill="#fbbf24" opacity="0.9"/>
|
|
24
|
+
<circle cx="11" cy="10.5" r="0.8" fill="#fbbf24" opacity="0.7"/>
|
|
25
|
+
<circle cx="17" cy="10" r="0.7" fill="#fbbf24" opacity="0.6"/>
|
|
26
|
+
</svg>
|
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import './globals.css';
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: 'Forge',
|
|
6
|
+
description: 'Unified AI workflow platform',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<html lang="en" className="dark">
|
|
12
|
+
<body className="min-h-screen bg-[var(--bg-primary)]">
|
|
13
|
+
{children}
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { signIn } from 'next-auth/react';
|
|
5
|
+
|
|
6
|
+
export default function LoginPage() {
|
|
7
|
+
const [password, setPassword] = useState('');
|
|
8
|
+
const [error, setError] = useState('');
|
|
9
|
+
|
|
10
|
+
const handleLocal = async (e: React.FormEvent) => {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
const result = await signIn('credentials', {
|
|
13
|
+
password,
|
|
14
|
+
callbackUrl: window.location.origin + '/',
|
|
15
|
+
}) as { error?: string } | undefined;
|
|
16
|
+
if (result?.error) setError('Wrong password');
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
21
|
+
<div className="w-80 space-y-6">
|
|
22
|
+
<div className="text-center">
|
|
23
|
+
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Forge</h1>
|
|
24
|
+
<p className="text-sm text-[var(--text-secondary)] mt-1">Unified AI Platform</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Local password login */}
|
|
28
|
+
<form onSubmit={handleLocal} className="space-y-3">
|
|
29
|
+
<input
|
|
30
|
+
type="password"
|
|
31
|
+
value={password}
|
|
32
|
+
onChange={e => setPassword(e.target.value)}
|
|
33
|
+
placeholder="Password"
|
|
34
|
+
className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
|
|
35
|
+
/>
|
|
36
|
+
{error && <p className="text-xs text-[var(--red)]">{error}</p>}
|
|
37
|
+
<button
|
|
38
|
+
type="submit"
|
|
39
|
+
className="w-full py-2 bg-[var(--accent)] text-white rounded text-sm hover:opacity-90"
|
|
40
|
+
>
|
|
41
|
+
Sign In
|
|
42
|
+
</button>
|
|
43
|
+
</form>
|
|
44
|
+
|
|
45
|
+
<div className="flex items-center gap-3 text-[var(--text-secondary)] text-xs">
|
|
46
|
+
<div className="flex-1 h-px bg-[var(--border)]" />
|
|
47
|
+
<span>or</span>
|
|
48
|
+
<div className="flex-1 h-px bg-[var(--border)]" />
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Google OAuth */}
|
|
52
|
+
<button
|
|
53
|
+
onClick={() => signIn('google', { callbackUrl: window.location.origin + '/' })}
|
|
54
|
+
className="w-full py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] hover:bg-[var(--border)] transition-colors"
|
|
55
|
+
>
|
|
56
|
+
Sign in with Google
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { auth } from '@/lib/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import Dashboard from '@/components/Dashboard';
|
|
4
|
+
|
|
5
|
+
export default async function Home() {
|
|
6
|
+
const session = await auth();
|
|
7
|
+
if (!session) redirect('/login');
|
|
8
|
+
return <Dashboard user={session.user} />;
|
|
9
|
+
}
|