@aion0/forge 0.3.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.
@@ -54,7 +54,8 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
54
54
  }
55
55
 
56
56
  const isDev = process.argv.includes('--dev');
57
- const isBackground = process.argv.includes('--background');
57
+ const isForeground = process.argv.includes('--foreground');
58
+ const isBackground = !isForeground && !isDev; // default background unless --foreground or --dev
58
59
  const isStop = process.argv.includes('--stop');
59
60
  const isRestart = process.argv.includes('--restart');
60
61
  const isRebuild = process.argv.includes('--rebuild');
@@ -62,8 +63,8 @@ const resetTerminal = process.argv.includes('--reset-terminal');
62
63
  const resetPassword = process.argv.includes('--reset-password');
63
64
 
64
65
  const webPort = parseInt(getArg('--port')) || 3000;
65
- const terminalPort = parseInt(getArg('--terminal-port')) || 3001;
66
- const DATA_DIR = getArg('--dir')?.replace(/^~/, homedir()) || join(homedir(), '.forge');
66
+ const terminalPort = parseInt(getArg('--terminal-port')) || (webPort + 1);
67
+ const DATA_DIR = getArg('--dir')?.replace(/^~/, homedir()) || join(homedir(), '.forge', 'data');
67
68
 
68
69
  const PID_FILE = join(DATA_DIR, 'forge.pid');
69
70
  const LOG_FILE = join(DATA_DIR, 'forge.log');
