diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1edaa82..fabb227 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -86,21 +86,56 @@ jobs:
- name: Install Wails CLI
run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.13.0
+ - name: Setup .NET SDK for WiX
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Install WiX Toolset 7 and UI extension
+ shell: pwsh
+ run: |
+ $wixVersion = '7.0.0'
+ $wixDirectory = Join-Path $env:TEMP "go-script-sql-runner-tools\wix\$wixVersion"
+ New-Item -ItemType Directory -Path $wixDirectory -Force | Out-Null
+ dotnet tool install wix --version $wixVersion --tool-path $wixDirectory --verbosity quiet
+ if ($LASTEXITCODE -ne 0) { throw 'Failed to install pinned WiX Toolset 7.0.0.' }
+
+ $env:WIX_EXTENSION = Join-Path $env:RUNNER_TEMP 'go-script-sql-runner-wix-extensions-7.0.0'
+ "WIX_EXTENSION=$env:WIX_EXTENSION" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+ & (Join-Path $wixDirectory 'wix.exe') extension add --global 'WixToolset.UI.wixext/7.0.0' -acceptEula wix7
+ if ($LASTEXITCODE -ne 0) { throw 'Failed to install pinned WiX UI extension 7.0.0.' }
+
- name: Build executable
- run: wails build -clean -webview2 embed -windowsconsole -ldflags "-X main.currentBuildTag=build-${{ github.run_number }}"
+ run: wails build -clean -webview2 embed -windowsconsole -ldflags "-X main.currentBuildTag=build-${{ github.run_number }}" -o go-script-sql-runner-portable.exe
- - name: Verify executable
+ - name: Build MSI installer
shell: pwsh
run: |
- if (-not (Test-Path 'build/bin/go-script-sql-runner.exe')) {
- throw 'Expected build/bin/go-script-sql-runner.exe was not produced.'
+ ./scripts/build-windows-installer.ps1 `
+ -AppVersion '0.1.${{ github.run_number }}' `
+ -ExecutablePath 'build/bin/go-script-sql-runner-portable.exe' `
+ -OutputDirectory 'build/bin'
+
+ - name: Verify Windows release files
+ shell: pwsh
+ run: |
+ $expectedFiles = @(
+ 'build/bin/go-script-sql-runner-portable.exe',
+ 'build/bin/go-script-sql-runner-setup.msi'
+ )
+ foreach ($file in $expectedFiles) {
+ if (-not (Test-Path -LiteralPath $file -PathType Leaf)) {
+ throw "Expected release file was not produced: $file"
+ }
}
- - name: Upload executable artifact
+ - name: Upload Windows release files
uses: actions/upload-artifact@v4
with:
name: go-script-sql-runner-windows
- path: build/bin/go-script-sql-runner.exe
+ path: |
+ build/bin/go-script-sql-runner-portable.exe
+ build/bin/go-script-sql-runner-setup.msi
if-no-files-found: error
retention-days: 14
@@ -112,17 +147,20 @@ jobs:
permissions:
contents: write
steps:
- - name: Download executable artifact
+ - name: Download Windows release files
uses: actions/download-artifact@v5
with:
name: go-script-sql-runner-windows
path: release
- - name: Verify release asset
+ - name: Verify Windows release files
shell: bash
- run: test -f release/go-script-sql-runner.exe
+ run: |
+ set -euo pipefail
+ test -f release/go-script-sql-runner-portable.exe
+ test -f release/go-script-sql-runner-setup.msi
- - name: Publish executable release
+ - name: Publish Windows release files
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
@@ -133,7 +171,8 @@ jobs:
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
gh release upload "${RELEASE_TAG}" \
- release/go-script-sql-runner.exe \
+ 'release/go-script-sql-runner-portable.exe#Windows portable executable' \
+ 'release/go-script-sql-runner-setup.msi#Windows installer (MSI)' \
--clobber
gh release edit "${RELEASE_TAG}" \
@@ -143,7 +182,8 @@ jobs:
--latest
else
gh release create "${RELEASE_TAG}" \
- 'release/go-script-sql-runner.exe#Windows executable' \
+ 'release/go-script-sql-runner-portable.exe#Windows portable executable' \
+ 'release/go-script-sql-runner-setup.msi#Windows installer (MSI)' \
--target "${GITHUB_SHA}" \
--title "Go Script SQL Runner - Build #${GITHUB_RUN_NUMBER}" \
--notes "Automated Windows build from main. Commit: ${GITHUB_SHA}" \
diff --git a/.gitignore b/.gitignore
index 2aa3aa9..8df9778 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ frontend/dist/*
!frontend/dist/index.html
# Local runtime/profile artifacts
+.worktrees/
GoScriptSQLRunner/
appdata/
.local/
diff --git a/README.md b/README.md
index 18c8234..eefe6c6 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,16 @@ For development/builds:
- Go 1.26+
- Node.js 24+
- Wails v2.13.0
+- .NET SDK 10.0+ (used by the WiX 7 MSI builder)
+
+## Windows downloads
+
+GitHub Releases provide two Windows assets:
+
+- `go-script-sql-runner-portable.exe` is the portable application executable. Run it directly; it does not install application files or create a Start Menu shortcut. Profiles and logs are still stored in the current Windows user's `%AppData%` directory described below, not beside the executable.
+- `go-script-sql-runner-setup.msi` is the wizard installer. Choose **Only for me** to install under `%LocalAppData%\Programs\Go Script SQL Runner` without administrator rights, or **For all users** to install under `%ProgramFiles%\Go Script SQL Runner`, which requires Windows administrator approval.
+
+Both MSI choices keep each Windows user's profiles and logs in that user's `%AppData%\GoScriptSQLRunner\profiles` and `%AppData%\GoScriptSQLRunner\logs` directories. The MSI installs or removes the application and shortcut; it does not move or delete this per-user data. Each Windows account continues to use its own profiles, including when the app is installed for all users.
## Build
@@ -25,12 +35,13 @@ Use the reproducible build script:
.\scripts\build.ps1
```
-It runs Go tests, installs the locked frontend dependencies with `npm ci`, runs frontend tests/build, and builds the Wails executable with the embedded WebView2 bootstrapper.
+It runs Go tests, installs the locked frontend dependencies with `npm ci`, runs frontend tests/build, and builds both the portable Wails executable with the embedded WebView2 bootstrapper and the MSI wizard installer.
Output:
```text
-build/bin/go-script-sql-runner.exe
+build/bin/go-script-sql-runner-portable.exe
+build/bin/go-script-sql-runner-setup.msi
```
## AppData location
@@ -50,7 +61,7 @@ Each profile owns its copied SQL scripts under its profile directory. The fronte
Start the executable without arguments:
```powershell
-.\go-script-sql-runner.exe
+.\go-script-sql-runner-portable.exe
```
The default desktop window exposes the essential workflow without navigating to another screen:
@@ -70,15 +81,15 @@ Run-level overrides do not silently modify the saved profile. A script-level tra
Supplying arguments routes the same executable to the CLI:
```powershell
-.\go-script-sql-runner.exe profile list
-.\go-script-sql-runner.exe profile show example
-.\go-script-sql-runner.exe run example
+.\go-script-sql-runner-portable.exe profile list
+.\go-script-sql-runner-portable.exe profile show example
+.\go-script-sql-runner-portable.exe run example
```
For the complete command list:
```powershell
-.\go-script-sql-runner.exe --help
+.\go-script-sql-runner-portable.exe --help
```
## Profile import/export warning
diff --git a/build/windows/installer/Package.wxs b/build/windows/installer/Package.wxs
new file mode 100644
index 0000000..6fa2336
--- /dev/null
+++ b/build/windows/installer/Package.wxs
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/windows/installer/ScopeSelectionDlg.wxs b/build/windows/installer/ScopeSelectionDlg.wxs
new file mode 100644
index 0000000..3644945
--- /dev/null
+++ b/build/windows/installer/ScopeSelectionDlg.wxs
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/superpowers/plans/2026-09-25-monaco-script-editor.md b/docs/superpowers/plans/2026-09-25-monaco-script-editor.md
new file mode 100644
index 0000000..94bce0b
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-25-monaco-script-editor.md
@@ -0,0 +1,187 @@
+# Monaco SQL Script Editor Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use Markdown task checkboxes.
+
+**Goal:** Add an in-app Monaco editor that safely reads and updates SQL text for an existing profile script.
+
+**Architecture:** The Go repository reads and atomically replaces a script file based on validated profile metadata. The application service, bridge, and Wails desktop facade expose content-only methods; React opens a dirty-aware Monaco dialog and saves through the typed RunnerApi.
+
+**Tech Stack:** Go, Wails v2, React 19, TypeScript, Monaco Editor, Vite, Vitest, React Testing Library.
+
+**Spec:** [2026-09-25-profile-script-editor-and-windows-installer-design.md](../specs/2026-09-25-profile-script-editor-and-windows-installer-design.md)
+
+## Global Constraints
+
+- Monaco Editor is configured for SQL syntax highlighting.
+- The editor loads content through the Wails API and saves it through the Go application service to the existing profile-owned SQL file.
+- The frontend will not write files directly.
+- Saving changes only the script content; its profile, ID, name, order, enabled state, transaction mode, and file reference remain unchanged.
+- Closing a modified editor without saving asks the user to confirm discarding the changes.
+- Editing is unavailable while a profile run is active.
+
+## Review Focus
+
+- Invalid IDs and ../-style script file metadata must not read or overwrite files outside a profile; pin in the storage path-validation tests.
+- Missing profile/script and non-regular/symlink SQL files must return errors without creating files; pin in repository and service tests.
+- Saving empty text and non-ASCII/multiline SQL must preserve exact bytes and UTF-8 text; pin in repository tests.
+- A failed save must leave the original SQL intact and preserve script metadata; pin in atomic write and service tests.
+- Dirty close via Cancel/Escape must preserve the draft until the user explicitly discards; pin in editor dialog tests.
+
+---
+
+### Task 1: Add safe SQL content operations in storage and application service
+
+**Files:**
+- Modify: internal/storage/repository.go
+- Modify: internal/storage/repository_test.go
+- Create: internal/app/script_content.go
+- Create: internal/app/script_content_test.go
+
+**Interfaces:**
+- Repository.ReadScriptContent(profileID, scriptID) returns (string, error).
+- Repository.WriteScriptContent(profileID, scriptID, content) returns error.
+- Service.GetScriptContent(ctx, profileID, scriptID) returns (string, error).
+- Service.SaveScriptContent(ctx, profileID, scriptID, content) returns error.
+
+- [ ] **Step 1: Add repository read/write tests**
+
+Use a temporary repository with a profile containing a real SQL script. Assert read returns exact text; writing the UTF-8 multiline text "-- Café\nSELECT 2;\n" changes only the SQL bytes and preserves all metadata. Also test saving empty text, missing profile, missing script ID, unknown script, malformed file reference ../escape.sql, and a symlink to an outside file where the platform permits symlinks.
+
+- [ ] **Step 2: Run the focused repository tests**
+
+Run: go test ./internal/storage -run ScriptContent -count=1
+
+Expected: current implementation lacks these methods and the new tests fail to compile.
+
+- [ ] **Step 3: Implement a validated path resolver and atomic writes**
+
+Resolve the profile through Get, find the exact script ID, validate its relative file reference with the profile path rules, and join it below the profile's scripts directory. Reject any symlink component and any non-regular final file before reading or writing. Reads use os.ReadFile; writes call existing atomicWrite(path, []byte(content), 0o600). Never accept a frontend-supplied path.
+
+- [ ] **Step 4: Add application service tests and methods**
+
+Use editingService(t) in the existing profile editing test fixture or a local fixture. Assert GetScriptContent and SaveScriptContent return exact content and that saving leaves every profile.Script field unchanged. Assert cancelled contexts and unknown IDs fail without changing content. Implement the context checks and wrap repository errors with %w.
+
+- [ ] **Step 5: Run the backend script content tests**
+
+Run: go test ./internal/storage ./internal/app -run 'ScriptContent|Script.*Content' -count=1
+
+Expected: PASS, including exact empty and UTF-8 content round trips.
+
+- [ ] **Step 6: Commit safe SQL content operations**
+
+```powershell
+git add internal/storage/repository.go internal/storage/repository_test.go internal/app/script_content.go internal/app/script_content_test.go
+git commit -m "feat: add safe SQL script content editing"
+```
+
+### Task 2: Expose script content calls to the Wails frontend
+
+**Files:**
+- Modify: internal/ui/bridge.go
+- Modify: internal/ui/bridge_test.go
+- Modify: internal/ui/wails/desktop_app.go
+- Modify: frontend/src/api/types.ts
+- Modify: frontend/src/api/runner.ts
+- Modify: frontend/src/state/useRunnerController.ts
+- Modify: frontend/src/state/useRunnerController.test.tsx
+
+**Interfaces:**
+- ServicePort consumes GetScriptContent(context.Context, string, string) (string, error) and SaveScriptContent(context.Context, string, string, string) error.
+- Bridge exposes GetScriptContent(ctx, profileID, scriptID) (string, error) and SaveScriptContent(ctx, profileID, scriptID, content) error.
+- RunnerApi exposes getScriptContent(profileID, scriptID): Promise and saveScriptContent(profileID, scriptID, content): Promise.
+- The controller exposes loadScriptContent(scriptID): Promise and saveScriptContent(scriptID, content): Promise.
+
+- [ ] **Step 1: Add bridge forwarding and error tests**
+
+Extend the fake service and assert profile ID, script ID, and exact text are passed unchanged for read/write. Assert service errors are propagated. Update existing bridge service fakes for the expanded ServicePort interface.
+
+- [ ] **Step 2: Add controller tests**
+
+Assert loading returns the selected script text. Assert save calls the selected profile ID and script ID, refreshes the selected profile on success, returns true, and reports errors while returning false on failure. Assert no selected profile prevents an API call.
+
+- [ ] **Step 3: Run API tests before implementation**
+
+Run: go test ./internal/ui -run ScriptContent -count=1
+
+Run in frontend: npm test -- --run src/state/useRunnerController.test.tsx
+
+Expected: FAIL because the new service methods and RunnerApi/controller actions have not been added.
+
+- [ ] **Step 4: Implement bridge, Wails, and frontend adapters**
+
+Add DesktopApp.GetScriptContent and DesktopApp.SaveScriptContent, each retrieving the startup context and delegating through Bridge. Add both methods to the desktop binding interface and Wails adapter. Implement the controller methods using existing error handling and refreshSelectedProfile patterns.
+
+- [ ] **Step 5: Run Go bridge tests and frontend controller tests**
+
+Run: go test ./internal/ui ./internal/ui/wails -count=1
+
+Run in frontend: npm test -- --run src/state/useRunnerController.test.tsx
+
+Expected: PASS; the script is refreshed after save and backend errors remain visible.
+
+- [ ] **Step 6: Commit script content API wiring**
+
+```powershell
+git add internal/ui/bridge.go internal/ui/bridge_test.go internal/ui/wails/desktop_app.go frontend/src/api/types.ts frontend/src/api/runner.ts frontend/src/state/useRunnerController.ts frontend/src/state/useRunnerController.test.tsx
+git commit -m "feat: expose SQL content editing through Wails"
+```
+
+### Task 3: Build and test the Monaco editor dialog
+
+**Files:**
+- Modify: frontend/package.json
+- Modify: frontend/package-lock.json
+- Modify: frontend/vite.config.ts
+- Create: frontend/src/components/ScriptEditorDialog.tsx
+- Create: frontend/src/components/ScriptEditorDialog.test.tsx
+- Modify: frontend/src/App.tsx
+- Modify: frontend/src/components/MainView.test.tsx
+
+**Interfaces:**
+- ScriptEditorDialog receives open, scriptName, initialContent, saving, onCancel, and onSave(content): Promise props; true means saved and false keeps the editor open with its draft.
+- App stores the script being edited, loads content through the controller, and saves through the controller.
+
+- [ ] **Step 1: Install and lock Monaco dependencies**
+
+Run in frontend: npm install @monaco-editor/react monaco-editor
+
+Expected: package.json and package-lock.json pin resolved dependencies for reproducible npm ci builds.
+
+- [ ] **Step 2: Add dialog behavior tests with a Monaco test double**
+
+Mock @monaco-editor/react as an accessible textarea-like editor that forwards value and onChange. Test loading initial SQL, editing multiline text, Save calling onSave with exact content, closing only when onSave resolves true, keeping the editor and draft open when onSave resolves false, disabled Save while saving, Cancel without edits, and dirty close prompting to discard. If the user declines discard, keep the editor and draft open; if confirmed, close it.
+
+- [ ] **Step 3: Add main-view editor lifecycle tests**
+
+In MainView.test.tsx, assert each row has an Edit action naming the script, clicking loads that script's SQL into the dialog, successful save calls saveScriptContent and closes the dialog, and the action is disabled during a run. Assert backend load/save errors appear in the existing error area and preserve the dialog draft on save failure.
+
+- [ ] **Step 4: Run the editor tests before implementation**
+
+Run in frontend: npm test -- --run src/components/ScriptEditorDialog.test.tsx src/components/MainView.test.tsx
+
+Expected: FAIL because ScriptEditorDialog and the script-row action have not been implemented yet; failures should identify missing behavior, not test setup errors.
+
+- [ ] **Step 5: Implement Monaco dialog and local bundle configuration**
+
+Render Monaco Editor with language sql, the current draft value, and an onChange handler in a labeled dialog with a minimum height of 420px, dark theme matching the app, and accessible Save/Cancel/close controls. Configure Monaco workers or Vite asset handling so editor assets are bundled locally and no CDN/network is required at runtime. Keep the original value to detect dirty state. When the draft is dirty, show an in-app alert dialog with Keep editing and Discard changes actions; do not use window.confirm. Close after save only when onSave resolves true.
+
+- [ ] **Step 6: Implement the script row action and editor lifecycle**
+
+In App.tsx, add an Edit action per script beside the ordering controls. Disable it during a run. Load selected SQL via the controller before opening the dialog, save through saveScriptContent, close only after successful save, and keep the draft/dialog open on save failure. Backend errors appear in the existing error area.
+
+- [ ] **Step 7: Run frontend editor tests, typecheck, and production build**
+
+Run in frontend: npm test -- --run src/components/ScriptEditorDialog.test.tsx src/components/MainView.test.tsx src/state/useRunnerController.test.tsx
+
+Run in frontend: npx tsc -b
+
+Run in frontend: npm run build
+
+Expected: PASS; the build emits the Monaco editor and worker assets into frontend/dist without remote loading.
+
+- [ ] **Step 8: Commit Monaco editor integration**
+
+```powershell
+git add frontend/package.json frontend/package-lock.json frontend/vite.config.ts frontend/src/components/ScriptEditorDialog.tsx frontend/src/components/ScriptEditorDialog.test.tsx frontend/src/App.tsx frontend/src/components/MainView.test.tsx
+git commit -m "feat: add Monaco SQL script editor"
+```
diff --git a/docs/superpowers/plans/2026-09-25-profile-management.md b/docs/superpowers/plans/2026-09-25-profile-management.md
new file mode 100644
index 0000000..5452c0e
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-25-profile-management.md
@@ -0,0 +1,186 @@
+# Profile Delete and Clone Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Let users delete a selected profile with confirmation and clone a fully independent copy of a profile.
+
+**Architecture:** Keep profile lifecycle operations in `internal/app.Service` and implement complete-directory behavior in `internal/storage.Repository`. Expose typed operations through `ui.Bridge` and `wails.DesktopApp`, then add controller state actions and confirmation dialogs in React.
+
+**Tech Stack:** Go, Wails v2, React 19, TypeScript, Vitest, React Testing Library.
+
+**Spec:** [2026-09-25-profile-script-editor-and-windows-installer-design.md](../specs/2026-09-25-profile-script-editor-and-windows-installer-design.md)
+
+## Global Constraints
+
+- Copy connection settings including the password, execution settings, all script metadata, and the SQL file contents into the new profile's directory.
+- Editing or deleting the clone must not change the source profile.
+- Confirmation precedes recursive profile removal.
+- The frontend does not copy or remove filesystem paths.
+- Application data remains per-user in AppData.
+
+## Review Focus
+
+- Invalid profile IDs, including path traversal strings, must be rejected before path resolution; pin in the repository deletion and clone tests.
+- A clone with a missing or non-regular SQL source must fail without leaving a partial destination or changing the source; pin in the repository clone tests.
+- Duplicate profile names must not overwrite a profile because IDs, not names, identify profiles; pin in the service clone test.
+- Cancelling deletion must preserve the current profile and make no API call; pin in the main-view deletion test.
+- Deleting the selected profile when it is the only one must close its active database connection and clear selection, while deleting one of several selects a remaining profile; pin connection behavior in the service test and selection behavior in the controller/UI test.
+
+---
+
+### Task 1: Add safe repository and service profile lifecycle operations
+
+**Files:**
+- Modify: `internal/storage/repository.go`
+- Modify: `internal/storage/repository_test.go`
+- Create: `internal/app/profile_lifecycle.go`
+- Create: `internal/app/profile_lifecycle_test.go`
+
+**Interfaces:**
+- Produces `func (r *Repository) Clone(sourceID string, destination profile.Profile) error`.
+- Produces `func (s *Service) DeleteProfile(ctx context.Context, profileID string) error`.
+- Produces `func (s *Service) CloneProfile(ctx context.Context, profileID string) (profile.Profile, error)`; the service assigns a generated ID and the name ` (copy)`.
+
+- [ ] **Step 1: Add repository deletion tests for invalid IDs and profile isolation**
+
+Extend `TestRepositoryCRUDAndLayout` with a second saved profile and assert deleting the first removes its profile directory while the second remains readable. Add table cases for `../outside`, `.`, and an empty ID; assert each returns an error and does not remove any directory outside `profiles`.
+
+- [ ] **Step 2: Run the focused repository test**
+
+Run: `go test ./internal/storage -run 'TestRepositoryCRUDAndLayout|TestDeleteRejectsInvalidIDs' -count=1`
+
+Expected: PASS; invalid IDs are rejected and the other profile remains readable.
+
+- [ ] **Step 3: Add repository clone tests**
+
+Create source and destination candidates with two SQL scripts. Call `Clone(source.ID, clone)` and assert the destination YAML retains connection/password/execution/script metadata, each destination script has identical bytes, and subsequent writes/deletion in the destination do not affect source files. Assert an existing destination ID is rejected. Add missing-file and directory-valued SQL source cases and assert the destination path does not remain.
+
+- [ ] **Step 4: Run the focused clone test before implementation**
+
+Run: `go test ./internal/storage -run 'Repository.*Clone|CloneProfile' -count=1`
+
+Expected: FAIL because Repository.Clone does not exist yet; the missing-source test should not fail during fixture setup.
+
+- [ ] **Step 5: Implement transactional repository cloning**
+
+Validate both IDs and the destination profile. Create a temporary staging directory under the profiles root, copy only regular SQL files named by source profile metadata after checking each path remains under that source profile, encode the destination profile into staging, then rename the complete staging directory to the destination. Remove staging on every error. Do not call `Save` on the final directory until all files are copied.
+
+- [ ] **Step 6: Add service deletion and clone tests**
+
+Add tests that `DeleteProfile` removes a profile and rejects invalid IDs, and `CloneProfile` creates a generated ID distinct from the source, appends ` (copy)` to the name, retains the password/settings/scripts, and does not collide when two profiles share the same name. Configure an active recording client for the profile being deleted and assert successful deletion closes and clears that connection; deleting another profile must leave the active connection open. Assert a cancelled context returns before mutation.
+
+- [ ] **Step 7: Implement the service operations**
+
+`CloneProfile` must check `ctx.Err()`, load the source, copy its value, clear the clone ID, set the derived name, call `id.New()`, preserve version and all other profile/script fields, then pass the completed clone to `Repository.Clone`. `DeleteProfile` checks context, takes `connectionMu` so an active run finishes first, deletes through the repository, then closes and clears the active client only when its profile ID matches the deleted profile. Preserve the connection when repository deletion fails. Wrap errors with operation and profile ID while preserving them with `%w`.
+
+- [ ] **Step 8: Run backend lifecycle tests**
+
+Run: `go test ./internal/storage ./internal/app -run 'Delete|Clone' -count=1`
+
+Expected: PASS; source and destination directories remain independent under the failure and mutation cases.
+
+- [ ] **Step 9: Commit the backend lifecycle operations**
+
+```powershell
+git add internal/storage/repository.go internal/storage/repository_test.go internal/app/profile_lifecycle.go internal/app/profile_lifecycle_test.go
+git commit -m "feat: add profile clone and delete operations"
+```
+
+### Task 2: Expose profile lifecycle operations through the desktop API and controller
+
+**Files:**
+- Modify: `internal/ui/bridge.go`
+- Modify: `internal/ui/bridge_test.go`
+- Modify: `internal/ui/wails/desktop_app.go`
+- Modify: `frontend/src/api/types.ts`
+- Modify: `frontend/src/api/runner.ts`
+- Modify: `frontend/src/state/useRunnerController.ts`
+- Modify: `frontend/src/state/useRunnerController.test.tsx`
+
+**Interfaces:**
+- `ServicePort` consumes `DeleteProfile(context.Context, string) error` and `CloneProfile(context.Context, string) (profile.Profile, error)`.
+- `Bridge` exposes `DeleteProfile(ctx, profileID) error` and `CloneProfile(ctx, profileID) (profile.Profile, error)`.
+- `RunnerApi` exposes `deleteProfile(profileID): Promise` and `cloneProfile(profileID): Promise`.
+- The controller exposes `deleteSelectedProfile(): Promise` and `cloneSelectedProfile(): Promise`.
+
+- [ ] **Step 1: Add bridge tests and extend service test doubles**
+
+Add bridge tests proving lifecycle calls forward the selected ID, result, and error unchanged. Update every `ServicePort` fake to implement the two new methods.
+
+- [ ] **Step 2: Run bridge tests before implementation**
+
+Run: `go test ./internal/ui -run 'Profile.*(Delete|Clone)|DeleteProfile|CloneProfile' -count=1`
+
+Expected: FAIL with missing ServicePort methods or forwarding methods, not an unrelated test failure.
+
+- [ ] **Step 3: Add controller tests for clone selection and delete fallback**
+
+With two fake profiles, assert cloning calls `api.cloneProfile` with the selected ID, reloads the profile list, loads the returned clone by ID, and selects it. Assert deletion reloads profiles and selects the first remaining profile; when the API returns an empty list, assert selection becomes `null`. Assert API errors set the controller error and do not fake a successful selection change.
+
+- [ ] **Step 4: Run controller lifecycle tests before implementation**
+
+Run in `frontend`: `npm test -- --run src/state/useRunnerController.test.tsx`
+
+Expected: FAIL because RunnerApi and controller lifecycle methods have not been added; failures identify missing API methods/actions.
+
+- [ ] **Step 5: Implement backend and frontend API bindings**
+
+Add `DesktopApp.DeleteProfile` and `DesktopApp.CloneProfile` methods that obtain the Wails context and delegate to `Bridge`. Add the two typed RunnerApi functions and Wails binding adapters. Update all typed RunnerApi test doubles with clone/delete functions. Implement controller actions using existing `listProfiles`, `getProfile`, `applySelectedProfile`, and error-state patterns; make `deleteSelectedProfile` return true only after deletion and profile-list refresh succeed, and false after an error.
+
+- [ ] **Step 6: Run bridge tests, controller tests, and typecheck**
+
+Run: `go test ./internal/ui ./internal/ui/wails -count=1`
+
+Run in `frontend`: `npm test -- --run src/state/useRunnerController.test.tsx`
+
+Run in `frontend`: `npx tsc -b`
+
+Expected: PASS with the new methods present in all fake APIs.
+
+- [ ] **Step 7: Commit desktop API and controller integration**
+
+```powershell
+git add internal/ui/bridge.go internal/ui/bridge_test.go internal/ui/wails/desktop_app.go frontend/src/api/types.ts frontend/src/api/runner.ts frontend/src/state/useRunnerController.ts frontend/src/state/useRunnerController.test.tsx
+git commit -m "feat: expose profile lifecycle actions to desktop"
+```
+
+### Task 3: Add profile clone and delete actions to the main UI
+
+**Files:**
+- Modify: `frontend/src/App.tsx`
+- Create: `frontend/src/components/ProfileDeleteDialog.tsx`
+- Create: `frontend/src/components/ProfileDeleteDialog.test.tsx`
+- Modify: `frontend/src/components/MainView.test.tsx`
+
+**Interfaces:**
+- `ProfileDeleteDialog` receives `open`, `profileName`, `onCancel`, and async `onConfirm` props.
+- `App` uses controller lifecycle actions and closes dialogs only after a successful operation.
+
+- [ ] **Step 1: Add UI tests for delete confirmation and clone action**
+
+Assert the selected profile name appears in the delete confirmation, Cancel performs no delete call, and Confirm calls the controller/API. Assert Clone calls the selected profile API and the cloned name becomes selected. Assert lifecycle actions are disabled while a run is active or while no profile is selected.
+
+- [ ] **Step 2: Run the focused UI tests before implementation**
+
+Run in `frontend`: `npm test -- --run src/components/ProfileDeleteDialog.test.tsx src/components/MainView.test.tsx`
+
+Expected: FAIL because the delete confirmation, profile actions, and controller integration do not exist yet.
+
+- [ ] **Step 3: Implement accessible profile actions and confirmation**
+
+Add **Clone** and **Delete** buttons beside profile actions. Use a confirmation dialog with `role="alertdialog"`, the exact profile name, and explicit Cancel/Delete buttons. Keep the dialog open on deletion error and surface the controller error in the existing error area. Invoke clone and select the returned profile; close confirmation and let the controller refresh selection after a successful delete.
+
+- [ ] **Step 4: Run frontend lifecycle tests and typecheck**
+
+Run in `frontend`: `npm test -- --run src/components/ProfileDeleteDialog.test.tsx src/components/MainView.test.tsx src/state/useRunnerController.test.tsx`
+
+Run in `frontend`: `npx tsc -b`
+
+Expected: PASS; cancel/error do not report success and deleting the last profile clears selection.
+
+- [ ] **Step 5: Commit the profile UI**
+
+```powershell
+git add frontend/src/App.tsx frontend/src/components/ProfileDeleteDialog.tsx frontend/src/components/ProfileDeleteDialog.test.tsx frontend/src/components/MainView.test.tsx
+git commit -m "feat: add profile clone and delete controls"
+```
diff --git a/docs/superpowers/plans/2026-09-25-windows-portable-and-msi.md b/docs/superpowers/plans/2026-09-25-windows-portable-and-msi.md
new file mode 100644
index 0000000..6e2a5ee
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-25-windows-portable-and-msi.md
@@ -0,0 +1,158 @@
+# Windows Portable and MSI Distribution Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use Markdown task checkboxes.
+
+**Goal:** Publish a portable Windows executable and a dual-scope guided MSI installer from the existing CI release workflow.
+
+**Architecture:** Keep Wails responsible for the application executable and add a WiX-authored MSI that installs that executable with a current-user or all-users scope choice. CI verifies and uploads both artifacts; the update checker points to the renamed portable asset.
+
+**Tech Stack:** Go, Wails v2, PowerShell, WiX Toolset, GitHub Actions, GitHub CLI.
+
+**Spec:** [2026-09-25-profile-script-editor-and-windows-installer-design.md](../specs/2026-09-25-profile-script-editor-and-windows-installer-design.md)
+
+## Global Constraints
+
+- Name the portable release asset go-script-sql-runner-portable.exe.
+- Name the installer release asset go-script-sql-runner-setup.msi.
+- Current-user installation goes under %LOCALAPPDATA%\Programs\Go Script SQL Runner without administrator rights.
+- All-users installation goes under %ProgramFiles%\Go Script SQL Runner and requests elevation.
+- Regardless of installation scope, application data remains per-user in AppData.
+- CI builds and verifies both artifacts and publishes both assets on the existing automated GitHub Release.
+- The update checker searches for go-script-sql-runner-portable.exe.
+
+## Review Focus
+
+- A typo or missing CI artifact must fail the job before attempting release publication; pin with workflow checks for both exact file names.
+- Per-user MSI installation must never write files or registry entries into machine locations; verify with a Windows install/uninstall smoke test and inspect authored directory/registry scopes.
+- Per-machine MSI installation must target Program Files and use Windows Installer elevation behavior; verify the selected MSI properties and manual UAC install.
+- Upgrade must replace the installed app while preserving user profiles; validate stable UpgradeCode/major upgrade behavior and inspect the user AppData directory after upgrade.
+- Missing portable Release asset must not break update checking or return an installer URL as the portable download; pin in update-checker tests.
+
+---
+
+### Task 1: Author and locally build the dual-scope MSI
+
+**Files:**
+- Create: build/windows/installer/Package.wxs
+- Create: build/windows/installer/ScopeSelectionDlg.wxs
+- Create: scripts/build-windows-installer.ps1
+- Modify: scripts/build.ps1
+
+**Interfaces:**
+- The installer consumes build/bin/go-script-sql-runner-portable.exe.
+- The installer build produces build/bin/go-script-sql-runner-setup.msi.
+- The MSI exposes a wizard choice for current user versus all users.
+
+- [ ] **Step 1: Add a WiX source validation/build script**
+
+Create a PowerShell build helper that accepts an application version and executable path, fails if the EXE is missing, invokes the pinned WiX Toolset CLI version 7.0.0 and UI extension version 7.0.0, and writes the MSI to the specified output directory. Keep the WiX and extension versions fixed in the repository and CI setup so the same source uses the same compiler.
+
+- [ ] **Step 2: Author MSI package identity and file layout**
+
+Define the product name, manufacturer, fixed UpgradeCode, package version, x64 architecture, and major-upgrade rule. Install the one portable executable under a scope-dependent install directory. Add a Start Menu shortcut and Add/Remove Programs metadata. Do not author profile/log directories under Program Files.
+
+- [ ] **Step 3: Add dual-scope selection and wizard navigation**
+
+Author the package as perUserOrMachine with ALLUSERS=2 and MSIINSTALLPERUSER=1 as the default. Add a wizard dialog with a radio choice that keeps MSIINSTALLPERUSER=1 for current user and clears MSIINSTALLPERUSER for all users. Current-user install directory is LocalAppDataFolder\Programs\Go Script SQL Runner; all-users install directory is ProgramFiles64Folder\Go Script SQL Runner. Confirm the selection occurs before the execute sequence so Windows Installer can request elevation when the user chooses all users.
+
+- [ ] **Step 4: Add MSI source-level checks**
+
+Add a PowerShell validation step to the helper that runs WiX compilation/linking and checks the output file exists and is non-empty. Include a test invocation using version 0.1.123; inspect the built MSI properties with Orca and assert package scope supports either context and UpgradeCode is stable.
+
+- [ ] **Step 5: Update the local build script to emit both artifacts**
+
+Change scripts/build.ps1 to build Wails output as go-script-sql-runner-portable.exe, verify it exists, then invoke the WiX build helper with the same output directory and app version. Keep existing frontend and Go build steps intact.
+
+- [ ] **Step 6: Build both artifacts locally**
+
+Run: .\\scripts\\build.ps1
+
+Expected: both build/bin/go-script-sql-runner-portable.exe and build/bin/go-script-sql-runner-setup.msi exist. The MSI wizard offers both scopes and the current-user selection completes without elevation on a standard account.
+
+- [ ] **Step 7: Commit installer authoring and local build integration**
+
+```powershell
+git add build/windows/installer scripts/build.ps1
+git commit -m "build: add dual-scope Windows MSI installer"
+```
+
+### Task 2: Publish both artifacts and update the portable download lookup
+
+**Files:**
+- Modify: .github/workflows/ci.yml
+- Modify: internal/updatecheck/checker.go
+- Modify: internal/updatecheck/checker_test.go
+
+**Interfaces:**
+- Release assets are go-script-sql-runner-portable.exe and go-script-sql-runner-setup.msi.
+- The checker returns DownloadURL for the portable EXE only.
+
+- [ ] **Step 1: Update checker tests for the renamed asset**
+
+Change fixture releases to include both portable and MSI assets. Assert the portable asset URL is selected, MSI URL is never selected, and absent portable asset leaves DownloadURL empty while the rest of release metadata is returned.
+
+- [ ] **Step 2: Change the checker asset constant**
+
+Set the expected asset name to go-script-sql-runner-portable.exe and keep the exact name match behavior. Do not silently fall back to the MSI asset.
+
+- [ ] **Step 3: Run focused checker tests**
+
+Run: go test ./internal/updatecheck -count=1
+
+Expected: PASS for portable selection, MSI exclusion, and missing asset behavior.
+
+- [ ] **Step 4: Update CI build, artifact upload, and release steps**
+
+Install the pinned WiX CLI 7.0.0 and WixToolset.UI.wixext 7.0.0 in the Windows build job. Build the portable EXE with Wails and the MSI with the repository helper using MSI version 0.1.. Verify both exact filenames, upload both in the same named artifact, download them in the release job, verify both again, and pass both paths to gh release upload/create. Set user-facing asset labels to “Windows portable executable” and “Windows installer (MSI)”.
+
+- [ ] **Step 5: Validate workflow syntax and release asset references**
+
+Run: `go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 .github/workflows/ci.yml`
+
+Search the workflow for stale go-script-sql-runner.exe references and confirm none remain in upload, verify, and publish commands.
+
+- [ ] **Step 6: Commit CI and update-check integration**
+
+```powershell
+git add .github/workflows/ci.yml internal/updatecheck/checker.go internal/updatecheck/checker_test.go
+git commit -m "ci: publish portable exe and Windows MSI"
+```
+
+### Task 3: Document distribution and perform Windows install matrix verification
+
+**Files:**
+- Modify: README.md
+- Modify: scripts/build.ps1 if smoke-test findings require a build correction
+- Modify: build/windows/installer sources if install-matrix findings require an MSI correction
+
+**Interfaces:**
+- README documents how to download/use the portable executable and how MSI scope affects the install destination.
+
+- [ ] **Step 1: Update download and local build documentation**
+
+Document both GitHub Release assets. Explain that the portable EXE does not install the application, the MSI offers current-user and all-users choices, the former installs under LocalAppData without administrator rights, and the latter installs under Program Files and requests elevation. Clarify that profiles and logs remain in each Windows user's AppData in both cases.
+
+- [ ] **Step 2: Run full Go/frontend build validation**
+
+Run: go test ./...
+
+Run in frontend: npm ci
+
+Run in frontend: npm test
+
+Run in frontend: npm run build
+
+Run: .\\scripts\\build.ps1
+
+Expected: PASS and both release artifacts are created with exact names.
+
+- [ ] **Step 3: Exercise the MSI context and upgrade matrix on Windows**
+
+On a standard user account, install current-user scope and verify files/shortcuts are under the user context, the app launches, and profiles/logs remain under that user's AppData. Upgrade that per-user install with a higher MSI version and confirm its profile remains. Uninstall, then install all-users scope and confirm Windows requests administrator approval and installs under Program Files. Create a test profile, upgrade that per-machine install with a higher MSI version and the same UpgradeCode, and verify the profile data remains unchanged. Uninstall and confirm profile data remains present.
+
+- [ ] **Step 4: Commit distribution documentation and any verified packaging fixes**
+
+```powershell
+git add README.md scripts/build.ps1 build/windows/installer
+git commit -m "docs: document portable and MSI distribution"
+```
diff --git a/docs/superpowers/specs/2026-09-25-profile-script-editor-and-windows-installer-design.md b/docs/superpowers/specs/2026-09-25-profile-script-editor-and-windows-installer-design.md
new file mode 100644
index 0000000..c7ca026
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-25-profile-script-editor-and-windows-installer-design.md
@@ -0,0 +1,108 @@
+# Profile Management, SQL Script Editing, and Windows Distribution Design
+
+**Date:** 2026-09-25
+**Status:** Reviewed and approved by the user on 2026-09-25.
+
+## Goal
+
+Extend the existing Wails desktop app so users can edit stored SQL scripts, delete and clone profiles, and choose between a portable executable and a guided Windows installer. The GitHub Release produced by CI will contain both Windows distribution options.
+
+## Current context
+
+- The React UI already lists, enables, removes, and reorders scripts, and edits profile connection and execution settings.
+- SQL files live under each profile directory. React calls Go application services through Wails bindings; it does not access profile files directly.
+- The storage repository already supports deleting a profile directory, but the service/backend/UI do not expose profile deletion.
+- The service supports updating a profile while retaining its ID and script references. It does not yet clone profiles or edit stored SQL content.
+- CI currently builds and publishes a single Wails executable named `go-script-sql-runner.exe`.
+- The update checker currently searches GitHub Release assets for that fixed executable name.
+
+## Approved design
+
+### Script editing with Monaco
+
+Add an **Edit** action for every script beside the existing ordering controls. It opens a larger in-app dialog containing Monaco Editor configured for SQL syntax highlighting. The editor loads content through the Wails API and saves it through the Go application service to the existing profile-owned SQL file. The frontend will not write files directly.
+
+Saving changes only the script content; its profile, ID, name, order, enabled state, transaction mode, and file reference remain unchanged. Closing a modified editor without saving asks the user to confirm discarding the changes. Editing is unavailable while a profile run is active.
+
+### Profile deletion
+
+Add a **Delete profile** action for the selected profile. Before deletion, show a confirmation that identifies the profile by name and explains that its stored scripts are removed with it. The service deletes the profile through the repository. On success, refresh the profile list and select another profile if one remains; on failure, preserve the selection and show the error.
+
+### Profile cloning
+
+Add a **Clone profile** action. Create a separate profile with a new generated ID and a derived copy name. Copy connection settings including the password, execution settings, all script metadata, and the SQL file contents into the new profile's directory. Editing or deleting the clone must not change the source profile. The cloned profile can be renamed later using the existing profile editing dialog.
+
+Cloning and deletion run through the Go application service and storage repository. The frontend does not copy or remove filesystem paths.
+
+### Portable executable and installer
+
+Keep a standalone, non-installing executable and name its release asset `go-script-sql-runner-portable.exe`. It continues to store profiles and logs in the current per-user AppData location.
+
+Add a WiX-authored MSI named `go-script-sql-runner-setup.msi` with a guided installation flow and a scope choice:
+
+- **Current user:** install under `%LOCALAPPDATA%\Programs\Go Script SQL Runner` without administrator rights.
+- **All users:** install under `%ProgramFiles%\Go Script SQL Runner`; Windows Installer requests elevation for this machine-wide installation.
+
+Regardless of installation scope, application data remains per-user in AppData. The installer supplies normal application shortcuts and an uninstall registration, and uses stable package identity/versioning so later releases can upgrade an earlier MSI installation.
+
+CI builds and verifies both artifacts and publishes both assets on the existing automated GitHub Release. The update checker switches its fixed asset lookup to `go-script-sql-runner-portable.exe`; opening an update continues to download the portable build.
+
+### Alternatives considered
+
+- **Editing through the system's default SQL editor:** avoids embedding an editor, but requires launching an external application and complicates the save/reload path. Monaco was selected for an in-app editing flow.
+- **Separate per-user and per-machine MSI files:** duplicates installer maintenance and makes release choice less clear. A single dual-purpose MSI with a scope choice was selected.
+- **Single executable only:** keeps the current release simple but does not provide the requested guided installation or machine-wide option.
+
+## Components and interfaces
+
+The implementation will follow existing boundaries:
+
+- `internal/storage`: safe read/write access to a script file, profile deletion, and cloning/copying profile-owned files.
+- `internal/app`: context-aware use cases for script content read/write, profile deletion, and profile cloning, with validation and clear errors.
+- `internal/ui/wails`: bind the new service operations to Wails; do not expose raw paths for frontend file access.
+- `frontend/src/api`: extend the `RunnerApi` and Wails adapter with the new calls.
+- `frontend/src/components`: Monaco SQL editor dialog and profile deletion confirmation.
+- `frontend/src/App.tsx` and `frontend/src/state/useRunnerController.ts`: add the script and profile actions and refresh/selection behavior.
+- `.github/workflows/ci.yml` and a WiX source/build script under `build/windows` or `scripts`: build, verify, upload, and publish the portable EXE and MSI.
+- `internal/updatecheck`: search for the portable release asset's new filename.
+- `README.md`: explain the portable executable and both MSI installation scopes.
+
+Exact file names and helper boundaries may follow nearby repository conventions while preserving the behavior above.
+
+## Error handling and data safety
+
+- Validate profile and script IDs before resolving paths; resolve SQL paths only from validated profile metadata and enforce that resolved paths remain inside the profile directory.
+- Use atomic replacement for SQL edits so an interrupted write does not leave a truncated script.
+- Cloning should fail without modifying the source; avoid leaving an incomplete destination profile if copying or persistence fails.
+- A failed deletion must be visible to the user. Confirmation precedes recursive profile removal.
+- Treat installer and portable artifacts as equivalent app versions. Data remains in per-user AppData and is not moved or deleted by install/uninstall.
+- Continue embedding WebView2 according to the current Wails build configuration.
+
+## Verification and acceptance criteria
+
+### Backend
+
+- Unit tests cover script content reads/writes, unknown profile/script IDs, validation, preserving all script metadata during content updates, and ensuring writes stay within profile storage.
+- Service/repository tests cover clone independence (new ID, retained settings/password, identical SQL bytes, independent subsequent edits/deletes), deletion of all files for a profile, and no effect on other profiles.
+- Wails-facing tests or the existing bridge tests verify calls and error propagation.
+
+### Frontend
+
+- Component tests cover opening the editor with the selected SQL, saving content, cancelling unchanged content, confirming a dirty close, and disabled state while running.
+- UI tests cover profile deletion confirmation/cancel/error/success and cloning plus profile selection/refresh.
+- Frontend typecheck/build confirms Monaco and worker assets bundle correctly for Wails' local WebView.
+
+### Windows distribution
+
+- CI verifies both expected output files exist before upload and both named assets are published.
+- Validate the MSI's wizard has a current-user and all-users choice; verify the former installs to LocalAppData without elevation and the latter targets Program Files and requests elevation.
+- Verify MSI upgrade/uninstall behavior and that application profiles remain in per-user AppData.
+- Update-checker tests select the new portable filename and continue to work when an expected asset is absent.
+- Run the Go and frontend test suites and typecheck through CI.
+
+## Out of scope
+
+- SQL semantic validation, database-aware completion, formatting, or schema introspection in Monaco.
+- Editing script names, order, or execution properties in the SQL editor dialog.
+- Moving profile data into the all-users installation directory or sharing profiles between Windows accounts.
+- Automatic in-place application of updates; the update notice continues to open the portable asset/release page behavior already supported by the app.
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 086d772..266d12f 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,8 @@
"name": "go-script-sql-runner-frontend",
"version": "0.1.0",
"dependencies": {
+ "@monaco-editor/react": "4.7.0",
+ "monaco-editor": "0.57.0",
"react": "19.2.8",
"react-dom": "19.2.8"
},
@@ -359,6 +361,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@monaco-editor/loader": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
+ "license": "MIT",
+ "dependencies": {
+ "state-local": "^1.0.6"
+ }
+ },
+ "node_modules/@monaco-editor/react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@monaco-editor/loader": "^1.5.0"
+ },
+ "peerDependencies": {
+ "monaco-editor": ">= 0.25.0 < 1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz",
@@ -1131,6 +1156,13 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@vitejs/plugin-react": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz",
@@ -1434,6 +1466,15 @@
"license": "MIT",
"peer": true
},
+ "node_modules/dompurify": {
+ "version": "3.4.15",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz",
+ "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.24.5",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
@@ -1921,6 +1962,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/marked": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/mdn-data": {
"version": "2.27.1",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
@@ -1938,6 +1991,16 @@
"node": ">=4"
}
},
+ "node_modules/monaco-editor": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.57.0.tgz",
+ "integrity": "sha512-5BkI9KGoqrNvBGUe15/QlZq3OooZ8WLg1AxTpaqHRCP3HNpzPPZKE2EDz8M7c+VRmCeUw1Brp4cx/PWm3kI/5A==",
+ "license": "MIT",
+ "dependencies": {
+ "dompurify": "3.4.15",
+ "marked": "14.0.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
@@ -2196,6 +2259,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/state-local": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
+ "license": "MIT"
+ },
"node_modules/std-env": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 943aa7a..60b54d9 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,6 +11,8 @@
"preview": "vite preview"
},
"dependencies": {
+ "@monaco-editor/react": "4.7.0",
+ "monaco-editor": "0.57.0",
"react": "19.2.8",
"react-dom": "19.2.8"
},
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8503a1d..ab1371f 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,8 +1,10 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { wailsRunnerApi } from './api/runner'
import type { OnError, RunnerApi, TransactionMode, UpdateInfo } from './api/types'
import ProfileDialog from './components/ProfileDialog'
+import ProfileDeleteDialog from './components/ProfileDeleteDialog'
import SchemaSelect from './components/SchemaSelect'
+import ScriptEditorDialog from './components/ScriptEditorDialog'
import UpdateNotice from './components/UpdateNotice'
import { useRunnerController } from './state/useRunnerController'
@@ -31,7 +33,13 @@ export default function App({ api = wailsRunnerApi }: AppProps) {
const controller = useRunnerController(api)
const [detailedLogs, setDetailedLogs] = useState(false)
const [profileDialogMode, setProfileDialogMode] = useState<'create' | 'edit' | null>(null)
+ const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null)
+ const [editorTarget, setEditorTarget] = useState<{ profileID: string; scriptID: string; scriptName: string; content: string } | null>(null)
+ const [editorLoading, setEditorLoading] = useState(false)
+ const [editorSaving, setEditorSaving] = useState(false)
const [updateInfo, setUpdateInfo] = useState(null)
+ const runningRef = useRef(controller.running)
+ runningRef.current = controller.running
const profile = controller.selectedProfile
const scripts = [...(profile?.scripts ?? [])].sort((left, right) => left.order - right.order)
const connectionSummary = profile
@@ -55,6 +63,20 @@ export default function App({ api = wailsRunnerApi }: AppProps) {
}
}, [api])
+ const openScriptEditor = async (profileID: string, scriptID: string, scriptName: string) => {
+ if (runningRef.current || editorLoading) return
+ controller.clearError()
+ setEditorLoading(true)
+ try {
+ const content = await controller.loadScriptContent(profileID, scriptID)
+ if (content !== null && !runningRef.current) {
+ setEditorTarget({ profileID, scriptID, scriptName, content })
+ }
+ } finally {
+ setEditorLoading(false)
+ }
+ }
+
return (
<>
@@ -92,6 +114,27 @@ export default function App({ api = wailsRunnerApi }: AppProps) {
>
Edit
+
+