From 8c8235b2850421896ab82b5ed48c53049f8e3b98 Mon Sep 17 00:00:00 2001 From: songzhenrui Date: Mon, 25 May 2026 10:36:19 +0800 Subject: [PATCH 1/2] feat: add NSIS installer and classroom admin checklist Closes #1983 - Add scripts/installer/codewhale.nsi: NSIS installer that installs both codewhale.exe and codewhale-tui.exe to %LOCALAPPDATA%\Programs\CodeWhale\bin, adds to current-user PATH, and includes an uninstaller that cleans PATH - Add docs/CLASSROOM_INSTALL.md: step-by-step checklist for IT admins deploying CodeWhale in labs/classrooms, covering silent install, manual fallback, API key provisioning, imaging notes, and troubleshooting - Update docs/INSTALL.md: add Windows NSIS Installer section referencing the new installer and classroom checklist --- docs/CLASSROOM_INSTALL.md | 178 ++++++++++++++++++++++++++++ docs/INSTALL.md | 37 ++++++ scripts/installer/codewhale.nsi | 202 ++++++++++++++++++++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 docs/CLASSROOM_INSTALL.md create mode 100644 scripts/installer/codewhale.nsi diff --git a/docs/CLASSROOM_INSTALL.md b/docs/CLASSROOM_INSTALL.md new file mode 100644 index 000000000..7ea5b7798 --- /dev/null +++ b/docs/CLASSROOM_INSTALL.md @@ -0,0 +1,178 @@ +# CodeWhale Classroom / Lab Install Checklist + +A step-by-step checklist for IT admins deploying CodeWhale on lab or classroom +machines running Windows. + +> **Audience**: IT staff, teaching assistants, lab managers. +> **Prereq**: Each target machine runs Windows 10 (1809+) or Windows 11. + +--- + +## Pre-install checklist (run once per machine) + +| # | Task | Done? | +|---|------|-------| +| 1 | Confirm Windows version: `winver` → 10 build 17763+ or 11 | ☐ | +| 2 | Ensure the user account is a **standard user** (not a local admin). The installer does not require elevation. | ☐ | +| 3 | Verify outbound HTTPS (port 443) is open to `api.openai.com` (or whichever LLM provider the course uses). | ☐ | +| 4 | Obtain the installer: download `CodeWhaleSetup.exe` from the [latest release](https://github.com/Hmbown/CodeWhale/releases/latest) or from your department mirror. | ☐ | +| 5 | (Optional) Verify SHA-256 hash matches the published manifest. | ☐ | + +--- + +## Installation + +### Option A — Silent install (recommended for imaging / SCCM / Intune) + +```powershell +# Run as admin or via deployment tool +CodeWhaleSetup.exe /S +``` + +The silent installer: +- Installs to `%LOCALAPPDATA%\Programs\CodeWhale\bin` +- Adds the bin directory to the **current user** PATH +- Registers in Windows "Apps & Features" for uninstall + +### Option B — Interactive install + +1. Double-click `CodeWhaleSetup.exe`. +2. Accept the license. +3. Choose the install directory (default is fine for most setups). +4. Click **Install**. + +### Option C — Manual fallback (no installer) + +If the NSIS installer is blocked by group policy, install manually: + +```powershell +# 1. Create directory +$binDir = "$env:LOCALAPPDATA\Programs\CodeWhale\bin" +New-Item -ItemType Directory -Force -Path $binDir + +# 2. Download binaries (adjust URL to your mirror or release tag) +$tag = "v0.9.0" # replace with desired version +Invoke-WebRequest -Uri "https://github.com/Hmbown/CodeWhale/releases/download/$tag/codewhale-x64.exe" -OutFile "$binDir\codewhale.exe" +Invoke-WebRequest -Uri "https://github.com/Hmbown/CodeWhale/releases/download/$tag/codewhale-tui-x64.exe" -OutFile "$binDir\codewhale-tui.exe" + +# 3. Add to user PATH (persistent) +$currentPath = [Environment]::GetEnvironmentVariable("Path", "User") +if ($currentPath -notlike "*$binDir*") { + [Environment]::SetEnvironmentVariable("Path", "$currentPath;$binDir", "User") +} + +# 4. Refresh current session PATH +$env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") +``` + +--- + +## Post-install verification + +Run these on **each machine** (or spot-check a sample): + +| # | Command | Expected output | Done? | +|---|---------|-----------------|-------| +| 1 | `codewhale --version` | Prints version string | ☐ | +| 2 | `codewhale doctor` | All checks pass | ☐ | +| 3 | `codewhale-tui --version` | Prints version string | ☐ | + +If `codewhale` is not found, the user may need to open a **new** terminal window for PATH changes to take effect. + +--- + +## API key provisioning + +Each student needs an API key. Options: + +| Method | Pros | Cons | +|--------|------|------| +| **Per-student key** | Individual usage tracking | More key management | +| **Shared lab key** | Simple to deploy | Harder to audit; rate limits shared | + +### Deploying a shared key via environment variable + +```powershell +# Set for current user (persists across reboots) +[Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "sk-...", "User") +``` + +Or create a `config.toml` in `%APPDATA%\codewhale\`: + +```toml +[provider] +api_key = "sk-..." +base_url = "https://api.openai.com/v1" +``` + +### Deploying per-student keys with Intune / GPO + +Use a Group Policy Preference or Intune PowerShell script to set the +`OPENAI_API_KEY` environment variable per user. The variable name depends on +your LLM provider — see [CONFIGURATION.md](CONFIGURATION.md). + +--- + +## Uninstall + +### Silent uninstall + +```powershell +& "$env:LOCALAPPDATA\Programs\CodeWhale\Uninstall.exe" /S +``` + +### Manual uninstall (if installer was not used) + +```powershell +$binDir = "$env:LOCALAPPDATA\Programs\CodeWhale\bin" +Remove-Item -Recurse -Force (Split-Path $binDir) + +# Remove from PATH +$currentPath = [Environment]::GetEnvironmentVariable("Path", "User") +$newPath = ($currentPath -split ";" | Where-Object { $_ -ne $binDir }) -join ";" +[Environment]::SetEnvironmentVariable("Path", $newPath, "User") +``` + +--- + +## Troubleshooting + +| Symptom | Fix | +|---------|-----| +| `codewhale` not found after install | Open a **new** terminal. If still missing, check PATH: `echo $env:Path` | +| `MISSING_COMPANION_BINARY` | Ensure both `codewhale.exe` and `codewhale-tui.exe` are in the same directory | +| `TLS handshake` errors | Check proxy settings or use the CNB mirror (see [INSTALL.md](INSTALL.md)) | +| Antivirus quarantines binaries | Add the install directory to AV exclusions | +| `codewhale doctor` fails API check | Verify `OPENAI_API_KEY` is set or `config.toml` exists | + +--- + +## Imaging / Golden Image Notes + +If building a golden image (WIM/FFU): + +1. Install CodeWhale using Option A (silent) or Option C (manual). +2. Do **not** set API keys in the image — these are per-user/per-student. +3. The install directory (`%LOCALAPPDATA%\Programs\CodeWhale\bin`) is per-user, + so it will be present for the user who installed it. For other users on the + same machine, run the installer again or use Option C. +4. Alternatively, install to a shared location like `C:\Tools\CodeWhale\bin` + and add it to the **machine** PATH: + ```powershell + [Environment]::SetEnvironmentVariable("Path", "$env:Path;C:\Tools\CodeWhale\bin", "Machine") + ``` + +--- + +## Quick Reference: All file paths + +| Item | Default location | +|------|-----------------| +| Binaries | `%LOCALAPPDATA%\Programs\CodeWhale\bin\` | +| User config | `%APPDATA%\codewhale\config.toml` | +| Uninstaller | `%LOCALAPPDATA%\Programs\CodeWhale\Uninstall.exe` | +| PATH entry | `HKCU\Environment\Path` (current user) | + +--- + +*Last updated: 2026-05-25* diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 2a9fd17b4..f50e371f0 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -290,6 +290,43 @@ Scoop manifests are maintained outside this repository's release workflow and can lag GitHub/npm/Cargo releases. Use npm or manual GitHub release downloads when you need the newest version immediately. +### Windows NSIS Installer + +A standalone NSIS-based installer is available for Windows users who prefer a +traditional double-click setup (no npm, no Scoop, no Cargo required). + +**Download** `CodeWhaleSetup.exe` from the +[Releases page](https://github.com/Hmbown/CodeWhale/releases/latest). + +**Install** by double-clicking the setup executable. The installer: + +- Installs `codewhale.exe` and `codewhale-tui.exe` side-by-side into + `%LOCALAPPDATA%\Programs\CodeWhale\bin` +- Adds the install directory to the **current user** `PATH` +- Registers in Windows **Apps & Features** for easy uninstall + +**Silent install** (for IT admins, SCCM, Intune): + +```powershell +CodeWhaleSetup.exe /S +``` + +**Build the installer yourself** (requires [NSIS](https://nsis.sourceforge.io)): + +```powershell +cd scripts\installer +# Place codewhale.exe and codewhale-tui.exe here, then: +makensis /DVERSION=0.9.0 codewhale.nsi +``` + +**Manual fallback** — if the installer is blocked by group policy, see the +[CLASSROOM_INSTALL.md](CLASSROOM_INSTALL.md) guide for step-by-step PowerShell +commands. + +> **Deploying to a classroom or lab?** See the full +> [Classroom Install Checklist](CLASSROOM_INSTALL.md) for silent install, +> API key provisioning, imaging notes, and troubleshooting. + --- ## 7. Build from source diff --git a/scripts/installer/codewhale.nsi b/scripts/installer/codewhale.nsi new file mode 100644 index 000000000..a88477867 --- /dev/null +++ b/scripts/installer/codewhale.nsi @@ -0,0 +1,202 @@ +; codewhale.nsi — NSIS installer for CodeWhale (Windows) +; +; Requirements (see https://github.com/Hmbown/CodeWhale/issues/1983): +; - Install codewhale.exe and codewhale-tui.exe side-by-side +; - Default to %LOCALAPPDATA%\Programs\CodeWhale\bin +; - Add install dir to current-user PATH +; - Uninstaller removes the PATH entry +; +; Usage: +; 1. Place both .exe files next to this script: +; codewhale.exe +; codewhale-tui.exe +; 2. Build: +; makensis codewhale.nsi +; 3. Output: CodeWhaleSetup.exe (in current directory) +; +; You can override version at build time: +; makensis /DVERSION=1.2.3 codewhale.nsi + +;-------------------------------- +; Includes +;-------------------------------- +!include "MUI2.nsh" +!include "FileFunc.nsh" +!include "StrFunc.nsh" + +${StrStr} + +;-------------------------------- +; General +;-------------------------------- +!ifndef VERSION + !define VERSION "0.0.0" +!endif + +!define PRODUCT_NAME "CodeWhale" +!define PRODUCT_PUBLISHER "Hmbown" +!define PRODUCT_WEB_SITE "https://github.com/Hmbown/CodeWhale" + +Name "${PRODUCT_NAME} ${VERSION}" +OutFile "CodeWhaleSetup.exe" +InstallDir "$LOCALAPPDATA\Programs\CodeWhale" +RequestExecutionLevel user +BrandingText "${PRODUCT_NAME} Installer" + +;-------------------------------- +; Interface Settings +;-------------------------------- +!define MUI_ABORTWARNING +!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" + +;-------------------------------- +; Pages +;-------------------------------- +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "..\..\LICENSE" +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +; Languages +;-------------------------------- +!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "SimpChinese" + +;-------------------------------- +; Installer Sections +;-------------------------------- +Section "Install" SecInstall + SetOutPath "$INSTDIR\bin" + + ; Copy binaries + File "codewhale.exe" + File "codewhale-tui.exe" + + ; Write uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + + ; Add to current-user PATH + ; Read existing PATH, append if not already present + ReadRegStr $0 HKCU "Environment" "Path" + ${StrStr} $1 $0 "$INSTDIR\bin" + StrCmp $1 "" 0 path_already_set + ; Not found — append + StrCmp $0 "" empty_path + WriteRegExpandStr HKCU "Environment" "Path" "$0;$INSTDIR\bin" + Goto path_done + empty_path: + WriteRegExpandStr HKCU "Environment" "Path" "$INSTDIR\bin" + path_done: + ; Notify the system about the environment change + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + path_already_set: + + ; Store install directory for uninstaller + WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME}" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" "$\"$INSTDIR\Uninstall.exe$\"" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "QuietUninstallString" "$\"$INSTDIR\Uninstall.exe$\" /S" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "${PRODUCT_PUBLISHER}" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${VERSION}" + WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "NoModify" 1 + WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "NoRepair" 1 + + ; Calculate and store installed size + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "EstimatedSize" "$0" +SectionEnd + +;-------------------------------- +; Uninstaller Section +;-------------------------------- +Section "Uninstall" + ; Remove binaries + Delete "$INSTDIR\bin\codewhale.exe" + Delete "$INSTDIR\bin\codewhale-tui.exe" + Delete "$INSTDIR\Uninstall.exe" + RMDir "$INSTDIR\bin" + RMDir "$INSTDIR" + + ; Remove from current-user PATH + ReadRegStr $0 HKCU "Environment" "Path" + ${StrStr} $1 $0 "$INSTDIR\bin" + StrCmp $1 "" path_clean_done + ; Remove the entry + ; Build new PATH without the install dir + ; This handles: "...\path;$INSTDIR\bin" and "$INSTDIR\bin;...\path" and standalone + Push "$0" + Push "$INSTDIR\bin" + Call un.RemoveFromPath + Pop $0 + WriteRegExpandStr HKCU "Environment" "Path" "$0" + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + path_clean_done: + + ; Remove registry keys + DeleteRegKey HKCU "Software\${PRODUCT_NAME}" + DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" +SectionEnd + +;-------------------------------- +; Helper: Remove a directory from PATH +; Input: PATH string (on stack), directory to remove (on stack) +; Output: cleaned PATH (on stack) +;-------------------------------- +Function un.RemoveFromPath + Exch $R0 ; directory to remove + Exch + Exch $R1 ; original PATH + Push $R2 + Push $R3 + Push $R4 + + StrCpy $R2 "" + StrCpy $R3 "" + + loop: + ${StrStr} $R4 $R1 $R0 + StrCmp $R4 "" done + ; Found — get substring before match + StrLen $R4 $R4 + StrLen $R3 $R1 + IntOp $R3 $R3 - $R4 + StrCpy $R2 $R1 $R3 + ; Get substring after match + dir length + StrLen $R3 $R0 + IntOp $R4 $R4 - $R3 + StrCpy $R3 $R1 "" $R4 + ; Strip leading semicolon from remainder + StrCpy $R4 $R3 1 + StrCmp $R4 ";" 0 +2 + StrCpy $R3 $R3 "" 1 + ; Strip trailing semicolon from prefix + StrLen $R4 $R2 + IntOp $R4 $R4 - 1 + StrCpy $R4 $R2 1 $R4 + StrCmp $R4 ";" 0 +2 + StrCpy $R2 $R2 $R4 + ; Concatenate + StrCmp $R2 "" 0 +2 + StrCpy $R2 $R3 + Goto done + StrCmp $R3 "" 0 +2 + StrCpy $R1 $R2 + Goto done + StrCpy $R1 "$R2;$R3" + Goto done + + done: + Pop $R4 + Pop $R3 + Pop $R2 + Pop $R0 + Exch $R1 +FunctionEnd From ec2905d380e89636250132eb2ee4b9ab45d8c2d8 Mon Sep 17 00:00:00 2001 From: songzhenrui Date: Mon, 25 May 2026 12:24:40 +0800 Subject: [PATCH 2/2] fix: address Gemini code review feedback - Define UnStrStr macro for uninstaller string functions - Use un.StrStr instead of StrStr in uninstaller context - Rewrite un.RemoveFromPath with correct offset calculations and semicolon handling to prevent PATH corruption - Use dynamic version fetch from GitHub API in CLASSROOM_INSTALL.md --- docs/CLASSROOM_INSTALL.md | 2 +- scripts/installer/codewhale.nsi | 80 ++++++++++++++++----------------- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/docs/CLASSROOM_INSTALL.md b/docs/CLASSROOM_INSTALL.md index 7ea5b7798..07a7d14ea 100644 --- a/docs/CLASSROOM_INSTALL.md +++ b/docs/CLASSROOM_INSTALL.md @@ -51,7 +51,7 @@ $binDir = "$env:LOCALAPPDATA\Programs\CodeWhale\bin" New-Item -ItemType Directory -Force -Path $binDir # 2. Download binaries (adjust URL to your mirror or release tag) -$tag = "v0.9.0" # replace with desired version +$tag = (Invoke-RestMethod -Uri "https://api.github.com/repos/Hmbown/CodeWhale/releases/latest").tag_name Invoke-WebRequest -Uri "https://github.com/Hmbown/CodeWhale/releases/download/$tag/codewhale-x64.exe" -OutFile "$binDir\codewhale.exe" Invoke-WebRequest -Uri "https://github.com/Hmbown/CodeWhale/releases/download/$tag/codewhale-tui-x64.exe" -OutFile "$binDir\codewhale-tui.exe" diff --git a/scripts/installer/codewhale.nsi b/scripts/installer/codewhale.nsi index a88477867..29e94f093 100644 --- a/scripts/installer/codewhale.nsi +++ b/scripts/installer/codewhale.nsi @@ -25,6 +25,7 @@ !include "StrFunc.nsh" ${StrStr} +${UnStrStr} ;-------------------------------- ; General @@ -127,11 +128,9 @@ Section "Uninstall" ; Remove from current-user PATH ReadRegStr $0 HKCU "Environment" "Path" - ${StrStr} $1 $0 "$INSTDIR\bin" + ${un.StrStr} $1 $0 "$INSTDIR\bin" StrCmp $1 "" path_clean_done ; Remove the entry - ; Build new PATH without the install dir - ; This handles: "...\path;$INSTDIR\bin" and "$INSTDIR\bin;...\path" and standalone Push "$0" Push "$INSTDIR\bin" Call un.RemoveFromPath @@ -146,7 +145,7 @@ Section "Uninstall" SectionEnd ;-------------------------------- -; Helper: Remove a directory from PATH +; Helper: Remove a directory from PATH (uninstaller version) ; Input: PATH string (on stack), directory to remove (on stack) ; Output: cleaned PATH (on stack) ;-------------------------------- @@ -154,44 +153,43 @@ Function un.RemoveFromPath Exch $R0 ; directory to remove Exch Exch $R1 ; original PATH - Push $R2 - Push $R3 - Push $R4 - - StrCpy $R2 "" - StrCpy $R3 "" - - loop: - ${StrStr} $R4 $R1 $R0 - StrCmp $R4 "" done - ; Found — get substring before match - StrLen $R4 $R4 - StrLen $R3 $R1 - IntOp $R3 $R3 - $R4 - StrCpy $R2 $R1 $R3 - ; Get substring after match + dir length - StrLen $R3 $R0 - IntOp $R4 $R4 - $R3 - StrCpy $R3 $R1 "" $R4 - ; Strip leading semicolon from remainder - StrCpy $R4 $R3 1 - StrCmp $R4 ";" 0 +2 - StrCpy $R3 $R3 "" 1 - ; Strip trailing semicolon from prefix - StrLen $R4 $R2 - IntOp $R4 $R4 - 1 - StrCpy $R4 $R2 1 $R4 - StrCmp $R4 ";" 0 +2 - StrCpy $R2 $R2 $R4 - ; Concatenate - StrCmp $R2 "" 0 +2 - StrCpy $R2 $R3 - Goto done - StrCmp $R3 "" 0 +2 - StrCpy $R1 $R2 - Goto done - StrCpy $R1 "$R2;$R3" + Push $R2 ; prefix + Push $R3 ; suffix + Push $R4 ; match result + + ${un.StrStr} $R4 $R1 $R0 + StrCmp $R4 "" done + + ; Calculate prefix + StrLen $R2 $R1 + StrLen $R3 $R4 + IntOp $R3 $R2 - $R3 ; Match offset + StrCpy $R2 $R1 $R3 ; Prefix string + + ; Calculate suffix + StrLen $R4 $R0 + IntOp $R4 $R3 + $R4 ; Suffix offset = Match offset + Dir length + StrCpy $R3 $R1 "" $R4 ; Suffix string + + ; Clean up semicolons + StrCpy $R4 $R3 1 + StrCmp $R4 ";" 0 +2 + StrCpy $R3 $R3 "" 1 ; Strip leading semicolon from suffix + + StrLen $R4 $R2 + IntOp $R4 $R4 - 1 + StrCpy $R0 $R2 1 $R4 + StrCmp $R0 ";" 0 +2 + StrCpy $R2 $R2 $R4 ; Strip trailing semicolon from prefix + + ; Concatenate + StrCmp $R2 "" 0 +3 + StrCpy $R1 $R3 + Goto done + StrCmp $R3 "" 0 +3 + StrCpy $R1 $R2 Goto done + StrCpy $R1 "$R2;$R3" done: Pop $R4