@@ -108,21 +109,29 @@ if (!isStop) {
108
109
 
109
110
  const readline = await import('node:readline');
110
111
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
111
- const ask = (q) => new Promise(resolve => rl.question(q, resolve));
112
+ const ask = (q) => new Promise((resolve, reject) => {
113
+ rl.question(q, resolve);
114
+ rl.once('close', () => reject(new Error('cancelled')));
115
+ });
112
116
 
113
117
  let pw = '';
114
- while (true) {
115
- pw = await ask(' Enter admin password: ');
116
- if (!pw || pw.length < 4) {
117
- console.log(' Password must be at least 4 characters');
118
- continue;
119
- }
120
- const confirm = await ask(' Confirm password: ');
121
- if (pw !== confirm) {
122
- console.log(' Passwords do not match, try again');
123
- continue;
118
+ try {
119
+ while (true) {
120
+ pw = await ask(' Enter admin password: ');
121
+ if (!pw || pw.length < 4) {
122
+ console.log(' Password must be at least 4 characters');
123
+ continue;
124
+ }
125
+ const confirm = await ask(' Confirm password: ');
126
+ if (pw !== confirm) {
127
+ console.log(' Passwords do not match, try again');
128
+ continue;
129
+ }
130
+ break;
124
131
  }
125
- break;
132
+ } catch {
133
+ console.log('\n[forge] Cancelled');
134
+ process.exit(0);
126
135
  }
127
136
  rl.close();
128
137
 
@@ -170,27 +179,34 @@ const protectedPids = new Set();
170
179
 
171
180
  function cleanupOrphans() {
172
181
  try {
173
- const out = execSync("ps aux | grep -E 'telegram-standalone|terminal-standalone|next-server|next start|next dev' | grep -v grep | awk '{print $2}'", {
174
- encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
175
- }).trim();
176
- if (out) {
177
- const myPid = String(process.pid);
178
- for (const pid of out.split('\n').filter(Boolean)) {
179
- const p = pid.trim();
180
- if (p === myPid) continue;
181
- if (protectedPids.has(p)) continue; // don't kill processes we just started
182
- try { process.kill(parseInt(p), 'SIGTERM'); } catch {}
183
- }
184
- setTimeout(() => {
185
- for (const pid of out.split('\n').filter(Boolean)) {
182
+ // Only kill processes on OUR ports, not other instances
183
+ for (const port of [webPort, terminalPort]) {
184
+ try {
185
+ const pids = execSync(`lsof -ti:${port}`, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
186
+ const myPid = String(process.pid);
187
+ for (const pid of pids.split('\n').filter(Boolean)) {
186
188
  const p = pid.trim();
187
- if (p === myPid) continue;
188
- if (protectedPids.has(p)) continue;
189
- try { process.kill(parseInt(p), 'SIGKILL'); } catch {}
189
+ if (p === myPid || protectedPids.has(p)) continue;
190
+ try { process.kill(parseInt(p), 'SIGTERM'); } catch {}
190
191
  }
191
- }, 2000);
192
- console.log('[forge] Cleaned up orphan processes');
192
+ } catch {}
193
193
  }
194
+ // Kill standalone processes that belong to this instance (match by FORGE_DATA_DIR)
195
+ try {
196
+ const out = execSync(`ps aux | grep -E 'telegram-standalone|terminal-standalone' | grep -v grep`, {
197
+ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
198
+ }).trim();
199
+ if (out) {
200
+ const myPid = String(process.pid);
201
+ for (const line of out.split('\n').filter(Boolean)) {
202
+ // Only kill if it matches our DATA_DIR or port
203
+ if (!line.includes(DATA_DIR) && !line.includes(`PORT=${webPort}`) && !line.includes(`TERMINAL_PORT=${terminalPort}`)) continue;
204
+ const pid = line.trim().split(/\s+/)[1];
205
+ if (pid === myPid || protectedPids.has(pid)) continue;
206
+ try { process.kill(parseInt(pid), 'SIGTERM'); } catch {}
207
+ }
208
+ }
209
+ } catch {}
194
210
  } catch {}
195
211
  }
196
212
 
package/cli/mw.ts CHANGED
@@ -73,6 +73,18 @@ async function main() {
73
73
  process.exit(0);
74
74
  }
75
75
 
76
+ if (cmd === '--reset-password') {
77
+ // Shortcut: delegate to forge-server.mjs --reset-password
78
+ const { execSync } = await import('node:child_process');
79
+ const { join, dirname } = await import('node:path');
80
+ const { fileURLToPath } = await import('node:url');
81
+ const serverScript = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'forge-server.mjs');
82
+ try {
83
+ execSync(`node ${serverScript} --reset-password`, { stdio: 'inherit' });
84
+ } catch {}
85
+ process.exit(0);
86
+ }
87
+
76
88
  switch (cmd) {
77
89
  case 'task':
78
90
  case 't': {
@@ -349,24 +361,30 @@ async function main() {
349
361
  break;
350
362
  }
351
363
 
352
- case 'password':
353
- case 'pw': {
364
+ case 'tunnel_code':
365
+ case 'tcode': {
354
366
  const { readFileSync, existsSync } = await import('node:fs');
355
- const { homedir } = await import('node:os');
356
367
  const { join } = await import('node:path');
357
- const dataDir = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
368
+ const { getDataDir: _gdd } = await import('../lib/dirs');
369
+ const dataDir = _gdd();
358
370
  const codeFile = join(dataDir, 'session-code.json');
359
371
  try {
360
372
  if (existsSync(codeFile)) {
361
373
  const data = JSON.parse(readFileSync(codeFile, 'utf-8'));
362
374
  if (data.code) {
363
- console.log(`Session code: ${data.code} (for remote login 2FA)`);
375
+ console.log(`Session code: ${data.code}`);
376
+ } else {
377
+ console.log('No session code. Start tunnel first.');
364
378
  }
379
+ } else {
380
+ console.log('No session code. Start tunnel first.');
365
381
  }
366
382
  } catch {}
367
- console.log('Admin password: configured in Settings Admin Password');
368
- console.log('Local login: admin password only');
369
- console.log('Remote login: admin password + session code');
383
+ // Also show tunnel URL if running
384
+ try {
385
+ const tunnelState = JSON.parse(readFileSync(join(dataDir, 'tunnel-state.json'), 'utf-8'));
386
+ if (tunnelState.url) console.log(`Tunnel URL: ${tunnelState.url}`);
387
+ } catch {}
370
388
  break;
371
389
  }
372
390
 
@@ -553,7 +571,7 @@ Shortcuts: t=task, ls=tasks, w=watch, s=status, l=log, f=flows, p=projects, pw=p
553
571
  }
554
572
  }
555
573
 
556
- const skipUpdateCheck = ['upgrade', 'uninstall', '--version', '-v'];
574
+ const skipUpdateCheck = ['upgrade', 'uninstall', '--version', '-v', '--reset-password'];
557
575
  main().then(() => { if (!skipUpdateCheck.includes(cmd)) return checkForUpdate(); }).catch(err => {
558
576
  console.error(err.message);
559
577
  process.exit(1);
@@ -84,6 +84,20 @@ export default function Dashboard({ user }: { user: any }) {
84
84
  }, []);
85
85
  useEffect(() => { refreshDisplayName(); }, [refreshDisplayName]);
86
86
 
87
+ // Listen for open-terminal events from ProjectManager
88
+ useEffect(() => {
89
+ const handler = (e: Event) => {
90
+ const { projectPath, projectName } = (e as CustomEvent).detail;
91
+ setViewMode('terminal');
92
+ // Give terminal time to render, then trigger open
93
+ setTimeout(() => {
94
+ terminalRef.current?.openProjectTerminal?.(projectPath, projectName);
95
+ }, 300);
96
+ };
97
+ window.addEventListener('forge:open-terminal', handler);
98
+ return () => window.removeEventListener('forge:open-terminal', handler);
99
+ }, []);
100
+
87
101
  // Version check (on mount + every 10 min)
88
102
  useEffect(() => {
89
103
  const check = () => fetch('/api/version').then(r => r.json()).then(setVersionInfo).catch(() => {});