commit cf0a5e515d76a67e96613d43afba0514fa2908f3 Author: aseimel Date: Mon Jun 29 19:36:31 2026 +0200 Initial GESIS OpenCode installer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..156e12a --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Local SSH key used only by Codex for pushing this repository. +.ssh/ + +# Build/download leftovers. +*.download +*.tmp +bin/ +obj/ + +# Local editor and OS metadata. +.vs/ +.vscode/ +Thumbs.db +Desktop.ini diff --git a/Build-GuiInstaller.ps1 b/Build-GuiInstaller.ps1 new file mode 100644 index 0000000..ef2bbb0 --- /dev/null +++ b/Build-GuiInstaller.ps1 @@ -0,0 +1,84 @@ +#Requires -Version 5.1 + +$ErrorActionPreference = "Stop" + +$sourcePath = Join-Path $PSScriptRoot "GesisOpenCodeInstaller.cs" +$outputPath = Join-Path $PSScriptRoot "GESIS-OpenCode-Installer.exe" +$logoPath = Join-Path $PSScriptRoot "GESIS Logo Blau EN.jpg" +$signetPath = Join-Path $PSScriptRoot "GESIS Signet Blau_transp.png" +$iconPath = Join-Path $PSScriptRoot "GESIS-OpenCode-Installer.ico" +$manifestPath = Join-Path $PSScriptRoot "app.manifest" +$cscPath = Join-Path $env:WINDIR "Microsoft.NET\Framework64\v4.0.30319\csc.exe" + +if (-not (Test-Path -LiteralPath $cscPath)) { + throw "Could not find the .NET Framework C# compiler at $cscPath" +} + +if (-not (Test-Path -LiteralPath $logoPath)) { + throw "Could not find logo file: $logoPath" +} + +if (-not (Test-Path -LiteralPath $signetPath)) { + throw "Could not find signet file: $signetPath" +} + +if (-not (Test-Path -LiteralPath $manifestPath)) { + throw "Could not find application manifest: $manifestPath" +} + +Add-Type -AssemblyName System.Drawing +$pngBytes = [System.IO.File]::ReadAllBytes($signetPath) +$image = [System.Drawing.Image]::FromFile($signetPath) +try { + $width = [Math]::Min($image.Width, 255) + $height = [Math]::Min($image.Height, 255) +} +finally { + $image.Dispose() +} + +$stream = [System.IO.File]::Open($iconPath, [System.IO.FileMode]::Create) +try { + $writer = New-Object System.IO.BinaryWriter($stream) + $writer.Write([UInt16]0) + $writer.Write([UInt16]1) + $writer.Write([UInt16]1) + $writer.Write([Byte]$width) + $writer.Write([Byte]$height) + $writer.Write([Byte]0) + $writer.Write([Byte]0) + $writer.Write([UInt16]1) + $writer.Write([UInt16]32) + $writer.Write([UInt32]$pngBytes.Length) + $writer.Write([UInt32]22) + $writer.Write($pngBytes) + $writer.Flush() +} +finally { + $stream.Dispose() +} + +$compilerArgs = @( + "/target:winexe", + "/platform:x64", + "/optimize+", + "/win32icon:$iconPath", + "/win32manifest:$manifestPath", + "/out:$outputPath", + "/resource:$logoPath,GesisLogo", + "/resource:$iconPath,GesisIcon", + "/reference:System.dll", + "/reference:System.Core.dll", + "/reference:System.Drawing.dll", + "/reference:System.Web.Extensions.dll", + "/reference:System.Windows.Forms.dll", + $sourcePath +) + +& $cscPath $compilerArgs + +if ($LASTEXITCODE -ne 0) { + throw "Compilation failed with exit code $LASTEXITCODE" +} + +Write-Host "Built $outputPath" diff --git a/GESIS Logo Blau EN.jpg b/GESIS Logo Blau EN.jpg new file mode 100644 index 0000000..f96d3f0 Binary files /dev/null and b/GESIS Logo Blau EN.jpg differ diff --git a/GESIS Signet Blau_transp.png b/GESIS Signet Blau_transp.png new file mode 100644 index 0000000..b37c1a5 Binary files /dev/null and b/GESIS Signet Blau_transp.png differ diff --git a/GESIS-OpenCode-Installer.exe b/GESIS-OpenCode-Installer.exe new file mode 100644 index 0000000..2853eed Binary files /dev/null and b/GESIS-OpenCode-Installer.exe differ diff --git a/GESIS-OpenCode-Installer.ico b/GESIS-OpenCode-Installer.ico new file mode 100644 index 0000000..f2f1652 Binary files /dev/null and b/GESIS-OpenCode-Installer.ico differ diff --git a/GESIS-OpenCode-Installer.zip b/GESIS-OpenCode-Installer.zip new file mode 100644 index 0000000..0dc357f Binary files /dev/null and b/GESIS-OpenCode-Installer.zip differ diff --git a/GesisOpenCodeInstaller.cs b/GesisOpenCodeInstaller.cs new file mode 100644 index 0000000..83afcfe --- /dev/null +++ b/GesisOpenCodeInstaller.cs @@ -0,0 +1,1174 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Web.Script.Serialization; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace GesisOpenCodeInstaller +{ + internal static class Program + { + [STAThread] + private static void Main() + { + TryEnableDpiAwareness(); + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new InstallerForm()); + } + + [DllImport("user32.dll")] + private static extern bool SetProcessDPIAware(); + + private static void TryEnableDpiAwareness() + { + try + { + SetProcessDPIAware(); + } + catch + { + } + } + } + + internal sealed class InstallerForm : Form + { + private const string Repo = "anomalyco/opencode"; + private const string ReleaseApiUrl = "https://api.github.com/repos/" + Repo + "/releases/latest"; + private const string WindowsAssetName = "opencode-desktop-win-x64.exe"; + private const string BaseUrl = "https://ai-openwebui.gesis.org/api"; + private const int DesktopInstallerWaitMilliseconds = 15000; + private const int DownloadTimeoutMilliseconds = 60000; + + private readonly TextBox apiKeyBox; + private readonly TextBox modelFilterBox; + private readonly TableLayoutPanel modelGridPanel; + private readonly Label modelIntroLabel; + private readonly Button checkApiKeyButton; + private readonly Button backButton; + private readonly Button installButton; + private readonly Button selectAllButton; + private readonly Button selectNoneButton; + private readonly ProgressBar progressBar; + private readonly Label statusLabel; + private readonly CheckBox skipDesktopCheckBox; + private readonly List availableModels = new List(); + + public InstallerForm() + { + Text = "GESIS OpenCode Installer"; + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.Sizable; + MaximizeBox = true; + MinimizeBox = true; + ClientSize = new Size(760, 560); + MinimumSize = new Size(620, 500); + Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point); + BackColor = Color.White; + AutoScaleMode = AutoScaleMode.Dpi; + AutoScroll = false; + + var logoBox = new PictureBox + { + Margin = new Padding(0, 0, 0, 18), + Size = new Size(330, 92), + SizeMode = PictureBoxSizeMode.Zoom, + Image = LoadEmbeddedImage("GesisLogo") + }; + + var title = new Label + { + Text = "Install OpenCode Desktop for GESIS", + Font = new Font(Font.FontFamily, 15F, FontStyle.Bold), + AutoSize = true, + MaximumSize = new Size(680, 0), + Margin = new Padding(0, 0, 0, 8) + }; + + var description = new Label + { + Text = "This installer checks your personal API key, lets you choose the GESIS AI Server models to show in OpenCode, and downloads the latest OpenCode Desktop for Windows.", + AutoSize = true, + MaximumSize = new Size(680, 0), + Margin = new Padding(0, 0, 0, 22) + }; + + var apiKeyLabel = new Label + { + Text = "Personal GESIS API key", + AutoSize = true, + Margin = new Padding(0, 0, 0, 6) + }; + + apiKeyBox = new TextBox + { + Dock = DockStyle.Fill, + Margin = new Padding(0, 0, 0, 14), + MinimumSize = new Size(420, 30), + UseSystemPasswordChar = true + }; + + checkApiKeyButton = new Button + { + Text = "Check API key", + AutoSize = true, + MinimumSize = new Size(150, 40), + Padding = new Padding(12, 4, 12, 4), + Margin = new Padding(0, 0, 0, 18) + }; + checkApiKeyButton.Click += CheckApiKeyButton_Click; + + modelIntroLabel = new Label + { + Text = "These are all models currently available to your API key on the GESIS AI Server. Select the models you want to use in OpenCode.", + AutoSize = true, + MaximumSize = new Size(680, 0), + Margin = new Padding(0, 0, 0, 10), + Visible = false + }; + + modelFilterBox = new TextBox + { + Dock = DockStyle.Fill, + Margin = new Padding(0, 0, 0, 10), + MinimumSize = new Size(420, 30), + Visible = false + }; + modelFilterBox.TextChanged += ModelFilterBox_TextChanged; + + modelGridPanel = new TableLayoutPanel + { + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + BackColor = Color.White, + ColumnCount = 1, + Dock = DockStyle.Fill, + Margin = new Padding(0, 0, 0, 10), + Visible = false + }; + + selectAllButton = new Button + { + Text = "Select all", + AutoSize = true, + MinimumSize = new Size(110, 36), + Padding = new Padding(10, 3, 10, 3), + Visible = false + }; + selectAllButton.Click += SelectAllButton_Click; + + selectNoneButton = new Button + { + Text = "Select none", + AutoSize = true, + MinimumSize = new Size(110, 36), + Padding = new Padding(10, 3, 10, 3), + Visible = false + }; + selectNoneButton.Click += SelectNoneButton_Click; + + skipDesktopCheckBox = new CheckBox + { + Text = "Only update the OpenCode configuration", + AutoSize = true, + Margin = new Padding(0, 0, 0, 18), + Visible = false + }; + + installButton = new Button + { + Text = "Install selected models", + AutoSize = true, + MinimumSize = new Size(190, 40), + Padding = new Padding(12, 4, 12, 4), + Anchor = AnchorStyles.Right, + Margin = new Padding(8, 0, 0, 18), + Visible = false + }; + installButton.Click += InstallButton_Click; + + backButton = new Button + { + Text = "Back", + AutoSize = true, + MinimumSize = new Size(100, 40), + Padding = new Padding(12, 4, 12, 4), + Margin = new Padding(8, 0, 0, 18), + Visible = false + }; + backButton.Click += BackButton_Click; + + progressBar = new ProgressBar + { + Dock = DockStyle.Fill, + MinimumSize = new Size(420, 18), + Margin = new Padding(0, 0, 0, 12), + Style = ProgressBarStyle.Continuous + }; + + statusLabel = new Label + { + Text = "Ready.", + AutoSize = true, + MaximumSize = new Size(680, 0), + Margin = new Padding(0) + }; + + var icon = LoadEmbeddedIcon("GesisIcon"); + if (icon != null) + { + Icon = icon; + } + + var selectionButtonPanel = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + FlowDirection = FlowDirection.LeftToRight, + WrapContents = true, + Margin = new Padding(0, 0, 0, 12) + }; + selectionButtonPanel.Controls.Add(selectAllButton); + selectionButtonPanel.Controls.Add(selectNoneButton); + + var primaryButtonPanel = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + FlowDirection = FlowDirection.RightToLeft, + WrapContents = true + }; + primaryButtonPanel.Controls.Add(installButton); + primaryButtonPanel.Controls.Add(backButton); + primaryButtonPanel.Controls.Add(checkApiKeyButton); + + var footerLayout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + BackColor = Color.White, + ColumnCount = 1, + RowCount = 4, + Padding = new Padding(32, 12, 32, 16) + }; + footerLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + footerLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + footerLayout.Controls.Add(skipDesktopCheckBox, 0, 0); + footerLayout.Controls.Add(primaryButtonPanel, 0, 1); + footerLayout.Controls.Add(progressBar, 0, 2); + footerLayout.Controls.Add(statusLabel, 0, 3); + + var layout = new TableLayoutPanel + { + Dock = DockStyle.Top, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + BackColor = Color.White, + ColumnCount = 1, + Padding = new Padding(32), + RowCount = 10 + }; + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + for (var i = 0; i < layout.RowCount; i++) + { + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + } + + layout.Controls.Add(logoBox, 0, 0); + layout.Controls.Add(title, 0, 1); + layout.Controls.Add(description, 0, 2); + layout.Controls.Add(apiKeyLabel, 0, 3); + layout.Controls.Add(apiKeyBox, 0, 4); + layout.Controls.Add(modelIntroLabel, 0, 5); + layout.Controls.Add(modelFilterBox, 0, 6); + layout.Controls.Add(selectionButtonPanel, 0, 7); + layout.Controls.Add(modelGridPanel, 0, 8); + + var contentPanel = new Panel + { + Dock = DockStyle.Fill, + AutoScroll = true, + BackColor = Color.White + }; + contentPanel.Controls.Add(layout); + + var rootLayout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + BackColor = Color.White, + ColumnCount = 1, + RowCount = 2 + }; + rootLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + rootLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + rootLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + rootLayout.Controls.Add(contentPanel, 0, 0); + rootLayout.Controls.Add(footerLayout, 0, 1); + + Controls.Add(rootLayout); + + AcceptButton = checkApiKeyButton; + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + if (WindowState == FormWindowState.Minimized) + { + WindowState = FormWindowState.Normal; + } + + Show(); + Activate(); + BringToFront(); + TopMost = true; + TopMost = false; + } + + private static Image LoadEmbeddedImage(string resourceName) + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); + if (stream == null) + { + return null; + } + + using (stream) + { + return Image.FromStream(stream); + } + } + + private static Icon LoadEmbeddedIcon(string resourceName) + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); + if (stream == null) + { + return null; + } + + return new Icon(stream); + } + + private async void CheckApiKeyButton_Click(object sender, EventArgs e) + { + var apiKey = apiKeyBox.Text.Trim(); + if (apiKey.Length == 0) + { + MessageBox.Show(this, "Please enter your personal GESIS API key.", "API key required", MessageBoxButtons.OK, MessageBoxIcon.Warning); + apiKeyBox.Focus(); + return; + } + + SetBusy(true); + + try + { + var models = await Task.Run(new Func>(() => ProbeAvailableModels(apiKey))); + if (models.Count == 0) + { + throw new InvalidOperationException("No models are available for this API key."); + } + + availableModels.Clear(); + availableModels.AddRange(models); + foreach (var model in availableModels) + { + model.IsSelected = true; + } + + WindowState = FormWindowState.Maximized; + ShowModelSelection(); + RefreshModelList(); + ResizeForModelGrid(); + progressBar.Value = 100; + statusLabel.Text = "Found " + availableModels.Count + " available model(s). Choose which models should appear in OpenCode."; + } + catch (Exception ex) + { + statusLabel.Text = "API key check failed."; + MessageBox.Show(this, ex.Message, "API key check failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetBusy(false); + } + } + + private async void InstallButton_Click(object sender, EventArgs e) + { + var apiKey = apiKeyBox.Text.Trim(); + var selectedModels = GetSelectedModels(); + + if (selectedModels.Count == 0) + { + MessageBox.Show(this, "Please select at least one model to use in OpenCode.", "Model selection required", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + SetBusy(true); + + try + { + await Task.Run(new Action(() => WriteOpenCodeConfig(apiKey, selectedModels))); + + if (!skipDesktopCheckBox.Checked) + { + await Task.Run(new Action(InstallOpenCodeDesktop)); + } + + progressBar.Value = 100; + statusLabel.Text = "Installation complete."; + MessageBox.Show(this, "OpenCode is configured for the GESIS AI Server. If the OpenCode Desktop installer is still open, please follow its prompts to finish.", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + statusLabel.Text = "Installation failed."; + MessageBox.Show(this, ex.Message, "Installation failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetBusy(false); + } + } + + private void BackButton_Click(object sender, EventArgs e) + { + ShowApiKeyScreen(); + } + + private void ModelFilterBox_TextChanged(object sender, EventArgs e) + { + SyncVisibleModelSelection(); + RefreshModelList(); + } + + private void SelectAllButton_Click(object sender, EventArgs e) + { + SetVisibleModelSelection(true); + } + + private void SelectNoneButton_Click(object sender, EventArgs e) + { + SetVisibleModelSelection(false); + } + + private void ShowModelSelection() + { + checkApiKeyButton.Visible = false; + apiKeyBox.Enabled = false; + modelIntroLabel.Visible = true; + modelFilterBox.Visible = true; + modelGridPanel.Visible = true; + selectAllButton.Visible = true; + selectNoneButton.Visible = true; + skipDesktopCheckBox.Visible = true; + installButton.Visible = true; + backButton.Visible = true; + AcceptButton = installButton; + } + + private void ShowApiKeyScreen() + { + WindowState = FormWindowState.Normal; + checkApiKeyButton.Visible = true; + apiKeyBox.Enabled = true; + modelIntroLabel.Visible = false; + modelFilterBox.Visible = false; + modelGridPanel.Visible = false; + selectAllButton.Visible = false; + selectNoneButton.Visible = false; + skipDesktopCheckBox.Visible = false; + installButton.Visible = false; + backButton.Visible = false; + progressBar.Value = 0; + statusLabel.Text = "Ready."; + AcceptButton = checkApiKeyButton; + } + + private void InstallOpenCodeDesktop() + { + if (!Environment.Is64BitOperatingSystem) + { + throw new InvalidOperationException("This installer requires 64-bit Windows."); + } + + SetStatus("Finding the latest OpenCode Desktop release...", 10); + var releaseJson = DownloadString(ReleaseApiUrl); + var serializer = new JavaScriptSerializer(); + var release = (Dictionary)serializer.DeserializeObject(releaseJson); + var tagName = release.ContainsKey("tag_name") ? Convert.ToString(release["tag_name"]) : "latest"; + var assets = release["assets"] as object[]; + Dictionary selectedAsset = null; + + if (assets != null) + { + foreach (var item in assets) + { + var asset = item as Dictionary; + if (asset == null || !asset.ContainsKey("name")) + { + continue; + } + + if (String.Equals(Convert.ToString(asset["name"]), WindowsAssetName, StringComparison.OrdinalIgnoreCase)) + { + selectedAsset = asset; + break; + } + } + } + + if (selectedAsset == null || !selectedAsset.ContainsKey("browser_download_url")) + { + throw new InvalidOperationException("Could not find the Windows desktop installer in the latest OpenCode release."); + } + + var downloadUrl = Convert.ToString(selectedAsset["browser_download_url"]); + var downloadPath = Path.Combine(Path.GetTempPath(), WindowsAssetName); + var expectedSize = selectedAsset.ContainsKey("size") ? Convert.ToInt64(selectedAsset["size"]) : -1; + + if (expectedSize > 0 && File.Exists(downloadPath) && new FileInfo(downloadPath).Length == expectedSize) + { + SetStatus("Using already downloaded OpenCode Desktop " + tagName + ".", 70); + } + else + { + SetStatus("Downloading OpenCode Desktop " + tagName + "...", 30); + DownloadFileWithProgress(downloadUrl, downloadPath, expectedSize, "OpenCode Desktop " + tagName, 30, 70); + } + + SetStatus("Starting the OpenCode Desktop installer...", 75); + var process = Process.Start(new ProcessStartInfo + { + FileName = downloadPath, + UseShellExecute = true + }); + + if (process == null) + { + throw new InvalidOperationException("Could not start the OpenCode Desktop installer."); + } + + SetStatus("Waiting briefly for the OpenCode Desktop installer...", 90); + if (process.WaitForExit(DesktopInstallerWaitMilliseconds)) + { + if (process.ExitCode != 0) + { + throw new InvalidOperationException("The OpenCode Desktop installer ended with exit code " + process.ExitCode + "."); + } + + return; + } + + SetStatus("OpenCode Desktop installer is still running. Continuing with GESIS setup.", 95); + } + + private void DownloadFileWithProgress(string url, string destinationPath, long expectedSize, string label, int startProgress, int endProgress) + { + var partialPath = destinationPath + ".download"; + if (File.Exists(partialPath)) + { + File.Delete(partialPath); + } + + var request = (HttpWebRequest)WebRequest.Create(url); + request.Method = "GET"; + request.Timeout = DownloadTimeoutMilliseconds; + request.ReadWriteTimeout = DownloadTimeoutMilliseconds; + request.UserAgent = "GESIS-OpenCode-Installer"; + request.Accept = "application/octet-stream"; + + using (var response = (HttpWebResponse)request.GetResponse()) + { + if (response.StatusCode != HttpStatusCode.OK) + { + throw new InvalidOperationException("Could not download OpenCode Desktop. The server returned " + response.StatusCode + "."); + } + + var totalBytes = response.ContentLength > 0 ? response.ContentLength : expectedSize; + var downloadedBytes = 0L; + var buffer = new byte[1024 * 128]; + var lastUpdate = DateTime.MinValue; + + using (var input = response.GetResponseStream()) + using (var output = File.Create(partialPath)) + { + if (input == null) + { + throw new InvalidOperationException("Could not download OpenCode Desktop. The server returned no file content."); + } + + while (true) + { + var read = input.Read(buffer, 0, buffer.Length); + if (read <= 0) + { + break; + } + + output.Write(buffer, 0, read); + downloadedBytes += read; + + if ((DateTime.Now - lastUpdate).TotalMilliseconds >= 250) + { + lastUpdate = DateTime.Now; + UpdateDownloadStatus(label, downloadedBytes, totalBytes, startProgress, endProgress); + } + } + } + + if (expectedSize > 0 && downloadedBytes != expectedSize) + { + throw new InvalidOperationException("The OpenCode Desktop download was incomplete. Please check your internet connection and try again."); + } + + if (File.Exists(destinationPath)) + { + File.Delete(destinationPath); + } + + File.Move(partialPath, destinationPath); + UpdateDownloadStatus(label, downloadedBytes, totalBytes, startProgress, endProgress); + } + } + + private void UpdateDownloadStatus(string label, long downloadedBytes, long totalBytes, int startProgress, int endProgress) + { + var downloadedMb = downloadedBytes / 1024.0 / 1024.0; + + if (totalBytes > 0) + { + var totalMb = totalBytes / 1024.0 / 1024.0; + var ratio = Math.Max(0, Math.Min(1, downloadedBytes / (double)totalBytes)); + var progress = startProgress + (int)((endProgress - startProgress) * ratio); + SetStatus("Downloading " + label + "... " + downloadedMb.ToString("0.0") + " MB of " + totalMb.ToString("0.0") + " MB", progress); + return; + } + + SetStatus("Downloading " + label + "... " + downloadedMb.ToString("0.0") + " MB", startProgress); + } + + private void WriteOpenCodeConfig(string apiKey, List selectedModels) + { + SetStatus("Writing OpenCode configuration...", 85); + + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var configDirectory = Path.Combine(userProfile, ".config", "opencode"); + var configPath = Path.Combine(configDirectory, "opencode.json"); + + Directory.CreateDirectory(configDirectory); + + if (File.Exists(configPath)) + { + var backupPath = configPath + ".backup-" + DateTime.Now.ToString("yyyyMMdd-HHmmss"); + File.Copy(configPath, backupPath, true); + } + + File.WriteAllText(configPath, BuildConfigJson(apiKey, selectedModels), new UTF8Encoding(false)); + } + + private static string BuildConfigJson(string apiKey, List selectedModels) + { + var modelsJson = new StringBuilder(); + for (var i = 0; i < selectedModels.Count; i++) + { + var model = selectedModels[i]; + modelsJson.Append(" \""); + modelsJson.Append(JsonEscape(model.Id)); + modelsJson.Append("\": { \"name\": \""); + modelsJson.Append(JsonEscape(model.Name)); + modelsJson.Append("\" }"); + if (i < selectedModels.Count - 1) + { + modelsJson.Append(","); + } + + modelsJson.Append("\n"); + } + + return "{\n" + + " \"$schema\": \"https://opencode.ai/config.json\",\n" + + " \"enabled_providers\": [\"gesis\"],\n" + + " \"disabled_providers\": [\"zen\", \"opencode\"],\n" + + " \"provider\": {\n" + + " \"gesis\": {\n" + + " \"npm\": \"@ai-sdk/openai-compatible\",\n" + + " \"name\": \"GESIS AI Server\",\n" + + " \"options\": {\n" + + " \"baseURL\": \"" + JsonEscape(BaseUrl) + "\",\n" + + " \"apiKey\": \"" + JsonEscape(apiKey) + "\"\n" + + " },\n" + + " \"models\": {\n" + + modelsJson + + " }\n" + + " }\n" + + " }\n" + + "}\n"; + } + + private List ProbeAvailableModels(string apiKey) + { + var endpoints = new[] + { + BaseUrl.TrimEnd('/') + "/models", + BaseUrl.TrimEnd('/') + "/v1/models" + }; + + Exception lastError = null; + var sawEmptyModelList = false; + foreach (var endpoint in endpoints) + { + try + { + SetStatus("Checking available models...", 20); + var json = DownloadString(endpoint, apiKey); + var models = ParseModels(json); + if (models.Count == 0) + { + sawEmptyModelList = true; + throw new InvalidOperationException("The GESIS AI Server returned no models for this API key."); + } + + models.Sort((left, right) => String.Compare(left.Name, right.Name, StringComparison.CurrentCultureIgnoreCase)); + return models; + } + catch (WebException ex) + { + lastError = ex; + var response = ex.Response as HttpWebResponse; + if (response != null && (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)) + { + throw new InvalidOperationException("The API key was rejected by the GESIS AI Server."); + } + } + catch (Exception ex) + { + lastError = ex; + } + } + + if (lastError is WebException) + { + throw new InvalidOperationException("The installer could not reach the GESIS AI Server or could not retrieve its model list."); + } + + if (sawEmptyModelList) + { + throw new InvalidOperationException("No models are available for this API key."); + } + + throw new InvalidOperationException("The GESIS AI Server returned a model list in an unexpected format."); + } + + private static List ParseModels(string json) + { + var serializer = new JavaScriptSerializer(); + var parsed = serializer.DeserializeObject(json); + var modelItems = FindModelItems(parsed); + if (modelItems == null) + { + throw new InvalidOperationException("The server returned a model list in an unexpected format."); + } + + var byId = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var item in modelItems) + { + var model = ToModelOption(item); + if (model != null && !byId.ContainsKey(model.Id)) + { + byId.Add(model.Id, model); + } + } + + return new List(byId.Values); + } + + private static object[] FindModelItems(object parsed) + { + var array = parsed as object[]; + if (array != null) + { + return array; + } + + var dictionary = parsed as Dictionary; + if (dictionary == null) + { + return null; + } + + var keys = new[] { "data", "models" }; + foreach (var key in keys) + { + if (dictionary.ContainsKey(key)) + { + var nestedArray = dictionary[key] as object[]; + if (nestedArray != null) + { + return nestedArray; + } + } + } + + return null; + } + + private static ModelOption ToModelOption(object item) + { + var id = ""; + var name = ""; + + var text = item as string; + if (text != null) + { + id = text.Trim(); + name = PrettyName(id); + } + else + { + var dictionary = item as Dictionary; + if (dictionary == null) + { + return null; + } + + id = GetStringValue(dictionary, "id"); + if (id.Length == 0) + { + id = GetStringValue(dictionary, "model"); + } + + name = GetStringValue(dictionary, "name"); + if (name.Length == 0) + { + name = GetStringValue(dictionary, "title"); + } + + if (name.Length == 0) + { + name = PrettyName(id); + } + } + + if (id.Length == 0) + { + return null; + } + + return new ModelOption(id, name); + } + + private static string GetStringValue(Dictionary dictionary, string key) + { + if (!dictionary.ContainsKey(key) || dictionary[key] == null) + { + return ""; + } + + return Convert.ToString(dictionary[key]).Trim(); + } + + private static string PrettyName(string modelId) + { + if (String.IsNullOrWhiteSpace(modelId)) + { + return ""; + } + + return modelId.Replace("-", " ").Replace("_", " "); + } + + private List GetSelectedModels() + { + SyncVisibleModelSelection(); + var selected = new List(); + foreach (var model in availableModels) + { + if (model.IsSelected) + { + selected.Add(model); + } + } + + return selected; + } + + private void RefreshModelList() + { + var filter = modelFilterBox.Text.Trim(); + modelGridPanel.SuspendLayout(); + try + { + modelGridPanel.Controls.Clear(); + modelGridPanel.ColumnStyles.Clear(); + modelGridPanel.RowStyles.Clear(); + + var visibleModels = new List(); + foreach (var model in availableModels) + { + if (filter.Length > 0 && + model.Id.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0 && + model.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + + visibleModels.Add(model); + } + + var columns = GetModelGridColumnCount(); + modelGridPanel.ColumnCount = columns; + modelGridPanel.RowCount = Math.Max(1, (int)Math.Ceiling(visibleModels.Count / (double)columns)); + + for (var column = 0; column < columns; column++) + { + modelGridPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F / columns)); + } + + for (var row = 0; row < modelGridPanel.RowCount; row++) + { + modelGridPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + } + + for (var i = 0; i < visibleModels.Count; i++) + { + var model = visibleModels[i]; + var checkBox = new CheckBox + { + Text = model.ToString(), + Checked = model.IsSelected, + AutoSize = true, + MaximumSize = new Size(GetModelColumnTextWidth(columns), 0), + Margin = new Padding(0, 4, 18, 4), + Tag = model + }; + checkBox.CheckedChanged += ModelCheckBox_CheckedChanged; + + var row = i / columns; + var column = i % columns; + modelGridPanel.Controls.Add(checkBox, column, row); + } + } + finally + { + modelGridPanel.ResumeLayout(true); + } + } + + private void SyncVisibleModelSelection() + { + foreach (Control control in modelGridPanel.Controls) + { + var checkBox = control as CheckBox; + if (checkBox == null) + { + continue; + } + + var model = checkBox.Tag as ModelOption; + if (model != null) + { + model.IsSelected = checkBox.Checked; + } + } + } + + private void SetVisibleModelSelection(bool selected) + { + foreach (Control control in modelGridPanel.Controls) + { + var checkBox = control as CheckBox; + if (checkBox == null) + { + continue; + } + + var model = checkBox.Tag as ModelOption; + if (model != null) + { + model.IsSelected = selected; + checkBox.Checked = selected; + } + } + } + + private void ModelCheckBox_CheckedChanged(object sender, EventArgs e) + { + var checkBox = sender as CheckBox; + if (checkBox == null) + { + return; + } + + var model = checkBox.Tag as ModelOption; + if (model != null) + { + model.IsSelected = checkBox.Checked; + } + } + + private int GetModelGridColumnCount() + { + var workingArea = Screen.FromControl(this).WorkingArea; + if (workingArea.Width >= 1200 && availableModels.Count >= 9) + { + return 3; + } + + if (workingArea.Width >= 800 && availableModels.Count >= 4) + { + return 2; + } + + return 1; + } + + private int GetModelColumnTextWidth(int columns) + { + var contentWidth = Math.Max(420, ClientSize.Width - 96); + return Math.Max(220, (contentWidth / Math.Max(1, columns)) - 28); + } + + private void ResizeForModelGrid() + { + if (InvokeRequired) + { + BeginInvoke(new Action(ResizeForModelGrid)); + return; + } + + if (WindowState == FormWindowState.Maximized) + { + return; + } + + var workingArea = Screen.FromControl(this).WorkingArea; + var maxWidth = (int)(workingArea.Width * 0.92); + var maxHeight = (int)(workingArea.Height * 0.92); + var columns = GetModelGridColumnCount(); + var rows = Math.Max(1, (int)Math.Ceiling(availableModels.Count / (double)columns)); + var desiredWidth = columns >= 3 ? 980 : columns == 2 ? 820 : 680; + var desiredHeight = 430 + (rows * Math.Max(34, Font.Height + 18)); + + Width = Math.Min(Math.Max(Width, desiredWidth), maxWidth); + Height = Math.Min(Math.Max(Height, desiredHeight), maxHeight); + CenterToScreen(); + } + + private static string JsonEscape(string value) + { + if (value == null) + { + return ""; + } + + return value + .Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("\t", "\\t"); + } + + private static string DownloadString(string url) + { + using (var client = CreateWebClient()) + { + return client.DownloadString(url); + } + } + + private static string DownloadString(string url, string apiKey) + { + using (var client = CreateWebClient()) + { + client.Headers.Add("Authorization", "Bearer " + apiKey); + return client.DownloadString(url); + } + } + + private static WebClient CreateWebClient() + { + var client = new WebClient(); + client.Headers.Add("Accept", "application/vnd.github+json"); + client.Headers.Add("User-Agent", "GESIS-OpenCode-Installer"); + return client; + } + + private void SetBusy(bool busy) + { + installButton.Enabled = !busy; + checkApiKeyButton.Enabled = !busy; + backButton.Enabled = !busy; + selectAllButton.Enabled = !busy; + selectNoneButton.Enabled = !busy; + apiKeyBox.Enabled = !busy; + if (!busy && installButton.Visible) + { + apiKeyBox.Enabled = false; + } + modelFilterBox.Enabled = !busy; + modelGridPanel.Enabled = !busy; + skipDesktopCheckBox.Enabled = !busy; + progressBar.Style = busy ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous; + if (!busy) + { + progressBar.Style = ProgressBarStyle.Continuous; + } + } + + private void SetStatus(string text, int progress) + { + if (InvokeRequired) + { + BeginInvoke(new Action(SetStatus), text, progress); + return; + } + + progressBar.Style = ProgressBarStyle.Continuous; + progressBar.Value = Math.Max(0, Math.Min(100, progress)); + statusLabel.Text = text; + } + } + + internal sealed class ModelOption + { + public ModelOption(string id, string name) + { + Id = id; + Name = String.IsNullOrWhiteSpace(name) ? id : name; + IsSelected = true; + } + + public string Id { get; private set; } + public string Name { get; private set; } + public bool IsSelected { get; set; } + + public override string ToString() + { + if (String.Equals(Id, Name, StringComparison.OrdinalIgnoreCase)) + { + return Id; + } + + return Name + " (" + Id + ")"; + } + } +} diff --git a/Install-OpenCode-GESIS.ps1 b/Install-OpenCode-GESIS.ps1 new file mode 100644 index 0000000..df92255 --- /dev/null +++ b/Install-OpenCode-GESIS.ps1 @@ -0,0 +1,169 @@ +#Requires -Version 5.1 + +[CmdletBinding()] +param( + [switch]$ConfigOnly, + [switch]$SkipDesktopInstall, + [switch]$SilentDesktopInstall, + [string]$GitHubToken = $env:GITHUB_TOKEN, + [string]$ConfigPath = (Join-Path $env:USERPROFILE ".config\opencode\opencode.json") +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$repo = "anomalyco/opencode" +$windowsAssetName = "opencode-desktop-win-x64.exe" +$releaseApiUrl = "https://api.github.com/repos/$repo/releases/latest" +$baseUrl = "https://ai-openwebui.gesis.org/api" + +function Write-Step { + param([string]$Message) + Write-Host "" + Write-Host "==> $Message" -ForegroundColor Cyan +} + +function ConvertFrom-SecureStringToPlainText { + param([Parameter(Mandatory = $true)][securestring]$SecureString) + + $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + try { + [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) + } + finally { + if ($bstr -ne [IntPtr]::Zero) { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + } + } +} + +function Invoke-GitHubGet { + param([Parameter(Mandatory = $true)][string]$Uri) + + $headers = @{ + "Accept" = "application/vnd.github+json" + "User-Agent" = "GESIS-OpenCode-Installer" + } + + if (-not [string]::IsNullOrWhiteSpace($GitHubToken)) { + $headers["Authorization"] = "Bearer $GitHubToken" + } + + Invoke-RestMethod -Uri $Uri -Headers $headers -Method Get +} + +function Install-OpenCodeDesktop { + if ($SkipDesktopInstall -or $ConfigOnly) { + Write-Host "Skipping OpenCode Desktop installation." + return + } + + if (-not [Environment]::Is64BitOperatingSystem) { + throw "This installer expects 64-bit Windows because OpenCode publishes a Windows x64 desktop installer." + } + + Write-Step "Finding latest OpenCode Desktop release" + $release = Invoke-GitHubGet -Uri $releaseApiUrl + $asset = $release.assets | Where-Object { $_.name -eq $windowsAssetName } | Select-Object -First 1 + + if ($null -eq $asset) { + throw "Could not find $windowsAssetName in the latest $repo release ($($release.tag_name))." + } + + Write-Host "Latest release: $($release.tag_name)" + Write-Host "Downloading: $($asset.name)" + + $downloadPath = Join-Path $env:TEMP $asset.name + Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $downloadPath -Headers @{ "User-Agent" = "GESIS-OpenCode-Installer" } + + Write-Step "Starting OpenCode Desktop installer" + if ($SilentDesktopInstall) { + $process = Start-Process -FilePath $downloadPath -ArgumentList "/S" -Wait -PassThru + } + else { + $process = Start-Process -FilePath $downloadPath -Wait -PassThru + } + + if ($process.ExitCode -ne 0) { + throw "OpenCode Desktop installer exited with code $($process.ExitCode)." + } +} + +function New-GesisOpenCodeConfig { + param([Parameter(Mandatory = $true)][string]$ApiKey) + + [ordered]@{ + '$schema' = 'https://opencode.ai/config.json' + enabled_providers = @('gesis') + disabled_providers = @('zen', 'opencode') + provider = [ordered]@{ + gesis = [ordered]@{ + npm = '@ai-sdk/openai-compatible' + name = 'GESIS AI Server' + options = [ordered]@{ + baseURL = $baseUrl + apiKey = $ApiKey + } + models = [ordered]@{ + 'gemma3:27b' = @{ name = 'Gemma 3 27B' } + 'gpt-4.1' = @{ name = 'GPT-4.1' } + 'gpt-4.1-mini' = @{ name = 'GPT-4.1 Mini' } + 'llama4:latest' = @{ name = 'Llama 4 Latest' } + 'o4-mini' = @{ name = 'O4 Mini' } + 'de-fr' = @{ name = 'De-Fr Translation' } + 'de-ru' = @{ name = 'De-Ru Translation' } + 'en-de' = @{ name = 'En-De Translation' } + 'en-fr' = @{ name = 'En-Fr Translation' } + 'en-ru' = @{ name = 'En-Ru Translation' } + 'pdf-extractor' = @{ name = 'PDF Extractor' } + 'gpt-5' = @{ name = 'GPT-5' } + 'gpt-5-mini' = @{ name = 'GPT-5 Mini' } + 'gpt-oss:120b' = @{ name = 'GPT-OSS 120B' } + 'gpt-oss:latest' = @{ name = 'GPT-OSS Latest' } + } + } + } + } +} + +function Write-OpenCodeConfig { + Write-Step "Configuring GESIS AI Server" + $secureApiKey = Read-Host "Enter your personal GESIS AI Server API key" -AsSecureString + $apiKey = ConvertFrom-SecureStringToPlainText -SecureString $secureApiKey + + if ([string]::IsNullOrWhiteSpace($apiKey)) { + throw "API key cannot be empty." + } + + $configDirectory = Split-Path -Parent $ConfigPath + New-Item -Path $configDirectory -ItemType Directory -Force | Out-Null + + if (Test-Path -LiteralPath $ConfigPath) { + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $backupPath = "$ConfigPath.backup-$timestamp" + Copy-Item -LiteralPath $ConfigPath -Destination $backupPath -Force + Write-Host "Existing config backed up to: $backupPath" + } + + $config = New-GesisOpenCodeConfig -ApiKey $apiKey + $json = $config | ConvertTo-Json -Depth 20 + + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($ConfigPath, $json, $utf8NoBom) + Write-Host "OpenCode config written to: $ConfigPath" +} + +try { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + Install-OpenCodeDesktop + Write-OpenCodeConfig + + Write-Step "Done" + Write-Host "OpenCode Desktop is installed and configured for the GESIS AI Server." +} +catch { + Write-Host "" + Write-Host "Installation failed: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b033f5 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# GESIS OpenCode Desktop Installer + +This folder contains a Windows GUI installer for OpenCode Desktop that configures the GESIS AI Server provider. + +The installer: + +- downloads the latest Windows x64 OpenCode Desktop release from `anomalyco/opencode` +- starts the official OpenCode Desktop installer +- asks each user for their personal GESIS AI Server API key +- checks the key against the GESIS AI Server before installing +- shows all models available to that key in a checkbox grid with every model selected by default +- writes only the selected models into the OpenCode config +- writes the OpenCode config to `%USERPROFILE%\.config\opencode\opencode.json` +- restricts OpenCode to the GESIS provider with `enabled_providers` +- explicitly disables known Zen/OpenCode default provider IDs +- backs up an existing config before replacing it + +## Install + +For normal users, distribute and run: + +`GESIS-OpenCode-Installer.exe` + +They paste their personal GESIS API key, click **Check API key**, choose which available models they want to use in OpenCode from the checkbox grid, and click **Install selected models**. + +The installer starts compact for API-key entry, then maximizes automatically after models are found so the model grid is visible. The main action buttons stay fixed at the bottom of the window, and only the content area scrolls as a fallback for high Windows text scaling or very large model sets. + +## Rebuild the EXE + +Open PowerShell in this folder and run: + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\Build-GuiInstaller.ps1 +``` + +The older PowerShell installer is kept as an administrator fallback: + +```powershell +.\Install-OpenCode-GESIS.ps1 +``` + +## Files + +- `GESIS-OpenCode-Installer.exe` is the GUI installer for distribution. +- `GesisOpenCodeInstaller.cs` is the GUI installer source. +- `Build-GuiInstaller.ps1` rebuilds the GUI installer. +- `app.manifest` keeps the installer DPI-aware on Windows. +- `GESIS Logo Blau EN.jpg` and `GESIS Signet Blau_transp.png` are embedded into the EXE when it is rebuilt. +- `Install-OpenCode-GESIS.ps1` is the PowerShell fallback installer. +- `opencode.gesis.template.json` is a safe template with no personal API key. +- `opencode.json` mirrors the template and no longer contains a real API key. diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..63f942c --- /dev/null +++ b/app.manifest @@ -0,0 +1,17 @@ + + + + + + + + + + + + + true + system + + + diff --git a/opencode.gesis.template.json b/opencode.gesis.template.json new file mode 100644 index 0000000..1a7f73c --- /dev/null +++ b/opencode.gesis.template.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://opencode.ai/config.json", + "enabled_providers": [ + "gesis" + ], + "disabled_providers": [ + "zen", + "opencode" + ], + "provider": { + "gesis": { + "npm": "@ai-sdk/openai-compatible", + "name": "GESIS AI Server", + "options": { + "baseURL": "https://ai-openwebui.gesis.org/api", + "apiKey": "REPLACE_WITH_PERSONAL_API_KEY" + }, + "models": { + "gemma3:27b": { + "name": "Gemma 3 27B" + }, + "gpt-4.1": { + "name": "GPT-4.1" + }, + "gpt-4.1-mini": { + "name": "GPT-4.1 Mini" + }, + "llama4:latest": { + "name": "Llama 4 Latest" + }, + "o4-mini": { + "name": "O4 Mini" + }, + "de-fr": { + "name": "De-Fr Translation" + }, + "de-ru": { + "name": "De-Ru Translation" + }, + "en-de": { + "name": "En-De Translation" + }, + "en-fr": { + "name": "En-Fr Translation" + }, + "en-ru": { + "name": "En-Ru Translation" + }, + "pdf-extractor": { + "name": "PDF Extractor" + }, + "gpt-5": { + "name": "GPT-5" + }, + "gpt-5-mini": { + "name": "GPT-5 Mini" + }, + "gpt-oss:120b": { + "name": "GPT-OSS 120B" + }, + "gpt-oss:latest": { + "name": "GPT-OSS Latest" + } + } + } + } +} diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..1a7f73c --- /dev/null +++ b/opencode.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://opencode.ai/config.json", + "enabled_providers": [ + "gesis" + ], + "disabled_providers": [ + "zen", + "opencode" + ], + "provider": { + "gesis": { + "npm": "@ai-sdk/openai-compatible", + "name": "GESIS AI Server", + "options": { + "baseURL": "https://ai-openwebui.gesis.org/api", + "apiKey": "REPLACE_WITH_PERSONAL_API_KEY" + }, + "models": { + "gemma3:27b": { + "name": "Gemma 3 27B" + }, + "gpt-4.1": { + "name": "GPT-4.1" + }, + "gpt-4.1-mini": { + "name": "GPT-4.1 Mini" + }, + "llama4:latest": { + "name": "Llama 4 Latest" + }, + "o4-mini": { + "name": "O4 Mini" + }, + "de-fr": { + "name": "De-Fr Translation" + }, + "de-ru": { + "name": "De-Ru Translation" + }, + "en-de": { + "name": "En-De Translation" + }, + "en-fr": { + "name": "En-Fr Translation" + }, + "en-ru": { + "name": "En-Ru Translation" + }, + "pdf-extractor": { + "name": "PDF Extractor" + }, + "gpt-5": { + "name": "GPT-5" + }, + "gpt-5-mini": { + "name": "GPT-5 Mini" + }, + "gpt-oss:120b": { + "name": "GPT-OSS 120B" + }, + "gpt-oss:latest": { + "name": "GPT-OSS Latest" + } + } + } + } +} diff --git a/source/README.md b/source/README.md new file mode 100644 index 0000000..f7688da --- /dev/null +++ b/source/README.md @@ -0,0 +1,12 @@ +# GESIS OpenCode Installer Source + +This folder contains the source and rebuild materials for `GESIS-OpenCode-Installer.exe`. + +To rebuild on Windows: + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\Build-GuiInstaller.ps1 +``` + +The built EXE embeds the GESIS logo assets and writes a user-specific OpenCode configuration after probing the GESIS AI Server for models available to the user's API key.