@aion0/forge 0.2.10 → 0.2.12
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 +42 -38
- package/README.md +175 -97
- package/app/api/settings/route.ts +52 -5
- package/app/api/tunnel/route.ts +12 -4
- package/app/login/page.tsx +35 -6
- package/bin/forge-server.mjs +1 -1
- package/cli/mw.ts +166 -38
- package/components/SettingsModal.tsx +297 -35
- package/components/TunnelToggle.tsx +48 -5
- package/install.sh +2 -2
- package/instrumentation.ts +9 -4
- package/lib/auth.ts +13 -9
- package/lib/cloudflared.ts +6 -0
- package/lib/crypto.ts +68 -0
- package/lib/init.ts +47 -9
- package/lib/password.ts +67 -47
- package/lib/settings.ts +38 -3
- package/lib/telegram-bot.ts +56 -23
- package/lib/telegram-standalone.ts +0 -1
- package/package.json +1 -1
package/app/login/page.tsx
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
4
|
import { signIn } from 'next-auth/react';
|
|
5
5
|
|
|
6
6
|
export default function LoginPage() {
|
|
7
7
|
const [password, setPassword] = useState('');
|
|
8
|
+
const [sessionCode, setSessionCode] = useState('');
|
|
8
9
|
const [error, setError] = useState('');
|
|
10
|
+
const [isRemote, setIsRemote] = useState(false);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const host = window.location.hostname;
|
|
14
|
+
setIsRemote(!['localhost', '127.0.0.1'].includes(host));
|
|
15
|
+
}, []);
|
|
9
16
|
|
|
10
17
|
const handleLocal = async (e: React.FormEvent) => {
|
|
11
18
|
e.preventDefault();
|
|
19
|
+
setError('');
|
|
12
20
|
const result = await signIn('credentials', {
|
|
13
21
|
password,
|
|
22
|
+
sessionCode: isRemote ? sessionCode : '',
|
|
23
|
+
isRemote: String(isRemote),
|
|
14
24
|
redirect: false,
|
|
15
25
|
}) as { error?: string; ok?: boolean } | undefined;
|
|
16
26
|
if (result?.error) {
|
|
17
|
-
setError('Wrong password');
|
|
27
|
+
setError(isRemote ? 'Wrong password or session code' : 'Wrong password');
|
|
18
28
|
} else if (result?.ok) {
|
|
19
29
|
window.location.href = window.location.origin + '/';
|
|
20
30
|
}
|
|
@@ -25,18 +35,32 @@ export default function LoginPage() {
|
|
|
25
35
|
<div className="w-80 space-y-6">
|
|
26
36
|
<div className="text-center">
|
|
27
37
|
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Forge</h1>
|
|
28
|
-
<p className="text-sm text-[var(--text-secondary)] mt-1">
|
|
38
|
+
<p className="text-sm text-[var(--text-secondary)] mt-1">
|
|
39
|
+
{isRemote ? 'Remote Access' : 'Local Access'}
|
|
40
|
+
</p>
|
|
29
41
|
</div>
|
|
30
42
|
|
|
31
|
-
{/* Local password login */}
|
|
32
43
|
<form onSubmit={handleLocal} className="space-y-3">
|
|
33
44
|
<input
|
|
34
45
|
type="password"
|
|
35
46
|
value={password}
|
|
36
|
-
onChange={e => setPassword(e.target.value)}
|
|
37
|
-
placeholder="Password"
|
|
47
|
+
onChange={e => { setPassword(e.target.value); setError(''); }}
|
|
48
|
+
placeholder="Admin Password"
|
|
49
|
+
autoFocus
|
|
38
50
|
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)]"
|
|
39
51
|
/>
|
|
52
|
+
{isRemote && (
|
|
53
|
+
<input
|
|
54
|
+
type="text"
|
|
55
|
+
inputMode="numeric"
|
|
56
|
+
pattern="[0-9]*"
|
|
57
|
+
maxLength={8}
|
|
58
|
+
value={sessionCode}
|
|
59
|
+
onChange={e => { setSessionCode(e.target.value.replace(/\D/g, '')); setError(''); }}
|
|
60
|
+
placeholder="Session Code (8 digits)"
|
|
61
|
+
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)]"
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
40
64
|
{error && <p className="text-xs text-[var(--red)]">{error}</p>}
|
|
41
65
|
<button
|
|
42
66
|
type="submit"
|
|
@@ -44,6 +68,11 @@ export default function LoginPage() {
|
|
|
44
68
|
>
|
|
45
69
|
Sign In
|
|
46
70
|
</button>
|
|
71
|
+
{isRemote && (
|
|
72
|
+
<p className="text-[10px] text-[var(--text-secondary)] text-center">
|
|
73
|
+
Session code is generated when tunnel starts. Use /tunnel_code in Telegram to get it.
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
47
76
|
</form>
|
|
48
77
|
|
|
49
78
|
</div>
|
package/bin/forge-server.mjs
CHANGED
|
@@ -195,7 +195,7 @@ function startBackground() {
|
|
|
195
195
|
console.log(`[forge] Terminal: ws://localhost:${terminalPort}`);
|
|
196
196
|
console.log(`[forge] Data: ${DATA_DIR}`);
|
|
197
197
|
console.log(`[forge] Log: ${LOG_FILE}`);
|
|
198
|
-
console.log(`[forge] Stop: forge
|
|
198
|
+
console.log(`[forge] Stop: forge server stop`);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
// ── Stop ──
|
package/cli/mw.ts
CHANGED
|
@@ -112,10 +112,9 @@ async function main() {
|
|
|
112
112
|
break;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
case 'status':
|
|
116
115
|
case 's': {
|
|
117
116
|
const id = args[0];
|
|
118
|
-
if (!id) { console.log('Usage:
|
|
117
|
+
if (!id) { console.log('Usage: forge status <id>'); process.exit(1); }
|
|
119
118
|
const task = await api(`/api/tasks/${id}`);
|
|
120
119
|
console.log(`Task: ${task.id}`);
|
|
121
120
|
console.log(`Project: ${task.projectName} (${task.projectPath})`);
|
|
@@ -323,22 +322,22 @@ async function main() {
|
|
|
323
322
|
|
|
324
323
|
case 'password':
|
|
325
324
|
case 'pw': {
|
|
326
|
-
const { readFileSync } = await import('node:fs');
|
|
325
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
327
326
|
const { homedir } = await import('node:os');
|
|
328
327
|
const { join } = await import('node:path');
|
|
329
|
-
const
|
|
328
|
+
const dataDir = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
|
|
329
|
+
const codeFile = join(dataDir, 'session-code.json');
|
|
330
330
|
try {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
} else {
|
|
337
|
-
console.log(`Password expired (was for ${data.date}). Restart server to generate new one.`);
|
|
331
|
+
if (existsSync(codeFile)) {
|
|
332
|
+
const data = JSON.parse(readFileSync(codeFile, 'utf-8'));
|
|
333
|
+
if (data.code) {
|
|
334
|
+
console.log(`Session code: ${data.code} (for remote login 2FA)`);
|
|
335
|
+
}
|
|
338
336
|
}
|
|
339
|
-
} catch {
|
|
340
|
-
|
|
341
|
-
|
|
337
|
+
} catch {}
|
|
338
|
+
console.log('Admin password: configured in Settings → Admin Password');
|
|
339
|
+
console.log('Local login: admin password only');
|
|
340
|
+
console.log('Remote login: admin password + session code');
|
|
342
341
|
break;
|
|
343
342
|
}
|
|
344
343
|
|
|
@@ -353,34 +352,163 @@ async function main() {
|
|
|
353
352
|
break;
|
|
354
353
|
}
|
|
355
354
|
|
|
355
|
+
case 'server': {
|
|
356
|
+
// Delegate to forge-server.mjs
|
|
357
|
+
const { execSync } = await import('node:child_process');
|
|
358
|
+
const { join, dirname } = await import('node:path');
|
|
359
|
+
const { fileURLToPath } = await import('node:url');
|
|
360
|
+
const serverScript = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'forge-server.mjs');
|
|
361
|
+
const sub = args[0] || 'start';
|
|
362
|
+
const serverArgs = args.slice(1);
|
|
363
|
+
|
|
364
|
+
const flagMap: Record<string, string[]> = {
|
|
365
|
+
'start': [],
|
|
366
|
+
'stop': ['--stop'],
|
|
367
|
+
'restart': ['--restart'],
|
|
368
|
+
'rebuild': ['--rebuild'],
|
|
369
|
+
'dev': ['--dev'],
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const flags = flagMap[sub] || [];
|
|
373
|
+
const allArgs = [...flags, ...serverArgs];
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
execSync(`node ${serverScript} ${allArgs.join(' ')}`, { stdio: 'inherit' });
|
|
377
|
+
} catch {}
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
case 'status': {
|
|
382
|
+
// If arg provided, show task details
|
|
383
|
+
if (args[0]) {
|
|
384
|
+
const task = await api(`/api/tasks/${args[0]}`);
|
|
385
|
+
console.log(`Task: ${task.id}`);
|
|
386
|
+
console.log(`Project: ${task.projectName} (${task.projectPath})`);
|
|
387
|
+
console.log(`Status: ${task.status}`);
|
|
388
|
+
console.log(`Prompt: ${task.prompt}`);
|
|
389
|
+
if (task.startedAt) console.log(`Started: ${task.startedAt}`);
|
|
390
|
+
if (task.completedAt) console.log(`Completed: ${task.completedAt}`);
|
|
391
|
+
if (task.costUSD != null) console.log(`Cost: $${task.costUSD.toFixed(4)}`);
|
|
392
|
+
if (task.error) console.log(`Error: ${task.error}`);
|
|
393
|
+
if (task.resultSummary) console.log(`\nResult:\n${task.resultSummary}`);
|
|
394
|
+
if (task.gitDiff) console.log(`\nGit Diff:\n${task.gitDiff.slice(0, 2000)}`);
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// No arg — show process status
|
|
399
|
+
const { execSync } = await import('node:child_process');
|
|
400
|
+
|
|
401
|
+
const check = (pattern: string) => {
|
|
402
|
+
try {
|
|
403
|
+
const out = execSync(`ps aux | grep '${pattern}' | grep -v grep | head -1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
404
|
+
return out ? out.split(/\s+/)[1] : null;
|
|
405
|
+
} catch { return null; }
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const nextPid = check('next-server');
|
|
409
|
+
const termPid = check('terminal-standalone');
|
|
410
|
+
const telePid = check('telegram-standalone');
|
|
411
|
+
const tunnPid = check('cloudflared tunnel');
|
|
412
|
+
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log(` ${nextPid ? '●' : '○'} Next.js ${nextPid ? `running (pid: ${nextPid})` : 'stopped'}`);
|
|
415
|
+
console.log(` ${termPid ? '●' : '○'} Terminal ${termPid ? `running (pid: ${termPid})` : 'stopped'}`);
|
|
416
|
+
console.log(` ${telePid ? '●' : '○'} Telegram ${telePid ? `running (pid: ${telePid})` : 'stopped'}`);
|
|
417
|
+
console.log(` ${tunnPid ? '●' : '○'} Tunnel ${tunnPid ? `running (pid: ${tunnPid})` : 'stopped'}`);
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const { execSync: ex } = await import('node:child_process');
|
|
421
|
+
const sessions = ex("tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null", { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
422
|
+
.trim().split('\n').filter(l => l.startsWith('mw-'));
|
|
423
|
+
console.log(`\n Sessions: ${sessions.length}`);
|
|
424
|
+
for (const s of sessions) {
|
|
425
|
+
const [name, att] = s.split(' ');
|
|
426
|
+
console.log(` ${att !== '0' ? '●' : '○'} ${name}`);
|
|
427
|
+
}
|
|
428
|
+
} catch {
|
|
429
|
+
console.log('\n Sessions: 0');
|
|
430
|
+
}
|
|
431
|
+
console.log('');
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case 'upgrade': {
|
|
436
|
+
const { execSync } = await import('node:child_process');
|
|
437
|
+
const { lstatSync } = await import('node:fs');
|
|
438
|
+
const { join, dirname } = await import('node:path');
|
|
439
|
+
const { fileURLToPath } = await import('node:url');
|
|
440
|
+
|
|
441
|
+
// Check if installed via npm link (symlink)
|
|
442
|
+
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
443
|
+
let isLinked = false;
|
|
444
|
+
try { isLinked = lstatSync(join(cliDir, '..')).isSymbolicLink(); } catch {}
|
|
445
|
+
|
|
446
|
+
if (isLinked) {
|
|
447
|
+
console.log('[forge] Installed via npm link (local source)');
|
|
448
|
+
console.log('[forge] Pull latest and rebuild:');
|
|
449
|
+
console.log(' cd ' + join(cliDir, '..'));
|
|
450
|
+
console.log(' git pull && pnpm install && pnpm build');
|
|
451
|
+
} else {
|
|
452
|
+
console.log('[forge] Upgrading from npm...');
|
|
453
|
+
try {
|
|
454
|
+
execSync('cd /tmp && npm install -g @aion0/forge', { stdio: 'inherit' });
|
|
455
|
+
console.log('[forge] Upgraded. Run: forge server restart');
|
|
456
|
+
} catch {
|
|
457
|
+
console.log('[forge] Upgrade failed');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
case 'uninstall': {
|
|
464
|
+
const { execSync } = await import('node:child_process');
|
|
465
|
+
console.log('[forge] Stopping server...');
|
|
466
|
+
try { execSync('forge server stop', { stdio: 'inherit' }); } catch {}
|
|
467
|
+
console.log('[forge] Uninstalling...');
|
|
468
|
+
try {
|
|
469
|
+
execSync('npm uninstall -g @aion0/forge', { stdio: 'inherit' });
|
|
470
|
+
console.log('[forge] Uninstalled. Data remains in ~/.forge/');
|
|
471
|
+
} catch {
|
|
472
|
+
console.log('[forge] Uninstall failed');
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
|
|
356
477
|
default:
|
|
357
478
|
console.log(`forge — Forge CLI (@aion0/forge)
|
|
358
479
|
|
|
359
480
|
Usage:
|
|
360
|
-
forge
|
|
361
|
-
forge
|
|
362
|
-
forge
|
|
363
|
-
forge
|
|
364
|
-
forge
|
|
365
|
-
|
|
366
|
-
forge
|
|
367
|
-
forge
|
|
368
|
-
forge
|
|
369
|
-
forge
|
|
370
|
-
forge
|
|
371
|
-
forge
|
|
372
|
-
forge
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
forge
|
|
379
|
-
|
|
380
|
-
forge
|
|
381
|
-
forge
|
|
382
|
-
|
|
383
|
-
|
|
481
|
+
forge server start [options] Start server (default: foreground)
|
|
482
|
+
forge server stop Stop server
|
|
483
|
+
forge server restart Restart server (safe for remote)
|
|
484
|
+
forge server dev Start in dev mode
|
|
485
|
+
forge server rebuild Force rebuild
|
|
486
|
+
|
|
487
|
+
forge task <project> <prompt> Submit a task
|
|
488
|
+
forge tasks [status] List tasks
|
|
489
|
+
forge watch <id> Live stream output
|
|
490
|
+
forge status [<id>] Process status / task details
|
|
491
|
+
forge log <id> Show execution log
|
|
492
|
+
forge cancel <id> Cancel a task
|
|
493
|
+
forge retry <id> Retry a failed task
|
|
494
|
+
|
|
495
|
+
forge run <flow> Run a workflow
|
|
496
|
+
forge flows List workflows
|
|
497
|
+
forge projects List projects
|
|
498
|
+
forge session [project] Show session info
|
|
499
|
+
forge password Show login password
|
|
500
|
+
|
|
501
|
+
forge upgrade Update to latest version
|
|
502
|
+
forge uninstall Remove forge
|
|
503
|
+
|
|
504
|
+
Options for 'forge server start':
|
|
505
|
+
--port 4000 Custom web port (default: 3000)
|
|
506
|
+
--terminal-port 4001 Custom terminal port (default: 3001)
|
|
507
|
+
--dir ~/.forge-staging Custom data directory
|
|
508
|
+
--background Run in background
|
|
509
|
+
--reset-terminal Kill terminal server on start
|
|
510
|
+
|
|
511
|
+
Shortcuts: t=task, ls=tasks, w=watch, s=status, l=log, f=flows, p=projects, pw=password`);
|
|
384
512
|
}
|
|
385
513
|
}
|
|
386
514
|
|