@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.
@@ -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>
@@ -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; // If set, auto-run claude --resume in this dir on session create
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
- loadSharedState().then(saved => {
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
- // Sync session labels ref
220
- const labels = { ...sessionLabelsRef.current };
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
- writeFileSync(STATE_FILE, JSON.stringify(data, null, 2));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {