Compare commits
9 commits
0cb00c4a46
...
d05835c931
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d05835c931 | ||
|
|
9a4afbf913 | ||
|
|
f72b4bd190 | ||
|
|
c3c8d8e501 | ||
|
|
82349dbe31 | ||
|
|
5b53b2a0d6 | ||
|
|
a10a3a8aa2 | ||
|
|
be7a7236df | ||
|
|
1198de3c49 |
20 changed files with 2027 additions and 444 deletions
63
.forgejo/workflows/release.yml
Normal file
63
.forgejo/workflows/release.yml
Normal 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})"
|
||||
|
|
@ -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
69
cmd/xetup/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
16
embed.go
Normal 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
32
go.mod
Normal 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
55
go.sum
Normal 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
145
internal/config/config.go
Normal 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
336
internal/runner/runner.go
Normal 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
416
internal/tui/tui.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
152
scripts/11-dell-update.ps1
Normal 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
|
||||
|
|
@ -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
18
web/get.ps1
Normal 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: $_"
|
||||
}
|
||||
214
web/index.html
214
web/index.html
|
|
@ -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>
|
||||
⬇ xetup.exe
|
||||
</a>
|
||||
<span class="dl-sep">·</span>
|
||||
<span class="dl-meta" id="dl-meta">nacitam...</span>
|
||||
<span class="dl-sep">·</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">⚙</span>
|
||||
|
|
@ -220,7 +214,7 @@
|
|||
<div class="card">
|
||||
<span class="card-icon">🚀</span>
|
||||
<h3>Go TUI launcher</h3>
|
||||
<p>xetup.exe — jednotny binarni spoustec. Zatim ve vyvoji.</p>
|
||||
<p>xetup.exe — 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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 – PC identita + C:\X9</a>
|
||||
<a href="#step-net">10 – Network discovery</a>
|
||||
<a href="#step-dell">11 – 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 – 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> – 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 – 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 – vsechny podporovane DCU Universal.<br><br>
|
||||
<strong>Casova narocnost:</strong> 5–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> – 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 – <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 – 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 – 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,'<') + ' · ' + timeAgo(c.created_at) + '</div>' +
|
||||
'<div class="req-item-body">' + text.replace(/</g,'<').replace(/>/g,'>') + '</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
BIN
xetup.exe
Executable file
Binary file not shown.
Loading…
Reference in a new issue