From c677c6907aaf1fdc76c5634581ceac1f60e5d3d3 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 15 Nov 2025 18:08:47 +0900 Subject: [PATCH 1/4] Add CLI support for running script and zip files Launcher now accepts a --file argument to run .js or .zip files directly from the command line. MainForm and Program.cs were refactored to support this, including new registry entries in setup.iss for file associations with WelsonJS Script extensions. --- .../WelsonJS.Launcher/MainForm.cs | 25 +- WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 215 ++++++++++++++++-- setup.iss | 12 + 3 files changed, 225 insertions(+), 27 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs index f55da4b..1ccdd06 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs @@ -20,6 +20,7 @@ namespace WelsonJS.Launcher private readonly string _dateTimeFormat; private readonly ICompatibleLogger _logger; + private string _filePath; private string _workingDirectory; private string _instanceId; private string _scriptName; @@ -122,6 +123,16 @@ namespace WelsonJS.Launcher private void btnRunFromZipFile_Click(object sender, EventArgs e) { + if (!String.IsNullOrEmpty(_filePath)) + { + string fileExtension = Path.GetExtension(_filePath); + if (fileExtension != null && fileExtension.Equals(".zip", StringComparison.OrdinalIgnoreCase)) + { + DisableUI(); + Task.Run(() => RunAppPackageFile()); + } + } + using (var openFileDialog = new OpenFileDialog()) { openFileDialog.Filter = "zip files (*.zip)|*.zip|All files (*.*)|*.*"; @@ -130,15 +141,15 @@ namespace WelsonJS.Launcher if (openFileDialog.ShowDialog() == DialogResult.OK) { - string filePath = openFileDialog.FileName; + _filePath = openFileDialog.FileName; DisableUI(); - Task.Run(() => RunAppPackageFile(filePath)); + Task.Run(() => RunAppPackageFile()); } } } - private void RunAppPackageFile(string filePath) + private void RunAppPackageFile() { _instanceId = Guid.NewGuid().ToString(); _workingDirectory = Program.GetWorkingDirectory(_instanceId); @@ -153,7 +164,7 @@ namespace WelsonJS.Launcher } // try to extract ZIP file - ZipFile.ExtractToDirectory(filePath, _workingDirectory); + ZipFile.ExtractToDirectory(_filePath, _workingDirectory); // record the first deploy time RecordFirstDeployTime(_workingDirectory, _instanceId); @@ -339,5 +350,11 @@ namespace WelsonJS.Launcher { Program.OpenWebBrowser(Program.GetAppConfig("RepositoryUrl")); } + + public void RunFromZipFile(string filePath) + { + _filePath = filePath; + btnRunFromZipFile.PerformClick(); + } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 616e1a5..fb6a04b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -41,8 +41,22 @@ namespace WelsonJS.Launcher } [STAThread] - static void Main() + static void Main(string[] args) { + // if set the target file path + string targetFilePath = GetTargetFilePath(args); + if (!string.IsNullOrEmpty(targetFilePath)) + { + try { + HandleTargetFilePath(targetFilePath); + } + catch (Exception e) + { + _logger.Error($"Initialization failed: {e}"); + } + return; + } + // create the mutex _mutex = new Mutex(true, "WelsonJS.Launcher", out bool createdNew); if (!createdNew) @@ -56,14 +70,165 @@ namespace WelsonJS.Launcher Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm(_logger)); - // destory the mutex - try { + // release the mutex + try + { _mutex.ReleaseMutex(); - } catch { /* ignore if not owned */ } + } + catch { /* ignore if not owned */ } _mutex.Dispose(); } - public static void RunCommandPrompt(string workingDirectory, string entryFileName, string scriptName, bool isConsoleApplication = false, bool isInteractiveServiceAapplication = false) + private static string GetTargetFilePath(string[] args) + { + if (args == null || args.Length == 0) return null; + + for (int i = 0; i < args.Length; i++) + { + string token = args[i]; + if (string.Equals(token, "--file", StringComparison.OrdinalIgnoreCase) || + string.Equals(token, "/file", StringComparison.OrdinalIgnoreCase)) + { + if (i + 1 < args.Length) + { + return args[i + 1]; + } + } + + if (token.StartsWith("--file=", StringComparison.OrdinalIgnoreCase)) + { + return token.Substring("--file=".Length).Trim('"'); + } + } + + return null; + } + + private static void HandleTargetFilePath(string filePath) + { + string fileExtension = Path.GetExtension(filePath); + + if (String.IsNullOrEmpty(fileExtension)) + { + throw new ArgumentException("The file extension is null or empty"); + } + + if (fileExtension.Equals(".zip", StringComparison.OrdinalIgnoreCase)) + { + var mainForm = new MainForm(_logger); + mainForm.Show(); + mainForm.RunFromZipFile(filePath); + return; + } + + if (fileExtension.Equals(".js", StringComparison.OrdinalIgnoreCase)) + { + string workingDirectory = CreateInstanceDirectory(); + + string appRoot = GetAppRootDirectory(); + string appBaseSource = Path.Combine(appRoot, "app.js"); + if (!File.Exists(appBaseSource)) + { + throw new FileNotFoundException("app.js not found in application root.", appBaseSource); + } + + string appBaseDestination = Path.Combine(workingDirectory, "app.js"); + File.Copy(appBaseSource, appBaseDestination, overwrite: true); + + string assetsSource = Path.Combine(appRoot, "app", "assets", "js"); + string assetsDestination = Path.Combine(workingDirectory, "app", "assets", "js"); + CopyDirectoryRecursive(assetsSource, assetsDestination); + + string entrypointDestination = Path.Combine(workingDirectory, "bootstrap.js"); + File.Copy(filePath, entrypointDestination, overwrite: true); + + RunCommandPrompt( + workingDirectory: workingDirectory, + entryFileName: "app.js", + scriptName: "bootstrap", + isConsoleApplication: true, + isInteractiveServiceApplication: false + ); + return; + } + + throw new NotSupportedException($"Unsupported file type: {fileExtension}"); + } + + private static string GetAppRootDirectory() + { + string[] candidates = new[] + { + GetAppConfig("AppRootDirectory"), + AppDomain.CurrentDomain.BaseDirectory, + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "WelsonJS"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WelsonJS"), + }; + + foreach (string dir in candidates) + { + if (string.IsNullOrEmpty(dir)) + continue; + + string appJs = Path.Combine(dir, "app.js"); + if (File.Exists(appJs)) + return dir; + } + + throw new FileNotFoundException("Could not locate app.js in any known application root directory."); + } + + private static string CreateInstanceDirectory() + { + string instanceId = Guid.NewGuid().ToString(); + string workingDirectory = GetWorkingDirectory(instanceId); + + try + { + // check if the working directory exists + if (Directory.Exists(workingDirectory)) + { + throw new InvalidOperationException("GUID validation failed. Directory already exists."); + } + + Directory.CreateDirectory(workingDirectory); + } + catch + { + throw new Exception("Instance Initialization failed"); + } + + return workingDirectory; + } + + private static void CopyDirectoryRecursive(string sourceDir, string destDir) + { + if (!Directory.Exists(sourceDir)) + { + throw new DirectoryNotFoundException("Source directory not found: " + sourceDir); + } + + Directory.CreateDirectory(destDir); + + foreach (var file in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories)) + { + string relativePath = file.Substring(sourceDir.Length).TrimStart( + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar + ); + + string targetPath = Path.Combine(destDir, relativePath); + string targetDir = Path.GetDirectoryName(targetPath); + if (!Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + File.Copy(file, targetPath, overwrite: true); + } + } + + public static void RunCommandPrompt(string workingDirectory, string entryFileName, string scriptName, bool isConsoleApplication = false, bool isInteractiveServiceApplication = false) { if (!isConsoleApplication) { @@ -93,33 +258,37 @@ namespace WelsonJS.Launcher }; process.Start(); - process.StandardInput.WriteLine("pushd " + workingDirectory); - process.StandardInput.WriteLine(); - process.StandardInput.Flush(); - process.StandardOutput.ReadLine(); + StreamWriter input = process.StandardInput; + StreamReader output = process.StandardOutput; - if (isInteractiveServiceAapplication) + input.WriteLine("pushd " + workingDirectory); + input.WriteLine(); + input.Flush(); + output.ReadLine(); + + if (isInteractiveServiceApplication) { - process.StandardInput.WriteLine($"start cmd /c startInteractiveService.bat"); - process.StandardInput.WriteLine(); - process.StandardInput.Flush(); - process.StandardOutput.ReadLine(); + input.WriteLine($"start cmd /c startInteractiveService.bat"); + input.WriteLine(); + input.Flush(); + output.ReadLine(); } else if (!isConsoleApplication) { - process.StandardInput.WriteLine(entryFileName); - process.StandardInput.WriteLine(); - process.StandardInput.Flush(); - process.StandardOutput.ReadLine(); + input.WriteLine(entryFileName); + input.WriteLine(); + input.Flush(); + output.ReadLine(); } else { - process.StandardInput.WriteLine($"start cmd /c cscript app.js {scriptName}"); - process.StandardInput.WriteLine(); - process.StandardInput.Flush(); - process.StandardOutput.ReadLine(); + input.WriteLine($"start cmd /c cscript app.js {scriptName}"); + input.WriteLine(); + input.Flush(); + output.ReadLine(); } - process.StandardInput.Close(); + input.Close(); + process.WaitForExit(); } diff --git a/setup.iss b/setup.iss index 3469c46..205ea96 100644 --- a/setup.iss +++ b/setup.iss @@ -25,12 +25,24 @@ DisableWelcomePage=yes DisableDirPage=yes DisableProgramGroupPage=yes LicenseFile=SECURITY.MD +ChangesAssociations=yes ; [Registry] ; Root: HKCR; Subkey: "welsonjs"; ValueType: "string"; ValueData: "URL:{cm:AppName}"; Flags: uninsdeletekey ; Root: HKCR; Subkey: "welsonjs"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "" ; Root: HKCR; Subkey: "welsonjs\DefaultIcon"; ValueType: "string"; ValueData: "{app}\app\favicon.ico,0" ; Root: HKCR; Subkey: "welsonjs\shell\open\command"; ValueType: "string"; ValueData: "cscript ""{app}\app.js"" uriloader ""%1""" +Root: HKCR; Subkey: "WelsonJS.Script"; ValueType: string; ValueData: "WelsonJS Script"; Flags: uninsdeletekey +Root: HKCR; Subkey: "WelsonJS.Script\DefaultIcon"; ValueType: string; ValueData: "{app}\app\favicon.ico,0"; Flags: uninsdeletekey +Root: HKCR; Subkey: "WelsonJS.Script\shell"; ValueType: string; ValueData: "open"; Flags: uninsdeletevalue +Root: HKCR; Subkey: "WelsonJS.Script\shell\open"; ValueType: string; ValueData: "Run with WelsonJS"; Flags: uninsdeletevalue +Root: HKCR; Subkey: "WelsonJS.Script\shell\open\command"; ValueType: string; ValueData: """{app}\bin\x86\WelsonJS.Launcher.exe"" --file ""%1"""; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".js"; ValueType: string; ValueData: "WelsonJS.Script"; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".ts"; ValueType: string; ValueData: "WelsonJS.Script"; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".re"; ValueType: string; ValueData: "WelsonJS.Script"; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".res"; ValueType: string; ValueData: "WelsonJS.Script"; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".ls"; ValueType: string; ValueData: "WelsonJS.Script"; Flags: uninsdeletevalue +Root: HKCR; Subkey: ".coffee"; ValueType: string; ValueData: "WelsonJS.Script"; Flags: uninsdeletevalue [Files] Source: "app.js"; DestDir: "{app}"; From a03ea7f3b3bfae5a2b9d31c79912cdf8cc3e228e Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 15 Nov 2025 18:23:02 +0900 Subject: [PATCH 2/4] Update WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index fb6a04b..99de06f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -193,9 +193,18 @@ namespace WelsonJS.Launcher Directory.CreateDirectory(workingDirectory); } - catch + catch (IOException ex) { - throw new Exception("Instance Initialization failed"); + throw new Exception("Instance Initialization failed due to an IO error.", ex); + } + catch (UnauthorizedAccessException ex) + { + throw new Exception("Instance Initialization failed due to insufficient permissions.", ex); + } + catch (InvalidOperationException) + { + // Let InvalidOperationException bubble up as it is thrown intentionally above + throw; } return workingDirectory; From c392b6a08ad68997b2bc2df926bb1baa9be25f87 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 15 Nov 2025 20:39:11 +0900 Subject: [PATCH 3/4] Update WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs index 1ccdd06..2f8d849 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs @@ -130,6 +130,7 @@ namespace WelsonJS.Launcher { DisableUI(); Task.Run(() => RunAppPackageFile()); + return; } } From daea458f64c2f46c40a07a43ab22fe29246299a7 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 16 Nov 2025 18:58:48 +0900 Subject: [PATCH 4/4] Update WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 99de06f..d143460 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -207,25 +207,21 @@ namespace WelsonJS.Launcher throw; } - return workingDirectory; - } - private static void CopyDirectoryRecursive(string sourceDir, string destDir) - { - if (!Directory.Exists(sourceDir)) - { - throw new DirectoryNotFoundException("Source directory not found: " + sourceDir); - } + var sourceDirInfo = new DirectoryInfo(sourceDir); - Directory.CreateDirectory(destDir); - - foreach (var file in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories)) - { - string relativePath = file.Substring(sourceDir.Length).TrimStart( - Path.DirectorySeparatorChar, - Path.AltDirectorySeparatorChar - ); + // Create all subdirectories + foreach (DirectoryInfo dir in sourceDirInfo.GetDirectories("*", SearchOption.AllDirectories)) + { + Directory.CreateDirectory(Path.Combine(destDir, dir.FullName.Substring(sourceDirInfo.FullName.Length + 1))); + } + // Copy all files + foreach (FileInfo file in sourceDirInfo.GetFiles("*", SearchOption.AllDirectories)) + { + string targetPath = Path.Combine(destDir, file.FullName.Substring(sourceDirInfo.FullName.Length + 1)); + file.CopyTo(targetPath, true); + } string targetPath = Path.Combine(destDir, relativePath); string targetDir = Path.GetDirectoryName(targetPath); if (!Directory.Exists(targetDir))