@ -1,11 +1,47 @@
import { NextResponse } from "next/server" ;
import { NextResponse } from "next/server" ;
import { exec } from "child_process" ;
import { exec , execFile } from "child_process" ;
import { promisify } from "util" ;
import { promisify } from "util" ;
import { writeFile , unlink } from "fs/promises" ;
import { writeFile , unlink } from "fs/promises" ;
import { tmpdir } from "os" ;
import { homedir , tmpdir } from "os" ;
import { join } from "path" ;
import { join } from "path" ;
const execAsync = promisify ( exec ) ;
const execAsync = promisify ( exec ) ;
const execFileAsync = promisify ( execFile ) ;
async function commandExists ( command : string ) : Promise < boolean > {
try {
await execFileAsync ( "sh" , [ "-lc" , ` command -v ${ command } ` ] ) ;
return true ;
} catch {
return false ;
}
}
async function findPowerShellCommand ( ) : Promise < string | null > {
for ( const command of [ "powershell.exe" , "powershell" , "pwsh.exe" , "pwsh" ] ) {
try {
await execFileAsync ( command , [ "-NoProfile" , "-Command" , "$PSVersionTable.PSVersion.ToString()" ] ) ;
return command ;
} catch {
// Try the next PowerShell executable name.
}
}
return null ;
}
function getFallbackDirectory ( ) : string {
return process . env . POPIART_WORKFLOWS_DIR || join ( homedir ( ) , "Documents" ) ;
}
function fallbackDirectoryResponse ( reason : string ) {
return NextResponse . json ( {
success : true ,
cancelled : false ,
fallback : true ,
path : getFallbackDirectory ( ) ,
warning : reason ,
} ) ;
}
/ * *
/ * *
* Normalize a path returned by native directory pickers .
* Normalize a path returned by native directory pickers .
@ -43,11 +79,20 @@ export async function GET() {
if ( platform === "darwin" ) {
if ( platform === "darwin" ) {
// macOS: Use osascript to open folder picker
// macOS: Use osascript to open folder picker
if ( ! ( await commandExists ( "osascript" ) ) ) {
return fallbackDirectoryResponse ( "Native folder picker is unavailable in this environment." ) ;
}
const { stdout } = await execAsync (
const { stdout } = await execAsync (
` osascript -e 'set folderPath to POSIX path of (choose folder with prompt "Select a folder to save workflows")' -e 'return folderPath' `
` osascript -e 'set folderPath to POSIX path of (choose folder with prompt "Select a folder to save workflows")' -e 'return folderPath' `
) ;
) ;
selectedPath = stdout . trim ( ) ;
selectedPath = stdout . trim ( ) ;
} else if ( platform === "win32" ) {
} else if ( platform === "win32" ) {
const powershellCommand = await findPowerShellCommand ( ) ;
if ( ! powershellCommand ) {
return fallbackDirectoryResponse ( "Windows folder picker is unavailable in this environment." ) ;
}
// Windows: Use the modern IFileOpenDialog (Vista+) for a nice folder picker
// Windows: Use the modern IFileOpenDialog (Vista+) for a nice folder picker
// Write script to temp file to avoid escaping issues
// Write script to temp file to avoid escaping issues
const psScript = `
const psScript = `
@ -98,39 +143,39 @@ if ($result) { Write-Output $result }
const tempFile = join ( tmpdir ( ) , ` browse-folder- ${ Date . now ( ) } .ps1 ` ) ;
const tempFile = join ( tmpdir ( ) , ` browse-folder- ${ Date . now ( ) } .ps1 ` ) ;
try {
try {
await writeFile ( tempFile , psScript , "utf-8" ) ;
await writeFile ( tempFile , psScript , "utf-8" ) ;
const { stdout } = await execAsync (
const { stdout } = await execFileAsync (
` powershell -NoProfile -ExecutionPolicy Bypass -File " ${ tempFile } " ` ,
powershellCommand ,
[ "-NoProfile" , "-ExecutionPolicy" , "Bypass" , "-File" , tempFile ] ,
{ timeout : 120000 }
{ timeout : 120000 }
) ;
) ;
selectedPath = stdout . trim ( ) ;
selectedPath = stdout . trim ( ) ;
} catch ( error ) {
if (
error instanceof Error &&
( error . message . includes ( "User canceled" ) || error . message . includes ( "-128" ) )
) {
selectedPath = null ;
} else {
return fallbackDirectoryResponse ( "Windows folder picker failed in this environment." ) ;
}
} finally {
} finally {
// Clean up temp file
// Clean up temp file
try { await unlink ( tempFile ) ; } catch { /* ignore */ }
try { await unlink ( tempFile ) ; } catch { /* ignore */ }
}
}
} else if ( platform === "linux" ) {
} else if ( platform === "linux" ) {
// Linux: Try zenity (common on GNOME) or kdialog (KDE)
// Linux: Try zenity (common on GNOME) or kdialog (KDE)
try {
if ( await commandExists ( "zenity" ) ) {
const { stdout } = await execAsync (
const { stdout } = await execAsync (
` zenity --file-selection --directory --title="Select a folder to save workflows" 2>/dev/null `
` zenity --file-selection --directory --title="Select a folder to save workflows" 2>/dev/null `
) ;
) ;
selectedPath = stdout . trim ( ) ;
selectedPath = stdout . trim ( ) ;
} catch {
} else if ( await commandExists ( "kdialog" ) ) {
// Try kdialog as fallback
const { stdout } = await execAsync (
try {
` kdialog --getexistingdirectory ~ --title "Select a folder to save workflows" `
const { stdout } = await execAsync (
) ;
` kdialog --getexistingdirectory ~ --title "Select a folder to save workflows" `
selectedPath = stdout . trim ( ) ;
) ;
} else {
selectedPath = stdout . trim ( ) ;
return fallbackDirectoryResponse ( "No supported folder picker was found. Using the default documents folder." ) ;
} catch {
return NextResponse . json (
{
success : false ,
error :
"No supported dialog tool found. Please install zenity or kdialog." ,
} ,
{ status : 500 }
) ;
}
}
}
} else {
} else {
return NextResponse . json (
return NextResponse . json (
@ -169,13 +214,6 @@ if ($result) { Write-Output $result }
} ) ;
} ) ;
}
}
return NextResponse . json (
return fallbackDirectoryResponse ( "Native folder picker failed in this environment." ) ;
{
success : false ,
error :
error instanceof Error ? error . message : "Failed to open dialog" ,
} ,
{ status : 500 }
) ;
}
}
}
}