@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.
- package/CLAUDE.md +9 -2
- package/README.md +9 -9
- package/app/api/claude-templates/route.ts +145 -0
- package/app/api/docs/sessions/route.ts +3 -3
- package/app/api/monitor/route.ts +2 -2
- package/app/api/pipelines/route.ts +2 -2
- package/app/api/preview/[...path]/route.ts +2 -2
- package/app/api/preview/route.ts +3 -4
- package/app/api/projects/route.ts +19 -0
- package/app/api/skills/local/route.ts +228 -0
- package/app/api/skills/route.ts +36 -11
- package/app/api/terminal-state/route.ts +2 -2
- package/app/login/page.tsx +1 -1
- package/bin/forge-server.mjs +83 -33
- package/cli/mw.ts +27 -9
- package/components/Dashboard.tsx +14 -0
- package/components/ProjectManager.tsx +581 -42
- package/components/SessionView.tsx +1 -1
- package/components/SettingsModal.tsx +18 -1
- package/components/SkillsPanel.tsx +515 -29
- package/components/WebTerminal.tsx +50 -5
- package/instrumentation.ts +2 -2
- package/lib/claude-sessions.ts +2 -2
- package/lib/claude-templates.ts +227 -0
- package/lib/cloudflared.ts +4 -3
- package/lib/crypto.ts +3 -4
- package/lib/dirs.ts +99 -0
- package/lib/flows.ts +2 -2
- package/lib/init.ts +5 -2
- package/lib/password.ts +2 -2
- package/lib/pipeline.ts +3 -3
- package/lib/session-watcher.ts +2 -2
- package/lib/settings.ts +4 -2
- package/lib/skills.ts +444 -79
- package/lib/telegram-bot.ts +12 -5
- package/lib/terminal-standalone.ts +3 -2
- package/package.json +1 -1
- package/src/config/index.ts +6 -7
- package/src/core/db/database.ts +17 -5
package/bin/forge-server.mjs
CHANGED
|
@@ -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
|
|
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')) ||
|
|
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 =>
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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 '
|
|
353
|
-
case '
|
|
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
|
|
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}
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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);
|
package/components/Dashboard.tsx
CHANGED
|
@@ -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(() => {});
|