@painda/wizard
v0.1.1
Published
Modular Electron installer wizard — drop-in replacement for NSIS with a modern animated UI.
Downloads
188
Maintainers
Readme
@painda/wizard
Modern Electron installer wizard — a drop-in replacement for NSIS with an animated, themeable setup UI for Windows.
Instead of writing .nsi scripts, you write a single TypeScript config. painda wraps your existing electron-builder output into a polished self-contained installer .exe with a professional wizard UI.
Features
- 🎨 Fully themeable — brand color, logo, dark/light/auto mode
- 📄 5 configurable pages — Welcome · License · Privacy · Options · Progress · Finish
- 🔧 Maintenance mode — Reinstall / Repair / Uninstall if already installed
- 🔄 Update mode — Block-assembly animation when launched via electron-updater
- 🗂️ Windows-native — Registry entries, desktop/start-menu shortcuts, uninstall script
- 🔏 Code signing — Pass your
.pfxcertificate, painda handles the rest - 🚫 No admin required — Installs to
%LOCALAPPDATA%by default (HKCU registry) - ⚡ Zero consumer build step — TypeScript config transpiled on the fly via
jiti
Installation
npm install -D @painda/wizardThat's it. No separate runtime package needed — everything is bundled.
Quick Start
1. Scaffold
npx painda initThis creates painda.config.ts and installer-assets/ in your project root, and adds a build:installer script to your package.json.
2. Configure
Edit the generated painda.config.ts:
import { defineConfig } from '@painda/wizard';
export default defineConfig({
app: {
id: 'com.acme.myapp', // reverse-DNS, used for registry + install path
name: 'My App',
version: '1.0.0',
publisher: 'Acme Inc.',
tagline: 'The best app ever',
},
branding: {
primaryColor: '#2563eb',
logo: './installer-assets/logo.png', // square PNG ≥ 256×256
theme: 'dark',
},
pages: {
welcome: {
title: 'Welcome to My App',
body: 'This wizard will install My App on your computer.',
},
options: {
defaultPath: '${LOCALAPPDATA}\\My App',
desktopShortcut: true,
startMenuShortcut: true,
},
finish: { runApp: true },
},
payload: {
// Path to the win-unpacked directory from electron-builder --dir
from: 'dist-electron-bin/win-unpacked',
executable: 'My App.exe',
},
output: {
dir: 'dist-installer',
filename: '${name}-Setup-${version}.exe',
},
});3. Build your Electron app
painda wraps an already-built Electron app. First produce the unpacked directory:
electron-builder --dir
# → dist-electron-bin/win-unpacked/Or in your package.json:
{
"build": {
"win": {
"target": [{ "target": "dir", "arch": ["x64"] }]
}
}
}4. Build the installer
npx painda build
# or
npm run build:installerOutput: dist-installer/My-App-Setup-1.0.0.exe
Config Reference
app
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | ✅ | Reverse-DNS app ID (e.g. com.acme.myapp). Used for registry keys and install path. |
| name | string | ✅ | Display name shown throughout the wizard. |
| version | string | ✅ | Semantic version (e.g. 1.2.3). |
| publisher | string | — | Publisher name in Add/Remove Programs. Default: Unknown Publisher. |
| publisherUrl | string | — | Link shown on the Finish page. |
| tagline | string | — | One-liner below the app name on the Welcome page. |
branding
| Field | Type | Description |
|-------|------|-------------|
| primaryColor | #rrggbb | Brand color for buttons, progress, highlights. Default: #4f46e5. |
| accentColor | #rrggbb | Secondary accent color. |
| logo | string | Path to a square PNG (≥ 256×256) shown in the wizard header. |
| icon | string | Path to PNG used as the installer .exe icon. Defaults to logo. |
| backgroundImage | string | Optional background image for the wizard window. |
| fontFamily | string | CSS font-family value (e.g. 'Inter', sans-serif). |
| theme | 'light' \| 'dark' \| 'auto' | Visual theme. Default: 'auto'. |
pages
All pages are optional. Omit a page to skip it.
welcome
welcome: {
enabled: true, // default
title?: string,
subtitle?: string,
body?: string,
}license
license: {
enabled: true,
file: './installer-assets/license.txt', // .txt or .md
mustScroll: false, // require scroll-to-bottom before accepting
}privacy
privacy: {
enabled: true,
file: './installer-assets/privacy.txt',
}options
options: {
enabled: true,
allowChangeInstallPath: true,
defaultPath: '${LOCALAPPDATA}\\My App', // supports: ${LOCALAPPDATA}, ${PROGRAMFILES}, ${appId}, ${appName}
desktopShortcut: true,
startMenuShortcut: true,
launchOnStartup: false,
}finish
finish: {
runApp: true, // auto-launch app after install
links?: [{ label: string; url: string }],
}payload
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| from | string | ✅ | Path to the win-unpacked directory from electron-builder --dir. |
| executable | string | ✅ | The .exe filename inside from. Case-sensitive. |
output
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| dir | string | dist-installer | Output directory for the produced .exe. |
| filename | string | ${name}-Setup-${version}.exe | Filename template. Tokens: ${name}, ${version}, ${arch}. |
| arch | 'x64' \| 'arm64' | x64 | Target architecture. |
signing (optional)
signing: {
certificateFile: './certs/my-cert.pfx',
certificatePasswordEnv: 'CERT_PASSWORD', // env var name (never hardcode the password)
timestampUrl: 'http://timestamp.digicert.com',
}Set CERT_PASSWORD=yourpassword in your environment before running painda build.
Path Tokens
The defaultPath field in options supports these tokens, resolved on the user's machine:
| Token | Resolves to |
|-------|-------------|
| ${LOCALAPPDATA} | C:\Users\<user>\AppData\Local |
| ${PROGRAMFILES} | C:\Program Files |
| ${appId} | The value of app.id |
| ${appName} | The value of app.name |
Recommended default paths:
// No admin required (recommended):
defaultPath: '${LOCALAPPDATA}\\My App'
// Requires admin (use only if needed):
defaultPath: '${PROGRAMFILES}\\My App'Package.json Integration
After painda init, your package.json will have:
{
"scripts": {
"build:installer": "painda build"
}
}Typical full workflow:
{
"scripts": {
"build": "tsc -b && vite build && tsc -p tsconfig.electron.json",
"build:dir": "npm run build && electron-builder --dir",
"build:installer": "npm run build:dir && painda build",
"release": "npm run build:installer"
}
}Maintenance Mode
If the user runs the installer when the app is already installed, painda automatically shows a Maintenance page with three options:
- Reinstall — overwrites existing files
- Repair — same as reinstall, useful for corrupt installs
- Uninstall — removes the app, shortcuts, and registry entries
No extra config needed — this is detected automatically.
Update Mode (electron-updater integration)
painda can be used as the update UI for electron-updater. When a new version is downloaded, instead of a plain dialog, launch the new installer with special flags:
// In your Electron main process (electron-updater hook):
import { spawn } from 'child_process';
import { app } from 'electron';
autoUpdater.on('update-downloaded', (info) => {
// Launch painda installer in update mode
const installerPath = info.downloadedFile; // path to downloaded installer
spawn(installerPath, [
'--painda-update',
'--install-path', app.getPath('exe').replace(/\\[^\\]+$/, ''),
], { detached: true, stdio: 'ignore' }).unref();
app.quit();
});In update mode, painda shows a block-assembly animation (no wizard pages) and silently installs the update.
CLI Commands
painda init Scaffold painda.config.ts + installer-assets/ + inject package.json scripts
painda init --force Overwrite existing config
painda init --no-scripts Skip package.json injection
painda build Build the installer EXE
painda build -v Verbose output (shows electron-builder logs)
painda check Validate config without buildingUsing with electron-builder
painda is not a replacement for electron-builder — it works alongside it. The typical setup:
electron-buildercompiles your Electron app with--dir(no packaging)painda buildwraps that directory into a polished installer EXE
Your package.json build config still controls how your Electron app is compiled. The win.target for the build:dir step should be "dir":
{
"build": {
"appId": "com.acme.myapp",
"win": {
"target": [{ "target": "dir", "arch": ["x64"] }]
},
"publish": [
{
"provider": "generic",
"url": "https://updates.example.com/win/"
}
]
}
}Tip: The
publishconfig ensuresapp-update.ymlis generated in thewin-unpacked/resources/directory, whichelectron-updaterneeds at runtime.
What Gets Installed
After running the installer, the user's machine has:
%LOCALAPPDATA%\My App\
My App.exe
resources\
app.asar
app-update.yml
...
uninstall.ps1 ← auto-generated uninstall script
%APPDATA%\Microsoft\Windows\Start Menu\Programs\My App.lnk
%USERPROFILE%\Desktop\My App.lnk
HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\com.acme.myapp
DisplayName = My App
DisplayVersion = 1.0.0
Publisher = Acme Inc.
UninstallString = powershell.exe ... uninstall.ps1The app appears in Settings → Apps and can be uninstalled normally.
TypeScript Support
Full TypeScript support out of the box. painda.config.ts is transpiled automatically — no ts-node or tsc step required.
import { defineConfig } from '@painda/wizard';
// Full autocomplete and type checking for all config fields
export default defineConfig({ ... });The defineConfig helper is a type-safe identity function that gives you IntelliSense without requiring a build step.
Monorepo / CI
In CI, just install as a devDependency and run:
- run: npm ci
- run: npm run build:dir # electron-builder --dir
- run: npx painda build # produce installerNo extra setup needed — the runtime is bundled inside @painda/wizard.
License
MIT
