1- import { exec } from "node:child_process" ;
1+ import { exec , execFile } from "node:child_process" ;
22import fs from "node:fs/promises" ;
33import path from "node:path" ;
44import { promisify } from "node:util" ;
@@ -14,6 +14,44 @@ import type { DetectedApplication } from "./schemas";
1414import type { AppDefinition } from "./types" ;
1515
1616const execAsync = promisify ( exec ) ;
17+ const execFileAsync = promisify ( execFile ) ;
18+
19+ export interface OpenCommand {
20+ file : string ;
21+ args : string [ ] ;
22+ }
23+
24+ // argv form, never a shell string: targetPath and app.path come from an opened
25+ // repository, so a crafted filename would run under /bin/sh if concatenated
26+ // into a command (VERIA-355). Returns null for unsupported platforms.
27+ export function resolveOpenCommand (
28+ platform : NodeJS . Platform ,
29+ app : { id : string ; path : string } ,
30+ targetPath : string ,
31+ isFile : boolean ,
32+ ) : OpenCommand | null {
33+ if ( platform === "darwin" ) {
34+ if ( app . id === "finder" && isFile ) {
35+ return { file : "open" , args : [ "-R" , targetPath ] } ;
36+ }
37+ if ( app . id === "gitkraken" ) {
38+ // GitKraken ignores positional args; it needs `--args -p <path>`.
39+ return {
40+ file : "open" ,
41+ args : [ "-na" , app . path , "--args" , "-p" , targetPath ] ,
42+ } ;
43+ }
44+ return { file : "open" , args : [ "-a" , app . path , targetPath ] } ;
45+ }
46+ if ( platform === "win32" ) {
47+ if ( app . id === "explorer" && isFile ) {
48+ // Explorer needs `/select,<path>` as a single token.
49+ return { file : "explorer.exe" , args : [ `/select,${ targetPath } ` ] } ;
50+ }
51+ return { file : app . path , args : [ targetPath ] } ;
52+ }
53+ return null ;
54+ }
1755
1856const LOCALAPPDATA = process . env . LOCALAPPDATA ?? "" ;
1957const PROGRAMFILES = process . env . PROGRAMFILES ?? "C:\\Program Files" ;
@@ -620,27 +658,17 @@ export class ExternalAppsService {
620658 isFile = false ;
621659 }
622660
623- let command : string ;
624-
625- if ( process . platform === "darwin" ) {
626- if ( appToOpen . id === "finder" && isFile ) {
627- command = `open -R "${ targetPath } "` ;
628- } else if ( appToOpen . id === "gitkraken" ) {
629- // GitKraken ignores positional args; it needs `--args -p <path>`.
630- command = `open -na "${ appToOpen . path } " --args -p "${ targetPath } "` ;
631- } else {
632- command = `open -a "${ appToOpen . path } " "${ targetPath } "` ;
633- }
634- } else if ( process . platform === "win32" ) {
635- command =
636- appToOpen . id === "explorer" && isFile
637- ? `explorer.exe /select,"${ targetPath } "`
638- : `"${ appToOpen . path } " "${ targetPath } "` ;
639- } else {
661+ const command = resolveOpenCommand (
662+ process . platform ,
663+ appToOpen ,
664+ targetPath ,
665+ isFile ,
666+ ) ;
667+ if ( ! command ) {
640668 return { success : false , error : "Unsupported platform" } ;
641669 }
642670
643- await execAsync ( command ) ;
671+ await execFileAsync ( command . file , command . args ) ;
644672 return { success : true } ;
645673 } catch ( error ) {
646674 return {
0 commit comments