@aion0/forge 0.3.0 → 0.3.2

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,13 +63,47 @@ 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');
70
71
 
71
72
  process.chdir(ROOT);
73
+
74
+ // ── Migrate old layout (~/.forge/*) to new (~/.forge/data/*) ──
75
+ if (!getArg('--dir')) {
76
+ const oldSettings = join(homedir(), '.forge', 'settings.yaml');
77
+ const newSettings = join(DATA_DIR, 'settings.yaml');
78
+ if (existsSync(oldSettings) && !existsSync(newSettings)) {
79
+ console.log('[forge] Migrating data from ~/.forge/ to ~/.forge/data/...');
80
+ mkdirSync(DATA_DIR, { recursive: true });
81
+ const migrateFiles = ['settings.yaml', '.encrypt-key', '.env.local', 'session-code.json', 'terminal-state.json', 'tunnel-state.json', 'preview.json', 'forge.pid', 'forge.log'];
82
+ for (const f of migrateFiles) {
83
+ const src = join(homedir(), '.forge', f);
84
+ const dest = join(DATA_DIR, f);
85
+ if (existsSync(src) && !existsSync(dest)) {
86
+ try { const { copyFileSync } = await import('node:fs'); copyFileSync(src, dest); console.log(` ${f}`); } catch {}
87
+ }
88
+ }
89
+ // data.db → workflow.db
90
+ const oldDb = join(homedir(), '.forge', 'data.db');
91
+ const newDb = join(DATA_DIR, 'workflow.db');
92
+ if (existsSync(oldDb) && !existsSync(newDb)) {
93
+ try { const { copyFileSync } = await import('node:fs'); copyFileSync(oldDb, newDb); console.log(' data.db → workflow.db'); } catch {}
94
+ }
95
+ // Migrate directories
96
+ for (const d of ['flows', 'pipelines']) {
97
+ const src = join(homedir(), '.forge', d);
98
+ const dest = join(DATA_DIR, d);
99
+ if (existsSync(src) && !existsSync(dest)) {
100
+ try { const { renameSync } = await import('node:fs'); renameSync(src, dest); console.log(` ${d}/`); } catch {}
101
+ }
102
+ }
103
+ console.log('[forge] Migration complete.');
104
+ }
105
+ }
106
+
72
107
  mkdirSync(DATA_DIR, { recursive: true });
73
108
 
74
109
  // ── Load <data-dir>/.env.local ──
@@ -108,21 +143,29 @@ if (!isStop) {
108
143
 
109
144
  const readline = await import('node:readline');
110
145
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
111
- const ask = (q) => new Promise(resolve => rl.question(q, resolve));
146
+ const ask = (q) => new Promise((resolve, reject) => {
147
+ rl.question(q, resolve);
148
+ rl.once('close', () => reject(new Error('cancelled')));
149
+ });
112
150
 
113
151
  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;
152
+ try {
153
+ while (true) {
154
+ pw = await ask(' Enter admin password: ');
155
+ if (!pw || pw.length < 4) {
156
+ console.log(' Password must be at least 4 characters');
157
+ continue;
158
+ }
159
+ const confirm = await ask(' Confirm password: ');
160
+ if (pw !== confirm) {
161
+ console.log(' Passwords do not match, try again');
162
+ continue;
163
+ }
164
+ break;
124
165
  }
125
- break;
166
+ } catch {
167
+ console.log('\n[forge] Cancelled');
168
+ process.exit(0);
126
169
  }
127
170
  rl.close();
128
171
 
@@ -170,27 +213,34 @@ const protectedPids = new Set();
170
213
 
171
214
  function cleanupOrphans() {
172
215
  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)) {
216
+ // Only kill processes on OUR ports, not other instances
217
+ for (const port of [webPort, terminalPort]) {
218
+ try {
219
+ const pids = execSync(`lsof -ti:${port}`, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
220
+ const myPid = String(process.pid);
221
+ for (const pid of pids.split('\n').filter(Boolean)) {
186
222
  const p = pid.trim();
187
- if (p === myPid) continue;
188
- if (protectedPids.has(p)) continue;
189
- try { process.kill(parseInt(p), 'SIGKILL'); } catch {}
223
+ if (p === myPid || protectedPids.has(p)) continue;
224
+ try { process.kill(parseInt(p), 'SIGTERM'); } catch {}
190
225
  }
191
- }, 2000);
192
- console.log('[forge] Cleaned up orphan processes');
226
+ } catch {}
193
227
  }
228
+ // Kill standalone processes that belong to this instance (match by FORGE_DATA_DIR)
229
+ try {
230
+ const out = execSync(`ps aux | grep -E 'telegram-standalone|terminal-standalone' | grep -v grep`, {
231
+ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
232
+ }).trim();
233
+ if (out) {
234
+ const myPid = String(process.pid);
235
+ for (const line of out.split('\n').filter(Boolean)) {
236
+ // Only kill if it matches our DATA_DIR or port
237
+ if (!line.includes(DATA_DIR) && !line.includes(`PORT=${webPort}`) && !line.includes(`TERMINAL_PORT=${terminalPort}`)) continue;
238
+ const pid = line.trim().split(/\s+/)[1];
239
+ if (pid === myPid || protectedPids.has(pid)) continue;
240
+ try { process.kill(parseInt(pid), 'SIGTERM'); } catch {}
241
+ }
242
+ }
243
+ } catch {}
194
244
  } catch {}
195
245
  }
196
246
 
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(() => {});