@aion0/forge 0.4.15 → 0.5.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 +1 -1
- package/README.md +2 -2
- package/RELEASE_NOTES.md +170 -13
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +23 -4
- package/check-forge-status.sh +9 -0
- package/cli/mw.ts +2 -2
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +12 -4
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +13 -8
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +34 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +259 -45
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2224 -0
- package/docs/LOCAL-DEPLOY.md +15 -15
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/cloudflared.ts +1 -1
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +8 -2
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +95 -6
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +204 -0
- package/lib/help-docs/CLAUDE.md +5 -2
- package/lib/init.ts +62 -12
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/telegram-standalone.ts +1 -1
- package/lib/terminal-server.ts +2 -2
- package/lib/terminal-standalone.ts +1 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1804 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +790 -0
- package/middleware.ts +1 -0
- package/next-env.d.ts +1 -1
- package/package.json +5 -2
- package/src/config/index.ts +13 -2
- package/src/core/db/database.ts +1 -0
- package/start.sh +10 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { listWorkspaces, findWorkspaceByProject, loadWorkspace, saveWorkspace, deleteWorkspace } from '@/lib/workspace';
|
|
3
|
+
import type { WorkspaceState } from '@/lib/workspace';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
// List workspaces, find by projectPath, or export template
|
|
7
|
+
export async function GET(req: Request) {
|
|
8
|
+
const url = new URL(req.url);
|
|
9
|
+
const projectPath = url.searchParams.get('projectPath');
|
|
10
|
+
const exportId = url.searchParams.get('export');
|
|
11
|
+
|
|
12
|
+
if (exportId) {
|
|
13
|
+
// Export workspace as template (agents + positions, no state/logs)
|
|
14
|
+
const ws = loadWorkspace(exportId);
|
|
15
|
+
if (!ws) return NextResponse.json({ error: 'not found' }, { status: 404 });
|
|
16
|
+
const template = {
|
|
17
|
+
name: ws.projectName + ' template',
|
|
18
|
+
agents: ws.agents.map(a => ({ ...a, entries: undefined })), // strip Input entries
|
|
19
|
+
nodePositions: ws.nodePositions,
|
|
20
|
+
exportedAt: Date.now(),
|
|
21
|
+
};
|
|
22
|
+
return NextResponse.json(template);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (projectPath) {
|
|
26
|
+
const ws = findWorkspaceByProject(projectPath);
|
|
27
|
+
return NextResponse.json(ws || null);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return NextResponse.json(listWorkspaces());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create workspace or import template
|
|
34
|
+
export async function POST(req: Request) {
|
|
35
|
+
const body = await req.json();
|
|
36
|
+
const { projectPath, projectName, template } = body;
|
|
37
|
+
|
|
38
|
+
if (!projectPath || !projectName) {
|
|
39
|
+
return NextResponse.json({ error: 'projectPath and projectName are required' }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const existing = findWorkspaceByProject(projectPath);
|
|
43
|
+
if (existing && !template) {
|
|
44
|
+
return NextResponse.json(existing);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const state: WorkspaceState = {
|
|
48
|
+
id: existing?.id || randomUUID(),
|
|
49
|
+
projectPath,
|
|
50
|
+
projectName,
|
|
51
|
+
agents: [],
|
|
52
|
+
agentStates: {},
|
|
53
|
+
nodePositions: {},
|
|
54
|
+
busLog: [],
|
|
55
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
56
|
+
updatedAt: Date.now(),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Import template: create agents from template with new IDs
|
|
60
|
+
if (template?.agents) {
|
|
61
|
+
const idMap = new Map<string, string>(); // old ID → new ID
|
|
62
|
+
const ts = Date.now();
|
|
63
|
+
for (const agent of template.agents) {
|
|
64
|
+
const newId = `${agent.label.toLowerCase().replace(/\s+/g, '-')}-${ts}-${Math.random().toString(36).slice(2, 5)}`;
|
|
65
|
+
idMap.set(agent.id, newId);
|
|
66
|
+
}
|
|
67
|
+
for (const agent of template.agents) {
|
|
68
|
+
state.agents.push({
|
|
69
|
+
...agent,
|
|
70
|
+
id: idMap.get(agent.id) || agent.id,
|
|
71
|
+
dependsOn: agent.dependsOn.map((d: string) => idMap.get(d) || d),
|
|
72
|
+
entries: agent.type === 'input' ? [] : undefined,
|
|
73
|
+
});
|
|
74
|
+
state.agentStates[idMap.get(agent.id) || agent.id] = { smithStatus: 'down', mode: 'auto', taskStatus: 'idle', history: [], artifacts: [] };
|
|
75
|
+
}
|
|
76
|
+
if (template.nodePositions) {
|
|
77
|
+
for (const [oldId, pos] of Object.entries(template.nodePositions)) {
|
|
78
|
+
const newId = idMap.get(oldId);
|
|
79
|
+
if (newId) state.nodePositions[newId] = pos as { x: number; y: number };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await saveWorkspace(state);
|
|
85
|
+
return NextResponse.json(state, { status: 201 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Delete a workspace
|
|
89
|
+
export async function DELETE(req: Request) {
|
|
90
|
+
const url = new URL(req.url);
|
|
91
|
+
const id = url.searchParams.get('id');
|
|
92
|
+
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
|
|
93
|
+
|
|
94
|
+
// Unload from daemon if active
|
|
95
|
+
const daemonUrl = `http://localhost:${Number(process.env.WORKSPACE_PORT) || 8405}`;
|
|
96
|
+
try { await fetch(`${daemonUrl}/workspace/${id}/unload`, { method: 'POST' }); } catch {}
|
|
97
|
+
|
|
98
|
+
deleteWorkspace(id);
|
|
99
|
+
return NextResponse.json({ ok: true });
|
|
100
|
+
}
|
package/app/global-error.tsx
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) {
|
|
3
|
+
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
|
4
4
|
return (
|
|
5
|
-
<html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charSet="utf-8" />
|
|
8
|
+
</head>
|
|
6
9
|
<body style={{ background: '#0a0a0a', color: '#e5e5e5', fontFamily: 'monospace', padding: '2rem' }}>
|
|
7
10
|
<h2>Something went wrong</h2>
|
|
8
|
-
<p style={{ color: '#999' }}>{error
|
|
9
|
-
<button
|
|
11
|
+
<p style={{ color: '#999' }}>{error?.message || 'Unknown error'}</p>
|
|
12
|
+
<button
|
|
13
|
+
onClick={() => reset()}
|
|
14
|
+
style={{ marginTop: '1rem', padding: '0.5rem 1rem', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
|
15
|
+
>
|
|
10
16
|
Try again
|
|
11
17
|
</button>
|
|
12
18
|
</body>
|
package/app/icon.ico
ADDED
|
Binary file
|
package/app/layout.tsx
CHANGED
|
@@ -8,8 +8,8 @@ export const metadata: Metadata = {
|
|
|
8
8
|
|
|
9
9
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
10
10
|
return (
|
|
11
|
-
<html lang="en" className="dark">
|
|
12
|
-
<body className="min-h-screen bg-[var(--bg-primary)]">
|
|
11
|
+
<html lang="en" className="dark" suppressHydrationWarning>
|
|
12
|
+
<body className="min-h-screen bg-[var(--bg-primary)]" suppressHydrationWarning>
|
|
13
13
|
{children}
|
|
14
14
|
</body>
|
|
15
15
|
</html>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { signIn } from 'next-auth/react';
|
|
5
|
+
|
|
6
|
+
export default function LoginForm({ isRemote }: { isRemote: boolean }) {
|
|
7
|
+
const [password, setPassword] = useState('');
|
|
8
|
+
const [sessionCode, setSessionCode] = useState('');
|
|
9
|
+
const [error, setError] = useState('');
|
|
10
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const saved = localStorage.getItem('forge-theme');
|
|
14
|
+
if (saved === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
setError('');
|
|
20
|
+
const result = await signIn('credentials', {
|
|
21
|
+
password,
|
|
22
|
+
sessionCode: isRemote ? sessionCode : '',
|
|
23
|
+
isRemote: String(isRemote),
|
|
24
|
+
redirect: false,
|
|
25
|
+
}) as { error?: string; ok?: boolean } | undefined;
|
|
26
|
+
if (result?.error) {
|
|
27
|
+
setError(isRemote ? 'Wrong password or session code' : 'Wrong password');
|
|
28
|
+
} else if (result?.ok) {
|
|
29
|
+
window.location.href = window.location.origin + '/';
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
35
|
+
<div className="w-80 space-y-6">
|
|
36
|
+
<div className="text-center">
|
|
37
|
+
<img src="/icon.png" alt="Forge" width={48} height={48} className="rounded mx-auto mb-2" />
|
|
38
|
+
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Forge</h1>
|
|
39
|
+
<p className="text-sm text-[var(--text-secondary)] mt-1">
|
|
40
|
+
{isRemote ? 'Remote Access' : 'Local Access'}
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
45
|
+
<input
|
|
46
|
+
type="password"
|
|
47
|
+
value={password}
|
|
48
|
+
onChange={e => { setPassword(e.target.value); setError(''); }}
|
|
49
|
+
placeholder="Admin Password"
|
|
50
|
+
autoFocus
|
|
51
|
+
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)]"
|
|
52
|
+
/>
|
|
53
|
+
{isRemote && (
|
|
54
|
+
<input
|
|
55
|
+
type="text"
|
|
56
|
+
inputMode="numeric"
|
|
57
|
+
pattern="[0-9]*"
|
|
58
|
+
maxLength={8}
|
|
59
|
+
value={sessionCode}
|
|
60
|
+
onChange={e => { setSessionCode(e.target.value.replace(/\D/g, '')); setError(''); }}
|
|
61
|
+
placeholder="Session Code (8 digits)"
|
|
62
|
+
className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] font-mono tracking-widest text-center focus:outline-none focus:border-[var(--accent)]"
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
{error && <p className="text-xs text-[var(--red)]">{error}</p>}
|
|
66
|
+
<button
|
|
67
|
+
type="submit"
|
|
68
|
+
className="w-full py-2 bg-[var(--accent)] text-white rounded text-sm hover:opacity-90"
|
|
69
|
+
>
|
|
70
|
+
Sign In
|
|
71
|
+
</button>
|
|
72
|
+
{isRemote && (
|
|
73
|
+
<p className="text-[10px] text-[var(--text-secondary)] text-center">
|
|
74
|
+
Session code is generated when tunnel starts. Use /tunnel_code in Telegram or <code>forge tcode</code> to get it.
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
<div className="text-center">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => setShowHelp(v => !v)}
|
|
81
|
+
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
82
|
+
>
|
|
83
|
+
Forgot password?
|
|
84
|
+
</button>
|
|
85
|
+
{showHelp && (
|
|
86
|
+
<p className="text-[10px] text-[var(--text-secondary)] mt-1 bg-[var(--bg-tertiary)] rounded p-2">
|
|
87
|
+
Run in terminal:<br />
|
|
88
|
+
<code className="text-[var(--accent)]">forge --reset-password</code>
|
|
89
|
+
</p>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</form>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
package/app/login/page.tsx
CHANGED
|
@@ -1,101 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { headers } from 'next/headers';
|
|
2
|
+
import LoginForm from './LoginForm';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
export default async function LoginPage() {
|
|
5
|
+
const headersList = await headers();
|
|
6
|
+
const host = headersList.get('host') || '';
|
|
7
|
+
const isRemote = host.endsWith('.trycloudflare.com');
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
const [password, setPassword] = useState('');
|
|
8
|
-
const [sessionCode, setSessionCode] = useState('');
|
|
9
|
-
const [error, setError] = useState('');
|
|
10
|
-
const [isRemote, setIsRemote] = useState(false);
|
|
11
|
-
const [showHelp, setShowHelp] = useState(false);
|
|
12
|
-
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const host = window.location.hostname;
|
|
15
|
-
setIsRemote(host.endsWith('.trycloudflare.com'));
|
|
16
|
-
// Restore theme
|
|
17
|
-
const saved = localStorage.getItem('forge-theme');
|
|
18
|
-
if (saved === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
19
|
-
}, []);
|
|
20
|
-
|
|
21
|
-
const handleLocal = async (e: React.FormEvent) => {
|
|
22
|
-
e.preventDefault();
|
|
23
|
-
setError('');
|
|
24
|
-
const result = await signIn('credentials', {
|
|
25
|
-
password,
|
|
26
|
-
sessionCode: isRemote ? sessionCode : '',
|
|
27
|
-
isRemote: String(isRemote),
|
|
28
|
-
redirect: false,
|
|
29
|
-
}) as { error?: string; ok?: boolean } | undefined;
|
|
30
|
-
if (result?.error) {
|
|
31
|
-
setError(isRemote ? 'Wrong password or session code' : 'Wrong password');
|
|
32
|
-
} else if (result?.ok) {
|
|
33
|
-
window.location.href = window.location.origin + '/';
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="min-h-screen flex items-center justify-center">
|
|
39
|
-
<div className="w-80 space-y-6">
|
|
40
|
-
<div className="text-center">
|
|
41
|
-
<img src="/icon.png" alt="Forge" width={48} height={48} className="rounded mx-auto mb-2" />
|
|
42
|
-
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Forge</h1>
|
|
43
|
-
<p className="text-sm text-[var(--text-secondary)] mt-1">
|
|
44
|
-
{isRemote ? 'Remote Access' : 'Local Access'}
|
|
45
|
-
</p>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<form onSubmit={handleLocal} className="space-y-3">
|
|
49
|
-
<input
|
|
50
|
-
type="password"
|
|
51
|
-
value={password}
|
|
52
|
-
onChange={e => { setPassword(e.target.value); setError(''); }}
|
|
53
|
-
placeholder="Admin Password"
|
|
54
|
-
autoFocus
|
|
55
|
-
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)]"
|
|
56
|
-
/>
|
|
57
|
-
{isRemote && (
|
|
58
|
-
<input
|
|
59
|
-
type="text"
|
|
60
|
-
inputMode="numeric"
|
|
61
|
-
pattern="[0-9]*"
|
|
62
|
-
maxLength={8}
|
|
63
|
-
value={sessionCode}
|
|
64
|
-
onChange={e => { setSessionCode(e.target.value.replace(/\D/g, '')); setError(''); }}
|
|
65
|
-
placeholder="Session Code (8 digits)"
|
|
66
|
-
className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] font-mono tracking-widest text-center focus:outline-none focus:border-[var(--accent)]"
|
|
67
|
-
/>
|
|
68
|
-
)}
|
|
69
|
-
{error && <p className="text-xs text-[var(--red)]">{error}</p>}
|
|
70
|
-
<button
|
|
71
|
-
type="submit"
|
|
72
|
-
className="w-full py-2 bg-[var(--accent)] text-white rounded text-sm hover:opacity-90"
|
|
73
|
-
>
|
|
74
|
-
Sign In
|
|
75
|
-
</button>
|
|
76
|
-
{isRemote && (
|
|
77
|
-
<p className="text-[10px] text-[var(--text-secondary)] text-center">
|
|
78
|
-
Session code is generated when tunnel starts. Use /tunnel_code in Telegram to get it.
|
|
79
|
-
</p>
|
|
80
|
-
)}
|
|
81
|
-
<div className="text-center">
|
|
82
|
-
<button
|
|
83
|
-
type="button"
|
|
84
|
-
onClick={() => setShowHelp(v => !v)}
|
|
85
|
-
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
86
|
-
>
|
|
87
|
-
Forgot password?
|
|
88
|
-
</button>
|
|
89
|
-
{showHelp && (
|
|
90
|
-
<p className="text-[10px] text-[var(--text-secondary)] mt-1 bg-[var(--bg-tertiary)] rounded p-2">
|
|
91
|
-
Run in terminal:<br />
|
|
92
|
-
<code className="text-[var(--accent)]">forge --reset-password</code>
|
|
93
|
-
</p>
|
|
94
|
-
)}
|
|
95
|
-
</div>
|
|
96
|
-
</form>
|
|
97
|
-
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
9
|
+
return <LoginForm isRemote={isRemote} />;
|
|
101
10
|
}
|
package/app/page.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { auth } from '@/lib/auth';
|
|
2
2
|
import { redirect } from 'next/navigation';
|
|
3
3
|
import { headers } from 'next/headers';
|
|
4
|
-
import
|
|
4
|
+
import DashboardWrapper from '@/components/DashboardWrapper';
|
|
5
5
|
|
|
6
6
|
export default async function Home({ searchParams }: { searchParams: Promise<{ force?: string }> }) {
|
|
7
7
|
const session = await auth();
|
|
@@ -17,5 +17,5 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ f
|
|
|
17
17
|
if (isMobile) redirect('/mobile');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
return <
|
|
20
|
+
return <DashboardWrapper user={session.user} />;
|
|
21
21
|
}
|
package/bin/forge-server.mjs
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* forge-server --stop Stop background server
|
|
10
10
|
* forge-server --restart Stop + start (safe for remote)
|
|
11
11
|
* forge-server --rebuild Force rebuild
|
|
12
|
-
* forge-server --port 4000 Custom web port (default:
|
|
13
|
-
* forge-server --terminal-port 4001 Custom terminal port (default:
|
|
12
|
+
* forge-server --port 4000 Custom web port (default: 8403)
|
|
13
|
+
* forge-server --terminal-port 4001 Custom terminal port (default: 8404)
|
|
14
14
|
* forge-server --dir ~/.forge-test Custom data directory (default: ~/.forge)
|
|
15
15
|
* forge-server --reset-terminal Kill terminal server before start (loses tmux sessions)
|
|
16
16
|
*
|
|
@@ -62,8 +62,9 @@ const isRebuild = process.argv.includes('--rebuild');
|
|
|
62
62
|
const resetTerminal = process.argv.includes('--reset-terminal');
|
|
63
63
|
const resetPassword = process.argv.includes('--reset-password');
|
|
64
64
|
|
|
65
|
-
const webPort = parseInt(getArg('--port')) ||
|
|
65
|
+
const webPort = parseInt(getArg('--port')) || 8403;
|
|
66
66
|
const terminalPort = parseInt(getArg('--terminal-port')) || (webPort + 1);
|
|
67
|
+
const workspacePort = parseInt(getArg('--workspace-port')) || (webPort + 2);
|
|
67
68
|
const DATA_DIR = getArg('--dir')?.replace(/^~/, homedir()) || join(homedir(), '.forge', 'data');
|
|
68
69
|
|
|
69
70
|
const PID_FILE = join(DATA_DIR, 'forge.pid');
|
|
@@ -124,6 +125,7 @@ if (existsSync(envFile)) {
|
|
|
124
125
|
// Set env vars for Next.js and terminal server
|
|
125
126
|
process.env.PORT = String(webPort);
|
|
126
127
|
process.env.TERMINAL_PORT = String(terminalPort);
|
|
128
|
+
process.env.WORKSPACE_PORT = String(workspacePort);
|
|
127
129
|
process.env.FORGE_DATA_DIR = DATA_DIR;
|
|
128
130
|
|
|
129
131
|
// ── Password setup (first run or --reset-password) ──
|
|
@@ -255,7 +257,7 @@ function cleanupOrphans() {
|
|
|
255
257
|
}
|
|
256
258
|
// Kill standalone processes: our instance's + orphans without any tag
|
|
257
259
|
try {
|
|
258
|
-
const out = execSync(`ps aux | grep -E 'telegram-standalone|terminal-standalone' | grep -v grep`, {
|
|
260
|
+
const out = execSync(`ps aux | grep -E 'telegram-standalone|terminal-standalone|workspace-standalone' | grep -v grep`, {
|
|
259
261
|
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
260
262
|
}).trim();
|
|
261
263
|
for (const line of out.split('\n').filter(Boolean)) {
|
|
@@ -298,6 +300,16 @@ function startServices() {
|
|
|
298
300
|
services.push(telegramChild);
|
|
299
301
|
console.log(`[forge] Telegram bot started (pid: ${telegramChild.pid})`);
|
|
300
302
|
|
|
303
|
+
// Workspace daemon
|
|
304
|
+
const workspaceScript = join(ROOT, 'lib', 'workspace-standalone.ts');
|
|
305
|
+
const workspaceChild = spawn('npx', ['tsx', workspaceScript, instanceTag], {
|
|
306
|
+
cwd: ROOT,
|
|
307
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
308
|
+
env: { ...process.env },
|
|
309
|
+
});
|
|
310
|
+
services.push(workspaceChild);
|
|
311
|
+
console.log(`[forge] Workspace daemon started (pid: ${workspaceChild.pid})`);
|
|
312
|
+
|
|
301
313
|
// Track all child PIDs for clean shutdown
|
|
302
314
|
const childPids = services.map(c => c.pid).filter(Boolean);
|
|
303
315
|
savePids(childPids);
|
|
@@ -379,6 +391,13 @@ function startBackground() {
|
|
|
379
391
|
startServices();
|
|
380
392
|
|
|
381
393
|
console.log(`[forge] Started in background (pid ${child.pid})`);
|
|
394
|
+
if (!getArg('--port')) {
|
|
395
|
+
console.log(`[forge] ⚠️ Default port changed: 3000 → ${webPort}`);
|
|
396
|
+
console.log(`[forge] ⚠️ Default port changed: 3000 → ${webPort}`);
|
|
397
|
+
console.log(`[forge] ⚠️ Default port changed: 3000 → ${webPort}`);
|
|
398
|
+
console.log(`[forge] ⚠️ Default port changed: 3000 → ${webPort}`);
|
|
399
|
+
console.log(`[forge] ⚠️ Default port changed: 3000 → ${webPort}`);
|
|
400
|
+
}
|
|
382
401
|
console.log(`[forge] Web: http://localhost:${webPort}`);
|
|
383
402
|
console.log(`[forge] Terminal: ws://localhost:${terminalPort}`);
|
|
384
403
|
console.log(`[forge] Data: ${DATA_DIR}`);
|
package/check-forge-status.sh
CHANGED
|
@@ -32,6 +32,15 @@ else
|
|
|
32
32
|
echo " ○ Telegram stopped"
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
+
# Workspace Daemon
|
|
36
|
+
count=$(ps aux | grep 'workspace-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | wc -l | tr -d ' ')
|
|
37
|
+
pid=$(ps aux | grep 'workspace-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | awk '{print $2}' | head -1)
|
|
38
|
+
if [ "$count" -gt 0 ]; then
|
|
39
|
+
echo " ● Workspace running (pid: $pid)"
|
|
40
|
+
else
|
|
41
|
+
echo " ○ Workspace stopped"
|
|
42
|
+
fi
|
|
43
|
+
|
|
35
44
|
# Cloudflare Tunnel
|
|
36
45
|
count=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | wc -l | tr -d ' ')
|
|
37
46
|
pid=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | awk '{print $2}' | head -1)
|
package/cli/mw.ts
CHANGED
|
@@ -562,8 +562,8 @@ Usage:
|
|
|
562
562
|
forge uninstall Remove forge
|
|
563
563
|
|
|
564
564
|
Options for 'forge server start':
|
|
565
|
-
--port 4000 Custom web port (default:
|
|
566
|
-
--terminal-port 4001 Custom terminal port (default:
|
|
565
|
+
--port 4000 Custom web port (default: 8403)
|
|
566
|
+
--terminal-port 4001 Custom terminal port (default: 8404)
|
|
567
567
|
--dir ~/.forge-staging Custom data directory
|
|
568
568
|
--background Run in background
|
|
569
569
|
--reset-terminal Kill terminal server on start
|