mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: implement web-based file picker for improved directory and file selection
- Replaced prompt-based directory input with a web-based directory picker in HttpApiClient. - Added server endpoint for resolving directory paths based on directory name and file structure. - Enhanced error handling and logging for directory and file selection processes. - Updated file picker to validate file existence with the server for better user feedback.
This commit is contained in:
279
apps/app/src/lib/file-picker.ts
Normal file
279
apps/app/src/lib/file-picker.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
/**
|
||||||
|
* File Picker Utility for Web Browsers
|
||||||
|
*
|
||||||
|
* Provides cross-platform file and directory selection using:
|
||||||
|
* 1. HTML5 webkitdirectory input - primary method (works on Windows)
|
||||||
|
* 2. File System Access API (showDirectoryPicker) - fallback for modern browsers
|
||||||
|
*
|
||||||
|
* Note: Browsers don't expose absolute file paths for security reasons.
|
||||||
|
* This implementation extracts directory information and may require
|
||||||
|
* user confirmation or server-side path resolution.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory picker result with structure information for server-side resolution
|
||||||
|
*/
|
||||||
|
export interface DirectoryPickerResult {
|
||||||
|
directoryName: string;
|
||||||
|
sampleFiles: string[]; // Relative paths of sample files for identification
|
||||||
|
fileCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a directory picker dialog
|
||||||
|
* @returns Promise resolving to directory information, or null if canceled
|
||||||
|
*
|
||||||
|
* Note: Browsers don't expose absolute file paths for security reasons.
|
||||||
|
* This function returns directory structure information that the server
|
||||||
|
* can use to locate the actual directory path.
|
||||||
|
*/
|
||||||
|
export async function openDirectoryPicker(): Promise<DirectoryPickerResult | null> {
|
||||||
|
// Use webkitdirectory (works on Windows and all modern browsers)
|
||||||
|
return new Promise<DirectoryPickerResult | null>((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.webkitdirectory = true;
|
||||||
|
input.style.display = "none";
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (input.parentNode) {
|
||||||
|
document.body.removeChild(input);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let changeEventFired = false;
|
||||||
|
let focusTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const safeResolve = (value: DirectoryPickerResult | null) => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
changeEventFired = true;
|
||||||
|
if (focusTimeout) {
|
||||||
|
clearTimeout(focusTimeout);
|
||||||
|
focusTimeout = null;
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
resolve(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener("change", (e) => {
|
||||||
|
changeEventFired = true;
|
||||||
|
if (focusTimeout) {
|
||||||
|
clearTimeout(focusTimeout);
|
||||||
|
focusTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[FilePicker] Change event fired");
|
||||||
|
const files = input.files;
|
||||||
|
console.log("[FilePicker] Files selected:", files?.length || 0);
|
||||||
|
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
console.log("[FilePicker] No files selected");
|
||||||
|
safeResolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstFile = files[0];
|
||||||
|
console.log("[FilePicker] First file:", {
|
||||||
|
name: firstFile.name,
|
||||||
|
webkitRelativePath: firstFile.webkitRelativePath,
|
||||||
|
// @ts-expect-error
|
||||||
|
path: firstFile.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract directory name from webkitRelativePath
|
||||||
|
// webkitRelativePath format: "directoryName/subfolder/file.txt" or "directoryName/file.txt"
|
||||||
|
let directoryName = "Selected Directory";
|
||||||
|
|
||||||
|
// Method 1: Try to get absolute path from File object (non-standard, works in Electron/Chromium)
|
||||||
|
// @ts-expect-error - path property is non-standard but available in some browsers
|
||||||
|
if (firstFile.path) {
|
||||||
|
// @ts-expect-error
|
||||||
|
const filePath = firstFile.path as string;
|
||||||
|
console.log("[FilePicker] Found file.path:", filePath);
|
||||||
|
// Extract directory path (remove filename)
|
||||||
|
const lastSeparator = Math.max(
|
||||||
|
filePath.lastIndexOf("\\"),
|
||||||
|
filePath.lastIndexOf("/")
|
||||||
|
);
|
||||||
|
if (lastSeparator > 0) {
|
||||||
|
const absolutePath = filePath.substring(0, lastSeparator);
|
||||||
|
console.log("[FilePicker] Found absolute path:", absolutePath);
|
||||||
|
// Return as directory name for now - server can validate it directly
|
||||||
|
directoryName = absolutePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Extract directory name from webkitRelativePath
|
||||||
|
if (directoryName === "Selected Directory" && firstFile.webkitRelativePath) {
|
||||||
|
const relativePath = firstFile.webkitRelativePath;
|
||||||
|
console.log("[FilePicker] Using webkitRelativePath:", relativePath);
|
||||||
|
const pathParts = relativePath.split("/");
|
||||||
|
if (pathParts.length > 0) {
|
||||||
|
directoryName = pathParts[0]; // Top-level directory name
|
||||||
|
console.log("[FilePicker] Extracted directory name:", directoryName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect sample file paths for server-side directory matching
|
||||||
|
// Take first 10 files to identify the directory
|
||||||
|
const sampleFiles: string[] = [];
|
||||||
|
const maxSamples = 10;
|
||||||
|
for (let i = 0; i < Math.min(files.length, maxSamples); i++) {
|
||||||
|
const file = files[i];
|
||||||
|
if (file.webkitRelativePath) {
|
||||||
|
sampleFiles.push(file.webkitRelativePath);
|
||||||
|
} else if (file.name) {
|
||||||
|
sampleFiles.push(file.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[FilePicker] Directory info:", {
|
||||||
|
directoryName,
|
||||||
|
fileCount: files.length,
|
||||||
|
sampleFiles: sampleFiles.slice(0, 5), // Log first 5
|
||||||
|
});
|
||||||
|
|
||||||
|
safeResolve({
|
||||||
|
directoryName,
|
||||||
|
sampleFiles,
|
||||||
|
fileCount: files.length,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle cancellation - but be very careful not to interfere with change event
|
||||||
|
// On Windows, the dialog might take time to process, so we wait longer
|
||||||
|
const handleFocus = () => {
|
||||||
|
// Wait longer on Windows - the dialog might take time to process
|
||||||
|
// Only resolve as canceled if change event hasn't fired after a delay
|
||||||
|
focusTimeout = setTimeout(() => {
|
||||||
|
if (!resolved && !changeEventFired && (!input.files || input.files.length === 0)) {
|
||||||
|
console.log("[FilePicker] Dialog canceled (no files after focus and no change event)");
|
||||||
|
safeResolve(null);
|
||||||
|
}
|
||||||
|
}, 2000); // Increased timeout for Windows - give it time
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to DOM temporarily
|
||||||
|
document.body.appendChild(input);
|
||||||
|
console.log("[FilePicker] Opening directory picker...");
|
||||||
|
|
||||||
|
// Try to show picker programmatically
|
||||||
|
if ("showPicker" in HTMLInputElement.prototype) {
|
||||||
|
try {
|
||||||
|
(input as any).showPicker();
|
||||||
|
console.log("[FilePicker] Using showPicker()");
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[FilePicker] showPicker() failed, using click()", error);
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[FilePicker] Using click()");
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up cancellation detection with longer delay
|
||||||
|
// Only add focus listener if we're not already resolved
|
||||||
|
window.addEventListener("focus", handleFocus, { once: true });
|
||||||
|
|
||||||
|
// Also handle blur as a cancellation signal (but with delay)
|
||||||
|
window.addEventListener("blur", () => {
|
||||||
|
// Dialog opened, wait for it to close
|
||||||
|
setTimeout(() => {
|
||||||
|
window.addEventListener("focus", handleFocus, { once: true });
|
||||||
|
}, 100);
|
||||||
|
}, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a file picker dialog
|
||||||
|
* @param options Optional configuration (multiple files, file types, etc.)
|
||||||
|
* @returns Promise resolving to selected file path(s), or null if canceled
|
||||||
|
*/
|
||||||
|
export async function openFilePicker(
|
||||||
|
options?: {
|
||||||
|
multiple?: boolean;
|
||||||
|
accept?: string;
|
||||||
|
}
|
||||||
|
): Promise<string | string[] | null> {
|
||||||
|
// Use standard file input (works on all browsers including Windows)
|
||||||
|
return new Promise<string | string[] | null>((resolve) => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.multiple = options?.multiple ?? false;
|
||||||
|
if (options?.accept) {
|
||||||
|
input.accept = options.accept;
|
||||||
|
}
|
||||||
|
input.style.display = "none";
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (input.parentNode) {
|
||||||
|
document.body.removeChild(input);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
const files = input.files;
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
cleanup();
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract paths from File objects
|
||||||
|
const extractPath = (file: File): string => {
|
||||||
|
// Try to get path from File object (non-standard, but available in some browsers)
|
||||||
|
// @ts-expect-error - path property is non-standard
|
||||||
|
if (file.path) {
|
||||||
|
// @ts-expect-error
|
||||||
|
return file.path as string;
|
||||||
|
}
|
||||||
|
// Fallback to filename (server will need to resolve)
|
||||||
|
return file.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options?.multiple) {
|
||||||
|
const paths = Array.from(files).map(extractPath);
|
||||||
|
cleanup();
|
||||||
|
resolve(paths);
|
||||||
|
} else {
|
||||||
|
const path = extractPath(files[0]);
|
||||||
|
cleanup();
|
||||||
|
resolve(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle window focus (user may have canceled)
|
||||||
|
const handleFocus = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!input.files || input.files.length === 0) {
|
||||||
|
cleanup();
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to DOM temporarily
|
||||||
|
document.body.appendChild(input);
|
||||||
|
|
||||||
|
// Try to show picker programmatically
|
||||||
|
// Note: showPicker() is available in modern browsers but TypeScript types it as void
|
||||||
|
// In practice, it may return a Promise in some implementations, but we'll handle errors via try/catch
|
||||||
|
if ("showPicker" in HTMLInputElement.prototype) {
|
||||||
|
try {
|
||||||
|
(input as any).showPicker();
|
||||||
|
} catch {
|
||||||
|
// Fallback to click if showPicker fails
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up cancellation detection
|
||||||
|
window.addEventListener("focus", handleFocus, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ import type {
|
|||||||
ModelDefinition,
|
ModelDefinition,
|
||||||
ProviderStatus,
|
ProviderStatus,
|
||||||
} from "@/types/electron";
|
} from "@/types/electron";
|
||||||
|
import { openDirectoryPicker, openFilePicker, type DirectoryPickerResult } from "./file-picker";
|
||||||
|
|
||||||
|
|
||||||
// Server URL - configurable via environment variable
|
// Server URL - configurable via environment variable
|
||||||
@@ -201,46 +202,96 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// File picker - uses prompt for path input
|
// File picker - uses web-based file picker (works on Windows)
|
||||||
async openDirectory(): Promise<DialogResult> {
|
async openDirectory(): Promise<DialogResult> {
|
||||||
const path = prompt("Enter project directory path:");
|
try {
|
||||||
if (!path) {
|
console.log("[HttpApiClient] Opening directory picker...");
|
||||||
|
const directoryInfo = await openDirectoryPicker();
|
||||||
|
console.log("[HttpApiClient] Directory info:", directoryInfo);
|
||||||
|
|
||||||
|
if (!directoryInfo) {
|
||||||
|
console.log("[HttpApiClient] No directory selected (user canceled)");
|
||||||
|
return { canceled: true, filePaths: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to resolve directory path using server endpoint
|
||||||
|
// First, try if we have an absolute path (from file.path property)
|
||||||
|
if (directoryInfo.directoryName && (directoryInfo.directoryName.includes("\\") || directoryInfo.directoryName.includes("/") || directoryInfo.directoryName.startsWith("/"))) {
|
||||||
|
// Looks like an absolute path, try validating it directly
|
||||||
|
console.log("[HttpApiClient] Attempting direct path validation:", directoryInfo.directoryName);
|
||||||
|
const directResult = await this.post<{
|
||||||
|
success: boolean;
|
||||||
|
path?: string;
|
||||||
|
error?: string;
|
||||||
|
}>("/api/fs/validate-path", { filePath: directoryInfo.directoryName });
|
||||||
|
|
||||||
|
if (directResult.success && directResult.path) {
|
||||||
|
console.log("[HttpApiClient] Direct path validation succeeded:", directResult.path);
|
||||||
|
return { canceled: false, filePaths: [directResult.path] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If direct validation failed or we only have a directory name,
|
||||||
|
// use the resolve endpoint with directory structure
|
||||||
|
console.log("[HttpApiClient] Resolving directory using structure info...");
|
||||||
|
const result = await this.post<{
|
||||||
|
success: boolean;
|
||||||
|
path?: string;
|
||||||
|
error?: string;
|
||||||
|
}>("/api/fs/resolve-directory", {
|
||||||
|
directoryName: directoryInfo.directoryName,
|
||||||
|
sampleFiles: directoryInfo.sampleFiles,
|
||||||
|
fileCount: directoryInfo.fileCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[HttpApiClient] Directory resolution result:", result);
|
||||||
|
|
||||||
|
if (result.success && result.path) {
|
||||||
|
console.log("[HttpApiClient] Directory resolved successfully:", result.path);
|
||||||
|
return { canceled: false, filePaths: [result.path] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If resolution failed, show error
|
||||||
|
console.warn("[HttpApiClient] Directory resolution failed:", result.error);
|
||||||
|
const errorMsg = result.error || "Could not locate directory. Please ensure the directory exists and try selecting it again.";
|
||||||
|
alert(errorMsg);
|
||||||
|
return { canceled: true, filePaths: [] };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[HttpApiClient] Failed to open directory picker:", error);
|
||||||
|
alert("Failed to open directory picker. Please try again.");
|
||||||
return { canceled: true, filePaths: [] };
|
return { canceled: true, filePaths: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate with server
|
|
||||||
const result = await this.post<{
|
|
||||||
success: boolean;
|
|
||||||
path?: string;
|
|
||||||
error?: string;
|
|
||||||
}>("/api/fs/validate-path", { filePath: path });
|
|
||||||
|
|
||||||
if (result.success && result.path) {
|
|
||||||
return { canceled: false, filePaths: [result.path] };
|
|
||||||
}
|
|
||||||
|
|
||||||
alert(result.error || "Invalid path");
|
|
||||||
return { canceled: true, filePaths: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async openFile(options?: object): Promise<DialogResult> {
|
async openFile(options?: object): Promise<DialogResult> {
|
||||||
// Prompt for file path
|
try {
|
||||||
const path = prompt("Enter file path:");
|
const selectedPath = await openFilePicker(options);
|
||||||
if (!path) {
|
if (!selectedPath) {
|
||||||
|
return { canceled: true, filePaths: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both single file and multiple files
|
||||||
|
const filePaths = Array.isArray(selectedPath) ? selectedPath : [selectedPath];
|
||||||
|
|
||||||
|
// Validate files exist with server
|
||||||
|
// For multiple files, check the first one as a validation step
|
||||||
|
const firstPath = filePaths[0];
|
||||||
|
const result = await this.post<{ success: boolean; exists: boolean }>(
|
||||||
|
"/api/fs/exists",
|
||||||
|
{ filePath: firstPath }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success && result.exists) {
|
||||||
|
return { canceled: false, filePaths };
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("File does not exist or cannot be accessed.");
|
||||||
|
return { canceled: true, filePaths: [] };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[HttpApiClient] Failed to open file picker:", error);
|
||||||
|
alert("Failed to open file picker. Please try again.");
|
||||||
return { canceled: true, filePaths: [] };
|
return { canceled: true, filePaths: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.post<{ success: boolean; exists: boolean }>(
|
|
||||||
"/api/fs/exists",
|
|
||||||
{ filePath: path }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success && result.exists) {
|
|
||||||
return { canceled: false, filePaths: [path] };
|
|
||||||
}
|
|
||||||
|
|
||||||
alert("File does not exist");
|
|
||||||
return { canceled: true, filePaths: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// File system operations
|
// File system operations
|
||||||
|
|||||||
@@ -217,6 +217,113 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Resolve directory path from directory name and file structure
|
||||||
|
// Used when browser file picker only provides directory name (not full path)
|
||||||
|
router.post("/resolve-directory", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { directoryName, sampleFiles, fileCount } = req.body as {
|
||||||
|
directoryName: string;
|
||||||
|
sampleFiles?: string[];
|
||||||
|
fileCount?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!directoryName) {
|
||||||
|
res.status(400).json({ success: false, error: "directoryName is required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If directoryName looks like an absolute path, try validating it directly
|
||||||
|
if (path.isAbsolute(directoryName) || directoryName.includes(path.sep)) {
|
||||||
|
try {
|
||||||
|
const resolvedPath = path.resolve(directoryName);
|
||||||
|
const stats = await fs.stat(resolvedPath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
addAllowedPath(resolvedPath);
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
path: resolvedPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not a valid absolute path, continue to search
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for directory in common locations
|
||||||
|
const searchPaths: string[] = [
|
||||||
|
process.cwd(), // Current working directory
|
||||||
|
process.env.HOME || process.env.USERPROFILE || "", // User home
|
||||||
|
path.join(process.env.HOME || process.env.USERPROFILE || "", "Documents"),
|
||||||
|
path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"),
|
||||||
|
// Common project locations
|
||||||
|
path.join(process.env.HOME || process.env.USERPROFILE || "", "Projects"),
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
// Also check parent of current working directory
|
||||||
|
try {
|
||||||
|
const parentDir = path.dirname(process.cwd());
|
||||||
|
if (!searchPaths.includes(parentDir)) {
|
||||||
|
searchPaths.push(parentDir);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for directory matching the name and file structure
|
||||||
|
for (const searchPath of searchPaths) {
|
||||||
|
try {
|
||||||
|
const candidatePath = path.join(searchPath, directoryName);
|
||||||
|
const stats = await fs.stat(candidatePath);
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
// Verify it matches by checking for sample files
|
||||||
|
if (sampleFiles && sampleFiles.length > 0) {
|
||||||
|
let matches = 0;
|
||||||
|
for (const sampleFile of sampleFiles.slice(0, 5)) {
|
||||||
|
// Remove directory name prefix from sample file path
|
||||||
|
const relativeFile = sampleFile.startsWith(directoryName + "/")
|
||||||
|
? sampleFile.substring(directoryName.length + 1)
|
||||||
|
: sampleFile.split("/").slice(1).join("/") || sampleFile.split("/").pop() || sampleFile;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePath = path.join(candidatePath, relativeFile);
|
||||||
|
await fs.access(filePath);
|
||||||
|
matches++;
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist, continue checking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If at least one file matches, consider it a match
|
||||||
|
if (matches === 0 && sampleFiles.length > 0) {
|
||||||
|
continue; // Try next candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found matching directory
|
||||||
|
addAllowedPath(candidatePath);
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
path: candidatePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Directory doesn't exist at this location, continue searching
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory not found
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: `Directory "${directoryName}" not found in common locations. Please ensure the directory exists.`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
res.status(500).json({ success: false, error: message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Save image to .automaker/images directory
|
// Save image to .automaker/images directory
|
||||||
router.post("/save-image", async (req: Request, res: Response) => {
|
router.post("/save-image", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user