Compare commits

..

9 commits

Author SHA1 Message Date
X9 Dev
d05835c931 Fix runner: add daemon entrypoint and docker socket permissions
Some checks failed
release / build-and-release (push) Failing after 18s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:40:36 +02:00
X9 Dev
9a4afbf913 Add CI/CD: auto-build and publish xetup.exe on push to main
- .forgejo/workflows/release.yml: builds Windows exe on Go/script changes,
  deletes and recreates floating 'latest' release with new asset
- web/nginx.conf: /dl now points to .../download/latest/xetup.exe permanently
- runner.go: fix embed.FS path separator (filepath -> path) for Windows compat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:37:17 +02:00
X9 Dev
f72b4bd190 Fix embed.FS path separator on Windows
Use path.Join (always '/') for embed.FS reads, filepath.Join only for OS paths.
filepath.Join on Windows produces backslashes which embed.FS doesn't accept,
causing "failed to extract scripts" on startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:33:57 +02:00
X9 Dev
c3c8d8e501 Simplify install command to plain curl, add /dl shortlink
- nginx.conf: add /dl -> latest xetup.exe release redirect (update on each release)
- index.html: replace irm/iex command with 'curl -Lo xetup.exe xetup.x9.cz/dl'
  works with built-in curl.exe on Win10/11, no PowerShell execution policy needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:30:22 +02:00
X9 Dev
82349dbe31 Add install command box and fix Forgejo API proxy on landing page
- nginx.conf: add /forgejo-api/ proxy location to xetup-forgejo:3000
- index.html: add install command box (irm xetup.x9.cz/get.ps1 | iex)
  with one-click copy button; remove broken API token from JS
- get.ps1: PowerShell installer that fetches latest release URL and runs exe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:28:28 +02:00
X9 Dev
5b53b2a0d6 Add per-feature toggles to PS scripts and Go TUI
- 02-software.ps1: wrap wingetInstalls, pdfDefault, ateraAgent in Get-Feature guards
- 03-system-registry.ps1: add Get-Feature, restructure into 5 gated blocks
  (systemTweaks, edgePolicies, oneDriveUninstall, powercfg, proxyDisable)
- 04-default-profile.ps1: add Get-Feature, wrap taskbarTweaks, startMenuTweaks,
  explorerTweaks; add missing explorerTweaks code (ShowRecent, ShowFrequent, FullPath)
- 11-dell-update.ps1: add Get-Feature, split update run by drivers/bios feature flags
- runner.go: add Feature/StepFeatures/SelectableItem/AllSelectableItems for TUI
- config.go: add Features type and defaults for all 4 gated steps
- tui.go: use AllSelectableItems for MultiSelect, build Features map in startRun,
  remove unused stepFeaturesMap variable
- xetup.exe: Windows amd64 build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:11:51 +02:00
X9 Dev
a10a3a8aa2 fix: move Dell Command | Update card to 'Stavajici kroky' section
Was incorrectly placed under 'Nove kroky (planovane)'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:51:33 +02:00
X9 Dev
be7a7236df feat: Dell Command | Update step (step 11) + download link on landing page
scripts/11-dell-update.ps1:
- Detects Dell via Win32_ComputerSystem.Manufacturer (skips silently on non-Dell)
- Installs Dell.CommandUpdate.Universal via winget (silent)
- Runs dcu-cli.exe /applyUpdates -silent -reboot=disable (all categories)
- BIOS/firmware staged, completes on restart after deployment
- Exit codes 0/1/5 all treated as success

Deploy-Windows.ps1:
- Added Step 11 - Dell Command | Update (dellUpdate=true default)

internal/runner/runner.go, internal/config/config.go:
- dellUpdate step registered in AllSteps() and DefaultConfig

web/spec/index.html:
- Step 11 card with flag-done rows, sidebar link, comment-widget issue #16
- STEP_SCRIPT map updated for step-dell

web/index.html:
- Dynamic download strip: fetches latest Forgejo release via API,
  shows Download xetup.exe with version + file size
- Updated Go TUI card text (no longer "zatim ve vyvoji")

web/data/descriptions.json: regenerated (13 scripts, 80 items)

Forgejo: issue #16 created, release v0.1.0 published with xetup.exe (5.2 MB)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:49:15 +02:00
X9 Dev
1198de3c49 feat: Go TUI launcher (xetup.exe) + spec page new-request section
- embed.go: root package exposes Scripts/Assets embed.FS
- internal/config: Config struct, Load/Save/Default
- internal/runner: Step list, Runner with context cancel, log streaming
- internal/tui: bubbletea model - huh form (phase 1) + live log view (phase 2) + summary (phase 3)
- cmd/xetup/main.go: main binary, extracts embedded content to tmpdir, runs TUI
- Builds to 5.2 MB xetup.exe (GOOS=windows GOARCH=amd64)

