@aion0/forge 0.2.31 → 0.2.33
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/app/login/page.tsx +16 -0
- package/bin/forge-server.mjs +62 -0
- package/components/SettingsModal.tsx +3 -0
- package/components/WebTerminal.tsx +37 -17
- package/lib/terminal-standalone.ts +11 -1
- package/package.json +1 -1
package/app/login/page.tsx
CHANGED
|
@@ -8,6 +8,7 @@ export default function LoginPage() {
|
|
|
8
8
|
const [sessionCode, setSessionCode] = useState('');
|
|
9
9
|
const [error, setError] = useState('');
|
|
10
10
|
const [isRemote, setIsRemote] = useState(false);
|
|
11
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
11
12
|
|
|
12
13
|
useEffect(() => {
|
|
13
14
|
const host = window.location.hostname;
|
|
@@ -76,6 +77,21 @@ export default function LoginPage() {
|
|
|
76
77
|
Session code is generated when tunnel starts. Use /tunnel_code in Telegram to get it.
|
|
77
78
|
</p>
|
|
78
79
|
)}
|
|
80
|
+
<div className="text-center">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => setShowHelp(v => !v)}
|
|
84
|
+
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
85
|
+
>
|
|
86
|
+
Forgot password?
|
|
87
|
+
</button>
|
|
88
|
+
{showHelp && (
|
|
89
|
+
<p className="text-[10px] text-[var(--text-secondary)] mt-1 bg-[var(--bg-tertiary)] rounded p-2">
|
|
90
|
+
Run in terminal:<br />
|
|
91
|
+
<code className="text-[var(--accent)]">forge server start --reset-password</code>
|
|
92
|
+
</p>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
79
95
|
</form>
|
|
80
96
|
|
|
81
97
|
</div>
|
package/bin/forge-server.mjs
CHANGED
|
@@ -59,6 +59,7 @@ const isStop = process.argv.includes('--stop');
|
|
|
59
59
|
const isRestart = process.argv.includes('--restart');
|
|
60
60
|
const isRebuild = process.argv.includes('--rebuild');
|
|
61
61
|
const resetTerminal = process.argv.includes('--reset-terminal');
|
|
62
|
+
const resetPassword = process.argv.includes('--reset-password');
|
|
62
63
|
|
|
63
64
|
const webPort = parseInt(getArg('--port')) || 3000;
|
|
64
65
|
const terminalPort = parseInt(getArg('--terminal-port')) || 3001;
|
|
@@ -89,6 +90,67 @@ process.env.PORT = String(webPort);
|
|
|
89
90
|
process.env.TERMINAL_PORT = String(terminalPort);
|
|
90
91
|
process.env.FORGE_DATA_DIR = DATA_DIR;
|
|
91
92
|
|
|
93
|
+
// ── Password setup (first run or --reset-password) ──
|
|
94
|
+
if (!isStop) {
|
|
95
|
+
const YAML = await import('yaml');
|
|
96
|
+
const settingsFile = join(DATA_DIR, 'settings.yaml');
|
|
97
|
+
let settings = {};
|
|
98
|
+
try { settings = YAML.parse(readFileSync(settingsFile, 'utf-8')) || {}; } catch {}
|
|
99
|
+
|
|
100
|
+
const hasPassword = !!settings.telegramTunnelPassword;
|
|
101
|
+
|
|
102
|
+
if (resetPassword || !hasPassword) {
|
|
103
|
+
if (resetPassword) {
|
|
104
|
+
console.log('[forge] Password reset requested');
|
|
105
|
+
} else {
|
|
106
|
+
console.log('[forge] First run — please set an admin password');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const readline = await import('node:readline');
|
|
110
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
111
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
112
|
+
|
|
113
|
+
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;
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
rl.close();
|
|
128
|
+
|
|
129
|
+
// Encrypt and save
|
|
130
|
+
const crypto = await import('node:crypto');
|
|
131
|
+
const KEY_FILE = join(DATA_DIR, '.encrypt-key');
|
|
132
|
+
let encKey;
|
|
133
|
+
if (existsSync(KEY_FILE)) {
|
|
134
|
+
encKey = Buffer.from(readFileSync(KEY_FILE, 'utf-8').trim(), 'hex');
|
|
135
|
+
} else {
|
|
136
|
+
encKey = crypto.randomBytes(32);
|
|
137
|
+
writeFileSync(KEY_FILE, encKey.toString('hex'), { mode: 0o600 });
|
|
138
|
+
}
|
|
139
|
+
const iv = crypto.randomBytes(12);
|
|
140
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', encKey, iv);
|
|
141
|
+
const encrypted = Buffer.concat([cipher.update(pw, 'utf-8'), cipher.final()]);
|
|
142
|
+
const tag = cipher.getAuthTag();
|
|
143
|
+
settings.telegramTunnelPassword = `enc:${iv.toString('base64')}.${tag.toString('base64')}.${encrypted.toString('base64')}`;
|
|
144
|
+
if (!existsSync(dirname(settingsFile))) mkdirSync(dirname(settingsFile), { recursive: true });
|
|
145
|
+
writeFileSync(settingsFile, YAML.stringify(settings), 'utf-8');
|
|
146
|
+
console.log('[forge] Admin password saved');
|
|
147
|
+
|
|
148
|
+
if (resetPassword && !isDev && !isBackground && !isRestart) {
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
92
154
|
// ── Reset terminal server (kill port + tmux sessions) ──
|
|
93
155
|
if (resetTerminal) {
|
|
94
156
|
console.log(`[forge] Resetting terminal server (port ${terminalPort})...`);
|
|
@@ -795,6 +795,9 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
795
795
|
isSet={!!secretStatus.telegramTunnelPassword}
|
|
796
796
|
onEdit={() => setEditingSecret({ field: 'telegramTunnelPassword', label: 'Admin Password' })}
|
|
797
797
|
/>
|
|
798
|
+
<p className="text-[9px] text-[var(--text-secondary)]">
|
|
799
|
+
Forgot? Run: <code className="text-[var(--accent)]">forge server start --reset-password</code>
|
|
800
|
+
</p>
|
|
798
801
|
</div>
|
|
799
802
|
|
|
800
803
|
{/* Actions */}
|
|
@@ -35,7 +35,7 @@ interface TabState {
|
|
|
35
35
|
tree: SplitNode;
|
|
36
36
|
ratios: Record<number, number>;
|
|
37
37
|
activeId: number;
|
|
38
|
-
projectPath?: string;
|
|
38
|
+
projectPath?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// ─── Layout persistence ──────────────────────────────────────
|
|
@@ -187,9 +187,32 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
187
187
|
|
|
188
188
|
// Restore shared state from server after mount
|
|
189
189
|
useEffect(() => {
|
|
190
|
-
|
|
190
|
+
// Fetch settings for skipPermissions
|
|
191
|
+
fetch('/api/settings').then(r => r.json())
|
|
192
|
+
.then((s: any) => { if (s.skipPermissions) setSkipPermissions(true); })
|
|
193
|
+
.catch(() => {});
|
|
194
|
+
// Load state + projects together, then patch missing projectPath
|
|
195
|
+
Promise.all([
|
|
196
|
+
loadSharedState(),
|
|
197
|
+
fetch('/api/projects').then(r => r.json()).catch(() => []),
|
|
198
|
+
]).then(([saved, projects]) => {
|
|
199
|
+
const projList: { name: string; path: string; root: string }[] = Array.isArray(projects) ? projects : [];
|
|
200
|
+
setAllProjects(projList);
|
|
201
|
+
setProjectRoots([...new Set(projList.map(p => p.root))]);
|
|
202
|
+
|
|
191
203
|
if (saved && saved.tabs.length > 0) {
|
|
192
204
|
initNextIdFromTabs(saved.tabs);
|
|
205
|
+
// Patch missing projectPath by matching tab label to project name
|
|
206
|
+
for (const tab of saved.tabs) {
|
|
207
|
+
if (!tab.projectPath) {
|
|
208
|
+
const match = projList.find(p => p.name.toLowerCase() === tab.label.toLowerCase());
|
|
209
|
+
if (match) {
|
|
210
|
+
tab.projectPath = match.path;
|
|
211
|
+
// Also patch tree node
|
|
212
|
+
if (tab.tree.type === 'terminal') tab.tree.projectPath = match.path;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
193
216
|
setTabs(saved.tabs);
|
|
194
217
|
setActiveTabId(saved.activeTabId);
|
|
195
218
|
sessionLabelsRef.current = saved.sessionLabels || {};
|
|
@@ -197,27 +220,24 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
197
220
|
}
|
|
198
221
|
setHydrated(true);
|
|
199
222
|
});
|
|
200
|
-
// Fetch settings for skipPermissions
|
|
201
|
-
fetch('/api/settings').then(r => r.json())
|
|
202
|
-
.then((s: any) => { if (s.skipPermissions) setSkipPermissions(true); })
|
|
203
|
-
.catch(() => {});
|
|
204
|
-
// Fetch projects and derive roots
|
|
205
|
-
fetch('/api/projects').then(r => r.json())
|
|
206
|
-
.then((p: { name: string; path: string; root: string }[]) => {
|
|
207
|
-
if (!Array.isArray(p)) return;
|
|
208
|
-
setAllProjects(p);
|
|
209
|
-
const roots = [...new Set(p.map(proj => proj.root))];
|
|
210
|
-
setProjectRoots(roots);
|
|
211
|
-
})
|
|
212
|
-
.catch(() => {});
|
|
213
223
|
}, []);
|
|
214
224
|
|
|
215
225
|
// Persist to server on changes (debounced, only after hydration)
|
|
216
226
|
const saveTimerRef = useRef(0);
|
|
217
227
|
useEffect(() => {
|
|
218
228
|
if (!hydrated) return;
|
|
219
|
-
//
|
|
220
|
-
const
|
|
229
|
+
// Collect all active session names from current tabs
|
|
230
|
+
const activeSessionNames = new Set<string>();
|
|
231
|
+
for (const tab of tabs) {
|
|
232
|
+
for (const sn of collectSessionNames(tab.tree)) {
|
|
233
|
+
activeSessionNames.add(sn);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Only keep labels for active sessions (clean up stale entries)
|
|
237
|
+
const labels: Record<string, string> = {};
|
|
238
|
+
for (const sn of activeSessionNames) {
|
|
239
|
+
labels[sn] = sessionLabelsRef.current[sn] || '';
|
|
240
|
+
}
|
|
221
241
|
for (const tab of tabs) {
|
|
222
242
|
for (const sn of collectSessionNames(tab.tree)) {
|
|
223
243
|
labels[sn] = tab.label;
|
|
@@ -53,7 +53,17 @@ function loadTerminalState(): unknown {
|
|
|
53
53
|
function saveTerminalState(data: unknown): void {
|
|
54
54
|
try {
|
|
55
55
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
56
|
-
|
|
56
|
+
const json = JSON.stringify(data, null, 2);
|
|
57
|
+
writeFileSync(STATE_FILE, json);
|
|
58
|
+
// Debug: check if projectPath is being saved
|
|
59
|
+
const parsed = JSON.parse(json);
|
|
60
|
+
if (parsed.tabs) {
|
|
61
|
+
for (const t of parsed.tabs) {
|
|
62
|
+
if (t.projectPath || t.tree?.projectPath) {
|
|
63
|
+
console.log(`[terminal] Saved tab "${t.label}" with projectPath: tab=${t.projectPath} tree=${t.tree?.projectPath}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
57
67
|
} catch (e) {
|
|
58
68
|
console.error('[terminal] Failed to save state:', e);
|
|
59
69
|
}
|
package/package.json
CHANGED