feat(FN-694): add DirectoryPicker component and redesign SetupWizardModal

- Add browse-directory API endpoint and client for native folder selection
- Create DirectoryPicker component with manual input, browse button, and validation
- Redesign SetupWizardModal to use DirectoryPicker for project path selection
- Add comprehensive tests for DirectoryPicker and SetupWizardModal
- Remove bundled fusion skill and related extension/sync code from CLI package
This commit is contained in:
gsxdsm
2026-04-02 13:30:08 -07:00
parent 2acd2cfb17
commit 7760bcc9cd
7 changed files with 1178 additions and 45 deletions

View File

@@ -6078,6 +6078,76 @@ Output ONLY the prompt text (no markdown, no explanations).`;
// Mount mission routes at /api/missions
router.use("/missions", createMissionRouter(store));
// ── Directory Browsing ────────────────────────────────────────────────────────
/**
* GET /api/browse-directory
* Browse filesystem directories for the directory picker.
* Query: { path?: string, showHidden?: "true" }
* Returns: { currentPath: string, parentPath: string | null, entries: Array<{ name: string, path: string, hasChildren: boolean }> }
*/
router.get("/browse-directory", async (req, res) => {
try {
const { resolve, dirname, join } = await import("node:path");
const { readdir, stat } = await import("node:fs/promises");
const rawPath = (req.query.path as string) || process.env.HOME || process.env.USERPROFILE || "/";
const showHidden = req.query.showHidden === "true";
// Validate: must be absolute, no .. traversal
const resolvedPath = resolve(rawPath);
if (rawPath.includes("..")) {
res.status(400).json({ error: "Path must not contain '..' traversal" });
return;
}
if (resolvedPath !== resolve(resolvedPath)) {
res.status(400).json({ error: "Path must be absolute" });
return;
}
// Check path exists and is a directory
let pathStat;
try {
pathStat = await stat(resolvedPath);
} catch {
res.status(404).json({ error: "Directory not found" });
return;
}
if (!pathStat.isDirectory()) {
res.status(400).json({ error: "Path is not a directory" });
return;
}
// Read directory entries
const dirEntries = await readdir(resolvedPath, { withFileTypes: true });
const entries: Array<{ name: string; path: string; hasChildren: boolean }> = [];
for (const entry of dirEntries) {
if (!entry.isDirectory()) continue;
if (!showHidden && entry.name.startsWith(".")) continue;
const entryPath = join(resolvedPath, entry.name);
let hasChildren = false;
try {
const subEntries = await readdir(entryPath, { withFileTypes: true });
hasChildren = subEntries.some((e) => e.isDirectory());
} catch {
// Can't read subdirectory — treat as no children
}
entries.push({ name: entry.name, path: entryPath, hasChildren });
}
entries.sort((a, b) => a.name.localeCompare(b.name));
const parentPath = resolvedPath === "/" ? null : dirname(resolvedPath);
res.json({ currentPath: resolvedPath, parentPath, entries });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// ── Project Management Routes (Multi-Project Support) ───────────────────────
// These routes require CentralCore which is imported dynamically to avoid
// circular dependencies and ensure the central database is initialized.