spec/index.html:
- arch-xetup section: mark 5 items flag-done (code now exists)
- Add "Nove nastaveni" section linked to Forgejo issue #15
- Add sidebar link for new-requests
- Add CSS + JS for request widget (loads/posts to issue #15 comments)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:35:22 +02:00
20 changed files with 2027 additions and 444 deletions

View file

@ -0,0 +1,63 @@
name: release
on:
push:
branches: [main]
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
- 'scripts/**'
- 'assets/**'
- 'embed.go'
jobs:
build-and-release:
runs-on: ubuntu-latest
container:
image: golang:1.23-alpine
steps:
- uses: actions/checkout@v4
- name: Build xetup.exe
run: |
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o xetup.exe ./cmd/xetup/
echo "Built: $(ls -lh xetup.exe | awk '{print $5}')"
- name: Publish latest release
env:
TOKEN: ${{ secrets.FORGEJO_TOKEN }}
API: ${{ github.server_url }}/api/v1
REPO: ${{ github.repository }}
run: |
apk add --no-cache curl jq
SHORT=$(echo "$GITHUB_SHA" | cut -c1-7)
# Delete existing 'latest' release (and its tag) to recreate cleanly
RID=$(curl -sf -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/tags/latest" | jq -r '.id // empty')
if [ -n "$RID" ]; then
curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/$RID" || true
fi
curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/tags/latest" || true
# Create new 'latest' release
RID=$(curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$API/repos/$REPO/releases" \
-d "{\"tag_name\":\"latest\",\"name\":\"latest\",\"body\":\"Auto-built from ${SHORT}\",\"prerelease\":true}" \
| jq -r '.id')
# Upload xetup.exe as release asset
curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/octet-stream" \
"$API/repos/$REPO/releases/$RID/assets?name=xetup.exe" \
--data-binary @xetup.exe
echo "Released xetup.exe (commit ${SHORT})"

View file

@ -113,6 +113,7 @@ $stepsEnabled = @{
network = $true
pcIdentity = $true
activation = $true
dellUpdate = $true
}
if ($Config -and $Config.steps) {
foreach ($key in @($stepsEnabled.Keys)) {
@ -221,6 +222,15 @@ if ($stepsEnabled['network']) {
}
} else { Skip-Step "Step 9 - Network" }
# -----------------------------------------------------------------------
# Step 11 - Dell Command | Update (auto-skipped on non-Dell hardware)
# -----------------------------------------------------------------------
if ($stepsEnabled['dellUpdate']) {
Invoke-Step -Name "Step 11 - Dell Command | Update" -Action {
& "$ScriptRoot\scripts\11-dell-update.ps1" -Config $Config -LogFile $LogFile
}
} else { Skip-Step "Step 11 - Dell Command | Update" }
# -----------------------------------------------------------------------
# Step 10 - PC identity (rename + C:\X9) - runs last, rename needs restart
# -----------------------------------------------------------------------

69
cmd/xetup/main.go Normal file
View file

@ -0,0 +1,69 @@
// Command xetup is the interactive TUI launcher for Windows deployment.
// It embeds all PowerShell scripts and assets into a single self-contained
// binary, extracts them to a temp directory at runtime, collects configuration
// via an interactive form, and streams live log output while the scripts run.
//
// Cross-compile for Windows:
//
// GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o xetup.exe ./cmd/xetup
package main
import (
"fmt"
"log"
"os"
"path/filepath"
tea "github.com/charmbracelet/bubbletea"
xetupembed "git.xetup.x9.cz/x9/xetup"
"git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/runner"
"git.xetup.x9.cz/x9/xetup/internal/tui"
)
func main() {
// Load config (silently falls back to defaults when missing)
cfgPath := config.ConfigPath()
cfg, err := config.Load(cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: config load failed (%v), using defaults\n", err)
}
// Create a temp working directory; cleaned up on exit
tmpDir, err := os.MkdirTemp("", "xetup-*")
if err != nil {
log.Fatalf("Cannot create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Extract embedded scripts
if err := runner.ExtractScripts(xetupembed.Scripts, tmpDir); err != nil {
log.Fatalf("Failed to extract scripts: %v", err)
}
// Extract embedded assets
if err := runner.ExtractAssets(xetupembed.Assets, tmpDir); err != nil {
log.Fatalf("Failed to extract assets: %v", err)
}
// Write runtime config JSON so PowerShell scripts can read it
cfgRuntimePath, err := runner.WriteConfig(cfg, tmpDir)
if err != nil {
log.Fatalf("Failed to write runtime config: %v", err)
}
runCfg := runner.RunConfig{
ScriptsDir: filepath.Join(tmpDir, "scripts"),
ConfigPath: cfgRuntimePath,
LogFile: `C:\Windows\Setup\Scripts\Deploy.log`,
ProfileType: cfg.Deployment.ProfileType,
}
// Launch the TUI
m := tui.NewModel(cfg, runCfg)
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
log.Fatalf("TUI error: %v", err)
}
}

View file

@ -38,6 +38,8 @@ services:
image: code.forgejo.org/forgejo/runner:6.3.1
container_name: xetup-runner
restart: unless-stopped
entrypoint: ["/bin/sh", "-c", "forgejo-runner daemon"]
user: "0:996" # root:docker - needed for /var/run/docker.sock access
depends_on:
- forgejo
environment:

16
embed.go Normal file
View file

@ -0,0 +1,16 @@
// Package xetup exposes embedded PowerShell scripts and assets for use by
// cmd/xetup. Placing the embed declarations here (at the module root) gives
// the //go:embed directives a clear, stable relative path to the content.
package xetup
import "embed"
// Scripts holds all PowerShell scripts from the scripts/ directory.
//
//go:embed scripts
var Scripts embed.FS
// Assets holds all deployment assets from the assets/ directory.
//
//go:embed assets
var Assets embed.FS

32
go.mod Normal file
View file

@ -0,0 +1,32 @@
module git.xetup.x9.cz/x9/xetup
go 1.24.0
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/huh v1.0.0 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.23.0 // indirect
)

55
go.sum Normal file
View file

@ -0,0 +1,55 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=

145
internal/config/config.go Normal file
View file

@ -0,0 +1,145 @@
package config
import (
"encoding/json"
"os"
"path/filepath"
)
// Config mirrors config.json structure.
type Config struct {
Deployment Deployment `json:"deployment"`
AdminAccount AdminAccount `json:"adminAccount"`
Activation Activation `json:"activation"`
Software Software `json:"software"`
Steps map[string]bool `json:"steps"`
Features Features `json:"features"`
}
type Deployment struct {
PCName string `json:"pcName"`
PCDescription string `json:"pcDescription"`
Timezone string `json:"timezone"`
ProfileType string `json:"profileType"` // default | admin | user
}
type AdminAccount struct {
Username string `json:"username"`
}
type Activation struct {
ProductKey string `json:"productKey"`
KMSServer string `json:"kmsServer"`
}
type SoftwareItem struct {
Name string `json:"name"`
WingetID string `json:"wingetId"`
}
type Software struct {
Install []SoftwareItem `json:"install"`
}
// Features holds per-step, per-feature toggle flags.
// Keys: stepID -> featureID -> enabled.
// A missing key defaults to true (feature enabled).
type Features map[string]map[string]bool
// DefaultConfig returns a config with sensible defaults.
func DefaultConfig() Config {
return Config{
Deployment: Deployment{
Timezone: "Central Europe Standard Time",
ProfileType: "default",
},
AdminAccount: AdminAccount{
Username: "adminx9",
},
Activation: Activation{
ProductKey: "",
},
Software: Software{
Install: []SoftwareItem{
{Name: "7-Zip", WingetID: "7zip.7zip"},
{Name: "Adobe Acrobat Reader 64-bit", WingetID: "Adobe.Acrobat.Reader.64-bit"},
{Name: "OpenVPN Connect", WingetID: "OpenVPNTechnologies.OpenVPNConnect"},
},
},
Steps: map[string]bool{
"adminAccount": true,
"bloatware": true,
"software": true,
"systemRegistry": true,
"defaultProfile": true,
"personalization": true,
"scheduledTasks": true,
"backinfo": true,
"activation": true,
"dellUpdate": true,
"network": true,
"pcIdentity": true,
},
Features: Features{
"software": {
"wingetInstalls": true,
"pdfDefault": true,
"ateraAgent": true,
},
"systemRegistry": {
"systemTweaks": true,
"edgePolicies": true,
"oneDriveUninstall": true,
"powercfg": true,
"proxyDisable": true,
},
"defaultProfile": {
"taskbarTweaks": true,
"startMenuTweaks": true,
"explorerTweaks": true,
},
"dellUpdate": {
"drivers": true,
"bios": true,
},
},
}
}
// Load reads config.json from the given path.
// If the file does not exist, returns DefaultConfig without error.
func Load(path string) (Config, error) {
cfg := DefaultConfig()
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return cfg, nil
}
if err != nil {
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}
// Save writes config to the given path (creates directories if needed).
func Save(cfg Config, path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
// ConfigPath returns the default config.json path (next to the executable).
func ConfigPath() string {
exe, err := os.Executable()
if err != nil {
return "config.json"
}
return filepath.Join(filepath.Dir(exe), "config.json")
}

336
internal/runner/runner.go Normal file
View file

@ -0,0 +1,336 @@
// Package runner executes PowerShell deployment scripts and streams log output.
package runner
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
)
// Step describes a single deployment step.
type Step struct {
ID string // e.g. "adminAccount"
Num string // display number e.g. "00"
Name string
ScriptName string // e.g. "00-admin-account.ps1"
Enabled bool
}
// AllSteps returns the ordered list of deployment steps.
func AllSteps() []Step {
return []Step{
{ID: "adminAccount", Num: "00", Name: "Admin ucet", ScriptName: "00-admin-account.ps1"},
{ID: "bloatware", Num: "01", Name: "Bloatware removal", ScriptName: "01-bloatware.ps1"},
{ID: "software", Num: "02", Name: "Software (winget)", ScriptName: "02-software.ps1"},
{ID: "systemRegistry", Num: "03", Name: "System Registry (HKLM)", ScriptName: "03-system-registry.ps1"},
{ID: "defaultProfile", Num: "04", Name: "Default Profile", ScriptName: "04-default-profile.ps1"},
{ID: "personalization", Num: "05", Name: "Personalizace", ScriptName: "05-personalization.ps1"},
{ID: "scheduledTasks", Num: "06", Name: "Scheduled Tasks", ScriptName: "06-scheduled-tasks.ps1"},
{ID: "backinfo", Num: "07", Name: "BackInfo", ScriptName: "07-backinfo.ps1"},
{ID: "activation", Num: "08", Name: "Windows aktivace", ScriptName: "08-activation.ps1"},
{ID: "dellUpdate", Num: "11", Name: "Dell Command | Update", ScriptName: "11-dell-update.ps1"},
{ID: "network", Num: "09", Name: "Network discovery", ScriptName: "10-network.ps1"},
{ID: "pcIdentity", Num: "10", Name: "PC identita", ScriptName: "09-pc-identity.ps1"},
}
}
// Feature is a single toggleable sub-item within a deployment step.
type Feature struct {
ID string
Label string
}
// StepFeatures returns per-step feature lists. Steps absent from this map
// have no sub-features and are controlled at the step level only.
func StepFeatures() map[string][]Feature {
return map[string][]Feature{
"software": {
{ID: "wingetInstalls", Label: "Instalace SW ze seznamu (winget)"},
{ID: "pdfDefault", Label: "Adobe Reader jako vychozi PDF"},
{ID: "ateraAgent", Label: "Atera RMM agent"},
},
"systemRegistry": {
{ID: "systemTweaks", Label: "Windows tweaky (Widgets, GameDVR, Recall...)"},
{ID: "edgePolicies", Label: "Edge policies (tlacitka, vyhledavac, telemetrie)"},
{ID: "oneDriveUninstall", Label: "OneDrive uninstall (consumer pre-install)"},
{ID: "powercfg", Label: "Nastaveni napajeni (timeout AC/DC)"},
{ID: "proxyDisable", Label: "Zakaz WPAD proxy auto-detect"},
},
"defaultProfile": {
{ID: "taskbarTweaks", Label: "Taskbar zarovnani, tlacitka, layout XML"},
{ID: "startMenuTweaks", Label: "Start menu cisteni pinu, Bing, Copilot"},
{ID: "explorerTweaks", Label: "Explorer pripony, LaunchTo, ShowRecent"},
},
"dellUpdate": {
{ID: "drivers", Label: "Dell drivery + firmware"},
{ID: "bios", Label: "Dell BIOS update"},
},
}
}
// SelectableItem is a single toggleable row in the TUI checklist.
// It represents either a whole step (FeatureID == "") or a specific feature.
type SelectableItem struct {
Key string // "stepID" or "stepID.featureID"
StepID string
FeatureID string // empty for step-level items
Label string
Num string
}
// AllSelectableItems returns the flat ordered list of all TUI toggle rows.
// Steps with features are expanded to individual feature rows.
// Steps without features appear as a single step-level row.
func AllSelectableItems() []SelectableItem {
steps := AllSteps()
features := StepFeatures()
var items []SelectableItem
for _, s := range steps {
feats, hasFeatures := features[s.ID]
if !hasFeatures {
items = append(items, SelectableItem{
Key: s.ID,
StepID: s.ID,
Label: s.Num + " " + s.Name,
Num: s.Num,
})
} else {
for _, f := range feats {
items = append(items, SelectableItem{
Key: s.ID + "." + f.ID,
StepID: s.ID,
FeatureID: f.ID,
Label: s.Num + " " + f.Label,
Num: s.Num,
})
}
}
}
return items
}
// RunConfig holds runtime parameters passed to each script.
type RunConfig struct {
ScriptsDir string
ConfigPath string
LogFile string
ProfileType string
}
// Result is the outcome of a single step.
type Result struct {
Step Step
Status string // "OK", "ERROR", "SKIPPED"
Elapsed time.Duration
}
// LogLine is a single output line from a running script.
type LogLine struct {
StepID string
Text string
Level string // INFO, OK, ERROR, WARN, STEP - parsed from [LEVEL] prefix
}
// Runner executes deployment steps sequentially.
type Runner struct {
cfg RunConfig
onLog func(LogLine)
onResult func(Result)
cancel context.CancelFunc
}
// New creates a Runner. onLog is called for each output line, onResult after each step.
func New(cfg RunConfig, onLog func(LogLine), onResult func(Result)) *Runner {
return &Runner{cfg: cfg, onLog: onLog, onResult: onResult}
}
// Run executes enabled steps sequentially. Blocks until done or context cancelled.
func (r *Runner) Run(ctx context.Context, steps []Step) []Result {
ctx, cancel := context.WithCancel(ctx)
r.cancel = cancel
defer cancel()
// Write config JSON to temp file so scripts can read it
cfgArg := r.cfg.ConfigPath
var results []Result
for _, step := range steps {
if !step.Enabled {
res := Result{Step: step, Status: "SKIPPED"}
r.onResult(res)
results = append(results, res)
continue
}
start := time.Now()
err := r.runScript(ctx, step, cfgArg)
elapsed := time.Since(start)
status := "OK"
if err != nil {
if ctx.Err() != nil {
status = "CANCELLED"
} else {
status = "ERROR"
}
}
res := Result{Step: step, Status: status, Elapsed: elapsed}
r.onResult(res)
results = append(results, res)
if ctx.Err() != nil {
break
}
}
return results
}
// Stop cancels the running deployment.
func (r *Runner) Stop() {
if r.cancel != nil {
r.cancel()
}
}
func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error {
scriptPath := filepath.Join(r.cfg.ScriptsDir, step.ScriptName)
// Build argument list
args := []string{
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-File", scriptPath,
"-LogFile", r.cfg.LogFile,
}
// Pass config object as JSON string (script reads it inline)
if cfgArg != "" {
args = append(args, "-Config", fmt.Sprintf("(Get-Content '%s' | ConvertFrom-Json)", cfgArg))
}
// ProfileType for step 04
if step.ID == "defaultProfile" && r.cfg.ProfileType != "" {
args = append(args, "-ProfileType", r.cfg.ProfileType)
}
cmd := exec.CommandContext(ctx, "powershell.exe", args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout // merge stderr into stdout
if err := cmd.Start(); err != nil {
return err
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
r.onLog(LogLine{
StepID: step.ID,
Text: line,
Level: parseLevel(line),
})
}
return cmd.Wait()
}
// parseLevel extracts the log level from lines formatted as "[HH:mm:ss] [LEVEL] message".
func parseLevel(line string) string {
if strings.Contains(line, "] [OK]") {
return "OK"
}
if strings.Contains(line, "] [ERROR]") {
return "ERROR"
}
if strings.Contains(line, "] [WARN]") {
return "WARN"
}
if strings.Contains(line, "] [STEP]") {
return "STEP"
}
return "INFO"
}
// ExtractScripts unpacks embedded scripts to a temp directory.
// Returns the directory path. Caller is responsible for cleanup.
func ExtractScripts(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, tmpDir string) error {
entries, err := fs.ReadDir("scripts")
if err != nil {
return fmt.Errorf("read embedded scripts: %w", err)
}
scriptsDir := filepath.Join(tmpDir, "scripts")
if err := os.MkdirAll(scriptsDir, 0755); err != nil {
return err
}
for _, e := range entries {
if e.IsDir() {
continue
}
// embed.FS always uses forward slashes regardless of OS
data, err := fs.ReadFile(path.Join("scripts", e.Name()))
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(scriptsDir, e.Name()), data, 0644); err != nil {
return err
}
}
return nil
}
// ExtractAssets unpacks embedded assets to tmpDir/assets.
func ExtractAssets(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, tmpDir string) error {
return extractDir(fs, "assets", tmpDir)
}
func extractDir(fs interface{ ReadDir(string) ([]os.DirEntry, error); ReadFile(string) ([]byte, error) }, src, dstBase string) error {
entries, err := fs.ReadDir(src)
if err != nil {
return err
}
dst := filepath.Join(dstBase, filepath.FromSlash(src))
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
for _, e := range entries {
// embed.FS always uses forward slashes regardless of OS
srcPath := path.Join(src, e.Name())
dstPath := filepath.Join(dstBase, filepath.FromSlash(srcPath))
if e.IsDir() {
if err := extractDir(fs, srcPath, dstBase); err != nil {
return err
}
continue
}
data, err := fs.ReadFile(srcPath)
if err != nil {
return err
}
if err := os.WriteFile(dstPath, data, 0644); err != nil {
return err
}
}
return nil
}
// WriteConfig serialises cfg to a temp JSON file and returns its path.
func WriteConfig(cfg interface{}, tmpDir string) (string, error) {
path := filepath.Join(tmpDir, "config-runtime.json")
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return "", err
}
return path, os.WriteFile(path, data, 0644)
}

416
internal/tui/tui.go Normal file
View file

@ -0,0 +1,416 @@
// Package tui implements the interactive terminal UI for xetup.
// Phase 1 configuration form (huh embedded in bubbletea).
// Phase 2 live log view while PowerShell scripts run.
// Phase 3 final summary.
package tui
import (
"context"
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/runner"
)
// --------------------------------------------------------------------------
// Phase
// --------------------------------------------------------------------------
type phase int
const (
phaseForm phase = iota
phaseRun
phaseDone
)
// --------------------------------------------------------------------------
// Async message types (runner → bubbletea)
// --------------------------------------------------------------------------
type logMsg runner.LogLine
type resultMsg runner.Result
type doneMsg []runner.Result
// --------------------------------------------------------------------------
// Model
// --------------------------------------------------------------------------
// Model is the top-level bubbletea model.
type Model struct {
phase phase
form *huh.Form
runCfg runner.RunConfig
cfg config.Config
// form-bound values (huh writes here via pointer)
pcName string
pcDesc string
productKey string
profileType string
selectedItemKeys []string
// runner state
r *runner.Runner
cancel context.CancelFunc
msgCh chan tea.Msg
logs []runner.LogLine
results []runner.Result
// terminal dimensions
width int
height int
}
// NewModel returns the initial model pre-populated from cfg.
func NewModel(cfg config.Config, runCfg runner.RunConfig) Model {
m := Model{
cfg: cfg,
runCfg: runCfg,
pcName: cfg.Deployment.PCName,
pcDesc: cfg.Deployment.PCDescription,
productKey: cfg.Activation.ProductKey,
profileType: cfg.Deployment.ProfileType,
width: 80,
height: 24,
}
m.form = buildForm(&m)
return m
}
// --------------------------------------------------------------------------
// Form construction
// --------------------------------------------------------------------------
func buildForm(m *Model) *huh.Form {
allItems := runner.AllSelectableItems()
opts := make([]huh.Option[string], len(allItems))
for i, item := range allItems {
opts[i] = huh.NewOption(item.Label, item.Key).Selected(true)
}
return huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Jmeno pocitace").
Placeholder("napr. NB-KLIENT-01 (prazdne = neprejmenovat)").
Value(&m.pcName),
huh.NewInput().
Title("Popis pocitace").
Placeholder("napr. PC recepce").
Value(&m.pcDesc),
huh.NewInput().
Title("Product Key").
Placeholder("prazdne = OA3 / GVLK fallback").
Value(&m.productKey),
huh.NewSelect[string]().
Title("Profil").
Options(
huh.NewOption("Default", "default"),
huh.NewOption("Admin (Explorer, PS, Edge)", "admin"),
huh.NewOption("User (Explorer, Edge)", "user"),
).
Value(&m.profileType),
),
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Kroky a nastaveni (mezernikem odzaskrtnout)").
Options(opts...).
Value(&m.selectedItemKeys),
),
)
}
// --------------------------------------------------------------------------
// bubbletea interface
// --------------------------------------------------------------------------
// Init implements tea.Model.
func (m Model) Init() tea.Cmd {
return m.form.Init()
}
// Update implements tea.Model.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
if m.cancel != nil {
m.cancel()
}
return m, tea.Quit
}
if m.phase == phaseDone {
return m, tea.Quit
}
// --- async runner messages ---
case logMsg:
m.logs = append(m.logs, runner.LogLine(msg))
return m, readMsg(m.msgCh)
case resultMsg:
m.results = append(m.results, runner.Result(msg))
return m, readMsg(m.msgCh)
case doneMsg:
m.results = []runner.Result(msg)
m.phase = phaseDone
return m, nil
}
// --- form phase ---
if m.phase == phaseForm {
form, cmd := m.form.Update(msg)
if f, ok := form.(*huh.Form); ok {
m.form = f
}
switch m.form.State {
case huh.StateCompleted:
return m.startRun()
case huh.StateAborted:
return m, tea.Quit
}
return m, cmd
}
return m, nil
}
// View implements tea.Model.
func (m Model) View() string {
switch m.phase {
case phaseForm:
return m.viewForm()
case phaseRun:
return m.viewRun()
case phaseDone:
return m.viewDone()
}
return ""
}
// --------------------------------------------------------------------------
// Phase transitions
// --------------------------------------------------------------------------
// startRun applies form values and kicks off the runner goroutine.
func (m Model) startRun() (Model, tea.Cmd) {
// persist form values into config
m.cfg.Deployment.PCName = m.pcName
m.cfg.Deployment.PCDescription = m.pcDesc
m.cfg.Activation.ProductKey = m.productKey
m.cfg.Deployment.ProfileType = m.profileType
m.runCfg.ProfileType = m.profileType
// Index selected keys for fast lookup
selectedSet := make(map[string]bool, len(m.selectedItemKeys))
for _, key := range m.selectedItemKeys {
selectedSet[key] = true
}
// Build Features map: feature enabled = its composite key was selected
features := make(config.Features)
for _, item := range runner.AllSelectableItems() {
if item.FeatureID == "" {
continue
}
if features[item.StepID] == nil {
features[item.StepID] = make(map[string]bool)
}
features[item.StepID][item.FeatureID] = selectedSet[item.Key]
}
m.cfg.Features = features
// Determine which steps are enabled:
// - step with features: enabled if at least one feature selected
// - step without features: enabled if step key selected
stepEnabled := make(map[string]bool)
for _, item := range runner.AllSelectableItems() {
if item.FeatureID == "" {
stepEnabled[item.StepID] = selectedSet[item.Key]
} else if selectedSet[item.Key] {
// any feature selected enables the step
stepEnabled[item.StepID] = true
}
}
allSteps := runner.AllSteps()
steps := make([]runner.Step, len(allSteps))
for i, s := range allSteps {
s.Enabled = stepEnabled[s.ID]
steps[i] = s
}
// set up channel and start runner
ch := make(chan tea.Msg, 200)
m.msgCh = ch
m.phase = phaseRun
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
rc := m.runCfg
r := runner.New(
rc,
func(l runner.LogLine) { ch <- logMsg(l) },
func(res runner.Result) { ch <- resultMsg(res) },
)
m.r = r
go func() {
results := r.Run(ctx, steps)
ch <- doneMsg(results)
}()
return m, readMsg(ch)
}
// readMsg returns a tea.Cmd that blocks until the next message arrives on ch.
func readMsg(ch <-chan tea.Msg) tea.Cmd {
return func() tea.Msg {
return <-ch
}
}
// --------------------------------------------------------------------------
// Styles
// --------------------------------------------------------------------------
var (
sTitle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#58a6ff"))
sOK = lipgloss.NewStyle().Foreground(lipgloss.Color("#2ea043"))
sError = lipgloss.NewStyle().Foreground(lipgloss.Color("#da3633"))
sWarn = lipgloss.NewStyle().Foreground(lipgloss.Color("#d29922"))
sStep = lipgloss.NewStyle().Foreground(lipgloss.Color("#58a6ff"))
sMuted = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
sBold = lipgloss.NewStyle().Bold(true)
)
// --------------------------------------------------------------------------
// Views
// --------------------------------------------------------------------------
func (m Model) viewForm() string {
header := sTitle.Render("xetup") + " " + sMuted.Render("Windows deployment vyplnte konfiguraci")
return header + "\n\n" + m.form.View()
}
func (m Model) viewRun() string {
allSteps := runner.AllSteps()
doneCount := len(m.results)
totalCount := len(allSteps)
var sb strings.Builder
// header bar
sb.WriteString(sTitle.Render("xetup") + " ")
if doneCount < totalCount {
s := allSteps[doneCount]
sb.WriteString(sBold.Render(fmt.Sprintf("krok %d/%d", doneCount+1, totalCount)))
sb.WriteString(" " + s.Num + " - " + s.Name)
} else {
sb.WriteString(sBold.Render("dokoncuji..."))
}
sb.WriteByte('\n')
sb.WriteString(strings.Repeat("─", clamp(m.width, 20, 90)))
sb.WriteByte('\n')
// log lines show last N to fit terminal
maxLines := m.height - 5
if maxLines < 5 {
maxLines = 5
}
start := 0
if len(m.logs) > maxLines {
start = len(m.logs) - maxLines
}
for _, l := range m.logs[start:] {
sb.WriteString(styledLogLine(l))
sb.WriteByte('\n')
}
sb.WriteString(sMuted.Render("\nCtrl+C pro zastaveni"))
return sb.String()
}
func (m Model) viewDone() string {
var sb strings.Builder
sb.WriteString(sTitle.Render("xetup ") + sBold.Render("hotovo") + "\n")
sb.WriteString(strings.Repeat("─", clamp(m.width, 20, 90)) + "\n\n")
okCount, errCount, skipCount := 0, 0, 0
for _, res := range m.results {
var prefix string
switch res.Status {
case "OK":
okCount++
prefix = sOK.Render(" OK ")
case "ERROR":
errCount++
prefix = sError.Render(" ERROR ")
default:
skipCount++
prefix = sMuted.Render(" SKIPPED ")
}
sb.WriteString(prefix + res.Step.Num + " - " + res.Step.Name)
if res.Elapsed > 0 {
sb.WriteString(sMuted.Render(fmt.Sprintf(" (%s)", res.Elapsed.Round(time.Second))))
}
sb.WriteByte('\n')
}
sb.WriteString("\n" + strings.Repeat("─", clamp(m.width, 20, 90)) + "\n")
summary := fmt.Sprintf("OK: %d CHYBY: %d PRESKOCENO: %d", okCount, errCount, skipCount)
if errCount > 0 {
sb.WriteString(sError.Render(summary))
} else {
sb.WriteString(sOK.Render(summary))
}
sb.WriteString(sMuted.Render("\n\nStisknete libovolnou klavesu pro ukonceni..."))
return sb.String()
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
func styledLogLine(l runner.LogLine) string {
switch l.Level {
case "OK":
return sOK.Render(l.Text)
case "ERROR":
return sError.Render(l.Text)
case "WARN":
return sWarn.Render(l.Text)
case "STEP":
return sStep.Render(l.Text)
default:
return sMuted.Render(l.Text)
}
}
func clamp(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}

View file

@ -31,6 +31,18 @@ function Write-Log {
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# Check winget availability
# -----------------------------------------------------------------------
@ -62,9 +74,10 @@ Write-Log "winget found: $($winget.Source -or $winget)" -Level OK
# -----------------------------------------------------------------------
# Install packages from config
# -----------------------------------------------------------------------
if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
if (Get-Feature $Config "software" "wingetInstalls") {
if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
Write-Log "No software list in config - skipping installs" -Level WARN
} else {
} else {
foreach ($pkg in $Config.software.install) {
Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
$result = & winget install --id $pkg.wingetId `
@ -85,17 +98,15 @@ if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
Write-Log " Output: $($result -join ' ')" -Level ERROR
}
}
}
} else {
Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Set Adobe Reader as default PDF app
# -----------------------------------------------------------------------
$forcePdf = $true
if ($Config -and $Config.pdfDefault) {
$forcePdf = [bool]$Config.pdfDefault.forceAdobeReader
}
if ($forcePdf) {
if (Get-Feature $Config "software" "pdfDefault") {
Write-Log "Setting Adobe Reader as default PDF app" -Level INFO
# Stop UCPD driver before writing file association.
@ -160,17 +171,20 @@ if ($forcePdf) {
Write-Log " Could not restart UCPD: $_" -Level WARN
}
}
} else {
Write-Log "pdfDefault feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Install Atera RMM Agent
# -----------------------------------------------------------------------
Write-Log "Installing Atera RMM Agent" -Level INFO
if (Get-Feature $Config "software" "ateraAgent") {
Write-Log "Installing Atera RMM Agent" -Level INFO
$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
$ateraMsi = "$env:TEMP\AteraAgent.msi"
$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
$ateraMsi = "$env:TEMP\AteraAgent.msi"
try {
try {
Write-Log " Downloading Atera agent..." -Level INFO
Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
Write-Log " Download complete" -Level OK
@ -185,12 +199,15 @@ try {
Write-Log " Atera agent install may have failed - binary not found at expected path" -Level WARN
Write-Log " msiexec output: $($msiResult -join ' ')" -Level WARN
}
}
catch {
}
catch {
Write-Log " Atera agent download/install failed: $_" -Level ERROR
}
finally {
}
finally {
Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
}
} else {
Write-Log "ateraAgent feature disabled - skipping" -Level INFO
}
Write-Log "Step 2 complete" -Level OK

View file

@ -40,6 +40,18 @@ function Write-Log {
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
@ -206,91 +218,7 @@ function Remove-Reg {
Write-Log "3 - Applying HKLM system registry tweaks" -Level STEP
# -----------------------------------------------------------------------
# Bypass Network Requirement on OOBE (BypassNRO)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" `
-Name "BypassNRO" -Value 1
# -----------------------------------------------------------------------
# Disable auto-install of Teams (Chat)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" `
-Name "ConfigureChatAutoInstall" -Value 0
# -----------------------------------------------------------------------
# Disable Cloud Optimized Content (ads in Start menu etc.)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" `
-Name "DisableCloudOptimizedContent" -Value 1
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" `
-Name "DisableWindowsConsumerFeatures" -Value 1
# -----------------------------------------------------------------------
# Disable Widgets (News and Interests)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Dsh" `
-Name "AllowNewsAndInterests" -Value 0
# -----------------------------------------------------------------------
# Microsoft Edge policies
# -----------------------------------------------------------------------
$edgePath = "HKLM:\SOFTWARE\Policies\Microsoft\Edge"
# UI / first run
Set-Reg -Path $edgePath -Name "HideFirstRunExperience" -Value 1
Set-Reg -Path $edgePath -Name "DefaultBrowserSettingEnabled" -Value 0
# New tab page / recommendations
Set-Reg -Path $edgePath -Name "NewTabPageContentEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ShowRecommendationsEnabled" -Value 0
Set-Reg -Path $edgePath -Name "SpotlightExperiencesAndRecommendationsEnabled" -Value 0
Set-Reg -Path $edgePath -Name "PersonalizationReportingEnabled" -Value 0
# Shopping / rewards / sidebar
Set-Reg -Path $edgePath -Name "EdgeShoppingAssistantEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ShowMicrosoftRewards" -Value 0
Set-Reg -Path $edgePath -Name "HubsSidebarEnabled" -Value 0
# Search suggestions
Set-Reg -Path $edgePath -Name "SearchSuggestEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ImportOnEachLaunch" -Value 0
# Telemetry / feedback
Set-Reg -Path $edgePath -Name "DiagnosticData" -Value 0
Set-Reg -Path $edgePath -Name "FeedbackSurveysEnabled" -Value 0
Set-Reg -Path $edgePath -Name "EdgeCollectionsEnabled" -Value 0
# Toolbar buttons - show
Set-Reg -Path $edgePath -Name "FavoritesBarEnabled" -Value 1 # Favorites bar always visible
Set-Reg -Path $edgePath -Name "DownloadsButtonEnabled" -Value 1
Set-Reg -Path $edgePath -Name "HistoryButtonEnabled" -Value 1
Set-Reg -Path $edgePath -Name "PerformanceButtonEnabled" -Value 1 # Sleeping Tabs / Performance
# Toolbar buttons - hide
Set-Reg -Path $edgePath -Name "HomeButtonEnabled" -Value 0
Set-Reg -Path $edgePath -Name "SplitScreenEnabled" -Value 0
Set-Reg -Path $edgePath -Name "EdgeEDropEnabled" -Value 0 # Drop
Set-Reg -Path $edgePath -Name "WebCaptureEnabled" -Value 0 # Screenshot
Set-Reg -Path $edgePath -Name "ShareAllowed" -Value 0 # Share
# Default search engine: Google
# SearchProviderEnabled must be 1, SearchProviderName + URL set the provider
Set-Reg -Path $edgePath -Name "DefaultSearchProviderEnabled" -Value 1 -Type "DWord"
Set-Reg -Path $edgePath -Name "DefaultSearchProviderName" -Value "Google" -Type "String"
Set-Reg -Path $edgePath -Name "DefaultSearchProviderSearchURL" `
-Value "https://www.google.com/search?q={searchTerms}" -Type "String"
# Remove other search engines (empty list = no other providers besides default)
Set-Reg -Path $edgePath -Name "ManagedSearchEngines" `
-Value '[{"is_default":true,"name":"Google","search_url":"https://www.google.com/search?q={searchTerms}","keyword":"google.com"}]' `
-Type "String"
# Disable desktop shortcut on install/update
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" `
-Name "CreateDesktopShortcutDefault" -Value 0
# -----------------------------------------------------------------------
# Password - no expiration
# Always-on: password and timezone (fundamental system settings)
# -----------------------------------------------------------------------
Write-Log " Setting password max age to UNLIMITED" -Level INFO
$pwResult = & net accounts /maxpwage:UNLIMITED 2>&1
@ -300,9 +228,6 @@ if ($LASTEXITCODE -eq 0) {
Write-Log " Failed to set password max age: $pwResult" -Level ERROR
}
# -----------------------------------------------------------------------
# Time zone
# -----------------------------------------------------------------------
$tz = "Central Europe Standard Time"
if ($Config -and $Config.deployment -and $Config.deployment.timezone) {
$tz = $Config.deployment.timezone
@ -317,47 +242,36 @@ catch {
}
# -----------------------------------------------------------------------
# OneDrive - uninstall from clean Windows (no policy block)
# NOTE: No policy key is set intentionally - M365 installation can reinstall
# and run OneDrive normally. Policy DisableFileSyncNGSC would prevent that.
# System tweaks (Windows features, cloud noise, Xbox, Recall, Search UI)
# -----------------------------------------------------------------------
Write-Log " Uninstalling OneDrive" -Level INFO
if (Get-Feature $Config "systemRegistry" "systemTweaks") {
Write-Log " Applying system tweaks" -Level INFO
# Remove OneDriveSetup.exe if present
$oneDrivePaths = @(
"$env:SystemRoot\System32\OneDriveSetup.exe"
"$env:SystemRoot\SysWOW64\OneDriveSetup.exe"
)
foreach ($odPath in $oneDrivePaths) {
if (Test-Path $odPath) {
try {
# Uninstall first
& $odPath /uninstall 2>&1 | Out-Null
Write-Log " OneDrive uninstalled via $odPath" -Level OK
}
catch {
Write-Log " OneDrive uninstall failed: $_" -Level WARN
}
}
}
# Bypass Network Requirement on OOBE (BypassNRO)
Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" `
-Name "BypassNRO" -Value 1
# Remove OneDrive Start Menu shortcut
$odLnk = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk"
if (Test-Path $odLnk) {
Remove-Item $odLnk -Force -ErrorAction SilentlyContinue
Write-Log " Removed OneDrive Start Menu shortcut" -Level OK
}
# Disable auto-install of Teams (Chat)
Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" `
-Name "ConfigureChatAutoInstall" -Value 0
# -----------------------------------------------------------------------
# Outlook (new) - disable auto-install via UScheduler
# -----------------------------------------------------------------------
Write-Log " Disabling Outlook (new) auto-install" -Level INFO
# Disable Cloud Optimized Content (ads in Start menu etc.)
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" `
-Name "DisableCloudOptimizedContent" -Value 1
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" `
-Name "DisableWindowsConsumerFeatures" -Value 1
$uschedulerPaths = @(
# Disable Widgets (News and Interests)
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Dsh" `
-Name "AllowNewsAndInterests" -Value 0
# Disable Outlook (new) auto-install via UScheduler
Write-Log " Disabling Outlook (new) auto-install" -Level INFO
$uschedulerPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate"
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate"
)
foreach ($uPath in $uschedulerPaths) {
)
foreach ($uPath in $uschedulerPaths) {
if (Test-Path $uPath) {
try {
Remove-Item -Path $uPath -Recurse -Force
@ -367,58 +281,158 @@ foreach ($uPath in $uschedulerPaths) {
Write-Log " Failed to remove UScheduler key: $_" -Level WARN
}
}
}
# Disable GameDVR
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" `
-Name "AllowGameDVR" -Value 0
# Disable Recall (Windows AI feature)
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI" `
-Name "DisableAIDataAnalysis" -Value 1
# Search on taskbar - hide via HKLM policy (Win11 22H2+ enforcement)
# User-level SearchboxTaskbarMode alone is insufficient on newer Win11 builds;
# this policy key ensures the setting survives Windows Updates.
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search" `
-Name "SearchOnTaskbarMode" -Value 0
# Start menu - hide Recommended section (Win11)
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer" `
-Name "HideRecommendedSection" -Value 1
} else {
Write-Log "systemTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Disable GameDVR
# Microsoft Edge policies
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" `
-Name "AllowGameDVR" -Value 0
if (Get-Feature $Config "systemRegistry" "edgePolicies") {
Write-Log " Applying Edge policies" -Level INFO
$edgePath = "HKLM:\SOFTWARE\Policies\Microsoft\Edge"
# UI / first run
Set-Reg -Path $edgePath -Name "HideFirstRunExperience" -Value 1
Set-Reg -Path $edgePath -Name "DefaultBrowserSettingEnabled" -Value 0
# New tab page / recommendations
Set-Reg -Path $edgePath -Name "NewTabPageContentEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ShowRecommendationsEnabled" -Value 0
Set-Reg -Path $edgePath -Name "SpotlightExperiencesAndRecommendationsEnabled" -Value 0
Set-Reg -Path $edgePath -Name "PersonalizationReportingEnabled" -Value 0
# Shopping / rewards / sidebar
Set-Reg -Path $edgePath -Name "EdgeShoppingAssistantEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ShowMicrosoftRewards" -Value 0
Set-Reg -Path $edgePath -Name "HubsSidebarEnabled" -Value 0
# Search suggestions
Set-Reg -Path $edgePath -Name "SearchSuggestEnabled" -Value 0
Set-Reg -Path $edgePath -Name "ImportOnEachLaunch" -Value 0
# Telemetry / feedback
Set-Reg -Path $edgePath -Name "DiagnosticData" -Value 0
Set-Reg -Path $edgePath -Name "FeedbackSurveysEnabled" -Value 0
Set-Reg -Path $edgePath -Name "EdgeCollectionsEnabled" -Value 0
# Toolbar buttons - show
Set-Reg -Path $edgePath -Name "FavoritesBarEnabled" -Value 1 # Favorites bar always visible
Set-Reg -Path $edgePath -Name "DownloadsButtonEnabled" -Value 1
Set-Reg -Path $edgePath -Name "HistoryButtonEnabled" -Value 1
Set-Reg -Path $edgePath -Name "PerformanceButtonEnabled" -Value 1 # Sleeping Tabs / Performance
# Toolbar buttons - hide
Set-Reg -Path $edgePath -Name "HomeButtonEnabled" -Value 0
Set-Reg -Path $edgePath -Name "SplitScreenEnabled" -Value 0
Set-Reg -Path $edgePath -Name "EdgeEDropEnabled" -Value 0 # Drop
Set-Reg -Path $edgePath -Name "WebCaptureEnabled" -Value 0 # Screenshot
Set-Reg -Path $edgePath -Name "ShareAllowed" -Value 0 # Share
# Default search engine: Google
# SearchProviderEnabled must be 1, SearchProviderName + URL set the provider
Set-Reg -Path $edgePath -Name "DefaultSearchProviderEnabled" -Value 1 -Type "DWord"
Set-Reg -Path $edgePath -Name "DefaultSearchProviderName" -Value "Google" -Type "String"
Set-Reg -Path $edgePath -Name "DefaultSearchProviderSearchURL" `
-Value "https://www.google.com/search?q={searchTerms}" -Type "String"
# Remove other search engines (empty list = no other providers besides default)
Set-Reg -Path $edgePath -Name "ManagedSearchEngines" `
-Value '[{"is_default":true,"name":"Google","search_url":"https://www.google.com/search?q={searchTerms}","keyword":"google.com"}]' `
-Type "String"
# Disable desktop shortcut on install/update
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" `
-Name "CreateDesktopShortcutDefault" -Value 0
} else {
Write-Log "edgePolicies feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Disable Recall (Windows AI feature)
# OneDrive - uninstall from clean Windows (no policy block)
# NOTE: No policy key is set intentionally - M365 installation can reinstall
# and run OneDrive normally. Policy DisableFileSyncNGSC would prevent that.
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI" `
-Name "DisableAIDataAnalysis" -Value 1
if (Get-Feature $Config "systemRegistry" "oneDriveUninstall") {
Write-Log " Uninstalling OneDrive" -Level INFO
# -----------------------------------------------------------------------
# Search on taskbar - hide via HKLM policy (Win11 22H2+ enforcement)
# User-level SearchboxTaskbarMode alone is insufficient on newer Win11 builds;
# this policy key ensures the setting survives Windows Updates.
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search" `
-Name "SearchOnTaskbarMode" -Value 0
$oneDrivePaths = @(
"$env:SystemRoot\System32\OneDriveSetup.exe"
"$env:SystemRoot\SysWOW64\OneDriveSetup.exe"
)
foreach ($odPath in $oneDrivePaths) {
if (Test-Path $odPath) {
try {
& $odPath /uninstall 2>&1 | Out-Null
Write-Log " OneDrive uninstalled via $odPath" -Level OK
}
catch {
Write-Log " OneDrive uninstall failed: $_" -Level WARN
}
}
}
# -----------------------------------------------------------------------
# Start menu - hide Recommended section (Win11)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer" `
-Name "HideRecommendedSection" -Value 1
# Remove OneDrive Start Menu shortcut
$odLnk = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk"
if (Test-Path $odLnk) {
Remove-Item $odLnk -Force -ErrorAction SilentlyContinue
Write-Log " Removed OneDrive Start Menu shortcut" -Level OK
}
} else {
Write-Log "oneDriveUninstall feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Power configuration
# -----------------------------------------------------------------------
Write-Log "Applying power configuration" -Level INFO
if (Get-Feature $Config "systemRegistry" "powercfg") {
Write-Log " Applying power configuration" -Level INFO
$powercfg = @(
$powercfgArgs = @(
@("/change", "standby-timeout-ac", "0"), # never sleep on AC
@("/change", "monitor-timeout-ac", "60"), # screen off after 60 min on AC
@("/change", "standby-timeout-dc", "30"), # sleep after 30 min on battery
@("/change", "monitor-timeout-dc", "15") # screen off after 15 min on battery
)
foreach ($args in $powercfg) {
$result = & powercfg @args 2>&1
)
foreach ($a in $powercfgArgs) {
$result = & powercfg @a 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Log " powercfg $($args -join ' ')" -Level OK
Write-Log " powercfg $($a -join ' ')" -Level OK
} else {
Write-Log " powercfg $($args -join ' ') failed: $result" -Level WARN
Write-Log " powercfg $($a -join ' ') failed: $result" -Level WARN
}
}
} else {
Write-Log "powercfg feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Proxy auto-detect disable (WPAD)
# -----------------------------------------------------------------------
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings" `
if (Get-Feature $Config "systemRegistry" "proxyDisable") {
Write-Log " Disabling WPAD proxy auto-detect" -Level INFO
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings" `
-Name "AutoDetect" -Value 0
} else {
Write-Log "proxyDisable feature disabled - skipping" -Level INFO
}
Write-Log "Step 3 complete" -Level OK

View file

@ -39,6 +39,18 @@ function Write-Log {
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# Helper - apply a registry setting to both Default hive and current HKCU
# -----------------------------------------------------------------------
@ -150,9 +162,10 @@ Write-Log "Default hive loaded" -Level OK
try {
# -----------------------------------------------------------------------
# Taskbar settings (Win10 + Win11)
# Taskbar tweaks (alignment, buttons, tray, layout XML)
# -----------------------------------------------------------------------
Write-Log "Applying taskbar settings" -Level STEP
if (Get-Feature $Config "defaultProfile" "taskbarTweaks") {
Write-Log "Applying taskbar tweaks" -Level STEP
$tbPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
@ -177,29 +190,18 @@ try {
# Hide Copilot button
Set-ProfileReg -SubKey $tbPath -Name "ShowCopilotButton" -Value 0
# Show file extensions in Explorer
Set-ProfileReg -SubKey $tbPath -Name "HideFileExt" -Value 0
# Open Explorer to This PC instead of Quick Access
Set-ProfileReg -SubKey $tbPath -Name "LaunchTo" -Value 1
# -----------------------------------------------------------------------
# System tray - show all icons
# -----------------------------------------------------------------------
# Show all tray icons
# EnableAutoTray = 0 works on Win10; Win11 ignores it but set anyway
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "EnableAutoTray" -Value 0
# Win11 workaround: clear cached tray icon streams so all icons appear on next login
# Windows rebuilds the streams with all icons visible when no cache exists
$trayNotifyKey = "HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify"
if (Test-Path $trayNotifyKey) {
Remove-ItemProperty -Path $trayNotifyKey -Name "IconStreams" -Force -ErrorAction SilentlyContinue
Remove-ItemProperty -Path $trayNotifyKey -Name "PastIconsStream" -Force -ErrorAction SilentlyContinue
Write-Log " Cleared TrayNotify icon streams (Win11 systray workaround)" -Level OK
}
# Also clear in Default hive so new users start with clean state
$defTrayKey = "Registry::HKU\DefaultProfile\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify"
if (Test-Path $defTrayKey) {
Remove-ItemProperty -Path $defTrayKey -Name "IconStreams" -Force -ErrorAction SilentlyContinue
@ -207,82 +209,24 @@ try {
Write-Log " Cleared TrayNotify icon streams in Default hive" -Level OK
}
# -----------------------------------------------------------------------
# Desktop icons - show This PC
# -----------------------------------------------------------------------
# CLSID {20D04FE0-3AEA-1069-A2D8-08002B30309D} = This PC / Computer
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" `
-Name "{20D04FE0-3AEA-1069-A2D8-08002B30309D}" -Value 0
# -----------------------------------------------------------------------
# Start menu settings
# -----------------------------------------------------------------------
Write-Log "Applying Start menu settings" -Level STEP
# Disable Bing search suggestions in Start menu
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\Explorer" `
-Name "DisableSearchBoxSuggestions" -Value 1
# Win11: empty Start menu pins
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Start" `
-Name "ConfigureStartPins" `
-Value '{"pinnedList":[]}' `
-Type "String"
# Hide "Recently added" apps in Start menu
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "Start_TrackProgs" -Value 0
# Hide recently opened files/docs from Start menu
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "Start_TrackDocs" -Value 0
# -----------------------------------------------------------------------
# Copilot - disable
# -----------------------------------------------------------------------
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\WindowsCopilot" `
-Name "TurnOffWindowsCopilot" -Value 1
# -----------------------------------------------------------------------
# GameDVR - disable
# -----------------------------------------------------------------------
Set-ProfileReg -SubKey "System\GameConfigStore" `
-Name "GameDVR_Enabled" -Value 0
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\GameDVR" `
-Name "AppCaptureEnabled" -Value 0
# -----------------------------------------------------------------------
# Num Lock on startup
# -----------------------------------------------------------------------
Set-ProfileReg -SubKey "Control Panel\Keyboard" `
-Name "InitialKeyboardIndicators" -Value 2 -Type "String"
# -----------------------------------------------------------------------
# Accent color on title bars
# -----------------------------------------------------------------------
Set-ProfileReg -SubKey "Software\Microsoft\Windows\DWM" `
-Name "ColorPrevalence" -Value 1
# -----------------------------------------------------------------------
# Taskbar pinned apps layout (Win10/11)
# ProfileType: default = empty, admin = Explorer+PS+Edge, user = Explorer+Edge
# Note: TaskbarLayoutModification.xml locks the taskbar temporarily.
# UnlockStartLayout scheduled task removes the lock 5 min after first boot
# so users can then customize pins freely.
# Win11 24H2+ may require ProvisionedLayoutModification.xml format instead.
# -----------------------------------------------------------------------
Write-Log "Writing taskbar layout (ProfileType=$ProfileType)" -Level INFO
Write-Log " Writing taskbar layout (ProfileType=$ProfileType)" -Level INFO
$taskbarLayoutDir = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell"
if (-not (Test-Path $taskbarLayoutDir)) {
New-Item -ItemType Directory -Path $taskbarLayoutDir -Force | Out-Null
}
# Build pin list based on profile type.
# Paths resolve relative to the new user at first login.
# Missing shortcuts are silently skipped by Windows.
$pinList = switch ($ProfileType) {
"admin" {
@'
@ -320,6 +264,82 @@ $pinList
$taskbarLayoutXml | Set-Content -Path "$taskbarLayoutDir\LayoutModification.xml" -Encoding UTF8 -Force
Write-Log " Taskbar LayoutModification.xml written (profile: $ProfileType)" -Level OK
# NumLock on startup
Set-ProfileReg -SubKey "Control Panel\Keyboard" `
-Name "InitialKeyboardIndicators" -Value 2 -Type "String"
# Accent color on title bars
Set-ProfileReg -SubKey "Software\Microsoft\Windows\DWM" `
-Name "ColorPrevalence" -Value 1
} else {
Write-Log "taskbarTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Start menu tweaks (pins, Bing, Copilot, GameDVR)
# -----------------------------------------------------------------------
if (Get-Feature $Config "defaultProfile" "startMenuTweaks") {
Write-Log "Applying Start menu tweaks" -Level STEP
# Disable Bing search suggestions in Start menu
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\Explorer" `
-Name "DisableSearchBoxSuggestions" -Value 1
# Win11: empty Start menu pins
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Start" `
-Name "ConfigureStartPins" `
-Value '{"pinnedList":[]}' `
-Type "String"
# Hide "Recently added" apps in Start menu
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "Start_TrackProgs" -Value 0
# Hide recently opened files/docs from Start menu
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" `
-Name "Start_TrackDocs" -Value 0
# Disable Copilot
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\WindowsCopilot" `
-Name "TurnOffWindowsCopilot" -Value 1
# Disable GameDVR
Set-ProfileReg -SubKey "System\GameConfigStore" `
-Name "GameDVR_Enabled" -Value 0
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\GameDVR" `
-Name "AppCaptureEnabled" -Value 0
} else {
Write-Log "startMenuTweaks feature disabled - skipping" -Level INFO
}
# -----------------------------------------------------------------------
# Explorer tweaks (file extensions, LaunchTo, ShowRecent, FullPath)
# -----------------------------------------------------------------------
if (Get-Feature $Config "defaultProfile" "explorerTweaks") {
Write-Log "Applying Explorer tweaks" -Level STEP
$advPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
# Show file extensions in Explorer
Set-ProfileReg -SubKey $advPath -Name "HideFileExt" -Value 0
# Open Explorer to This PC instead of Quick Access
Set-ProfileReg -SubKey $advPath -Name "LaunchTo" -Value 1
# Hide Recent files from Quick Access
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "ShowRecent" -Value 0
# Hide Frequent folders from Quick Access
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
-Name "ShowFrequent" -Value 0
# Show full path in Explorer title bar
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState" `
-Name "FullPath" -Value 1
} else {
Write-Log "explorerTweaks feature disabled - skipping" -Level INFO
}
}
finally {
# -----------------------------------------------------------------------

152
scripts/11-dell-update.ps1 Normal file
View file

@ -0,0 +1,152 @@
<#
.SYNOPSIS
Detects Dell hardware, installs Dell Command | Update, and applies all available updates.
.DESCRIPTION
Checks Win32_ComputerSystem.Manufacturer - if not Dell, the step exits silently without
error so the same deployment script works on any hardware. On Dell machines, installs
Dell Command | Update (Universal) via winget and immediately runs /applyUpdates with
-reboot=disable. This covers drivers, firmware, and BIOS. BIOS and firmware updates are
staged at this point and finalize automatically during the restart that closes the
deployment. The operator does not need to run a separate update pass after setup.
.ITEMS
detekce-dell-hw-win32-computersystem: Reads Win32_ComputerSystem.Manufacturer. If the string does not contain "Dell", the entire step is skipped without error. The deployment continues normally on HP, Lenovo, or any other brand.
instalace-dell-command-update-via-winget: Installs Dell.CommandUpdate.Universal silently via winget. This is the current DCU generation (v5+) that supports Latitude, OptiPlex, Precision, Vostro, and XPS on Win10 and Win11.
spusteni-vsech-aktualizaci-drivery-firmware-bios: Runs dcu-cli.exe /applyUpdates -silent -reboot=disable. Covers driver, firmware, and BIOS categories in a single pass. The -reboot=disable flag prevents DCU from rebooting mid-deployment.
bios-firmware-staging-reboot: BIOS and firmware updates are staged by DCU and finalize on the next system restart. The deployment already ends with a restart (step 09 - computer rename), so no extra reboot is needed.
#>
param(
[object]$Config,
[string]$LogFile
)
$ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
Add-Content -Path $LogFile -Value $line -Encoding UTF8
switch ($Level) {
"OK" { Write-Host $line -ForegroundColor Green }
"ERROR" { Write-Host $line -ForegroundColor Red }
"WARN" { Write-Host $line -ForegroundColor Yellow }
"STEP" { Write-Host $line -ForegroundColor Cyan }
default { Write-Host $line }
}
}
function Get-Feature {
param([object]$Cfg, [string]$StepID, [string]$FeatureID, [bool]$Default = $true)
try {
if ($null -eq $Cfg) { return $Default }
$stepFeatures = $Cfg.features.$StepID
if ($null -eq $stepFeatures) { return $Default }
$val = $stepFeatures.$FeatureID
if ($null -eq $val) { return $Default }
return [bool]$val
} catch { return $Default }
}
# -----------------------------------------------------------------------
# Detect Dell hardware
# -----------------------------------------------------------------------
Write-Log "Checking hardware manufacturer" -Level INFO
try {
$cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
$manufacturer = $cs.Manufacturer
$model = $cs.Model
Write-Log " Manufacturer: $manufacturer Model: $model" -Level INFO
}
catch {
Write-Log " Failed to query Win32_ComputerSystem: $_" -Level ERROR
return
}
if ($manufacturer -notmatch "Dell") {
Write-Log "Not a Dell machine ($manufacturer) - step skipped" -Level WARN
return
}
Write-Log "Dell confirmed: $model" -Level OK
# -----------------------------------------------------------------------
# Install Dell Command | Update via winget
# -----------------------------------------------------------------------
Write-Log "Installing Dell Command | Update (Universal)..." -Level STEP
$wingetArgs = @(
"install",
"--id", "Dell.CommandUpdate.Universal",
"--silent",
"--accept-package-agreements",
"--accept-source-agreements"
)
$wingetOutput = & winget @wingetArgs 2>&1
$wingetExit = $LASTEXITCODE
$wingetOutput | ForEach-Object { Write-Log " [winget] $_" -Level INFO }
if ($wingetExit -ne 0 -and $wingetExit -ne 1638) { # 1638 = already installed
Write-Log " winget exit code $wingetExit - checking if DCU is already present" -Level WARN
}
# Locate dcu-cli.exe (path is the same for x64 and Universal edition)
$dcuCandidates = @(
"C:\Program Files\Dell\CommandUpdate\dcu-cli.exe",
"C:\Program Files (x86)\Dell\CommandUpdate\dcu-cli.exe"
)
$dcuCli = $dcuCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $dcuCli) {
Write-Log " dcu-cli.exe not found - cannot run updates" -Level ERROR
return
}
Write-Log " dcu-cli.exe found: $dcuCli" -Level OK
# -----------------------------------------------------------------------
# Run updates - categories controlled by feature flags
# -reboot=disable -> no mid-deployment reboot; BIOS/firmware staged for next restart
# -----------------------------------------------------------------------
$runDrivers = Get-Feature $Config "dellUpdate" "drivers"
$runBios = Get-Feature $Config "dellUpdate" "bios"
if (-not $runDrivers -and -not $runBios) {
Write-Log "Both drivers and bios features disabled - skipping update run" -Level INFO
} else {
# Build update type list from enabled features
$updateTypes = @()
if ($runDrivers) {
$updateTypes += "driver"
$updateTypes += "firmware"
}
if ($runBios) {
$updateTypes += "bios"
}
$updateTypeArg = $updateTypes -join ","
Write-Log "Running Dell Command | Update (updateType=$updateTypeArg, no auto-reboot)..." -Level STEP
Write-Log " This may take several minutes depending on available updates" -Level INFO
$dcuOutput = & $dcuCli /applyUpdates -silent -reboot=disable "-updateType=$updateTypeArg" 2>&1
$exitCode = $LASTEXITCODE
$dcuOutput | ForEach-Object { Write-Log " [DCU] $_" -Level INFO }
Write-Log " DCU exit code: $exitCode" -Level INFO
# Dell Command | Update exit codes:
# 0 = completed, no updates required or updates applied (no reboot needed)
# 1 = updates applied, reboot required to finalize BIOS/firmware
# 5 = no applicable updates found for this system
# others = error or partial failure
switch ($exitCode) {
0 { Write-Log "Dell Command | Update: complete (no reboot required)" -Level OK }
1 { Write-Log "Dell Command | Update: updates staged - BIOS/firmware will finalize on restart" -Level OK }
5 { Write-Log "Dell Command | Update: no applicable updates for this model" -Level OK }
default { Write-Log "Dell Command | Update: exit code $exitCode - review DCU log in C:\ProgramData\Dell\UpdateService\Logs" -Level WARN }
}
}
Write-Log "Step 11 complete" -Level OK

View file

@ -146,5 +146,15 @@
"povolit-ping-icmp-firewall": "Enables \"File and Printer Sharing (Echo Request)\" firewall rules for ICMPv4 and ICMPv6. ICMP echo is disabled by default on clean Windows. Required for network diagnostics, monitoring tools, and basic connectivity verification.",
"zapnout-network-discovery": "Enables the Network Discovery firewall rule group (FPS-NB_Name-In-UDP, LLMNR, etc.) for Private and Domain profiles via Set-NetFirewallRule. Allows this PC to appear in Network Neighborhood and browse other machines."
}
},
"11-dell-update": {
"synopsis": "Detects Dell hardware, installs Dell Command | Update, and applies all available updates.",
"description": "Checks Win32_ComputerSystem.Manufacturer - if not Dell, the step exits silently without\nerror so the same deployment script works on any hardware. On Dell machines, installs\nDell Command | Update (Universal) via winget and immediately runs /applyUpdates with\n-reboot=disable. This covers drivers, firmware, and BIOS. BIOS and firmware updates are\nstaged at this point and finalize automatically during the restart that closes the\ndeployment. The operator does not need to run a separate update pass after setup.",
"items": {
"detekce-dell-hw-win32-computersystem": "Reads Win32_ComputerSystem.Manufacturer. If the string does not contain \"Dell\", the entire step is skipped without error. The deployment continues normally on HP, Lenovo, or any other brand.",
"instalace-dell-command-update-via-winget": "Installs Dell.CommandUpdate.Universal silently via winget. This is the current DCU generation (v5+) that supports Latitude, OptiPlex, Precision, Vostro, and XPS on Win10 and Win11.",
"spusteni-vsech-aktualizaci-drivery-firmware-bios": "Runs dcu-cli.exe /applyUpdates -silent -reboot=disable. Covers driver, firmware, and BIOS categories in a single pass. The -reboot=disable flag prevents DCU from rebooting mid-deployment.",
"bios-firmware-staging-reboot": "BIOS and firmware updates are staged by DCU and finalize on the next system restart. The deployment already ends with a restart (step 09 - computer rename), so no extra reboot is needed."
}
}
}

18
web/get.ps1 Normal file
View file

@ -0,0 +1,18 @@
# xetup installer - downloads and launches the latest release
# Usage: irm xetup.x9.cz/get.ps1 | iex
$api = 'https://xetup.x9.cz/forgejo-api/repos/x9/xetup/releases?limit=1'
try {
$rel = (Invoke-RestMethod -Uri $api -UseBasicParsing)[0]
$asset = $rel.assets | Where-Object { $_.name -eq 'xetup.exe' } | Select-Object -First 1
if (-not $asset) { Write-Error "xetup.exe not found in latest release"; exit 1 }
$out = "$env:TEMP\xetup.exe"
Write-Host "Downloading xetup $($rel.tag_name)..."
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $out -UseBasicParsing
Write-Host "Launching..."
Start-Process -FilePath $out -Verb RunAs
} catch {
Write-Error "Failed: $_"
}

View file

@ -34,28 +34,10 @@
align-items: center;
gap: .75rem;
}
.logo-text {
font-size: 1.2rem;
font-weight: 700;
color: #fff;
letter-spacing: -.02em;
}
.logo-sub {
font-size: .8rem;
color: var(--muted);
margin-left: .2rem;
}
header nav {
margin-left: auto;
display: flex;
gap: 1.5rem;
}
header nav a {
color: var(--muted);
text-decoration: none;
font-size: .88rem;
transition: color .15s;
}
.logo-text { font-size: 1.2rem; font-weight: 700; color: #fff; letter-spacing: -.02em; }
.logo-sub { font-size: .8rem; color: var(--muted); margin-left: .2rem; }
header nav { margin-left: auto; display: flex; gap: 1.5rem; }
header nav a { color: var(--muted); text-decoration: none; font-size: .88rem; transition: color .15s; }
header nav a:hover { color: var(--text); }
main {
@ -81,92 +63,86 @@
}
h1 {
font-size: 2.8rem;
font-weight: 800;
color: #fff;
letter-spacing: -.04em;
line-height: 1.1;
margin-bottom: 1rem;
max-width: 600px;
font-size: 2.8rem; font-weight: 800; color: #fff;
letter-spacing: -.04em; line-height: 1.1;
margin-bottom: 1rem; max-width: 600px;
}
h1 span { color: var(--blue); }
.tagline {
font-size: 1.1rem;
color: var(--muted);
max-width: 480px;
line-height: 1.6;
margin-bottom: 2.5rem;
font-size: 1.1rem; color: var(--muted); max-width: 480px;
line-height: 1.6; margin-bottom: 2.5rem;
}
/* ---- ACTIONS ---- */
.actions {
display: flex;
gap: .75rem;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 4rem;
display: flex; gap: .75rem; flex-wrap: wrap;
justify-content: center; margin-bottom: 1.5rem;
}
.btn-primary {
padding: .6rem 1.4rem;
background: var(--accent-bright);
color: #fff;
border: 1px solid transparent;
border-radius: 8px;
font-size: .95rem;
font-weight: 600;
text-decoration: none;
transition: opacity .15s;
padding: .6rem 1.4rem; background: var(--accent-bright); color: #fff;
border: 1px solid transparent; border-radius: 8px;
font-size: .95rem; font-weight: 600; text-decoration: none; transition: opacity .15s;
}
.btn-primary:hover { opacity: .85; }
.btn-secondary {
padding: .6rem 1.4rem;
background: transparent;
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
font-size: .95rem;
text-decoration: none;
transition: background .15s;
padding: .6rem 1.4rem; background: transparent; color: var(--text);
border: 1px solid var(--border); border-radius: 8px;
font-size: .95rem; text-decoration: none; transition: background .15s;
}
.btn-secondary:hover { background: var(--card); }
/* ---- INSTALL COMMAND ---- */
.install-box {
margin-bottom: 1.5rem;
display: flex; align-items: center; gap: 0;
background: #0d1117; border: 1px solid var(--border);
border-radius: 10px; overflow: hidden;
font-size: .88rem; max-width: 520px; width: 100%;
}
.install-box code {
flex: 1; padding: .65rem 1rem;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
color: var(--blue); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.install-box button {
padding: .65rem .9rem; background: var(--card);
border: none; border-left: 1px solid var(--border);
color: var(--muted); cursor: pointer; font-size: .8rem;
transition: color .15s, background .15s; white-space: nowrap;
}
.install-box button:hover { background: var(--accent); color: #fff; }
.install-box button.copied { color: var(--green); }
/* ---- DOWNLOAD STRIP ---- */
.download-strip {
margin-bottom: 3.5rem;
display: flex; align-items: center; gap: .6rem;
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: .6rem 1.1rem;
font-size: .85rem;
}
.download-strip a {
color: var(--blue); text-decoration: none; font-weight: 600;
display: flex; align-items: center; gap: .35rem;
}
.download-strip a:hover { text-decoration: underline; }
.download-strip .dl-meta { color: var(--muted); font-size: .78rem; }
.download-strip .dl-sep { color: var(--border); }
/* ---- CARDS ---- */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
max-width: 720px;
width: 100%;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.2rem;
text-align: left;
}
.card-icon {
font-size: 1.3rem;
margin-bottom: .6rem;
display: block;
}
.card h3 {
font-size: .9rem;
font-weight: 600;
color: #fff;
margin-bottom: .3rem;
}
.card p {
font-size: .82rem;
color: var(--muted);
line-height: 1.4;
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; max-width: 720px; width: 100%;
}
.card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 1.2rem; text-align: left; }
.card-icon { font-size: 1.3rem; margin-bottom: .6rem; display: block; }
.card h3 { font-size: .9rem; font-weight: 600; color: #fff; margin-bottom: .3rem; }
.card p { font-size: .82rem; color: var(--muted); line-height: 1.4; }
footer {
border-top: 1px solid var(--border);
padding: 1.2rem 2rem;
text-align: center;
font-size: .8rem;
color: var(--muted);
border-top: 1px solid var(--border); padding: 1.2rem 2rem;
text-align: center; font-size: .8rem; color: var(--muted);
}
footer a { color: var(--muted); text-decoration: none; }
footer a:hover { color: var(--text); }
@ -201,6 +177,24 @@
<a href="https://git.xetup.x9.cz/x9/xetup" class="btn-secondary">Git repozitar</a>
</div>
<div class="install-box">
<code id="install-cmd">curl -Lo xetup.exe xetup.x9.cz/dl</code>
<button id="copy-btn" onclick="copyCmd()">Kopirovat</button>
</div>
<!-- Dynamic download strip filled by JS from Forgejo releases API -->
<div class="download-strip" id="dl-strip" style="display:none">
<a id="dl-link" href="#" download>
&#11015; xetup.exe
</a>
<span class="dl-sep">&middot;</span>
<span class="dl-meta" id="dl-meta">nacitam...</span>
<span class="dl-sep">&middot;</span>
<a href="https://git.xetup.x9.cz/x9/xetup/releases" style="color:var(--muted);font-size:.78rem;text-decoration:none">
vsechny verze
</a>
</div>
<div class="cards">
<div class="card">
<span class="card-icon">&#9881;</span>
@ -220,7 +214,7 @@
<div class="card">
<span class="card-icon">&#128640;</span>
<h3>Go TUI launcher</h3>
<p>xetup.exe &mdash; jednotny binarni spoustec. Zatim ve vyvoji.</p>
<p>xetup.exe &mdash; jednotny binarni spoustec s TUI formularom a live logem.</p>
</div>
</div>
</main>
@ -233,5 +227,45 @@
<a href="/spec/">Specifikace</a>
</footer>
<script>
(function() {
const API = '/forgejo-api';
const REPO = 'x9/xetup';
fetch(API + '/repos/' + REPO + '/releases?limit=1')
.then(r => r.json())
.then(releases => {
if (!Array.isArray(releases) || !releases.length) return;
const rel = releases[0];
const asset = (rel.assets || []).find(a => a.name === 'xetup.exe');
if (!asset) return;
const strip = document.getElementById('dl-strip');
const link = document.getElementById('dl-link');
const meta = document.getElementById('dl-meta');
const sizeMB = (asset.size / 1048576).toFixed(1);
link.href = asset.browser_download_url;
link.textContent = '\u2b15 xetup.exe';
meta.textContent = rel.tag_name + ' \u00b7 ' + sizeMB + ' MB \u00b7 Windows x64';
strip.style.display = '';
})
.catch(() => {});
})();
function copyCmd() {
const cmd = document.getElementById('install-cmd').textContent;
const btn = document.getElementById('copy-btn');
navigator.clipboard.writeText(cmd).then(function() {
btn.textContent = 'Skopirovano!';
btn.classList.add('copied');
setTimeout(function() {
btn.textContent = 'Kopirovat';
btn.classList.remove('copied');
}, 2000);
});
}
</script>
</body>
</html>

View file

@ -14,5 +14,20 @@ server {
add_header X-Content-Type-Options nosniff always;
}
# Permanent shortlink to latest xetup.exe never needs updating
location = /dl {
return 302 https://git.xetup.x9.cz/x9/xetup/releases/download/latest/xetup.exe;
}
# Proxy Forgejo API calls so browser doesn't need CORS or direct access to Forgejo
location /forgejo-api/ {
proxy_pass http://xetup-forgejo:3000/api/v1/;
proxy_set_header Host xetup-forgejo;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Cache-Control "no-store" always;
}
error_page 404 /404.html;
}

View file

@ -419,6 +419,44 @@
.comment-submit:disabled { opacity: .4; cursor: default; }
.comment-error { font-size: .75rem; color: var(--red); }
/* ---- NEW REQUEST WIDGET ---- */
.req-list {
margin-bottom: .8rem;
}
.req-item {
padding: .55rem .7rem;
border: 1px solid var(--border);
border-radius: 7px;
margin-bottom: .45rem;
font-size: .83rem;
line-height: 1.5;
background: var(--card2);
}
.req-item-meta { font-size: .72rem; color: var(--muted); margin-bottom: .2rem; }
.req-item-body { color: var(--text); white-space: pre-wrap; word-break: break-word; }
.req-form { display: flex; flex-direction: column; gap: .45rem; }
.req-name {
width: 220px;
background: var(--bg); border: 1px solid var(--border); border-radius: 5px;
color: var(--text); font-size: .8rem; padding: .35rem .5rem; font-family: inherit;
}
.req-text {
width: 100%;
background: var(--bg); border: 1px solid var(--border); border-radius: 5px;
color: var(--text); font-size: .82rem; padding: .4rem .5rem;
resize: vertical; min-height: 70px; font-family: inherit; line-height: 1.4;
}
.req-name:focus, .req-text:focus { outline: none; border-color: var(--accent-bright); }
.req-submit {
align-self: flex-start;
background: var(--accent-bright); color: #fff; border: none;
border-radius: 5px; padding: .32rem .9rem; font-size: .8rem; cursor: pointer;
transition: opacity .15s;
}
.req-submit:hover { opacity: .85; }
.req-submit:disabled { opacity: .4; cursor: default; }
.req-error { font-size: .75rem; color: var(--red); margin-top: .1rem; }
footer {
border-top: 1px solid var(--border);
padding: 1.2rem 2rem;
@ -470,11 +508,14 @@
<h4>Planovane kroky</h4>
<a href="#step-pc">09 &ndash; PC identita + C:\X9</a>
<a href="#step-net">10 &ndash; Network discovery</a>
<a href="#step-dell">11 &ndash; Dell Command | Update</a>
<a href="#step-taskbar">Taskbar profily</a>
<hr class="sidebar-divider">
<h4>Architektura</h4>
<a href="#arch-xetup">xetup.exe (Go TUI)</a>
<a href="#arch-spec">spec.yaml</a>
<hr class="sidebar-divider">
<a href="#new-requests">+ Novy pozadavek</a>
</aside>
<!-- CONTENT -->
@ -744,6 +785,32 @@
</div>
</div>
<!-- STEP 11 -->
<div class="step" id="step-dell">
<div class="step-header">
<span class="step-num">11</span>
<span class="step-title">Dell Command | Update</span>
<span class="badge badge-ok">OK</span>
</div>
<div class="step-body">
<table class="items">
<tr class="flag-done"><td>Detekce Dell hardware (<code>Win32_ComputerSystem</code>)</td><td>Non-Dell stroj krok preskoci bez chyby &ndash; stejny skript pro vsechny HW</td></tr>
<tr class="flag-done"><td>Instalace Dell Command | Update via winget</td><td><code>Dell.CommandUpdate.Universal</code> &ndash; silent, Win10 + Win11</td></tr>
<tr class="flag-done"><td>Spusteni vsech aktualizaci: drivery, firmware, BIOS</td><td><code>dcu-cli.exe /applyUpdates -silent -reboot=disable</code></td></tr>
<tr class="flag-done"><td>BIOS/firmware se staging &ndash; dokonci se pri restartu</td><td>Restart po konci deploymenty (krok 10 rename) vse dokonci</td></tr>
</table>
<div class="note">
Non-Dell stroje: krok se preskoci automaticky, zadna chyba. Dell Latitude, OptiPlex,
Precision, Vostro, XPS &ndash; vsechny podporovane DCU Universal.<br><br>
<strong>Casova narocnost:</strong> 5&ndash;20 minut podle poctu dostupnych aktualizaci a rychlosti siteho pripojeni.
</div>
</div>
<div class="step-footer">
<span class="step-status">Script: <code>11-dell-update.ps1</code></span>
<div class="comment-widget" data-issue="16"></div>
</div>
</div>
<!-- ============================================================ -->
<p class="section-label">Nove kroky (planovane)</p>
@ -829,11 +896,11 @@
</div>
<div class="step-body">
<table class="items">
<tr class="flag-todo"><td>Single binary (go:embed scripty + assets)</td><td>Offline provoz, jedna stazitelna .exe</td></tr>
<tr class="flag-todo"><td>TUI form (huh/bubbletea): PC name, popis, product key</td><td>Interaktivni zadani dat technikem</td></tr>
<tr class="flag-todo"><td>Checklist kroku (on/off per-script) + ulozit do config.json</td><td>Opakovatelne nasazeni u stejneho klienta</td></tr>
<tr class="flag-todo"><td>Live log output behem spousteni PS scriptu</td><td>Stdout z powershell.exe v realnem case</td></tr>
<tr class="flag-todo"><td>Finalni summary OK/ERROR</td><td>Na konci nasazeni</td></tr>
<tr class="flag-done"><td>Single binary (go:embed scripty + assets)</td><td><code>embed.go</code> + <code>cmd/xetup/main.go</code>; builduje se jako 5 MB .exe</td></tr>
<tr class="flag-done"><td>TUI form (huh/bubbletea): PC name, popis, product key</td><td><code>internal/tui/tui.go</code> &ndash; huh form, 2 stranky</td></tr>
<tr class="flag-done"><td>Checklist kroku (on/off per-script) + ulozit do config.json</td><td>MultiSelect v TUI; <code>internal/config/config.go</code></td></tr>
<tr class="flag-done"><td>Live log output behem spousteni PS scriptu</td><td><code>internal/runner/runner.go</code>; channel + bubbletea cmd</td></tr>
<tr class="flag-done"><td>Finalni summary OK/ERROR</td><td>viewDone() v tui.go</td></tr>
<tr class="flag-todo"><td>Self-update: stahnout novou verzi z xetup.x9.cz</td><td>Overit hash pred spustenim</td></tr>
<tr class="flag-future"><td>config.json: per-klient preset (prefix jmena PC, SW, klic)</td><td>Lezi vedle .exe na USB klienta</td></tr>
<tr class="flag-future"><td>OpenVPN soubor + doménovy join + domén. uzivatel pro profil</td><td>Rozsireni TUI formulare v budoucnu</td></tr>
@ -846,7 +913,7 @@
</div>
</div>
<div class="step-footer">
<span class="step-status">Status: navrh, zatim zadny kod</span>
<span class="step-status">Status: v implementaci &ndash; <code>cmd/xetup/</code>, <code>internal/{config,runner,tui}/</code></span>
<div class="comment-widget" data-issue="11"></div>
</div>
</div>
@ -885,6 +952,34 @@
</div>
</div>
<!-- ============================================================ -->
<p class="section-label">Nove nastaveni &ndash; pozadavky</p>
<!-- NEW REQUESTS -->
<div class="step" id="new-requests">
<div class="step-header">
<span class="step-num" style="font-size:1.1rem">+</span>
<span class="step-title">Novy pozadavek na automatizaci</span>
<span class="badge badge-new">Pozadavky</span>
</div>
<div class="step-body">
<p style="color:var(--muted);font-size:.84rem;line-height:1.5;margin-bottom:1rem">
Chcete automatizovat neco, co skript zatim neresi?
Napiste pozadavek sem &ndash; ulozi se do repozitare.
Technicky tym ho projde a zaradi do planu.
</p>
<div class="req-list" id="req-list">
<div style="font-size:.8rem;color:var(--muted);font-style:italic">Nacitam pozadavky...</div>
</div>
<div class="req-form" id="req-form">
<input class="req-name" id="req-name" placeholder="Vase jmeno (volitelne)" maxlength="60">
<textarea class="req-text" id="req-text" placeholder="Co by mel automat delat? Popiste konkretne, idealne i proc." rows="3"></textarea>
<button class="req-submit" id="req-submit">Odeslat pozadavek</button>
<span class="req-error" id="req-error"></span>
</div>
</div>
</div>
</div><!-- /content -->
</div><!-- /layout -->
@ -1037,6 +1132,7 @@
'step-pc': '09-pc-identity',
'step-net': '10-network',
'step-08': '08-activation',
'step-dell': '11-dell-update',
};
function getItemDesc(stepId, slug) {
@ -1189,6 +1285,69 @@
});
}
// ---- NEW-REQUEST WIDGET (issue #15) ----
(function() {
const ISSUE = 15;
const listEl = document.getElementById('req-list');
const nameEl = document.getElementById('req-name');
const textEl = document.getElementById('req-text');
const submitEl = document.getElementById('req-submit');
const errorEl = document.getElementById('req-error');
if (!listEl) return;
function renderRequests(data) {
listEl.innerHTML = '';
if (!data.length) {
listEl.innerHTML = '<div style="font-size:.8rem;color:var(--muted);font-style:italic;margin-bottom:.5rem">Zatim zadne pozadavky. Bud prvni.</div>';
return;
}
data.forEach(c => {
const m = c.body.match(/^\*\*(.+?)\*\*\n([\s\S]*)$/);
const name = m ? m[1] : (c.user.login === 'xetup-bot' ? 'anon' : c.user.login);
const text = m ? m[2] : c.body;
const el = document.createElement('div');
el.className = 'req-item';
el.innerHTML =
'<div class="req-item-meta">' + name.replace(/</g,'&lt;') + ' &middot; ' + timeAgo(c.created_at) + '</div>' +
'<div class="req-item-body">' + text.replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</div>';
listEl.appendChild(el);
});
}
fetch(API + '/repos/' + REPO + '/issues/' + ISSUE + '/comments', { headers: HEADS })
.then(r => r.json())
.then(d => renderRequests(Array.isArray(d) ? d : []))
.catch(() => { listEl.innerHTML = '<div style="font-size:.8rem;color:var(--red)">Chyba nacitani.</div>'; });
submitEl.addEventListener('click', () => {
const text = textEl.value.trim();
if (!text) { errorEl.textContent = 'Pozadavek nesmi byt prazdny.'; return; }
errorEl.textContent = '';
submitEl.disabled = true;
submitEl.textContent = 'Odesilam...';
const name = nameEl.value.trim() || 'anon';
const body = '**' + name + '**\n' + text;
fetch(API + '/repos/' + REPO + '/issues/' + ISSUE + '/comments', {
method: 'POST', headers: HEADS, body: JSON.stringify({ body: body })
})
.then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
.then(c => {
textEl.value = ''; nameEl.value = '';
submitEl.disabled = false;
submitEl.textContent = 'Odeslat pozadavek';
// re-fetch to show updated list
return fetch(API + '/repos/' + REPO + '/issues/' + ISSUE + '/comments', { headers: HEADS });
})
.then(r => r.json())
.then(d => renderRequests(Array.isArray(d) ? d : []))
.catch(() => {
errorEl.textContent = 'Chyba odeslani. Zkus znovu.';
submitEl.disabled = false;
submitEl.textContent = 'Odeslat pozadavek';
});
});
})();
enhanceRows();
})();
</script>

BIN
xetup.exe Executable file

Binary file not shown.