Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
51 changed files with 479 additions and 6713 deletions
|
|
@ -1,7 +0,0 @@
|
|||
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-04-15T18:56:06.304Z","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","content":"hello"}
|
||||
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-04-15T18:56:06.305Z","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399"}
|
||||
{"parentUuid":null,"isSidechain":false,"promptId":"66ae766f-c1d3-410d-ae3e-b981c0724f21","type":"user","message":{"role":"user","content":"hello"},"uuid":"1e762f24-47aa-4036-9a94-8971b320b1c4","timestamp":"2026-04-15T18:56:06.454Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","version":"2.1.109","gitBranch":"HEAD"}
|
||||
{"parentUuid":"1e762f24-47aa-4036-9a94-8971b320b1c4","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"d095c33b-07ad-4e15-a52c-aa116a7cb2fa","timestamp":"2026-04-15T18:56:06.453Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","version":"2.1.109","gitBranch":"HEAD"}
|
||||
{"parentUuid":"d095c33b-07ad-4e15-a52c-aa116a7cb2fa","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"d2e44913-c8b0-4ab7-8fc9-8bb0ab8ba863","timestamp":"2026-04-15T18:56:06.453Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","version":"2.1.109","gitBranch":"HEAD"}
|
||||
{"parentUuid":"d2e44913-c8b0-4ab7-8fc9-8bb0ab8ba863","isSidechain":false,"message":{"model":"claude-sonnet-4-6","id":"msg_01WwmhgEvkxkYCsd7hXfhbKG","type":"message","role":"assistant","content":[{"type":"text","text":"Hello! How can I help you today?"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":4742,"cache_read_input_tokens":11877,"output_tokens":12,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":4742},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":12,"cache_read_input_tokens":11877,"cache_creation_input_tokens":4742,"cache_creation":{"ephemeral_5m_input_tokens":4742,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"}},"requestId":"req_011Ca64pEERr5BfpcFu72ehk","type":"assistant","uuid":"8f1ae4f9-0b1d-4d74-bbe4-1b0112f3f259","timestamp":"2026-04-15T18:56:07.826Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399","version":"2.1.109","gitBranch":"HEAD"}
|
||||
{"type":"last-prompt","lastPrompt":"hello","sessionId":"1b488982-247d-4af2-8dd6-75a38d3f0399"}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +0,0 @@
|
|||
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-04-16T05:31:24.106Z","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","content":"hello"}
|
||||
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-04-16T05:31:24.107Z","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453"}
|
||||
{"parentUuid":null,"isSidechain":false,"promptId":"733c4645-7420-4083-a8d9-f02e3a109bcb","type":"user","message":{"role":"user","content":"hello"},"uuid":"228cfd6f-7aca-446d-899f-ec53687a2036","timestamp":"2026-04-16T05:31:24.266Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"228cfd6f-7aca-446d-899f-ec53687a2036","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"506972de-2be9-4015-8118-71b1d369045f","timestamp":"2026-04-16T05:31:24.265Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"506972de-2be9-4015-8118-71b1d369045f","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"75dcf71c-c24a-42fa-a4fd-80f37473a551","timestamp":"2026-04-16T05:31:24.265Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"75dcf71c-c24a-42fa-a4fd-80f37473a551","isSidechain":false,"message":{"model":"claude-sonnet-4-6","id":"msg_01VXSefWtX3DKqx9aHW2UdHi","type":"message","role":"assistant","content":[{"type":"text","text":"Ahoj! Jak ti můžu pomoct?"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":7263,"cache_read_input_tokens":11877,"output_tokens":16,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":7263},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":16,"cache_read_input_tokens":11877,"cache_creation_input_tokens":7263,"cache_creation":{"ephemeral_5m_input_tokens":7263,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"}},"requestId":"req_011Ca6uG4DV96LqFZMvx9go7","type":"assistant","uuid":"9d7646ca-bff7-4cc1-8ec7-99c8e4fde647","timestamp":"2026-04-16T05:31:25.566Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453","version":"2.1.109","gitBranch":"main"}
|
||||
{"type":"last-prompt","lastPrompt":"hello","sessionId":"5fe1aaca-13a5-4494-83fd-61bbd946f453"}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +0,0 @@
|
|||
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-04-16T05:31:09.921Z","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","content":"hello"}
|
||||
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-04-16T05:31:09.922Z","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514"}
|
||||
{"parentUuid":null,"isSidechain":false,"promptId":"1ac9be3b-4664-42a9-9890-085b4ce24690","type":"user","message":{"role":"user","content":"hello"},"uuid":"1d92e5f9-131a-4af0-bec5-54480d51e923","timestamp":"2026-04-16T05:31:10.163Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"1d92e5f9-131a-4af0-bec5-54480d51e923","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"9bd20a36-2eb7-42ff-8444-8f3f0e4f87fb","timestamp":"2026-04-16T05:31:10.162Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"9bd20a36-2eb7-42ff-8444-8f3f0e4f87fb","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"9f5dd7c2-375c-4d65-99fa-852b77d6f336","timestamp":"2026-04-16T05:31:10.162Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"9f5dd7c2-375c-4d65-99fa-852b77d6f336","isSidechain":false,"type":"assistant","uuid":"6792e80d-c530-441b-8d46-44182aff574f","timestamp":"2026-04-16T05:31:10.222Z","message":{"id":"167497be-9d34-45dd-b1be-faa189c74034","container":null,"model":"<synthetic>","role":"assistant","stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"Not logged in · Please run /login"}],"context_management":null},"error":"authentication_failed","isApiErrorMessage":true,"userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514","version":"2.1.109","gitBranch":"main"}
|
||||
{"type":"last-prompt","lastPrompt":"hello","sessionId":"9ff22271-fc02-43f2-83cd-199c4d72b514"}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,8 +0,0 @@
|
|||
{"type":"permission-mode","permissionMode":"bypassPermissions","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b"}
|
||||
{"type":"file-history-snapshot","messageId":"c33f10bb-3085-442c-a697-657579e695df","snapshot":{"messageId":"c33f10bb-3085-442c-a697-657579e695df","trackedFileBackups":{},"timestamp":"2026-04-16T05:30:32.482Z"},"isSnapshotUpdate":false}
|
||||
{"parentUuid":null,"isSidechain":false,"promptId":"9dbdde24-7495-438b-abbf-70c4418cbf22","type":"user","message":{"role":"user","content":"shrň aktuálnní stav a pojďme plánovat dnešek"},"uuid":"c33f10bb-3085-442c-a697-657579e695df","timestamp":"2026-04-16T05:30:32.482Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"cli","cwd":"/opt/xetup","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"c33f10bb-3085-442c-a697-657579e695df","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskCreate","TaskGet","TaskList","TaskOutput","TaskStop","TaskUpdate","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"5f7d7dc7-d196-48da-ab8e-04f31de266c3","timestamp":"2026-04-16T05:30:32.480Z","userType":"external","entrypoint":"cli","cwd":"/opt/xetup","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"5f7d7dc7-d196-48da-ab8e-04f31de266c3","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"2125fb51-964a-4ffa-9f81-9f768e18ea06","timestamp":"2026-04-16T05:30:32.480Z","userType":"external","entrypoint":"cli","cwd":"/opt/xetup","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"2125fb51-964a-4ffa-9f81-9f768e18ea06","isSidechain":false,"type":"assistant","uuid":"3c7ff654-8f1f-484b-acbd-f5fc7b7ae6ab","timestamp":"2026-04-16T05:30:32.555Z","message":{"id":"14b11517-d5d8-4616-8e78-f427f214c837","container":null,"model":"<synthetic>","role":"assistant","stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"Not logged in · Please run /login"}],"context_management":null},"error":"authentication_failed","isApiErrorMessage":true,"userType":"external","entrypoint":"cli","cwd":"/opt/xetup","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b","version":"2.1.109","gitBranch":"main"}
|
||||
{"type":"last-prompt","lastPrompt":"shrň aktuálnní stav a pojďme plánovat dnešek","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b"}
|
||||
{"type":"permission-mode","permissionMode":"bypassPermissions","sessionId":"de707b1a-05ac-4396-b84d-6e78d2d9e89b"}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-04-16T06:09:34.704Z","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","content":"hello"}
|
||||
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-04-16T06:09:34.704Z","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3"}
|
||||
{"parentUuid":null,"isSidechain":false,"promptId":"0d818749-ad6c-4820-b5b2-2feaf7649ad8","type":"user","message":{"role":"user","content":"hello"},"uuid":"30d2e6d5-0cde-49a4-a649-54567315d0f7","timestamp":"2026-04-16T06:09:34.857Z","permissionMode":"bypassPermissions","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"30d2e6d5-0cde-49a4-a649-54567315d0f7","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch"],"removedNames":[]},"type":"attachment","uuid":"24063f49-fa5f-4064-b778-ed196a8faf6d","timestamp":"2026-04-16T06:09:34.856Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"24063f49-fa5f-4064-b778-ed196a8faf6d","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, use Config tool.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (triggers) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/triggers.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching.\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- statusline: Set up Claude Code's status line UI\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch\n- insights: Generate a report analyzing your Claude Code sessions\n- team-onboarding: Help teammates ramp on Claude Code with a guide from your usage","skillCount":12,"isInitial":true},"type":"attachment","uuid":"195c31bf-837a-4c76-8577-52e7013962d4","timestamp":"2026-04-16T06:09:34.856Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","version":"2.1.109","gitBranch":"main"}
|
||||
{"parentUuid":"195c31bf-837a-4c76-8577-52e7013962d4","isSidechain":false,"message":{"model":"claude-sonnet-4-6","id":"msg_01CbdnUgvNLh5zj337pQznQK","type":"message","role":"assistant","content":[{"type":"text","text":"Ahoj! Jak mohu pomoci?"}],"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":7263,"cache_read_input_tokens":11877,"output_tokens":14,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":7263},"inference_geo":"","iterations":[{"input_tokens":3,"output_tokens":14,"cache_read_input_tokens":11877,"cache_creation_input_tokens":7263,"cache_creation":{"ephemeral_5m_input_tokens":7263,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"}},"requestId":"req_011Ca6xAuUbG4ihREHZN9SF1","type":"assistant","uuid":"4862843a-e4de-4928-a0d3-795303fac911","timestamp":"2026-04-16T06:09:36.076Z","userType":"external","entrypoint":"sdk-cli","cwd":"/opt/xetup","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3","version":"2.1.109","gitBranch":"main"}
|
||||
{"type":"last-prompt","lastPrompt":"hello","sessionId":"e618bcf1-ee78-42a9-9fe6-22c6a60b5aa3"}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,81 +0,0 @@
|
|||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'scripts/**'
|
||||
- 'assets/**'
|
||||
- 'embed.go'
|
||||
- 'cmd/xetup/app.manifest'
|
||||
- '.forgejo/workflows/release.yml'
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
# Runner label 'ubuntu-latest' maps to golang:1.24-alpine container (see runner config)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: sh
|
||||
working-directory: /repo
|
||||
|
||||
steps:
|
||||
- name: Setup
|
||||
working-directory: /
|
||||
run: |
|
||||
apk add --no-cache git curl jq
|
||||
git clone --depth=1 \
|
||||
"http://x9:${{ secrets.FORGEJO_TOKEN }}@xetup-forgejo:3000/${{ github.repository }}.git" \
|
||||
/repo
|
||||
cd /repo
|
||||
git checkout "${{ github.sha }}"
|
||||
|
||||
- name: Generate rsrc.syso (manifest + UAC)
|
||||
run: |
|
||||
go install github.com/akavel/rsrc@latest
|
||||
rsrc -manifest cmd/xetup/app.manifest -o cmd/xetup/rsrc.syso
|
||||
echo "rsrc.syso: $(ls -lh cmd/xetup/rsrc.syso | awk '{print $5}')"
|
||||
|
||||
- name: Build xetup.exe
|
||||
run: |
|
||||
GOOS=windows GOARCH=amd64 \
|
||||
go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup/
|
||||
echo "Built: $(ls -lh xetup.exe | awk '{print $5}')"
|
||||
|
||||
- name: Publish latest release
|
||||
env:
|
||||
TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
API: http://xetup-forgejo:3000/api/v1
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
|
||||
# Delete existing 'latest' release and 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
|
||||
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})"
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -21,7 +21,3 @@ flash.zip
|
|||
|
||||
# Large reference files
|
||||
W11.pdf
|
||||
|
||||
# Claude Code runtime artifacts
|
||||
.claude/scheduled_tasks.lock
|
||||
web/data/deploy.json
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ $stepsEnabled = @{
|
|||
network = $true
|
||||
pcIdentity = $true
|
||||
activation = $true
|
||||
dellUpdate = $true
|
||||
}
|
||||
if ($Config -and $Config.steps) {
|
||||
foreach ($key in @($stepsEnabled.Keys)) {
|
||||
|
|
@ -222,15 +221,6 @@ 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
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -56,11 +56,8 @@
|
|||
;;
|
||||
|
||||
[General]
|
||||
; #223B47 in COLORREF (0x00BBGGRR): R=34 G=59 B=71 = 71*65536 + 59*256 + 34 = 4668194
|
||||
BackgroundColor = 4668194
|
||||
; AutoBackground=0: do NOT read live desktop color - unreliable during deployment
|
||||
; (live session may not yet reflect the registry background color change)
|
||||
AutoBackground = 0
|
||||
BackgroundColor = 2097152
|
||||
AutoBackground = 1
|
||||
Output = %temp%\backinfo.bmp
|
||||
UpdateDesktop = 1
|
||||
LineSpacing = 2
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="amd64"
|
||||
name="xetup"
|
||||
type="win32"
|
||||
/>
|
||||
<description>xetup Windows deployment tool</description>
|
||||
|
||||
<!-- Request administrator privileges -->
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<!-- Enable modern ComCtl32 v6 (themed controls, proper checkboxes, etc.) -->
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
|
||||
<!-- DPI awareness: system-DPI aware (no blurry scaling) -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
// Command xetup is the GUI launcher for Windows deployment.
|
||||
//
|
||||
// All PowerShell scripts and assets are embedded in the binary and extracted
|
||||
// to a temp directory at runtime. The GUI collects configuration, streams
|
||||
// live log output while the scripts run, and shows a final summary.
|
||||
//
|
||||
// Cross-compile for Windows (requires MinGW):
|
||||
//
|
||||
// CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
|
||||
// GOOS=windows GOARCH=amd64 \
|
||||
// go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
xetupembed "git.xetup.x9.cz/x9/xetup"
|
||||
"git.xetup.x9.cz/x9/xetup/internal/config"
|
||||
"git.xetup.x9.cz/x9/xetup/internal/gui"
|
||||
"git.xetup.x9.cz/x9/xetup/internal/runner"
|
||||
"git.xetup.x9.cz/x9/xetup/internal/state"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 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 and assets (required in both initial and resume mode)
|
||||
if err := runner.ExtractScripts(xetupembed.Scripts, tmpDir); err != nil {
|
||||
log.Fatalf("Failed to extract scripts: %v", err)
|
||||
}
|
||||
if err := runner.ExtractAssets(xetupembed.Assets, tmpDir); err != nil {
|
||||
log.Fatalf("Failed to extract assets: %v", err)
|
||||
}
|
||||
|
||||
// Determine config: use stored state in resume mode, load from file otherwise
|
||||
cfgPath := config.ConfigPath()
|
||||
var cfg config.Config
|
||||
|
||||
if st, err := state.Load(); err == nil {
|
||||
// Resume mode: use the config that was active when the run started
|
||||
cfg = st.Config
|
||||
if st.ConfigPath != "" {
|
||||
cfgPath = st.ConfigPath
|
||||
}
|
||||
} else {
|
||||
// Normal mode: load config from disk (falls back to defaults when absent)
|
||||
cfg, err = config.Load(cfgPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: config load failed (%v), using defaults\n", 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,
|
||||
}
|
||||
|
||||
gui.Run(cfg, runCfg, cfgPath)
|
||||
}
|
||||
|
|
@ -38,8 +38,6 @@ services:
|
|||
image: code.forgejo.org/forgejo/runner:6.3.1
|
||||
container_name: xetup-runner
|
||||
restart: unless-stopped
|
||||
entrypoint: ["/bin/sh", "-c", "forgejo-runner daemon --config /etc/runner/config.yml"]
|
||||
user: "0:996" # root:docker - needed for /var/run/docker.sock access
|
||||
depends_on:
|
||||
- forgejo
|
||||
environment:
|
||||
|
|
@ -47,7 +45,6 @@ services:
|
|||
volumes:
|
||||
- runner-data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./runner-config.yml:/etc/runner/config.yml:ro
|
||||
networks:
|
||||
- xetup
|
||||
|
||||
|
|
|
|||
16
embed.go
16
embed.go
|
|
@ -1,16 +0,0 @@
|
|||
// 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
|
||||
11
go.mod
11
go.mod
|
|
@ -1,11 +0,0 @@
|
|||
module git.xetup.x9.cz/x9/xetup
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
|
||||
|
||||
require (
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
|
||||
)
|
||||
9
go.sum
9
go.sum
|
|
@ -1,9 +0,0 @@
|
|||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
37
heal.sh
37
heal.sh
|
|
@ -1,37 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Self-heal: pull latest git changes, deploy web content, keep containers up.
|
||||
# Runs every 5 minutes via systemd timer.
|
||||
set -euo pipefail
|
||||
cd /opt/xetup
|
||||
|
||||
LOG="[heal $(date '+%H:%M:%S')]"
|
||||
|
||||
# Pull latest changes (ff-only: never merge, just update)
|
||||
BEFORE=$(git rev-parse HEAD 2>/dev/null || echo "none")
|
||||
git fetch --quiet origin main 2>/dev/null || true
|
||||
REMOTE=$(git rev-parse origin/main 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$REMOTE" ] && [ "$BEFORE" != "$REMOTE" ]; then
|
||||
git reset --hard origin/main --quiet 2>/dev/null || true
|
||||
AFTER=$(git rev-parse HEAD)
|
||||
echo "$LOG deployed $BEFORE → $AFTER"
|
||||
|
||||
# Write deployed commit info for the web (spec page footer)
|
||||
echo "{\"sha\":\"$(git rev-parse --short HEAD)\",\"ts\":\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
||||
> web/data/deploy.json
|
||||
|
||||
# If web content changed, reload nginx config
|
||||
if git diff --name-only "$BEFORE" "$AFTER" 2>/dev/null | grep -q '^web/'; then
|
||||
echo "$LOG web/ changed - reloading nginx"
|
||||
/usr/bin/docker compose exec -T web nginx -s reload 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
# Always keep deploy.json fresh on first run if missing
|
||||
if [ ! -f web/data/deploy.json ]; then
|
||||
echo "{\"sha\":\"$(git rev-parse --short HEAD)\",\"ts\":\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
|
||||
> web/data/deploy.json
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure all containers are running
|
||||
/usr/bin/docker compose up -d --remove-orphans 2>&1 | grep -v "up-to-date\|unchanged" || true
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
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,
|
||||
"windowsUpdate": 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")
|
||||
}
|
||||
|
|
@ -1,739 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
// Package gui implements the Walk-based graphical interface for xetup.
|
||||
//
|
||||
// Three sequential phases, each with its own window:
|
||||
// 1. Config form – PC name, product key, profile, step checkboxes,
|
||||
// load/save config buttons for per-client presets
|
||||
// 2. Live run – real-time log streamed from PowerShell scripts
|
||||
// 3. Summary – per-step OK / ERROR / SKIPPED with elapsed time
|
||||
//
|
||||
// Resume mode (state file present): skips the config form and runs only
|
||||
// the pending steps recorded in the state file. On completion, disables
|
||||
// autologon and removes the X9-Resume scheduled task.
|
||||
package gui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lxn/walk"
|
||||
. "github.com/lxn/walk/declarative"
|
||||
|
||||
"git.xetup.x9.cz/x9/xetup/internal/config"
|
||||
"git.xetup.x9.cz/x9/xetup/internal/prereboot"
|
||||
"git.xetup.x9.cz/x9/xetup/internal/runner"
|
||||
"git.xetup.x9.cz/x9/xetup/internal/state"
|
||||
)
|
||||
|
||||
// Run opens the xetup window and blocks until the user closes it.
|
||||
func Run(cfg config.Config, runCfg runner.RunConfig, cfgPath string) {
|
||||
// Resume mode: state file present from a previous interrupted run
|
||||
if st, err := state.Load(); err == nil {
|
||||
resumePhase(st, runCfg)
|
||||
return
|
||||
}
|
||||
|
||||
// Normal flow: config form → run → summary
|
||||
for {
|
||||
res := formPhase(cfg, runCfg, cfgPath)
|
||||
switch res.action {
|
||||
case "quit":
|
||||
return
|
||||
case "reload":
|
||||
cfg = res.cfg
|
||||
cfgPath = res.cfgPath
|
||||
case "run":
|
||||
runCfg.ProfileType = res.cfg.Deployment.ProfileType
|
||||
results, needsReboot := runPhase(runCfg, res.steps, false)
|
||||
if needsReboot {
|
||||
prepareRebootAndRestart(res.cfg, res.steps, results, cfgPath, runCfg)
|
||||
return
|
||||
}
|
||||
donePhase(results, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Resume mode
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
func resumePhase(st *state.State, runCfg runner.RunConfig) {
|
||||
runCfg.LogFile = st.LogFile
|
||||
|
||||
steps := runner.StepsByIDs(st.PendingSteps)
|
||||
results, needsReboot := runPhase(runCfg, steps, true)
|
||||
|
||||
// Accumulate completed results (NeedsReboot step excluded – runs again)
|
||||
newResults := append(st.Results, toStateResults(results)...) //nolint:gocritic
|
||||
|
||||
if needsReboot {
|
||||
// Update state and reboot – infrastructure already in place (running as adminx9)
|
||||
pending := pendingStepIDs(steps, results)
|
||||
newSt := &state.State{
|
||||
Config: st.Config,
|
||||
ConfigPath: st.ConfigPath,
|
||||
LogFile: st.LogFile,
|
||||
PendingSteps: pending,
|
||||
Results: newResults,
|
||||
}
|
||||
if err := state.Save(newSt); err != nil {
|
||||
walk.MsgBox(nil, "Chyba", "Nelze ulozit stav: "+err.Error(), walk.MsgBoxIconError)
|
||||
}
|
||||
reboot()
|
||||
return
|
||||
}
|
||||
|
||||
// All steps complete
|
||||
prereboot.Cleanup()
|
||||
_ = state.Delete()
|
||||
donePhase(results, st.Results)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Phase 1 – Config form
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
type formResult struct {
|
||||
action string // "run", "reload", "quit"
|
||||
cfg config.Config
|
||||
cfgPath string
|
||||
steps []runner.Step
|
||||
}
|
||||
|
||||
func formPhase(cfg config.Config, runCfg runner.RunConfig, cfgPath string) formResult {
|
||||
var mw *walk.MainWindow
|
||||
result := formResult{action: "quit", cfgPath: cfgPath}
|
||||
|
||||
var (
|
||||
pcNameLE *walk.LineEdit
|
||||
pcDescLE *walk.LineEdit
|
||||
productKeyLE *walk.LineEdit
|
||||
profileCB *walk.ComboBox
|
||||
)
|
||||
|
||||
items := runner.AllSelectableItems()
|
||||
checkPtrs := make([]*walk.CheckBox, len(items))
|
||||
checkWidgets := make([]Widget, len(items))
|
||||
for i, item := range items {
|
||||
i := i // capture for closure
|
||||
checkWidgets[i] = CheckBox{
|
||||
AssignTo: &checkPtrs[i],
|
||||
Text: item.Label,
|
||||
Checked: itemEnabled(cfg, item),
|
||||
}
|
||||
}
|
||||
|
||||
collectCfg := func() config.Config {
|
||||
out := cfg
|
||||
out.Deployment.PCName = pcNameLE.Text()
|
||||
out.Deployment.PCDescription = pcDescLE.Text()
|
||||
out.Activation.ProductKey = productKeyLE.Text()
|
||||
profiles := []string{"default", "admin", "user"}
|
||||
if idx := profileCB.CurrentIndex(); idx >= 0 && idx < len(profiles) {
|
||||
out.Deployment.ProfileType = profiles[idx]
|
||||
}
|
||||
selected := make(map[string]bool, len(items))
|
||||
for i, item := range items {
|
||||
selected[item.Key] = checkPtrs[i].Checked()
|
||||
}
|
||||
_, features := buildStepsAndFeatures(selected)
|
||||
out.Features = features
|
||||
return out
|
||||
}
|
||||
|
||||
if err := (MainWindow{
|
||||
AssignTo: &mw,
|
||||
Title: "xetup \u2014 Windows deployment",
|
||||
Size: Size{Width: 760, Height: 740},
|
||||
Layout: VBox{},
|
||||
Children: []Widget{
|
||||
// ── Form fields ──────────────────────────────────────────────────
|
||||
Composite{
|
||||
Layout: Grid{Columns: 2},
|
||||
Children: []Widget{
|
||||
Label{Text: "PC jmeno:"},
|
||||
LineEdit{
|
||||
AssignTo: &pcNameLE,
|
||||
Text: cfg.Deployment.PCName,
|
||||
CueBanner: "napr. NB-KLIENT-01 (prazdne = neprejmenovat)",
|
||||
},
|
||||
Label{Text: "Popis PC:"},
|
||||
LineEdit{
|
||||
AssignTo: &pcDescLE,
|
||||
Text: cfg.Deployment.PCDescription,
|
||||
CueBanner: "napr. PC recepce",
|
||||
},
|
||||
Label{Text: "Product Key:"},
|
||||
LineEdit{
|
||||
AssignTo: &productKeyLE,
|
||||
Text: cfg.Activation.ProductKey,
|
||||
CueBanner: "prazdne = OA3 / GVLK fallback",
|
||||
},
|
||||
Label{Text: "Profil:"},
|
||||
ComboBox{
|
||||
AssignTo: &profileCB,
|
||||
Model: []string{"default", "admin", "user"},
|
||||
CurrentIndex: profileIndex(cfg.Deployment.ProfileType),
|
||||
},
|
||||
},
|
||||
},
|
||||
HSeparator{},
|
||||
// ── Step checkboxes ──────────────────────────────────────────────
|
||||
Label{Text: "Kroky a nastaveni (odskrtnete co nechcete spustit):"},
|
||||
ScrollView{
|
||||
MinSize: Size{Height: 400},
|
||||
Layout: VBox{MarginsZero: true},
|
||||
Children: checkWidgets,
|
||||
},
|
||||
HSeparator{},
|
||||
// ── Buttons ──────────────────────────────────────────────────────
|
||||
Composite{
|
||||
Layout: HBox{MarginsZero: true},
|
||||
Children: []Widget{
|
||||
PushButton{
|
||||
Text: "Nacist config...",
|
||||
OnClicked: func() {
|
||||
dlg := new(walk.FileDialog)
|
||||
dlg.Filter = "JSON soubory (*.json)|*.json|Vsechny soubory (*.*)|*.*"
|
||||
dlg.Title = "Nacist konfiguraci"
|
||||
ok, err := dlg.ShowOpen(mw)
|
||||
if err != nil || !ok {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(dlg.FilePath)
|
||||
if err != nil {
|
||||
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
|
||||
return
|
||||
}
|
||||
newCfg := config.DefaultConfig()
|
||||
if err := json.Unmarshal(data, &newCfg); err != nil {
|
||||
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
|
||||
return
|
||||
}
|
||||
result = formResult{
|
||||
action: "reload",
|
||||
cfg: newCfg,
|
||||
cfgPath: dlg.FilePath,
|
||||
}
|
||||
mw.Close()
|
||||
},
|
||||
},
|
||||
PushButton{
|
||||
Text: "Ulozit config...",
|
||||
OnClicked: func() {
|
||||
dlg := new(walk.FileDialog)
|
||||
dlg.Filter = "JSON soubory (*.json)|*.json|Vsechny soubory (*.*)|*.*"
|
||||
dlg.Title = "Ulozit konfiguraci"
|
||||
dlg.FilePath = "config.json"
|
||||
ok, err := dlg.ShowSave(mw)
|
||||
if err != nil || !ok {
|
||||
return
|
||||
}
|
||||
data, err := json.MarshalIndent(collectCfg(), "", " ")
|
||||
if err != nil {
|
||||
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(dlg.FilePath, data, 0644); err != nil {
|
||||
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
|
||||
}
|
||||
},
|
||||
},
|
||||
HSpacer{},
|
||||
PushButton{
|
||||
Text: " SPUSTIT ",
|
||||
OnClicked: func() {
|
||||
finalCfg := collectCfg()
|
||||
selected := make(map[string]bool, len(items))
|
||||
for i, item := range items {
|
||||
selected[item.Key] = checkPtrs[i].Checked()
|
||||
}
|
||||
steps, features := buildStepsAndFeatures(selected)
|
||||
finalCfg.Features = features
|
||||
_ = config.Save(finalCfg, cfgPath)
|
||||
result = formResult{
|
||||
action: "run",
|
||||
cfg: finalCfg,
|
||||
cfgPath: cfgPath,
|
||||
steps: steps,
|
||||
}
|
||||
mw.Close()
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}.Create()); err != nil {
|
||||
walk.MsgBox(nil, "Chyba", fmt.Sprintf("Nelze vytvorit okno: %v", err), walk.MsgBoxIconError)
|
||||
return result
|
||||
}
|
||||
|
||||
mw.Run()
|
||||
return result
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Phase 2 – Live run view
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// stepIndicator tracks a step's label and current status.
|
||||
type stepIndicator struct {
|
||||
label *walk.Label
|
||||
stepID string
|
||||
status string // "pending", "running", "ok", "error", "skipped", "cancelled"
|
||||
}
|
||||
|
||||
// runPhase runs steps and streams live log output.
|
||||
// resuming=true changes the title and initial status message.
|
||||
// Returns (results, needsReboot).
|
||||
func runPhase(runCfg runner.RunConfig, steps []runner.Step, resuming bool) ([]runner.Result, bool) {
|
||||
var (
|
||||
mw *walk.MainWindow
|
||||
statusLbl *walk.Label
|
||||
logTE *walk.TextEdit
|
||||
cancelFn context.CancelFunc
|
||||
)
|
||||
|
||||
// Build step indicator labels (one per step, in order)
|
||||
indicators := make([]stepIndicator, len(steps))
|
||||
stepIndex := make(map[string]int, len(steps))
|
||||
indWidgets := make([]Widget, len(steps))
|
||||
for i, s := range steps {
|
||||
indicators[i] = stepIndicator{stepID: s.ID, status: "pending"}
|
||||
stepIndex[s.ID] = i
|
||||
indWidgets[i] = Label{
|
||||
AssignTo: &indicators[i].label,
|
||||
Text: s.Num + " \u00b7",
|
||||
Font: Font{Family: "Consolas", PointSize: 9},
|
||||
MinSize: Size{Width: 38},
|
||||
}
|
||||
}
|
||||
|
||||
title := "xetup \u2014 probiha instalace"
|
||||
initialStatus := "Spoustim..."
|
||||
if resuming {
|
||||
title = "xetup \u2014 pokracuji po restartu"
|
||||
initialStatus = "Pokracuji po restartu..."
|
||||
}
|
||||
|
||||
if err := (MainWindow{
|
||||
AssignTo: &mw,
|
||||
Title: title,
|
||||
Size: Size{Width: 760, Height: 740},
|
||||
Layout: VBox{},
|
||||
Children: []Widget{
|
||||
Label{AssignTo: &statusLbl, Text: initialStatus},
|
||||
// Step progress strip
|
||||
Composite{
|
||||
Layout: HBox{MarginsZero: true},
|
||||
Children: indWidgets,
|
||||
},
|
||||
HSeparator{},
|
||||
TextEdit{
|
||||
AssignTo: &logTE,
|
||||
ReadOnly: true,
|
||||
VScroll: true,
|
||||
Font: Font{Family: "Consolas", PointSize: 9},
|
||||
MinSize: Size{Height: 530},
|
||||
},
|
||||
HSeparator{},
|
||||
Composite{
|
||||
Layout: HBox{MarginsZero: true},
|
||||
Children: []Widget{
|
||||
HSpacer{},
|
||||
PushButton{
|
||||
Text: " ZASTAVIT ",
|
||||
OnClicked: func() {
|
||||
if cancelFn != nil {
|
||||
cancelFn()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}.Create()); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// updateIndicator refreshes a step label's text and color — call inside Synchronize.
|
||||
updateIndicator := func(idx int) {
|
||||
ind := &indicators[idx]
|
||||
s := steps[idx]
|
||||
switch ind.status {
|
||||
case "running":
|
||||
ind.label.SetText(s.Num + " \u25ba") // ►
|
||||
ind.label.SetTextColor(walk.RGB(30, 144, 255))
|
||||
case "ok":
|
||||
ind.label.SetText(s.Num + " \u2713") // ✓
|
||||
ind.label.SetTextColor(walk.RGB(0, 180, 0))
|
||||
case "error":
|
||||
ind.label.SetText(s.Num + " \u2717") // ✗
|
||||
ind.label.SetTextColor(walk.RGB(220, 50, 50))
|
||||
case "skipped", "cancelled":
|
||||
ind.label.SetText(s.Num + " \u2013") // –
|
||||
ind.label.SetTextColor(walk.RGB(140, 140, 140))
|
||||
default:
|
||||
ind.label.SetText(s.Num + " \u00b7") // ·
|
||||
ind.label.SetTextColor(walk.RGB(180, 180, 180))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
results []runner.Result
|
||||
done = make(chan struct{})
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancelFn = cancel
|
||||
|
||||
mw.Closing().Attach(func(_ *bool, _ walk.CloseReason) {
|
||||
cancelFn()
|
||||
})
|
||||
|
||||
r := runner.New(
|
||||
runCfg,
|
||||
func(l runner.LogLine) {
|
||||
mw.Synchronize(func() {
|
||||
logTE.AppendText(l.Text + "\r\n")
|
||||
})
|
||||
},
|
||||
func(step runner.Step) {
|
||||
mw.Synchronize(func() {
|
||||
statusLbl.SetText(fmt.Sprintf(
|
||||
"Krok %s \u2013 %s...", step.Num, step.Name,
|
||||
))
|
||||
if idx, ok := stepIndex[step.ID]; ok {
|
||||
indicators[idx].status = "running"
|
||||
updateIndicator(idx)
|
||||
}
|
||||
})
|
||||
},
|
||||
func(res runner.Result) {
|
||||
mw.Synchronize(func() {
|
||||
statusLbl.SetText(fmt.Sprintf(
|
||||
"Krok %s \u2013 %s: %s", res.Step.Num, res.Step.Name, res.Status,
|
||||
))
|
||||
if idx, ok := stepIndex[res.Step.ID]; ok {
|
||||
indicators[idx].status = strings.ToLower(res.Status)
|
||||
updateIndicator(idx)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
res := r.Run(ctx, steps)
|
||||
mu.Lock()
|
||||
results = res
|
||||
mu.Unlock()
|
||||
mw.Synchronize(func() {
|
||||
mw.Close()
|
||||
})
|
||||
}()
|
||||
|
||||
mw.Run()
|
||||
cancel()
|
||||
<-done
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
needsReboot := len(results) > 0 && results[len(results)-1].NeedsReboot
|
||||
return results, needsReboot
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Pre-reboot helper
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// prepareRebootAndRestart saves state, sets up resume infrastructure, and reboots.
|
||||
func prepareRebootAndRestart(
|
||||
cfg config.Config,
|
||||
allSteps []runner.Step,
|
||||
completedResults []runner.Result,
|
||||
cfgPath string,
|
||||
runCfg runner.RunConfig,
|
||||
) {
|
||||
pending := pendingStepIDs(allSteps, completedResults)
|
||||
st := &state.State{
|
||||
Config: cfg,
|
||||
ConfigPath: cfgPath,
|
||||
LogFile: runCfg.LogFile,
|
||||
PendingSteps: pending,
|
||||
Results: toStateResults(completedResults),
|
||||
}
|
||||
if err := state.Save(st); err != nil {
|
||||
walk.MsgBox(nil, "Chyba", "Nelze ulozit stav: "+err.Error(), walk.MsgBoxIconError)
|
||||
return
|
||||
}
|
||||
if err := prereboot.Prepare(); err != nil {
|
||||
walk.MsgBox(nil, "Chyba", "Prereboot setup selhal: "+err.Error(), walk.MsgBoxIconError)
|
||||
return
|
||||
}
|
||||
reboot()
|
||||
}
|
||||
|
||||
// pendingStepIDs returns IDs of steps that still need to run.
|
||||
// Steps with NeedsReboot=true (re-run after reboot) and steps not yet
|
||||
// reached (absent from completedResults) are both included.
|
||||
func pendingStepIDs(allSteps []runner.Step, completedResults []runner.Result) []string {
|
||||
done := make(map[string]bool, len(completedResults))
|
||||
for _, r := range completedResults {
|
||||
if !r.NeedsReboot {
|
||||
done[r.Step.ID] = true
|
||||
}
|
||||
}
|
||||
var pending []string
|
||||
for _, s := range allSteps {
|
||||
if !done[s.ID] {
|
||||
pending = append(pending, s.ID)
|
||||
}
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
// toStateResults converts runner results to state.StepResult for persistence.
|
||||
// Steps with NeedsReboot=true are excluded (they will run again next round).
|
||||
func toStateResults(results []runner.Result) []state.StepResult {
|
||||
var sr []state.StepResult
|
||||
for _, r := range results {
|
||||
if r.NeedsReboot {
|
||||
continue
|
||||
}
|
||||
sr = append(sr, state.StepResult{
|
||||
StepID: r.Step.ID,
|
||||
Num: r.Step.Num,
|
||||
Name: r.Step.Name,
|
||||
Status: r.Status,
|
||||
Elapsed: r.Elapsed,
|
||||
})
|
||||
}
|
||||
return sr
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Phase 3 – Summary with auto-reboot countdown
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
const rebootCountdown = 60 // seconds before automatic reboot
|
||||
|
||||
// donePhase shows the final summary screen.
|
||||
// currentResults are from the last run; prevResults accumulate earlier rounds.
|
||||
func donePhase(currentResults []runner.Result, prevResults []state.StepResult) {
|
||||
var (
|
||||
mw *walk.MainWindow
|
||||
countdownLbl *walk.Label
|
||||
)
|
||||
|
||||
// Combined ordered list: previous rounds first, then current round
|
||||
type displayRow struct {
|
||||
Num string
|
||||
Name string
|
||||
Status string
|
||||
Elapsed time.Duration
|
||||
}
|
||||
|
||||
var rows []displayRow
|
||||
for _, r := range prevResults {
|
||||
rows = append(rows, displayRow{r.Num, r.Name, r.Status, r.Elapsed})
|
||||
}
|
||||
for _, r := range currentResults {
|
||||
if r.NeedsReboot {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, displayRow{r.Step.Num, r.Step.Name, r.Status, r.Elapsed})
|
||||
}
|
||||
|
||||
ok, errs, skipped := 0, 0, 0
|
||||
widgets := make([]Widget, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
var icon string
|
||||
switch row.Status {
|
||||
case "OK":
|
||||
ok++
|
||||
icon = "OK "
|
||||
case "ERROR":
|
||||
errs++
|
||||
icon = "ERR "
|
||||
default:
|
||||
skipped++
|
||||
icon = "\u2013 "
|
||||
}
|
||||
text := icon + row.Num + " \u2013 " + row.Name
|
||||
if row.Elapsed > 0 {
|
||||
text += fmt.Sprintf(" (%s)", row.Elapsed.Round(time.Second))
|
||||
}
|
||||
widgets = append(widgets, Label{
|
||||
Text: text,
|
||||
Font: Font{Family: "Consolas", PointSize: 9},
|
||||
})
|
||||
}
|
||||
|
||||
summaryText := fmt.Sprintf("OK: %d CHYBY: %d PRESKOCENO: %d", ok, errs, skipped)
|
||||
|
||||
cancelReboot := make(chan struct{})
|
||||
|
||||
if err := (MainWindow{
|
||||
AssignTo: &mw,
|
||||
Title: "xetup \u2014 hotovo",
|
||||
Size: Size{Width: 760, Height: 740},
|
||||
Layout: VBox{},
|
||||
Children: []Widget{
|
||||
Label{
|
||||
Text: "Hotovo",
|
||||
Alignment: AlignHCenterVNear,
|
||||
Font: Font{Bold: true, PointSize: 12},
|
||||
},
|
||||
HSeparator{},
|
||||
ScrollView{
|
||||
MinSize: Size{Height: 510},
|
||||
Layout: VBox{MarginsZero: true},
|
||||
Children: widgets,
|
||||
},
|
||||
HSeparator{},
|
||||
Label{
|
||||
Text: summaryText,
|
||||
Alignment: AlignHCenterVNear,
|
||||
Font: Font{Bold: true},
|
||||
},
|
||||
Label{
|
||||
AssignTo: &countdownLbl,
|
||||
Text: fmt.Sprintf("Restart za %ds...", rebootCountdown),
|
||||
Alignment: AlignHCenterVNear,
|
||||
},
|
||||
Composite{
|
||||
Layout: HBox{MarginsZero: true},
|
||||
Children: []Widget{
|
||||
HSpacer{},
|
||||
PushButton{
|
||||
Text: " Restartovat ted ",
|
||||
OnClicked: func() {
|
||||
close(cancelReboot)
|
||||
reboot()
|
||||
mw.Close()
|
||||
},
|
||||
},
|
||||
PushButton{
|
||||
Text: " Zrusit restart ",
|
||||
OnClicked: func() {
|
||||
select {
|
||||
case <-cancelReboot:
|
||||
default:
|
||||
close(cancelReboot)
|
||||
}
|
||||
countdownLbl.SetText("Restart zrusen.")
|
||||
},
|
||||
},
|
||||
HSpacer{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}.Create()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Countdown goroutine
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
remaining := rebootCountdown
|
||||
for {
|
||||
select {
|
||||
case <-cancelReboot:
|
||||
return
|
||||
case <-ticker.C:
|
||||
remaining--
|
||||
r := remaining
|
||||
mw.Synchronize(func() {
|
||||
if r > 0 {
|
||||
countdownLbl.SetText(fmt.Sprintf("Restart za %ds...", r))
|
||||
} else {
|
||||
countdownLbl.SetText("Restartuji...")
|
||||
}
|
||||
})
|
||||
if remaining <= 0 {
|
||||
reboot()
|
||||
mw.Synchronize(func() { mw.Close() })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
mw.Run()
|
||||
}
|
||||
|
||||
// reboot issues an immediate Windows restart.
|
||||
func reboot() {
|
||||
exec.Command("shutdown", "/r", "/t", "0").Run() //nolint:errcheck
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
func itemEnabled(cfg config.Config, item runner.SelectableItem) bool {
|
||||
if item.FeatureID == "" {
|
||||
if v, ok := cfg.Steps[item.StepID]; ok {
|
||||
return v
|
||||
}
|
||||
return true
|
||||
}
|
||||
if feats, ok := cfg.Features[item.StepID]; ok {
|
||||
if v, ok2 := feats[item.FeatureID]; ok2 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func buildStepsAndFeatures(selected map[string]bool) ([]runner.Step, config.Features) {
|
||||
items := runner.AllSelectableItems()
|
||||
features := make(config.Features)
|
||||
stepOn := make(map[string]bool)
|
||||
|
||||
for _, item := range items {
|
||||
if item.FeatureID == "" {
|
||||
stepOn[item.StepID] = selected[item.Key]
|
||||
} else {
|
||||
if features[item.StepID] == nil {
|
||||
features[item.StepID] = make(map[string]bool)
|
||||
}
|
||||
features[item.StepID][item.FeatureID] = selected[item.Key]
|
||||
if selected[item.Key] {
|
||||
stepOn[item.StepID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allSteps := runner.AllSteps()
|
||||
steps := make([]runner.Step, len(allSteps))
|
||||
for i, s := range allSteps {
|
||||
s.Enabled = stepOn[s.ID]
|
||||
steps[i] = s
|
||||
}
|
||||
return steps, features
|
||||
}
|
||||
|
||||
func profileIndex(p string) int {
|
||||
switch p {
|
||||
case "admin":
|
||||
return 1
|
||||
case "user":
|
||||
return 2
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
// Package prereboot ensures the reboot-resume infrastructure is in place
|
||||
// before xetup triggers a system restart mid-deployment.
|
||||
package prereboot
|
||||
|
||||
// StablePath is the platform stub for the stable binary path.
|
||||
const StablePath = "/tmp/xetup-resume"
|
||||
|
||||
// TaskName is the scheduled task that re-launches xetup after each reboot.
|
||||
const TaskName = "X9-Resume"
|
||||
|
||||
// IsAdminx9 always returns false on non-Windows platforms.
|
||||
func IsAdminx9() bool { return false }
|
||||
|
||||
// Prepare is a no-op on non-Windows platforms.
|
||||
func Prepare() error { return nil }
|
||||
|
||||
// Cleanup is a no-op on non-Windows platforms.
|
||||
func Cleanup() {}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
// Package prereboot ensures the reboot-resume infrastructure is in place
|
||||
// before xetup triggers a system restart mid-deployment.
|
||||
package prereboot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// StablePath is where xetup.exe is copied so it survives across reboots.
|
||||
const StablePath = `C:\Windows\Setup\Scripts\xetup.exe`
|
||||
|
||||
// TaskName is the scheduled task that re-launches xetup after each reboot.
|
||||
const TaskName = "X9-Resume"
|
||||
|
||||
const adminUser = "adminx9"
|
||||
const winlogonKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`
|
||||
|
||||
// IsAdminx9 reports whether the current process runs as the adminx9 account.
|
||||
func IsAdminx9() bool {
|
||||
return strings.EqualFold(os.Getenv("USERNAME"), adminUser)
|
||||
}
|
||||
|
||||
// Prepare ensures the reboot-resume infrastructure is in place.
|
||||
// When not running as adminx9 (first reboot trigger):
|
||||
// - creates the adminx9 local account if it does not exist
|
||||
// - copies the running binary to StablePath
|
||||
// - enables autologon for adminx9 (AutoLogonCount=20 safety cap)
|
||||
// - registers the X9-Resume scheduled task (AtLogOn adminx9, RunLevel Highest)
|
||||
//
|
||||
// When already running as adminx9 (subsequent rounds) all of the above is
|
||||
// already in place; the function returns immediately without changes.
|
||||
// Safe to call repeatedly (idempotent).
|
||||
func Prepare() error {
|
||||
if IsAdminx9() {
|
||||
return nil
|
||||
}
|
||||
if err := ensureAdminx9User(); err != nil {
|
||||
return fmt.Errorf("adminx9 account: %w", err)
|
||||
}
|
||||
if err := copySelfTo(StablePath); err != nil {
|
||||
return fmt.Errorf("copy binary: %w", err)
|
||||
}
|
||||
if err := setAutologon(); err != nil {
|
||||
return fmt.Errorf("autologon: %w", err)
|
||||
}
|
||||
if err := registerResumeTask(); err != nil {
|
||||
return fmt.Errorf("resume task: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup disables autologon and removes the X9-Resume scheduled task.
|
||||
// Called when all deployment steps have completed successfully.
|
||||
func Cleanup() {
|
||||
_ = disableAutologon()
|
||||
_ = unregisterResumeTask()
|
||||
}
|
||||
|
||||
// ensureAdminx9User creates the adminx9 local account if absent.
|
||||
func ensureAdminx9User() error {
|
||||
// "net user adminx9" exits 0 when user exists
|
||||
if err := newHiddenCmd("net", "user", adminUser).Run(); err == nil {
|
||||
return nil // already exists
|
||||
}
|
||||
// Create with empty password
|
||||
if b, err := newHiddenCmd("net", "user", adminUser, "", "/add").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("net user /add: %s: %w", strings.TrimSpace(string(b)), err)
|
||||
}
|
||||
// Add to local Administrators group
|
||||
if b, err := newHiddenCmd("net", "localgroup", "Administrators", adminUser, "/add").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("net localgroup: %s: %w", strings.TrimSpace(string(b)), err)
|
||||
}
|
||||
// Hide from the Windows login screen
|
||||
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE,
|
||||
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList`,
|
||||
registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("registry hide: %w", err)
|
||||
}
|
||||
defer k.Close()
|
||||
return k.SetDWordValue(adminUser, 0)
|
||||
}
|
||||
|
||||
// copySelfTo copies the running executable to dst.
|
||||
// Skips the copy when src and dst resolve to the same path.
|
||||
func copySelfTo(dst string) error {
|
||||
src, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(src); err == nil {
|
||||
src = resolved
|
||||
}
|
||||
if strings.EqualFold(src, dst) {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return copyFile(src, dst)
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
r, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
w, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
_, err = io.Copy(w, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func setAutologon() error {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, winlogonKey, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer k.Close()
|
||||
for _, kv := range []struct{ name, val string }{
|
||||
{"AutoAdminLogon", "1"},
|
||||
{"DefaultUserName", adminUser},
|
||||
{"DefaultPassword", ""},
|
||||
{"DefaultDomainName", "."},
|
||||
} {
|
||||
if err := k.SetStringValue(kv.name, kv.val); err != nil {
|
||||
return fmt.Errorf("set %s: %w", kv.name, err)
|
||||
}
|
||||
}
|
||||
// Safety cap: self-limits even if cleanup task fails to run
|
||||
return k.SetDWordValue("AutoLogonCount", 20)
|
||||
}
|
||||
|
||||
func disableAutologon() error {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, winlogonKey, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer k.Close()
|
||||
_ = k.SetStringValue("AutoAdminLogon", "0")
|
||||
_ = k.DeleteValue("DefaultPassword")
|
||||
_ = k.DeleteValue("AutoLogonCount")
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerResumeTask() error {
|
||||
ps := fmt.Sprintf(`
|
||||
$action = New-ScheduledTaskAction -Execute '%s' -Argument '--resume'
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn -User '%s'
|
||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 4) -MultipleInstances IgnoreNew
|
||||
$principal = New-ScheduledTaskPrincipal -UserId '%s' -LogonType Interactive -RunLevel Highest
|
||||
Unregister-ScheduledTask -TaskName 'X9-Resume' -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Register-ScheduledTask -TaskName 'X9-Resume' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null
|
||||
`, StablePath, adminUser, adminUser)
|
||||
return runPS(ps)
|
||||
}
|
||||
|
||||
func unregisterResumeTask() error {
|
||||
return runPS(`Unregister-ScheduledTask -TaskName 'X9-Resume' -Confirm:$false -ErrorAction SilentlyContinue`)
|
||||
}
|
||||
|
||||
func runPS(script string) error {
|
||||
cmd := newHiddenCmd("powershell.exe",
|
||||
"-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", strings.TrimSpace(string(b)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHiddenCmd(name string, args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package runner
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func hideWindow(cmd *exec.Cmd) {}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// hideWindow prevents the child process from creating a visible console window.
|
||||
// Without this, powershell.exe opens a full-size window on top of the GUI.
|
||||
func hideWindow(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
}
|
||||
|
|
@ -1,417 +0,0 @@
|
|||
// Package runner executes PowerShell deployment scripts and streams log output.
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrNeedsReboot is returned by runScript when a PowerShell script exits with
|
||||
// code 9, signalling that it completed successfully but requires a system
|
||||
// reboot before deployment can continue.
|
||||
var ErrNeedsReboot = errors.New("reboot required")
|
||||
|
||||
// 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: "pcIdentity", Num: "09", Name: "PC identita", ScriptName: "09-pc-identity.ps1"},
|
||||
{ID: "network", Num: "10", Name: "Network discovery", ScriptName: "10-network.ps1"},
|
||||
{ID: "dellUpdate", Num: "11", Name: "Dell Command | Update", ScriptName: "11-dell-update.ps1"},
|
||||
{ID: "windowsUpdate", Num: "12", Name: "Windows Update", ScriptName: "12-windows-update.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", "CANCELLED"
|
||||
Elapsed time.Duration
|
||||
NeedsReboot bool // true when the script exited with code 9
|
||||
}
|
||||
|
||||
// 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)
|
||||
onStepStart func(Step)
|
||||
onResult func(Result)
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates a Runner. onLog is called for each output line, onResult after each step.
|
||||
// onStepStart (optional) is called immediately before a step's script is launched.
|
||||
func New(cfg RunConfig, onLog func(LogLine), onStepStart func(Step), onResult func(Result)) *Runner {
|
||||
return &Runner{cfg: cfg, onLog: onLog, onStepStart: onStepStart, 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
|
||||
}
|
||||
|
||||
if r.onStepStart != nil {
|
||||
r.onStepStart(step)
|
||||
}
|
||||
start := time.Now()
|
||||
err := r.runScript(ctx, step, cfgArg)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
status := "OK"
|
||||
needsReboot := false
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNeedsReboot) {
|
||||
needsReboot = true
|
||||
// status remains "OK": step completed, reboot is just required to continue
|
||||
} else if ctx.Err() != nil {
|
||||
status = "CANCELLED"
|
||||
} else {
|
||||
status = "ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
res := Result{Step: step, Status: status, Elapsed: elapsed, NeedsReboot: needsReboot}
|
||||
r.onResult(res)
|
||||
results = append(results, res)
|
||||
|
||||
if ctx.Err() != nil || needsReboot {
|
||||
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...)
|
||||
hideWindow(cmd) // prevent PS console window from appearing over the GUI
|
||||
|
||||
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()
|
||||
if skipPSNoiseLine(line) {
|
||||
continue
|
||||
}
|
||||
r.onLog(LogLine{
|
||||
StepID: step.ID,
|
||||
Text: line,
|
||||
Level: parseLevel(line),
|
||||
})
|
||||
}
|
||||
|
||||
waitErr := cmd.Wait()
|
||||
if waitErr != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(waitErr, &exitErr) && exitErr.ExitCode() == 9 {
|
||||
return ErrNeedsReboot
|
||||
}
|
||||
}
|
||||
return waitErr
|
||||
}
|
||||
|
||||
// skipPSNoiseLine returns true for PowerShell stderr noise that clutters the log:
|
||||
// multi-line error blocks (At line:N, CategoryInfo, FullyQualifiedErrorId, etc.),
|
||||
// blank lines, and VERBOSE: prefix lines already handled by Write-Log.
|
||||
func skipPSNoiseLine(line string) bool {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
return true
|
||||
}
|
||||
for _, prefix := range []string{
|
||||
"At line:",
|
||||
"+ CategoryInfo",
|
||||
"+ FullyQualifiedErrorId",
|
||||
"+ PositionMessage",
|
||||
"VERBOSE:",
|
||||
"DEBUG:",
|
||||
} {
|
||||
if strings.HasPrefix(trimmed, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// PS error continuation lines start with spaces + "+" or "~"
|
||||
if len(trimmed) > 0 && (trimmed[0] == '+' || strings.HasPrefix(trimmed, "~")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// StepsByIDs returns steps from AllSteps() whose IDs are in the given list,
|
||||
// preserving the canonical AllSteps() order.
|
||||
func StepsByIDs(ids []string) []Step {
|
||||
want := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
want[id] = true
|
||||
}
|
||||
var result []Step
|
||||
for _, s := range AllSteps() {
|
||||
if want[s.ID] {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// StepByID returns the Step with the given ID, and true if found.
|
||||
func StepByID(id string) (Step, bool) {
|
||||
for _, s := range AllSteps() {
|
||||
if s.ID == id {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return Step{}, false
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package state
|
||||
|
||||
import "os"
|
||||
|
||||
func statePath() string {
|
||||
if h, err := os.UserHomeDir(); err == nil {
|
||||
return h + "/xetup-state.json"
|
||||
}
|
||||
return "/tmp/xetup-state.json"
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
package state
|
||||
|
||||
func statePath() string {
|
||||
return `C:\Windows\Setup\Scripts\xetup-state.json`
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
// Package state manages the persistent xetup deployment state used to resume
|
||||
// after a system reboot. The state file is written before each reboot and
|
||||
// deleted once all deployment steps have completed.
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.xetup.x9.cz/x9/xetup/internal/config"
|
||||
)
|
||||
|
||||
// StepResult records the outcome of a single deployment step.
|
||||
// Results are accumulated across reboot rounds so the final summary
|
||||
// can show every step regardless of which round it ran in.
|
||||
type StepResult struct {
|
||||
StepID string `json:"stepID"`
|
||||
Num string `json:"num"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // "OK", "ERROR", "SKIPPED"
|
||||
Elapsed time.Duration `json:"elapsed"`
|
||||
}
|
||||
|
||||
// State holds everything xetup needs to resume after a reboot.
|
||||
type State struct {
|
||||
Config config.Config `json:"config"`
|
||||
ConfigPath string `json:"configPath"` // path to original config.json
|
||||
LogFile string `json:"logFile"`
|
||||
PendingSteps []string `json:"pendingSteps"` // step IDs to run next, in canonical order
|
||||
Results []StepResult `json:"results"` // accumulated across all rounds
|
||||
}
|
||||
|
||||
// Load reads the state file. Returns a non-nil error when the file is absent.
|
||||
func Load() (*State, error) {
|
||||
data, err := os.ReadFile(statePath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var s State
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Save writes the state file, creating parent directories as needed.
|
||||
func Save(s *State) error {
|
||||
p := statePath()
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, data, 0644)
|
||||
}
|
||||
|
||||
// Delete removes the state file. Silently ignores not-found.
|
||||
func Delete() error {
|
||||
err := os.Remove(statePath())
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Exists reports whether a state file is present on disk.
|
||||
func Exists() bool {
|
||||
_, err := os.Stat(statePath())
|
||||
return err == nil
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
log:
|
||||
level: info
|
||||
|
||||
runner:
|
||||
file: /data/.runner
|
||||
capacity: 1
|
||||
timeout: 3h
|
||||
fetch_timeout: 5s
|
||||
fetch_interval: 2s
|
||||
report_interval: 1s
|
||||
|
||||
cache:
|
||||
enabled: true
|
||||
|
||||
container:
|
||||
network: xetup
|
||||
privileged: false
|
||||
valid_volumes:
|
||||
- '**'
|
||||
docker_host: "-"
|
||||
force_pull: false
|
||||
|
|
@ -27,7 +27,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,22 +28,9 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
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
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
@ -75,39 +62,40 @@ Write-Log "winget found: $($winget.Source -or $winget)" -Level OK
|
|||
# -----------------------------------------------------------------------
|
||||
# Install packages from config
|
||||
# -----------------------------------------------------------------------
|
||||
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 {
|
||||
foreach ($pkg in $Config.software.install) {
|
||||
Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
|
||||
$result = & winget install --id $pkg.wingetId `
|
||||
--silent `
|
||||
--accept-package-agreements `
|
||||
--accept-source-agreements `
|
||||
--disable-interactivity `
|
||||
2>&1
|
||||
if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
|
||||
Write-Log "No software list in config - skipping installs" -Level WARN
|
||||
} else {
|
||||
foreach ($pkg in $Config.software.install) {
|
||||
Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO
|
||||
$result = & winget install --id $pkg.wingetId `
|
||||
--silent `
|
||||
--accept-package-agreements `
|
||||
--accept-source-agreements `
|
||||
--disable-interactivity `
|
||||
2>&1
|
||||
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Log " Installed OK: $($pkg.name)" -Level OK
|
||||
} elseif ($exitCode -eq -1978335189) {
|
||||
# 0x8A150011 = already installed
|
||||
Write-Log " Already installed: $($pkg.name)" -Level OK
|
||||
} else {
|
||||
Write-Log " Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
|
||||
Write-Log " Output: $($result -join ' ')" -Level ERROR
|
||||
}
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Log " Installed OK: $($pkg.name)" -Level OK
|
||||
} elseif ($exitCode -eq -1978335189) {
|
||||
# 0x8A150011 = already installed
|
||||
Write-Log " Already installed: $($pkg.name)" -Level OK
|
||||
} else {
|
||||
Write-Log " Failed: $($pkg.name) (exit $exitCode)" -Level ERROR
|
||||
Write-Log " Output: $($result -join ' ')" -Level ERROR
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Set Adobe Reader as default PDF app
|
||||
# -----------------------------------------------------------------------
|
||||
if (Get-Feature $Config "software" "pdfDefault") {
|
||||
$forcePdf = $true
|
||||
if ($Config -and $Config.pdfDefault) {
|
||||
$forcePdf = [bool]$Config.pdfDefault.forceAdobeReader
|
||||
}
|
||||
|
||||
if ($forcePdf) {
|
||||
Write-Log "Setting Adobe Reader as default PDF app" -Level INFO
|
||||
|
||||
# Stop UCPD driver before writing file association.
|
||||
|
|
@ -142,11 +130,6 @@ if (Get-Feature $Config "software" "pdfDefault") {
|
|||
} else {
|
||||
Write-Log " Found: $acroExe" -Level INFO
|
||||
|
||||
# Mount HKCR PSDrive - not available by default in PS sessions
|
||||
if (-not (Get-PSDrive -Name HKCR -ErrorAction SilentlyContinue)) {
|
||||
New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | Out-Null
|
||||
}
|
||||
|
||||
# Set file type association via HKCR (system-wide, requires admin)
|
||||
$progId = "AcroExch.Document.DC"
|
||||
$openCmd = "`"$acroExe`" `"%1`""
|
||||
|
|
@ -177,43 +160,37 @@ if (Get-Feature $Config "software" "pdfDefault") {
|
|||
Write-Log " Could not restart UCPD: $_" -Level WARN
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Log "pdfDefault feature disabled - skipping" -Level INFO
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Install Atera RMM Agent
|
||||
# -----------------------------------------------------------------------
|
||||
if (Get-Feature $Config "software" "ateraAgent") {
|
||||
Write-Log "Installing Atera RMM Agent" -Level INFO
|
||||
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 {
|
||||
Write-Log " Downloading Atera agent..." -Level INFO
|
||||
Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
|
||||
Write-Log " Download complete" -Level OK
|
||||
try {
|
||||
Write-Log " Downloading Atera agent..." -Level INFO
|
||||
Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
|
||||
Write-Log " Download complete" -Level OK
|
||||
|
||||
$msiResult = & msiexec /i $ateraMsi /qn 2>&1
|
||||
Start-Sleep -Seconds 5
|
||||
$msiResult = & msiexec /i $ateraMsi /qn 2>&1
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
$ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
|
||||
if (Test-Path $ateraExe) {
|
||||
Write-Log " Atera agent installed" -Level OK
|
||||
} else {
|
||||
Write-Log " Atera agent install may have failed - binary not found at expected path" -Level WARN
|
||||
Write-Log " msiexec output: $($msiResult -join ' ')" -Level WARN
|
||||
}
|
||||
$ateraExe = "$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
|
||||
if (Test-Path $ateraExe) {
|
||||
Write-Log " Atera agent installed" -Level OK
|
||||
} else {
|
||||
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 {
|
||||
Write-Log " Atera agent download/install failed: $_" -Level ERROR
|
||||
}
|
||||
finally {
|
||||
Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
Write-Log "ateraAgent feature disabled - skipping" -Level INFO
|
||||
}
|
||||
catch {
|
||||
Write-Log " Atera agent download/install failed: $_" -Level ERROR
|
||||
}
|
||||
finally {
|
||||
Remove-Item $ateraMsi -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Log "Step 2 complete" -Level OK
|
||||
|
|
|
|||
|
|
@ -37,22 +37,9 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
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;
|
||||
|
|
@ -219,7 +206,91 @@ function Remove-Reg {
|
|||
Write-Log "3 - Applying HKLM system registry tweaks" -Level STEP
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Always-on: password and timezone (fundamental system settings)
|
||||
# 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
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log " Setting password max age to UNLIMITED" -Level INFO
|
||||
$pwResult = & net accounts /maxpwage:UNLIMITED 2>&1
|
||||
|
|
@ -229,6 +300,9 @@ 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
|
||||
|
|
@ -242,201 +316,109 @@ catch {
|
|||
Write-Log " Failed to set time zone: $_" -Level ERROR
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# System tweaks (Windows features, cloud noise, Xbox, Recall, Search UI)
|
||||
# -----------------------------------------------------------------------
|
||||
if (Get-Feature $Config "systemRegistry" "systemTweaks") {
|
||||
Write-Log " Applying system tweaks" -Level INFO
|
||||
|
||||
# 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
|
||||
|
||||
# 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) {
|
||||
if (Test-Path $uPath) {
|
||||
try {
|
||||
Remove-Item -Path $uPath -Recurse -Force
|
||||
Write-Log " Removed UScheduler key: $uPath" -Level OK
|
||||
}
|
||||
catch {
|
||||
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
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Microsoft Edge policies
|
||||
# -----------------------------------------------------------------------
|
||||
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 - disable all visual clutter
|
||||
Set-Reg -Path $edgePath -Name "NewTabPageContentEnabled" -Value 0 # feed / obsah
|
||||
Set-Reg -Path $edgePath -Name "NewTabPageQuickLinksEnabled" -Value 0 # rychle odkazy
|
||||
Set-Reg -Path $edgePath -Name "NewTabPageBackgroundEnabled" -Value 0 # pozadi
|
||||
Set-Reg -Path $edgePath -Name "NewTabPageAllowedBackgroundTypes" -Value 3 # 3 = only solid color
|
||||
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
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 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.
|
||||
# -----------------------------------------------------------------------
|
||||
if (Get-Feature $Config "systemRegistry" "oneDriveUninstall") {
|
||||
Write-Log " Uninstalling OneDrive" -Level INFO
|
||||
Write-Log " Uninstalling OneDrive" -Level INFO
|
||||
|
||||
$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
|
||||
}
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Outlook (new) - disable 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) {
|
||||
if (Test-Path $uPath) {
|
||||
try {
|
||||
Remove-Item -Path $uPath -Recurse -Force
|
||||
Write-Log " Removed UScheduler key: $uPath" -Level OK
|
||||
}
|
||||
catch {
|
||||
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
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Power configuration
|
||||
# -----------------------------------------------------------------------
|
||||
if (Get-Feature $Config "systemRegistry" "powercfg") {
|
||||
Write-Log " Applying power configuration" -Level INFO
|
||||
Write-Log "Applying power configuration" -Level INFO
|
||||
|
||||
$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 ($a in $powercfgArgs) {
|
||||
$result = & powercfg @a 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Log " powercfg $($a -join ' ')" -Level OK
|
||||
} else {
|
||||
Write-Log " powercfg $($a -join ' ') failed: $result" -Level WARN
|
||||
}
|
||||
$powercfg = @(
|
||||
@("/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
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Log " powercfg $($args -join ' ')" -Level OK
|
||||
} else {
|
||||
Write-Log " powercfg $($args -join ' ') failed: $result" -Level WARN
|
||||
}
|
||||
} else {
|
||||
Write-Log "powercfg feature disabled - skipping" -Level INFO
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Proxy auto-detect disable (WPAD)
|
||||
# -----------------------------------------------------------------------
|
||||
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
|
||||
}
|
||||
Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings" `
|
||||
-Name "AutoDetect" -Value 0
|
||||
|
||||
Write-Log "Step 3 complete" -Level OK
|
||||
|
|
|
|||
|
|
@ -36,22 +36,9 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
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
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
@ -163,123 +150,157 @@ Write-Log "Default hive loaded" -Level OK
|
|||
|
||||
try {
|
||||
# -----------------------------------------------------------------------
|
||||
# Taskbar tweaks (alignment, buttons, tray, layout XML)
|
||||
# Taskbar settings (Win10 + Win11)
|
||||
# -----------------------------------------------------------------------
|
||||
if (Get-Feature $Config "defaultProfile" "taskbarTweaks") {
|
||||
Write-Log "Applying taskbar tweaks" -Level STEP
|
||||
Write-Log "Applying taskbar settings" -Level STEP
|
||||
|
||||
$tbPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
|
||||
$tbPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
|
||||
|
||||
# Win11: align taskbar to left (0 = left, 1 = center)
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarAl" -Value 0
|
||||
# Win11: align taskbar to left (0 = left, 1 = center)
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarAl" -Value 0
|
||||
|
||||
# Hide Search box / button - Win10/11 (0 = hidden, 1 = icon, 2 = full box)
|
||||
# Note: Win11 uses Search subkey, Win10 uses Explorer\Advanced - set both
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Search" `
|
||||
-Name "SearchboxTaskbarMode" -Value 0
|
||||
Set-ProfileReg -SubKey $tbPath -Name "SearchboxTaskbarMode" -Value 0
|
||||
# Hide Search box / button - Win10/11 (0 = hidden, 1 = icon, 2 = full box)
|
||||
# Note: Win11 uses Search subkey, Win10 uses Explorer\Advanced - set both
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Search" `
|
||||
-Name "SearchboxTaskbarMode" -Value 0
|
||||
Set-ProfileReg -SubKey $tbPath -Name "SearchboxTaskbarMode" -Value 0
|
||||
|
||||
# Hide Task View button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "ShowTaskViewButton" -Value 0
|
||||
# Hide Task View button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "ShowTaskViewButton" -Value 0
|
||||
|
||||
# Hide Widgets button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarDa" -Value 0
|
||||
# Hide Widgets button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarDa" -Value 0
|
||||
|
||||
# Hide Chat / Teams button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarMn" -Value 0
|
||||
# Hide Chat / Teams button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "TaskbarMn" -Value 0
|
||||
|
||||
# Hide Copilot button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "ShowCopilotButton" -Value 0
|
||||
# Hide Copilot button
|
||||
Set-ProfileReg -SubKey $tbPath -Name "ShowCopilotButton" -Value 0
|
||||
|
||||
# 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
|
||||
# Show file extensions in Explorer
|
||||
Set-ProfileReg -SubKey $tbPath -Name "HideFileExt" -Value 0
|
||||
|
||||
# Win11 workaround: clear cached tray icon streams so all icons appear on next login
|
||||
$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
|
||||
}
|
||||
$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
|
||||
Remove-ItemProperty -Path $defTrayKey -Name "PastIconsStream" -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " Cleared TrayNotify icon streams in Default hive" -Level OK
|
||||
}
|
||||
# Open Explorer to This PC instead of Quick Access
|
||||
Set-ProfileReg -SubKey $tbPath -Name "LaunchTo" -Value 1
|
||||
|
||||
# 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
|
||||
# -----------------------------------------------------------------------
|
||||
# System tray - show all icons
|
||||
# -----------------------------------------------------------------------
|
||||
# EnableAutoTray = 0 works on Win10; Win11 ignores it but set anyway
|
||||
Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" `
|
||||
-Name "EnableAutoTray" -Value 0
|
||||
|
||||
# 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
|
||||
# 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
|
||||
}
|
||||
|
||||
# Ensure File Explorer shortcut exists in Default profile's Start Menu.
|
||||
# On a clean Windows 11 install the System Tools folder may be missing
|
||||
# from C:\Users\Default\AppData\Roaming - without it the XML pin is silently skipped.
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
$defRoaming = "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
|
||||
# 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
|
||||
Remove-ItemProperty -Path $defTrayKey -Name "PastIconsStream" -Force -ErrorAction SilentlyContinue
|
||||
Write-Log " Cleared TrayNotify icon streams in Default hive" -Level OK
|
||||
}
|
||||
|
||||
$explorerLnkDir = "$defRoaming\System Tools"
|
||||
$explorerLnk = "$explorerLnkDir\File Explorer.lnk"
|
||||
if (-not (Test-Path $explorerLnk)) {
|
||||
if (-not (Test-Path $explorerLnkDir)) { New-Item -ItemType Directory -Path $explorerLnkDir -Force | Out-Null }
|
||||
$sc = $wsh.CreateShortcut($explorerLnk)
|
||||
$sc.TargetPath = "$env:WINDIR\explorer.exe"
|
||||
$sc.Save()
|
||||
Write-Log " Created File Explorer.lnk in Default profile Start Menu" -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
|
||||
|
||||
# Same for PowerShell (admin profile)
|
||||
if ($ProfileType -eq "admin") {
|
||||
$psLnkDir = "$defRoaming\Windows PowerShell"
|
||||
$psLnk = "$psLnkDir\Windows PowerShell.lnk"
|
||||
if (-not (Test-Path $psLnk)) {
|
||||
if (-not (Test-Path $psLnkDir)) { New-Item -ItemType Directory -Path $psLnkDir -Force | Out-Null }
|
||||
$sc = $wsh.CreateShortcut($psLnk)
|
||||
$sc.TargetPath = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$sc.Save()
|
||||
Write-Log " Created Windows PowerShell.lnk in Default profile Start Menu" -Level OK
|
||||
}
|
||||
}
|
||||
# -----------------------------------------------------------------------
|
||||
# Start menu settings
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Applying Start menu settings" -Level STEP
|
||||
|
||||
$taskbarLayoutDir = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell"
|
||||
if (-not (Test-Path $taskbarLayoutDir)) {
|
||||
New-Item -ItemType Directory -Path $taskbarLayoutDir -Force | Out-Null
|
||||
}
|
||||
# Disable Bing search suggestions in Start menu
|
||||
Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\Explorer" `
|
||||
-Name "DisableSearchBoxSuggestions" -Value 1
|
||||
|
||||
$pinList = switch ($ProfileType) {
|
||||
"admin" {
|
||||
# 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
|
||||
|
||||
$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" {
|
||||
@'
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\Windows PowerShell\Windows PowerShell.lnk"/>
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
|
||||
'@
|
||||
}
|
||||
"user" {
|
||||
@'
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
|
||||
'@
|
||||
}
|
||||
default {
|
||||
@'
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
|
||||
'@
|
||||
} # explicit pins with Replace = no Store, no other defaults
|
||||
}
|
||||
"user" {
|
||||
@'
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
|
||||
<taskbar:DesktopApp DesktopApplicationLinkPath="%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk"/>
|
||||
'@
|
||||
}
|
||||
default { "" } # empty = clean slate
|
||||
}
|
||||
|
||||
$taskbarLayoutXml = @"
|
||||
$taskbarLayoutXml = @"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LayoutModificationTemplate
|
||||
xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
|
||||
|
|
@ -296,85 +317,9 @@ $pinList
|
|||
</CustomTaskbarLayoutCollection>
|
||||
</LayoutModificationTemplate>
|
||||
"@
|
||||
$taskbarLayoutXml | Set-Content -Path "$taskbarLayoutDir\LayoutModification.xml" -Encoding UTF8 -Force
|
||||
Write-Log " Taskbar LayoutModification.xml written (profile: $ProfileType)" -Level OK
|
||||
$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 {
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
@ -94,17 +93,6 @@ function Apply-ThemeSettings {
|
|||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" `
|
||||
-Name "ColorPrevalence" -Value 1
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Accent color for taskbar
|
||||
# Windows taskbar reads AccentColorMenu from Explorer\Accent, NOT DWM\AccentColor.
|
||||
# Without this key the taskbar keeps the Windows default accent (light blue).
|
||||
# -----------------------------------------------------------------------
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent" `
|
||||
-Name "AccentColorMenu" -Value $AccentColorABGR -Type "DWord"
|
||||
|
||||
Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent" `
|
||||
-Name "StartColorMenu" -Value $AccentColorABGR -Type "DWord"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Wallpaper - solid color #223B47 (fallback before DesktopInfo runs)
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
@ -112,12 +100,6 @@ function Apply-ThemeSettings {
|
|||
Set-Reg -Path "$HiveRoot\Control Panel\Colors" `
|
||||
-Name "Background" -Value "34 59 71" -Type "String"
|
||||
|
||||
# Empty Wallpaper path = solid color from Background key above.
|
||||
# Without this, new users created from Default hive inherit a broken/missing
|
||||
# wallpaper path and Windows falls back to black desktop.
|
||||
Set-Reg -Path "$HiveRoot\Control Panel\Desktop" `
|
||||
-Name "Wallpaper" -Value "" -Type "String"
|
||||
|
||||
Set-Reg -Path "$HiveRoot\Control Panel\Desktop" `
|
||||
-Name "WallpaperStyle" -Value "0" -Type "String"
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ $ErrorActionPreference = "Continue"
|
|||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
<#
|
||||
.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"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
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
|
||||
Write-Log "Step 11 complete - signalling reboot needed (exit 9)" -Level OK
|
||||
exit 9
|
||||
}
|
||||
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
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Installs all available Windows Updates via PSWindowsUpdate module.
|
||||
|
||||
.DESCRIPTION
|
||||
Installs all currently available updates without rebooting.
|
||||
Exits with code 9 when updates were installed (reboot required for
|
||||
further rounds) or code 0 when the system is already fully up to date.
|
||||
|
||||
The xetup.exe state machine handles the reboot cycle:
|
||||
- exit 9 -> xetup saves state, sets autologon + X9-Resume task, reboots
|
||||
- on each subsequent logon X9-Resume launches xetup --resume
|
||||
- xetup re-runs this step until it exits 0 (no more updates)
|
||||
- then disables autologon, removes X9-Resume, shows the summary screen
|
||||
|
||||
.ITEMS
|
||||
nainstalovat-pswindowsupdate-modul: Installs NuGet provider and PSWindowsUpdate module from PSGallery.
|
||||
spustit-kolo-windows-update: One update pass without reboot. Exits 9 when updates were applied (more rounds needed). Exits 0 when system is fully up to date.
|
||||
#>
|
||||
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"
|
||||
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
|
||||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||||
Write-Output $line
|
||||
}
|
||||
|
||||
Write-Log "=== Step 12 - Windows Update ===" -Level STEP
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 1. NuGet provider + PSWindowsUpdate module
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Setting up PSWindowsUpdate module..." -Level INFO
|
||||
try {
|
||||
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null
|
||||
$existing = Get-Module -ListAvailable -Name PSWindowsUpdate | Select-Object -First 1
|
||||
if ($existing) {
|
||||
Write-Log " PSWindowsUpdate $($existing.Version) already installed" -Level INFO
|
||||
} else {
|
||||
Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber | Out-Null
|
||||
Write-Log " PSWindowsUpdate installed" -Level OK
|
||||
}
|
||||
Import-Module PSWindowsUpdate -Force
|
||||
} catch {
|
||||
Write-Log " Module setup failed: $_" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 2. Check and install available updates
|
||||
# -----------------------------------------------------------------------
|
||||
Write-Log "Checking for available Windows Updates..." -Level INFO
|
||||
try {
|
||||
$available = Get-WindowsUpdate -AcceptAll -IgnoreReboot -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Log " Failed to check for updates: $_" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $available -or $available.Count -eq 0) {
|
||||
Write-Log " System is fully up to date" -Level OK
|
||||
Write-Log "Step 12 complete" -Level OK
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Log " Found $($available.Count) update(s) - installing..." -Level INFO
|
||||
try {
|
||||
$result = Install-WindowsUpdate -AcceptAll -IgnoreReboot -Verbose 2>&1
|
||||
$result | Where-Object { "$_" -match '\S' } | ForEach-Object { Write-Log " $_" -Level INFO }
|
||||
Write-Log " Update pass complete - reboot required for next round" -Level OK
|
||||
} catch {
|
||||
Write-Log " Update install failed: $_" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Signal xetup that a reboot is needed before running this step again
|
||||
Write-Log "Step 12 - reboot required (exit 9)" -Level OK
|
||||
exit 9
|
||||
|
|
@ -146,15 +146,5 @@
|
|||
"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
18
web/get.ps1
|
|
@ -1,18 +0,0 @@
|
|||
# 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: $_"
|
||||
}
|
||||
217
web/index.html
217
web/index.html
|
|
@ -34,10 +34,28 @@
|
|||
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 {
|
||||
|
|
@ -63,86 +81,92 @@
|
|||
}
|
||||
|
||||
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: 1.5rem;
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
.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%;
|
||||
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;
|
||||
}
|
||||
.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); }
|
||||
|
|
@ -177,27 +201,6 @@
|
|||
<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 https://xetup.x9.cz/dl</code>
|
||||
<button id="copy-btn" onclick="copyCmd()">Kopirovat</button>
|
||||
</div>
|
||||
<p style="color:var(--muted);font-size:.78rem;margin-top:.5rem">
|
||||
Pokud ve stazeni brani antivirus, pouzijte curl – staci bezny prikazovy radek, neni potreba administrator.
|
||||
</p>
|
||||
|
||||
<!-- 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>
|
||||
|
|
@ -217,7 +220,7 @@
|
|||
<div class="card">
|
||||
<span class="card-icon">🚀</span>
|
||||
<h3>Go TUI launcher</h3>
|
||||
<p>xetup.exe — jednotny binarni spoustec s TUI formularom a live logem.</p>
|
||||
<p>xetup.exe — jednotny binarni spoustec. Zatim ve vyvoji.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -230,45 +233,5 @@
|
|||
<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,20 +14,5 @@ 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,44 +419,6 @@
|
|||
.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;
|
||||
|
|
@ -504,16 +466,15 @@
|
|||
<a href="#step-06">06 – Scheduled Tasks</a>
|
||||
<a href="#step-07">07 – BackInfo</a>
|
||||
<a href="#step-08">08 – Aktivace Windows</a>
|
||||
<a href="#step-09">09 – PC identita</a>
|
||||
<a href="#step-10">10 – Network discovery</a>
|
||||
<a href="#step-11">11 – Dell Command | Update</a>
|
||||
<a href="#step-12">12 – Windows Update</a>
|
||||
<hr class="sidebar-divider">
|
||||
<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-taskbar">Taskbar profily</a>
|
||||
<hr class="sidebar-divider">
|
||||
<h4>Architektura</h4>
|
||||
<a href="#arch-xetup">xetup.exe (Go GUI)</a>
|
||||
<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 -->
|
||||
|
|
@ -521,7 +482,7 @@
|
|||
|
||||
<h1>Specifikace & anotace</h1>
|
||||
<div class="meta">
|
||||
<span>Verze: 0.4</span>
|
||||
<span>Verze: 0.3-draft</span>
|
||||
<span>Datum: 2026-04-16</span>
|
||||
<span><a href="https://git.xetup.x9.cz/x9/xetup">x9/xetup</a></span>
|
||||
<span>Status: aktivni vyvoj</span>
|
||||
|
|
@ -639,9 +600,6 @@
|
|||
<tr class="flag-done"><td>OneDrive uninstall (intentional)</td><td>OneDriveSetup.exe /uninstall – odstrani pre-installed verzi. M365 si nainstaluje vlastni.</td></tr>
|
||||
<tr class="flag-done"><td>Powercfg nastaveni (spotreba energie)</td><td>standby-ac 0, monitor-ac 60, standby-dc 30, monitor-dc 15</td></tr>
|
||||
<tr class="flag-done"><td>Proxy auto-detect zakaz (AutoDetect = 0)</td><td>HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings</td></tr>
|
||||
<tr class="flag-done"><td>Edge nova karta – zakaz rychlych odkazu</td><td>NewTabPageQuickLinksEnabled = 0</td></tr>
|
||||
<tr class="flag-done"><td>Edge nova karta – zakaz pozadi</td><td>NewTabPageBackgroundEnabled = 0</td></tr>
|
||||
<tr class="flag-done"><td>Edge nova karta – zakaz obsahu / feedu</td><td>NewTabPageAllowedBackgroundTypes = 3 (pouze plna barva)</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="step-footer">
|
||||
|
|
@ -662,7 +620,7 @@
|
|||
<tr class="flag-done"><td>Taskbar: zarovnat vlevo (TaskbarAl = 0)</td><td>Win11 default je center</td></tr>
|
||||
<tr class="flag-done"><td>Taskbar: skryt Search, Copilot, Task View, Widgets, Chat</td><td>OK</td></tr>
|
||||
<tr class="flag-done"><td>Taskbar: zobrazit vsechny ikonky v tray (Scheduled task)</td><td>ShowAllTrayIcons</td></tr>
|
||||
<tr class="flag-done"><td>Taskbar: explicitni pinlist (TaskbarLayoutModification.xml)</td><td>default/user: Pruzkumnik + Edge; admin: Pruzkumnik + Edge + PowerShell. <code>PinListPlacement="Replace"</code> – prazdny seznam by dovoloval Windows pridat Store a dalsi vychozi.</td></tr>
|
||||
<tr class="flag-done"><td>Taskbar: vyprazdnit pinlist (TaskbarLayoutModification.xml)</td><td>OK</td></tr>
|
||||
<tr class="flag-done"><td>Explorer: zobrazovat pripony souboru (HideFileExt = 0)</td><td>OK</td></tr>
|
||||
<tr class="flag-done"><td>Explorer: otevrit na This PC (LaunchTo = 1)</td><td>OK</td></tr>
|
||||
<tr class="flag-done"><td>Start menu: vyprazdnit piny (Win11)</td><td>ConfigureStartPins = {"pinnedList":[]}</td></tr>
|
||||
|
|
@ -702,7 +660,6 @@
|
|||
<tr class="flag-done"><td>Accent barva na Start a taskbaru: ano</td><td>OK</td></tr>
|
||||
<tr class="flag-done"><td>Pruhlednost: vypnuta</td><td>OK</td></tr>
|
||||
<tr class="flag-done"><td>Tapeta: jednobarevna #223B47 (bez obrazku)</td><td>BackInfo prepise tapetu svym BMP</td></tr>
|
||||
<tr class="flag-done"><td>Wallpaper="" v default hive (oprava cerne tapety)</td><td>Bez tohoto klice novy uzivatel zdedi neplatnou cestu k tapete a dostane cernou plochu. Prazdny retezec = solid barva z <code>Control Panel\Colors\Background</code></td></tr>
|
||||
</table>
|
||||
<div class="note">
|
||||
BackInfo.exe (STEP 07) prepise tapetu BMP se systemovymi informacemi.
|
||||
|
|
@ -787,8 +744,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<p class="section-label">Nove kroky (planovane)</p>
|
||||
|
||||
<!-- STEP 09 -->
|
||||
<div class="step" id="step-09">
|
||||
<div class="step" id="step-pc">
|
||||
<div class="step-header">
|
||||
<span class="step-num">09</span>
|
||||
<span class="step-title">PC identita – Rename + C:\X9</span>
|
||||
|
|
@ -796,13 +756,13 @@
|
|||
</div>
|
||||
<div class="step-body">
|
||||
<table class="items">
|
||||
<tr class="flag-done"><td>Rename-Computer dle parametru z GUI nebo config.json</td><td><code>deployment.pcName</code> v config.json; preskoci pokud neni nastaveno</td></tr>
|
||||
<tr class="flag-done"><td>Rename-Computer dle parametru z TUI nebo config.json</td><td><code>deployment.pcName</code> v config.json; preskoci pokud neni nastaveno</td></tr>
|
||||
<tr class="flag-done"><td>Nastavit popis pocitace (Computer Description)</td><td>LanmanServer\Parameters\SrvComment; default "X9 deployment"</td></tr>
|
||||
<tr class="flag-done"><td>Vytvorit <code>C:\X9\</code> adresarovou strukturu</td><td>C:\X9\Logs, Scripts, Assets</td></tr>
|
||||
<tr class="flag-done"><td>Vlastni ikonka pro <code>C:\X9\</code> slozku</td><td>Desktop.ini + X9-ikona.ico z assets\Logo\</td></tr>
|
||||
</table>
|
||||
<div class="note">
|
||||
Rename-Computer vyzaduje restart. Tento krok bezi jako posledni pred finalnim shrnutim – po nem nasleduje automaticky odpocet a restart v GUI.
|
||||
Rename-Computer vyzaduje restart. Tento krok bezi jako posledni pred finalnim shrnutim.
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-footer">
|
||||
|
|
@ -812,7 +772,7 @@
|
|||
</div>
|
||||
|
||||
<!-- STEP 10 -->
|
||||
<div class="step" id="step-10">
|
||||
<div class="step" id="step-net">
|
||||
<div class="step-header">
|
||||
<span class="step-num">10</span>
|
||||
<span class="step-title">Network discovery + firewall</span>
|
||||
|
|
@ -831,59 +791,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 11 -->
|
||||
<div class="step" id="step-11">
|
||||
<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 na konci deploymetu 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 sitoveho 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>
|
||||
|
||||
<!-- STEP 12 -->
|
||||
<div class="step" id="step-12">
|
||||
<div class="step-header">
|
||||
<span class="step-num">12</span>
|
||||
<span class="step-title">Windows Update</span>
|
||||
<span class="badge badge-ok">OK</span>
|
||||
</div>
|
||||
<div class="step-body">
|
||||
<table class="items">
|
||||
<tr class="flag-done"><td>Instalace NuGet providera + PSWindowsUpdate modulu ze PSGallery</td><td>Install-PackageProvider + Install-Module PSWindowsUpdate -Force</td></tr>
|
||||
<tr class="flag-done"><td>Prvni pruchod aktualizaci (bez rebootu)</td><td>Install-WindowsUpdate -AcceptAll -IgnoreReboot</td></tr>
|
||||
<tr class="flag-done"><td>X9-WindowsUpdate scheduled task – pokracovani po restartu</td><td>Bezi pri kazdem logonu jako SYSTEM; po pruchodu bez novych update se task sam smaze</td></tr>
|
||||
<tr class="flag-done"><td>Automaticky restart po skonceni deploymetu (GUI odpocet)</td><td>xetup.exe zobrazi 60s odpocet + tlacitka "Restartovat ted" / "Zrusit restart"</td></tr>
|
||||
</table>
|
||||
<div class="note">
|
||||
Windows Update typicky vyzaduje vice restartovacich kolu. Prvni pruchod probehne behem deploymetu,
|
||||
dalsi kola zaridti scheduled task, ktery se po dokonceni sam odregistruje.<br><br>
|
||||
<strong>Pozor:</strong> Prvni restart spusti jak Windows Update task, tak Dell firmware staging (krok 11).
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-footer">
|
||||
<span class="step-status">Script: <code>12-windows-update.ps1</code></span>
|
||||
<div class="comment-widget" data-issue="17"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TASKBAR PROFILY - detail implementace kroku 04 -->
|
||||
<!-- TASKBAR -->
|
||||
<div class="step" id="step-taskbar">
|
||||
<div class="step-header">
|
||||
<span class="step-num">04+</span>
|
||||
|
|
@ -893,13 +801,14 @@
|
|||
</div>
|
||||
<div class="step-body">
|
||||
<table class="items">
|
||||
<tr class="flag-done"><td><code>-ProfileType</code> parametr: default / admin / user</td><td>Parametr GUI nebo config.json; predano do skriptu 04</td></tr>
|
||||
<tr class="flag-done"><td>XML layout pro "default" a "user": Pruzkumnik + Edge</td><td><code>PinListPlacement="Replace"</code> – explicitni seznam zamezuje pridani Store a dalsich vychozich appek</td></tr>
|
||||
<tr class="flag-done"><td>XML layout pro "admin": Pruzkumnik + Edge + PowerShell</td><td>TaskbarLayoutModification.xml; PowerShell.lnk z System32</td></tr>
|
||||
<tr class="flag-done"><td><code>-ProfileType</code> parametr: admin vs user varianta</td><td>Deploy-Windows.ps1 -ProfileType [default|admin|user]; predano do 04</td></tr>
|
||||
<tr class="flag-done"><td>XML layout pro "admin": Explorer, PS, Edge</td><td>TaskbarLayoutModification.xml; File Explorer.lnk + PowerShell.lnk + Edge.lnk</td></tr>
|
||||
<tr class="flag-done"><td>XML layout pro "user": Explorer, Edge</td><td>Konzervativni sada – Outlook/Teams pridany az po instalaci M365</td></tr>
|
||||
<tr class="flag-open"><td>Win11 24H2 kompatibilita layoutu</td><td>24H2 vyzaduje ProvisionedLayoutModification.xml – nutno otestovat na realne instalaci</td></tr>
|
||||
</table>
|
||||
<div class="note">
|
||||
Layout se pri aplikaci zamkne. UnlockStartLayout task (krok 06) ho odemkne 5 min po startu, aby uzivatel mohl dale upravovat.
|
||||
Aplikace pinnutych appek: <code>Deploy-Windows.ps1 -ProfileType admin</code> nebo <code>-ProfileType user</code>.<br>
|
||||
Layout se zablokuje, UnlockStartLayout task (krok 06) ho odemkne 5 min po startu.
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-footer">
|
||||
|
|
@ -909,37 +818,35 @@
|
|||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<p class="section-label">Architektura</p>
|
||||
<p class="section-label">Architektura (budoucnost)</p>
|
||||
|
||||
<!-- ARCH XETUP -->
|
||||
<div class="step" id="arch-xetup">
|
||||
<div class="step-header">
|
||||
<span class="step-num">Arc</span>
|
||||
<span class="step-title">xetup.exe – Go GUI (Walk / Win32)</span>
|
||||
<span class="badge badge-ok">OK</span>
|
||||
<span class="step-title">xetup.exe – Go TUI launcher</span>
|
||||
<span class="badge badge-future">Future</span>
|
||||
</div>
|
||||
<div class="step-body">
|
||||
<table class="items">
|
||||
<tr class="flag-done"><td>Single binary (go:embed scripty + assets)</td><td><code>embed.go</code> + <code>cmd/xetup/main.go</code>; ~5 MB .exe, zadne externi zavislosti</td></tr>
|
||||
<tr class="flag-done"><td>Walk GUI – Win32 nativni ovladky, bez OpenGL</td><td>Funguje na VMware ESXi / SVGA II (Fyne/OpenGL nefungovalo). <code>internal/gui/gui.go</code></td></tr>
|
||||
<tr class="flag-done"><td>Formular: PC jmeno, popis, product key, profil</td><td>Faze 1 – config form pred spustenim</td></tr>
|
||||
<tr class="flag-done"><td>Checklist kroku (on/off per-feature) + nacist / ulozit config.json</td><td>Tlacitka "Nacist config..." a "Ulozit config..." pro per-klient presety</td></tr>
|
||||
<tr class="flag-done"><td>Live log + prubehovy strip kroku (0–12)</td><td>Barevne indikatory: · cekajici, ► bezici (modra), ✓ OK (zelena), ✗ chyba (cervena)</td></tr>
|
||||
<tr class="flag-done"><td>Summary s odpoctem a automatickym restartem</td><td>Faze 3 – 60s odpocet, tlacitka "Restartovat ted" / "Zrusit restart"</td></tr>
|
||||
<tr class="flag-done"><td>UAC requireAdministrator (app.manifest + rsrc)</td><td>Windows vyzada elevaci pri spusteni; ComCtl32 v6 + DPI awareness</td></tr>
|
||||
<tr class="flag-done"><td>PowerShell okno skryte na pozadi</td><td>SysProcAttr.HideWindow = true; PS chyby filtrovane (At line:, CategoryInfo atd.)</td></tr>
|
||||
<tr class="flag-todo"><td>Self-update: stahnout novou verzi z xetup.x9.cz/dl</td><td>Overit hash pred spustenim</td></tr>
|
||||
<tr class="flag-future"><td>config.json: per-klient preset na USB</td><td>Jmeno PC prefix, SW seznam, klic – lezi vedle .exe</td></tr>
|
||||
<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-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>
|
||||
</table>
|
||||
<div class="note">
|
||||
<strong>Struktura:</strong> <code>cmd/xetup/</code>, <code>internal/config/</code>, <code>internal/gui/</code>, <code>internal/runner/</code><br><br>
|
||||
<strong>Go zavislosti:</strong> github.com/lxn/walk (Win32 GUI), golang.org/x/sys<br><br>
|
||||
<strong>Build:</strong> <code>GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui" ./cmd/xetup/</code><br>
|
||||
CI (Forgejo Actions) sestavi a publikuje automaticky pri kazdem push na main.
|
||||
<strong>Struktura:</strong> <code>cmd/xetup/</code>, <code>internal/config/</code>,
|
||||
<code>internal/spec/</code>, <code>internal/tui/</code>, <code>internal/runner/</code><br><br>
|
||||
<strong>Go zavislosti:</strong>
|
||||
bubbletea (TUI framework), huh (forms), lipgloss (styling)
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-footer">
|
||||
<span class="step-status">Status: v provozu – <a href="https://xetup.x9.cz/dl" style="color:var(--blue)">xetup.x9.cz/dl</a></span>
|
||||
<span class="step-status">Status: navrh, zatim zadny kod</span>
|
||||
<div class="comment-widget" data-issue="11"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -978,34 +885,6 @@
|
|||
</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 -->
|
||||
|
||||
|
|
@ -1015,8 +894,6 @@
|
|||
<a href="https://git.xetup.x9.cz/x9/xetup">Forgejo</a>
|
||||
·
|
||||
<a href="/">xetup.x9.cz</a>
|
||||
·
|
||||
web: <span id="deploy-sha" style="font-family:monospace">...</span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
|
|
@ -1157,11 +1034,9 @@
|
|||
'step-05': '05-personalization',
|
||||
'step-06': '06-scheduled-tasks',
|
||||
'step-07': '07-backinfo',
|
||||
'step-pc': '09-pc-identity',
|
||||
'step-net': '10-network',
|
||||
'step-08': '08-activation',
|
||||
'step-09': '09-pc-identity',
|
||||
'step-10': '10-network',
|
||||
'step-11': '11-dell-update',
|
||||
'step-12': '12-windows-update',
|
||||
};
|
||||
|
||||
function getItemDesc(stepId, slug) {
|
||||
|
|
@ -1314,83 +1189,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
// ---- 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();
|
||||
|
||||
// Show deployed commit in footer
|
||||
fetch('/data/deploy.json')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const el = document.getElementById('deploy-sha');
|
||||
if (el && d.sha) {
|
||||
const ts = d.ts ? ' (' + new Date(d.ts).toLocaleString('cs-CZ', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) + ')' : '';
|
||||
el.textContent = d.sha + ts;
|
||||
el.title = d.ts || '';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
BIN
xetup.exe
BIN
xetup.exe
Binary file not shown.
Loading…
Reference in a new issue