@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.
@@ -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">Unified AI Platform</p>
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>
@@ -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-server --stop${DATA_DIR !== join(homedir(), '.forge') ? ` --dir ${DATA_DIR}` : ''}`);
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: mw status <id>'); process.exit(1); }
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 pwFile = join(homedir(), '.forge', 'password.json');
328
+ const dataDir = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
329
+ const codeFile = join(dataDir, 'session-code.json');
330
330
  try {
331
- const data = JSON.parse(readFileSync(pwFile, 'utf-8'));
332
- const today = new Date().toISOString().slice(0, 10);
333
- if (data.date === today) {
334
- console.log(`Login password: ${data.password}`);
335
- console.log(`Valid for: ${data.date}`);
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
- console.log('No password file found. Password is set via MW_PASSWORD env var.');
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 task <project> <prompt> Submit a task (auto-continues project session)
361
- forge task <project> <prompt> --new Force a fresh session
362
- forge run <flow-name> Run a YAML workflow
363
- forge tasks [status] List tasks (running|queued|done|failed)
364
- forge watch <id> Live stream task output
365
- forge log <id> Show execution log
366
- forge status <id> Task details + result
367
- forge session [project] Show session IDs → local claude --resume
368
- forge session link <project> <id> Link a local CLI session to the web system
369
- forge cancel <id> Cancel a task
370
- forge retry <id> Retry a failed task
371
- forge flows List workflows
372
- forge projects List projects
373
- forge password Show login password
374
-
375
- Shortcuts: t=task, r=run, ls=tasks, w=watch, l=log, s=status, f=flows, p=projects, pw=password
376
-
377
- Examples:
378
- forge task accord "Fix the authentication bug in login.ts"
379
- forge watch abc123
380
- forge run daily-review
381
- forge tasks running
382
- forge session accord Show session ID, then:
383
- cd ~/IdeaProjects/accord && claude --resume <session-id>`);
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