@aion0/forge 0.10.5 → 0.10.6
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/RELEASE_NOTES.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.6
|
|
2
2
|
|
|
3
3
|
Released: 2026-05-30
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.5
|
|
6
6
|
|
|
7
7
|
### Other
|
|
8
|
-
-
|
|
8
|
+
- feat(settings): folder picker for Add Project / Add Document Directory
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
11
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.5...v0.10.6
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/fs/browse?path=/some/dir
|
|
3
|
+
* Returns immediate subdirectories of `path` (defaults to $HOME).
|
|
4
|
+
* Used by the Add Project folder picker so users don't have to type
|
|
5
|
+
* paths. Auth-gated by middleware like the rest of /api/*.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextResponse } from 'next/server';
|
|
9
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { join, resolve, dirname } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
|
|
13
|
+
export async function GET(req: Request) {
|
|
14
|
+
const url = new URL(req.url);
|
|
15
|
+
const requested = url.searchParams.get('path') || homedir();
|
|
16
|
+
const path = resolve(requested);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const stat = statSync(path);
|
|
20
|
+
if (!stat.isDirectory()) {
|
|
21
|
+
return NextResponse.json({ ok: false, error: 'not a directory', path }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
const entries = readdirSync(path, { withFileTypes: true })
|
|
24
|
+
.filter(e => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith('.'))
|
|
25
|
+
.map(e => ({ name: e.name, path: join(path, e.name) }))
|
|
26
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
27
|
+
return NextResponse.json({
|
|
28
|
+
ok: true,
|
|
29
|
+
path,
|
|
30
|
+
parent: path === '/' ? null : dirname(path),
|
|
31
|
+
home: homedir(),
|
|
32
|
+
entries,
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return NextResponse.json({ ok: false, error: (err as Error).message, path }, { status: 404 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface Entry { name: string; path: string }
|
|
6
|
+
|
|
7
|
+
export function FolderPicker({ onSelect, onClose }: {
|
|
8
|
+
onSelect: (path: string) => void;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}) {
|
|
11
|
+
const [path, setPath] = useState<string>('');
|
|
12
|
+
const [parent, setParent] = useState<string | null>(null);
|
|
13
|
+
const [home, setHome] = useState<string>('');
|
|
14
|
+
const [entries, setEntries] = useState<Entry[]>([]);
|
|
15
|
+
const [error, setError] = useState<string>('');
|
|
16
|
+
|
|
17
|
+
const load = (p?: string) => {
|
|
18
|
+
setError('');
|
|
19
|
+
const q = p ? `?path=${encodeURIComponent(p)}` : '';
|
|
20
|
+
fetch(`/api/fs/browse${q}`).then(r => r.json()).then(d => {
|
|
21
|
+
if (!d.ok) { setError(d.error || 'failed'); return; }
|
|
22
|
+
setPath(d.path);
|
|
23
|
+
setParent(d.parent);
|
|
24
|
+
setHome(d.home);
|
|
25
|
+
setEntries(d.entries);
|
|
26
|
+
}).catch(e => setError(e.message));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
useEffect(() => { load(); }, []);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[60]" onClick={onClose}>
|
|
33
|
+
<div
|
|
34
|
+
className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg w-[520px] max-h-[70vh] flex flex-col"
|
|
35
|
+
onClick={e => e.stopPropagation()}
|
|
36
|
+
>
|
|
37
|
+
<div className="px-4 py-3 border-b border-[var(--border)] flex items-center gap-2">
|
|
38
|
+
<span className="text-xs font-semibold">Select Folder</span>
|
|
39
|
+
<button onClick={() => load(home)} className="text-[10px] px-2 py-0.5 rounded bg-[var(--bg-tertiary)] hover:bg-[var(--bg-primary)]">~ Home</button>
|
|
40
|
+
<button onClick={onClose} className="ml-auto text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]">✕</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="px-4 py-2 border-b border-[var(--border)] flex items-center gap-2">
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => parent && load(parent)}
|
|
46
|
+
disabled={!parent}
|
|
47
|
+
className="text-[10px] px-2 py-0.5 rounded bg-[var(--bg-tertiary)] hover:bg-[var(--bg-primary)] disabled:opacity-40"
|
|
48
|
+
>↑ Up</button>
|
|
49
|
+
<input
|
|
50
|
+
value={path}
|
|
51
|
+
onChange={e => setPath(e.target.value)}
|
|
52
|
+
onKeyDown={e => e.key === 'Enter' && load(path)}
|
|
53
|
+
className="flex-1 px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[11px] font-mono"
|
|
54
|
+
/>
|
|
55
|
+
<button onClick={() => load(path)} className="text-[10px] px-2 py-0.5 rounded bg-[var(--bg-tertiary)] hover:bg-[var(--bg-primary)]">Go</button>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="flex-1 overflow-y-auto px-2 py-2 min-h-[200px]">
|
|
59
|
+
{error && <div className="text-[11px] text-red-400 px-2 py-1">{error}</div>}
|
|
60
|
+
{entries.map(e => (
|
|
61
|
+
<button
|
|
62
|
+
key={e.path}
|
|
63
|
+
onClick={() => load(e.path)}
|
|
64
|
+
className="w-full text-left px-2 py-1 rounded text-[11px] font-mono hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
65
|
+
>
|
|
66
|
+
<span className="text-[var(--text-secondary)]">📁</span>
|
|
67
|
+
<span>{e.name}</span>
|
|
68
|
+
</button>
|
|
69
|
+
))}
|
|
70
|
+
{!error && entries.length === 0 && (
|
|
71
|
+
<div className="text-[11px] text-[var(--text-secondary)] px-2 py-4 text-center">(no subdirectories)</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="px-4 py-3 border-t border-[var(--border)] flex items-center justify-between gap-2">
|
|
76
|
+
<span className="text-[10px] text-[var(--text-secondary)] font-mono truncate flex-1" title={path}>{path}</span>
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => { onSelect(path); onClose(); }}
|
|
79
|
+
disabled={!path}
|
|
80
|
+
className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded disabled:opacity-50"
|
|
81
|
+
>Select this folder</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { FolderPicker } from './FolderPicker';
|
|
4
5
|
|
|
5
6
|
function SecretInput({ value, onChange, placeholder, className }: {
|
|
6
7
|
value: string;
|
|
@@ -236,6 +237,7 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
236
237
|
const [secretStatus, setSecretStatus] = useState<Record<string, boolean>>({});
|
|
237
238
|
const [newRoot, setNewRoot] = useState('');
|
|
238
239
|
const [newDocRoot, setNewDocRoot] = useState('');
|
|
240
|
+
const [pickerFor, setPickerFor] = useState<null | 'project' | 'doc'>(null);
|
|
239
241
|
const [saved, setSaved] = useState(false);
|
|
240
242
|
const [tunnel, setTunnel] = useState<TunnelStatus>({
|
|
241
243
|
status: 'stopped', url: null, error: null, installed: false, log: [],
|
|
@@ -322,6 +324,23 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
322
324
|
};
|
|
323
325
|
|
|
324
326
|
return (
|
|
327
|
+
<>
|
|
328
|
+
{pickerFor && (
|
|
329
|
+
<FolderPicker
|
|
330
|
+
onClose={() => setPickerFor(null)}
|
|
331
|
+
onSelect={picked => {
|
|
332
|
+
if (pickerFor === 'project') {
|
|
333
|
+
if (!settings.projectRoots.includes(picked)) {
|
|
334
|
+
setSettings({ ...settings, projectRoots: [...settings.projectRoots, picked] });
|
|
335
|
+
}
|
|
336
|
+
} else if (pickerFor === 'doc') {
|
|
337
|
+
if (!(settings.docRoots || []).includes(picked)) {
|
|
338
|
+
setSettings({ ...settings, docRoots: [...(settings.docRoots || []), picked] });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
)}
|
|
325
344
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => {
|
|
326
345
|
if (hasUnsaved && !confirm('You have unsaved changes. Close anyway?')) return;
|
|
327
346
|
onClose();
|
|
@@ -363,6 +382,11 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
363
382
|
placeholder="/Users/you/projects"
|
|
364
383
|
className="flex-1 px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
|
|
365
384
|
/>
|
|
385
|
+
<button
|
|
386
|
+
onClick={() => setPickerFor('project')}
|
|
387
|
+
className="text-[10px] px-2 py-1.5 border border-[var(--border)] rounded hover:bg-[var(--bg-tertiary)]"
|
|
388
|
+
title="Browse local folders"
|
|
389
|
+
>📁</button>
|
|
366
390
|
<button
|
|
367
391
|
onClick={addRoot}
|
|
368
392
|
className="text-[10px] px-3 py-1.5 bg-[var(--accent)] text-white rounded hover:opacity-90"
|
|
@@ -410,6 +434,11 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
410
434
|
placeholder="/Users/you/obsidian-vault"
|
|
411
435
|
className="flex-1 px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
|
|
412
436
|
/>
|
|
437
|
+
<button
|
|
438
|
+
onClick={() => setPickerFor('doc')}
|
|
439
|
+
className="text-[10px] px-2 py-1.5 border border-[var(--border)] rounded hover:bg-[var(--bg-tertiary)]"
|
|
440
|
+
title="Browse local folders"
|
|
441
|
+
>📁</button>
|
|
413
442
|
<button
|
|
414
443
|
onClick={() => {
|
|
415
444
|
if (newDocRoot.trim() && !settings.docRoots.includes(newDocRoot.trim())) {
|
|
@@ -858,6 +887,7 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
858
887
|
/>
|
|
859
888
|
)}
|
|
860
889
|
</div>
|
|
890
|
+
</>
|
|
861
891
|
);
|
|
862
892
|
}
|
|
863
893
|
|
package/package.json
CHANGED