psadt-deploy-skill 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Taubert, PHAT Consulting GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,714 @@
1
+ <h1 align="center">PSADT v4 → Intune Deployment Skill</h1>
2
+
3
+ <p align="center">
4
+ <em>A Claude Code skill that drives the full lifecycle of a PowerShell App Deployment Toolkit (PSADT) v4.x Intune Win32 package — from first conversation to a tested, upload-ready <code>.intunewin</code>.</em>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" /></a>
9
+ <img src="https://img.shields.io/badge/PSADT-v4.x-0a7bbb?style=flat-square" alt="PSADT v4.x" />
10
+ <img src="https://img.shields.io/badge/Platform-Windows-0078d6?style=flat-square&logo=windows&logoColor=white" alt="Windows" />
11
+ <img src="https://img.shields.io/badge/Claude%20Code-Skill-d97757?style=flat-square" alt="Claude Code Skill" />
12
+ </p>
13
+
14
+ <p align="center"><sub><a href="#quick-start">Quick start</a> · <a href="#how-it-works">How it works</a> · <a href="#features">Features</a> · <a href="#first-run-setup">Setup</a> · <a href="#roadmap">Roadmap</a> · <a href="#changelog">Changelog</a></sub></p>
15
+
16
+ ---
17
+
18
+ ## What is this?
19
+
20
+ A **Claude Code skill** (not a plugin): a reusable instruction package that teaches the agent how to build,
21
+ package, test, troubleshoot and deploy a **PSADT v4.x Intune Win32 app**. You describe the application; the
22
+ skill runs the workflow — intake, web research, scaffolding, all three deployment types
23
+ (Install / Uninstall / Repair), pre-flight checks, the SYSTEM test, packaging, the dossier, and the
24
+ optional Graph upload.
25
+
26
+ A skill is a folder with a `SKILL.md` (YAML frontmatter + Markdown instructions), here bundled with
27
+ `scripts/` and `references/`. It loads progressively: the agent sees only the name and description until a
28
+ task makes it relevant, then the full body loads on demand.
29
+
30
+ <img width="1024" height="254" alt="image" src="https://github.com/user-attachments/assets/7c7931ba-dcae-4476-a648-11115eceb3b5" />
31
+
32
+ ## Quick start
33
+
34
+ ```powershell
35
+ npx psadt-deploy-skill
36
+ ```
37
+
38
+ That installs the skill into `~/.claude/skills/psadt-deploy` and runs the setup doctor, which provisions
39
+ everything it can and names the handful of values only you can supply (see
40
+ [First-run setup](#first-run-setup)). Then open Claude Code in any folder and say what you want:
41
+
42
+ > *"Baue mir ein Intune-Paket für 7-Zip 24.09"* — or *"package Notepad++ for Intune"*
43
+
44
+ The skill asks at most **four decision gates** (scope · deployment semantics · SYSTEM-test consent ·
45
+ upload confirmation). Everything else it researches and states as an assumption instead of asking.
46
+
47
+ ## How it works
48
+
49
+ Twelve phases, each owned by a script rather than by prose, so a step either passed or did not:
50
+
51
+ | Phase | What happens | Owner |
52
+ |---|---|---|
53
+ | **0** Setup | 13 prerequisite checks, GREEN/YELLOW/RED, `-Fix` provisions | `Initialize-PsadtSkill.ps1` |
54
+ | **1–2** Intake + research | blocker questions as clickable options; parallel research of version, silent switches, Intune pitfalls | agent (gates 1–2) |
55
+ | **3** Scaffold | a generator writes launcher + detection + per-run log name + manifest; `New-ADTTemplate` only when none fits | `New-MsiPackage` · `New-BrowserExtensionPackage` · `New-WindowsFeaturePackage` · `New-DriverPackage` |
56
+ | **4** Customize | all three hooks filled from the research, helpers in the Extensions module | agent |
57
+ | **5** Pre-flight | 10 checks (encoding, AST parse, v3 cmdlets, structure, detection contract, manifest, log name, driver trust …) → GREEN/RED | `Invoke-PsadtPreflight.ps1` |
58
+ | **6** SYSTEM test | installs/uninstalls as **SYSTEM** like the IME does; **binding before any upload** | `Invoke-PsadtSystemTest.ps1` |
59
+ | **7** Package | one command → verified `.intunewin`, named after the app | `Invoke-PsadtPackage.ps1` |
60
+ | **8** Dossier | always, uploaded or not: bilingual self-contained HTML | `New-PsadtReport.ps1` |
61
+ | **9** Upload *(opt-in)* | dry run → confirm → `win32LobApp` via raw Graph | `Invoke-IntuneWin32Upload.ps1` |
62
+ | **10** Assignment *(opt-in)* | create/reuse Entra groups by naming scheme | `Invoke-IntuneAppAssignment.ps1` |
63
+ | **11–12** Test + rollout | DEV-VM cycles, test group, pilot → staged production | agent |
64
+
65
+ **Everything one app knows lives in `<pkg>\psadt-package.json`** — identity, the decisions taken at the
66
+ gates, the research findings, every phase's result and the artifacts produced. The generators write it,
67
+ every later phase reads and updates it, and pre-flight fails without it. That is what stops two packages of
68
+ the same app from disagreeing about their own version.
69
+
70
+ Depth lives in `references/PSADTv4-Deployment-Guide.md` (Phases 0–12 + Appendices A–Q); `SKILL.md` stays the
71
+ control plane.
72
+
73
+ ## Features
74
+
75
+ ### Setup and prerequisites
76
+
77
+ - **Setup doctor** — one idempotent script checks PowerShell 7, Windows PowerShell 5.1, elevation, git,
78
+ PSAppDeployToolkit, the content-prep tool, `Invoke-CommandAs`, Pester, the config, a legacy config, the
79
+ skill tree, a pending update and the Intune credentials. Each line carries a concrete fix; `-Fix` applies
80
+ the ones that need no decision.
81
+ - **Self-healing prerequisites** — installs the PSAppDeployToolkit module from the PowerShell Gallery and
82
+ downloads `IntuneWinAppUtil.exe`, keeping both current against their official sources.
83
+ - **Config outside the skill folder** — `config.json`, `secret.dpapi` and `tools/` live in
84
+ `%LOCALAPPDATA%\psadt-deploy\` (override: `$env:PSADT_DEPLOY_HOME`), so a `git pull`, a re-clone or a
85
+ re-install can no longer take your setup with it. A pre-0.19 config keeps working and is migrated on
86
+ request, never deleted.
87
+ - **One-line install** — `npx psadt-deploy-skill` (Node 18+, zero dependencies) does clone-or-update plus
88
+ the doctor run in one step.
89
+
90
+ ### Build and verify
91
+
92
+ - **Guided intake** — the blocker questions up front as clickable options, pre-filled with researched
93
+ defaults (app, latest version, installer type, package type).
94
+ - **Autonomous research** — checks the installed PSADT version against the latest release *and* whether
95
+ commands changed; researches silent install / uninstall / repair switches and known Intune pitfalls.
96
+ - **All three deployment types from the start** — Install, Uninstall *and* Repair, acid-tested, so
97
+ Company-Portal uninstalls actually work.
98
+ - **Pre-flight gate** — encoding/BOM, AST parse, launcher acid test, v3-cmdlet scan, hook structure,
99
+ detection-script contract, the package manifest, the per-run log name and driver trust. GREEN or RED,
100
+ with the failing file named.
101
+ - **Automated SYSTEM test loop** *(opt-in, binding before upload)* — installs, uninstalls and reinstalls as
102
+ the **SYSTEM** account via `Invoke-CommandAs`, mirroring the Intune Management Extension; reads the fresh
103
+ session log and the detection result, and hands back a structured verdict for the fix-and-retry loop.
104
+ Needs an elevated session; belongs on a VM with a snapshot.
105
+ - **Deterministic packaging** — `<outputRoot>\<Vendor>_<App>_<Version>_<Arch>\<same stem>.intunewin`,
106
+ verified after the fact (`Detection.xml`, `SetupFile`, size, SHA256), with the detection script and the
107
+ real logo beside it. It refuses an output folder inside the package, and never deletes a foreign
108
+ `.intunewin` it finds there.
109
+ - **One PSADT log per run** — `<Vendor>_<App>_<Version>_<Arch>_<Install|Uninstall|Repair>_<timestamp>.log`
110
+ instead of every run of every version appending to one unreadable file.
111
+
112
+ ### Package types
113
+
114
+ The app's **native installer is always the default**. Everything else is opt-in and only on request:
115
+
116
+ - **MSI / EXE** — the ordinary case, via `New-MsiPackage.ps1` or a hand-filled scaffold.
117
+ - **WinGet** — the full `PSAppDeployToolkit.WinGet` lifecycle: the extension module self-heals into the
118
+ package, the Package ID is discovered with `Find-ADTWinGetPackage`, hooks use `*-ADTWinGet*`
119
+ (`-Scope Machine`). Never selected on its own initiative.
120
+ - **Browser extensions** — force-install via the Edge/Chrome/Firefox policy keys (including the Firefox
121
+ `REG_MULTI_SZ` trap), with selective removal on uninstall.
122
+ - **Windows features** — `Enable-WindowsOptionalFeature` and `Add-WindowsCapability`, offline source or a
123
+ temporary WSUS bypass that is restored afterwards, `EnablePending` handled honestly.
124
+ - **Third-party drivers** — the trust situation is classified *before* anything is built: Microsoft-signed
125
+ installs silently, vendor-signed needs the signer certificate owned in exactly one place, and a
126
+ vendor-signed **kernel** driver is refused because `TrustedPublisher` satisfies the PnP prompt but never
127
+ Code Integrity — it would install and then not load. Unsigned is refused outright, with three honest
128
+ options and no testsigning. Staging is per-INF `pnputil`; uninstall resolves `oemNN.inf` by original name
129
+ instead of a remembered index.
130
+ - **Script-only / remediation packages** — ESP-safe patterns for fix packages with no installer at all.
131
+
132
+ ### Deliverables
133
+
134
+ - **HTML dossier — always generated**, uploaded or not. One self-contained file
135
+ (`Intune-Dossier.html`) built from a fixed template, never hand-assembled: the **Intune dossier** (App
136
+ Info, return-code map, detection rule, requirements, assignments, driver trust, and a ready-to-paste
137
+ **Markdown** description for the Company-Portal field) plus a **technical package report** (the three
138
+ hooks, PSADT cmdlets used, pre-flight and SYSTEM-test results, logo and `.intunewin` verification).
139
+ Bilingual with a DE/EN toggle, browser-translatable, logo embedded as a data URI.
140
+ - **Real logo only** — finds and downloads the actual application logo (vendor source or Wikimedia
141
+ Commons), verifies real pixel transparency *and* looks at the image. The PSADT default `AppIcon.png` is
142
+ blocked by hash.
143
+ - **Start Menu only** — creates Start Menu entries and removes stray desktop icons.
144
+
145
+ ### Intune
146
+
147
+ - **Access as state, not as a 403** — `Test-PsadtIntuneAccess.ps1` answers before Phase 9 whether the app
148
+ can upload, assign groups or create policies, and for how long the credential lives. Verified / refused /
149
+ **unknown** are three different answers, and an offline check never overwrites what was verified before.
150
+ - **Direct upload via Microsoft Graph** *(opt-in)* — pushes the `.intunewin` as a `win32LobApp` (app +
151
+ logo), self-contained raw Graph, no third-party module. Identity comes from the manifest, so Intune shows
152
+ the same name and version as the artifact and the dossier. Read-only dry run → confirm → upload. Fills the
153
+ whole App-information tab, **never deletes an older version** (new versions coexist, with optional
154
+ supersedence wiring), never auto-assigns categories or notes.
155
+ - **One-time Entra bootstrap** — `New-PsadtEntraApp.ps1` signs in via **WAM**, creates the app, grants and
156
+ admin-consents the roles and stores the credential: a **certificate** (preferred — nothing secret at rest,
157
+ JWT client-assertion auth) or a DPAPI-encrypted client secret. Re-running it is normal: it finds the
158
+ recorded app, merges requested permissions instead of replacing them, and never prompts.
159
+ - **Opt-in group assignment** — creates/reuses Entra security groups by a configured naming scheme and
160
+ assigns Required / Available / Uninstall. Least-privilege (`Group.Create` + `GroupMember.Read.All`),
161
+ dry run → confirm, idempotent, and it never deletes a group or another app's assignment.
162
+ - **Certificate + firewall policies** — Custom OMA-URI profiles for `TrustedPublisher` / `TrustedPeople`
163
+ (the built-in template cannot reach those stores) and settings-catalog firewall-rule policies. Both
164
+ scripts are self-contained deliverables: they can be copied to a test client that has no skill installed.
165
+
166
+ ### Operations
167
+
168
+ - **Troubleshooting** — decodes Intune error/HRESULT codes, maps symptoms to root causes, and triages the
169
+ right log (`AppWorkload.log`, the PSADT session log, `setupapi.dev.log` for drivers).
170
+ - **Self-update** — `scripts/Update-PsadtSkill.ps1` compares against GitHub, shows what changed, and
171
+ updates in place on your confirmation (`git pull --ff-only` for a clone, otherwise a branch-zip overwrite
172
+ of tracked files only). Machine-local state is never touched. Say *"update skill"* or *"psadt update"*.
173
+ - **326 Pester tests** over the helper scripts, including drift guards that fail when the docs and the code
174
+ disagree.
175
+
176
+ ## Requirements
177
+
178
+ - Windows with PowerShell 5.1+ / PowerShell 7+
179
+ - For the `npx` installer only: **Node 18+** (the skill itself never needs Node)
180
+ - [PSAppDeployToolkit](https://psappdeploytoolkit.com/) v4.x *(installed/updated automatically from the
181
+ PowerShell Gallery if missing)*
182
+ - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
183
+ *(provisioned automatically)*
184
+ - For the **SYSTEM test loop**: an **elevated** session; the
185
+ [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs) module is installed automatically
186
+ - For the **direct Intune upload**: an Entra app with the Graph application role
187
+ `DeviceManagementApps.ReadWrite.All` (admin-consented) — created in one run by
188
+ `scripts/New-PsadtEntraApp.ps1` (WAM sign-in as Global Admin / Privileged Role Admin, device-code
189
+ fallback). Check what is actually in place with `scripts/Test-PsadtIntuneAccess.ps1`. Full permission
190
+ matrix and the manual portal route: `references/app-registration.md`.
191
+ - For **Pester tests**: Pester 5+ (`Install-Module Pester -MinimumVersion 5.0 -Scope CurrentUser`)
192
+ - **Optional (recommended): the [superpowers](https://github.com/obra/superpowers) plugin** — if installed,
193
+ the research fan-out and the reviewer gate use it. Not required: without it the skill falls back to the
194
+ native Agent tool and `/code-review`, and nothing in the workflow depends on the plugin.
195
+
196
+ ## Installation
197
+
198
+ ```powershell
199
+ npx psadt-deploy-skill
200
+ ```
201
+
202
+ Installs into `~/.claude/skills/psadt-deploy` and runs the setup doctor. Flags: `--dir <path>` ·
203
+ `--project` (into `./.claude/skills`) · `--ref <branch|tag>` · `--no-setup`. Node 18+ and Windows; the
204
+ installer itself has zero dependencies and the package carries only `bin/` — the skill is fetched from
205
+ GitHub at install time.
206
+
207
+ Re-running it updates an existing installation, and so does saying *"update skill"* to Claude Code
208
+ (`git pull --ff-only` for a clone, otherwise a branch-zip overwrite of tracked files only — machine-local
209
+ state is never touched).
210
+
211
+ **Or clone it yourself** — the repo root *is* the skill folder:
212
+
213
+ ```powershell
214
+ git clone https://github.com/pt1987/claude-code-psadt-skill.git "$env:USERPROFILE\.claude\skills\psadt-deploy"
215
+ pwsh "$env:USERPROFILE\.claude\skills\psadt-deploy\scripts\Initialize-PsadtSkill.ps1" -Fix
216
+ ```
217
+
218
+ `npx skills add pt1987/claude-code-psadt-skill` works too, since `SKILL.md` sits in the repository root.
219
+
220
+ > Needs read access to this repository: the installer clones over your existing git credentials.
221
+
222
+ The skill activates automatically when you ask Claude Code to build an Intune package, or when you work in
223
+ a folder containing `Invoke-AppDeployToolkit.ps1`.
224
+
225
+ ## First-run setup
226
+
227
+ `scripts/Initialize-PsadtSkill.ps1` (also reachable by saying *"psadt setup"* / *"psadt doctor"*) checks
228
+ every prerequisite in one pass and reports **GREEN / YELLOW / RED**. Every line comes with a concrete fix
229
+ hint, and `-Fix` applies the ones that need no decision (module installs, the tool download, the
230
+ `language.*` defaults, `paths.intuneWinAppUtil`, and migrating a pre-0.19 config). It is idempotent — run it
231
+ as often as you like.
232
+
233
+ Only four values genuinely need you; the doctor lists them in `.Missing` and takes them via `-Set`:
234
+
235
+ ```powershell
236
+ pwsh scripts/Initialize-PsadtSkill.ps1 -Fix -Set @{
237
+ 'paths.packageRoot' = 'D:\Pakete'; 'paths.outputRoot' = 'D:\Intune'
238
+ 'author.person' = 'Pat Taubert'; 'author.company' = 'PHAT Consulting'
239
+ }
240
+ ```
241
+
242
+ | Setting | Purpose |
243
+ |---|---|
244
+ | `paths.packageRoot` / `outputRoot` | Where packages are built and where artifacts are written |
245
+ | `paths.intuneWinAppUtil` | Content-prep tool location — filled by `-Fix` |
246
+ | `language.script` / `dossier` | Script language (EN) vs. dossier language (DE for the Company Portal) — filled by `-Fix` |
247
+ | `author.person` / `company` | Stamped into every package's `AppScriptAuthor` |
248
+ | `intune.*` *(optional)* | Direct upload: tenant/client, credential reference, verified roles — written by `New-PsadtEntraApp.ps1` |
249
+ | `intune.groups.*` *(optional)* | Opt-in group assignment (`enabled` / `create` / `membershipType` / `naming`) — guide Appendix M |
250
+
251
+ ### Where the setup is stored
252
+
253
+ `config.json`, `secret.dpapi` and `tools/` live in the **config home** — `%LOCALAPPDATA%\psadt-deploy\`,
254
+ overridable with `$env:PSADT_DEPLOY_HOME` — **not** in the skill folder, so they survive a `git pull`, a
255
+ re-clone and a re-install. They are machine-local and never committed. A `config.json` from a pre-0.19
256
+ install (beside `scripts/`) keeps working read-only; the doctor flags it and `-Fix` migrates it, renaming
257
+ the originals to `*.migrated` rather than deleting anything.
258
+
259
+ > DPAPI is bound to the Windows user profile: a re-installed OS invalidates a stored client secret. The
260
+ > doctor and `Test-PsadtIntuneAccess.ps1` both say so, and the fix is one `New-PsadtEntraApp.ps1` run.
261
+
262
+ ## Project structure
263
+
264
+ ```
265
+ psadt-deploy/
266
+ ├─ SKILL.md · README.md · CHANGELOG.md · LICENSE
267
+ ├─ package.json · bin/install.mjs the npx installer (Node 18+, zero dependencies)
268
+ ├─ scripts/
269
+ │ │ setup + config
270
+ │ ├─ Initialize-PsadtSkill.ps1 setup doctor (Phase 0, GREEN/YELLOW/RED, -Fix/-Set)
271
+ │ ├─ Get-PsadtConfig.ps1 config read + config-home resolver
272
+ │ ├─ Set-PsadtConfig.ps1 config write (deep merge, DPAPI secret, -Remove)
273
+ │ ├─ Get-PsadtModule.ps1 PSADT module (self-heal)
274
+ │ ├─ Get-IntuneWinAppUtil.ps1 content-prep tool (self-heal)
275
+ │ ├─ Get-WinGetModule.ps1 WinGet extension (opt-in)
276
+ │ ├─ Update-PsadtSkill.ps1 self-update from GitHub
277
+ │ │ per-package truth
278
+ │ ├─ Get-PsadtPackageManifest.ps1 manifest read (+ the artifact stem)
279
+ │ ├─ Set-PsadtPackageManifest.ps1 manifest write (merge / append)
280
+ │ │ package generators
281
+ │ ├─ New-MsiPackage.ps1 MSI packages
282
+ │ ├─ New-BrowserExtensionPackage.ps1 browser-extension force-install (opt-in)
283
+ │ ├─ New-WindowsFeaturePackage.ps1 optional features / capabilities (opt-in)
284
+ │ ├─ New-DriverPackage.ps1 driver packages, pnputil staging (opt-in)
285
+ │ ├─ Get-DriverSignatureInfo.ps1 driver trust classifier (signed? kernel? deployable?)
286
+ │ │ gates + deliverables
287
+ │ ├─ Invoke-PsadtPreflight.ps1 pre-flight GREEN/RED gate (Phase 5, 10 checks)
288
+ │ ├─ Invoke-PsadtSystemTest.ps1 SYSTEM test (Phase 6)
289
+ │ ├─ Invoke-PsadtPackage.ps1 build the .intunewin (Phase 7, named + verified)
290
+ │ ├─ New-PsadtReport.ps1 HTML dossier (Phase 8, always)
291
+ │ │ intune / graph
292
+ │ ├─ New-PsadtEntraApp.ps1 Entra app bootstrap (WAM)
293
+ │ ├─ Get-GraphToken.ps1 app-only Graph token (cert / DPAPI)
294
+ │ ├─ Test-PsadtIntuneAccess.ps1 access verdict (roles, capabilities, expiry)
295
+ │ ├─ Invoke-IntuneWin32Upload.ps1 direct upload (Phase 9)
296
+ │ ├─ Invoke-IntuneAppAssignment.ps1 group assignment (Phase 10, opt-in)
297
+ │ ├─ New-IntuneTrustedCertPolicy.ps1 Custom OMA-URI cert policy (self-contained)
298
+ │ ├─ New-IntuneFirewallPolicy.ps1 firewall-rule policy (self-contained)
299
+ │ ├─ _GraphCommon.ps1 shared Graph helpers (retry, errors, token roles)
300
+ │ └─ _GraphInteractive.ps1 shared WAM sign-in
301
+ ├─ references/
302
+ │ ├─ PSADTv4-Deployment-Guide.md Phases 0-12 + Appendices A-Q
303
+ │ ├─ Report-Template.html the fixed dossier template
304
+ │ └─ app-registration.md THE Graph permission matrix + manual portal route
305
+ └─ tests/ Pester suite, 326 tests
306
+ ```
307
+
308
+ Machine-local state lives outside the skill folder:
309
+
310
+ ```
311
+ %LOCALAPPDATA%\psadt-deploy\ ($env:PSADT_DEPLOY_HOME overrides)
312
+ ├─ config.json settings incl. the optional intune.* block
313
+ ├─ secret.dpapi DPAPI client secret (only without cert auth)
314
+ └─ tools/ IntuneWinAppUtil.exe + WinGet module
315
+ ```
316
+
317
+ And per package, next to `Invoke-AppDeployToolkit.ps1`:
318
+
319
+ ```
320
+ psadt-package.json identity · gate decisions · research · results · artifacts
321
+ ```
322
+
323
+ ## Status
324
+
325
+ In active use for the full build → package → test → dossier workflow, with the direct Graph upload
326
+ verified against a live tenant. The helper scripts are covered by 326 Pester tests.
327
+
328
+ One open point, honestly: **the driver `pnputil` exit-code semantics are documented, not verified here.**
329
+ `0` / `259` / `3010` and the two `0xE...` failures come from Microsoft's documentation; confirming them
330
+ against `setupapi.dev.log` on a DEV VM with a real vendor-signed and a real Microsoft-signed driver is
331
+ still open.
332
+
333
+ ## Roadmap
334
+
335
+ Designed and waiting to be built:
336
+
337
+ - **Sync finished packages to a GitHub repo** — a setup option (`output.target` = `local` / `git` / `both`)
338
+ to push the per-app artifacts (`.intunewin`, dossier, detection, logo) to a Git repo instead of, or in
339
+ addition to, a local folder — versioned and shareable. Will need **Git LFS** for large `.intunewin` files
340
+ (GitHub's 100 MB per-file limit).
341
+
342
+ Have a request? Open an issue.
343
+
344
+ ## Contributing
345
+
346
+ Issues and pull requests are welcome. Keep `SKILL.md`, the references and the docs in **English**. The only
347
+ non-English content is the generated end-user output (the Intune dossier and the Company-Portal app
348
+ description), whose language follows the `language.dossier` config value — **default German**, but
349
+ configurable per machine.
350
+
351
+ Two conventions worth knowing before you send a patch: generated `.ps1` content is **7-bit ASCII** (the
352
+ pre-flight fails on non-ASCII without a BOM), and anything that lands in a package's output folder must be
353
+ **self-contained** — it gets copied to test clients that have no skill installed.
354
+
355
+ ## License
356
+
357
+ [MIT](LICENSE) © Patrick Taubert, PHAT Consulting GmbH
358
+
359
+ ## Acknowledgements
360
+
361
+ - [PSAppDeployToolkit](https://github.com/PSAppDeployToolkit/PSAppDeployToolkit)
362
+ - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
363
+ - [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs)
364
+ - README structure inspired by [ComposioHQ/awesome-claude-skills](https://github.com/ComposioHQ/awesome-claude-skills)
365
+
366
+ ## Changelog
367
+
368
+ Recent releases below; the complete history is in **[CHANGELOG.md](CHANGELOG.md)** (nothing is ever removed
369
+ from either).
370
+
371
+ ### 0.23.0 - 04.09.2026
372
+ - **`npx psadt-deploy-skill`.** One line installs or updates the skill into `~/.claude/skills/psadt-deploy`
373
+ and runs the setup doctor. Flags `--dir`, `--project`, `--ref`, `--no-setup`. Cloning to exactly the right
374
+ path by hand was the first thing a new user could get wrong.
375
+ - **Zero dependencies** (Node 18's `fetch` + the `tar.exe` Windows ships). Three acquisition routes:
376
+ update an existing clone, else `git clone --depth 1`, else the branch tarball — because plenty of managed
377
+ machines have no git. The npm package ships only `bin/`; the skill comes from GitHub at install time, so
378
+ a new skill version needs no republish.
379
+ - **The installer never writes the config itself** — it spawns `Set-PsadtConfig.ps1` and
380
+ `Initialize-PsadtSkill.ps1 -Fix`, because a second config-home implementation in JavaScript would drift
381
+ from `Get-PsadtConfig`. `Update-PsadtSkill.ps1` now tracks `package.json` and `bin/` so the archive
382
+ update route stops dropping them. Suite 307 → 326 tests.
383
+
384
+ ### 0.22.0 - 04.09.2026
385
+ - **Third-party drivers.** New `scripts/Get-DriverSignatureInfo.ps1` classifies a driver folder before
386
+ anything is built — Microsoft-signed / vendor-signed / unsigned — by checking the **catalog** signature
387
+ rather than the `.sys` (a dual-signed `.sys` reports only its primary signature). New
388
+ `scripts/New-DriverPackage.ps1` builds the package: pnputil staging per INF, uninstall that resolves
389
+ `oemNN.inf` by original name instead of a remembered index, `Get-WindowsDriver` detection.
390
+ - **The rule that saves the most time:** a vendor-signed *kernel* driver is RED, not a warning. With Secure
391
+ Boot on, only Microsoft Dev-Portal-signed kernel drivers load — importing the signer certificate removes
392
+ the "install device software?" prompt but does nothing for Code Integrity, so the driver installs and
393
+ then never loads. Unsigned drivers are refused outright, with three honest options and no testsigning.
394
+ - Pre-flight gained a `DriverTrust` check that fires for **any** package shipping an `.inf` (a vendor
395
+ installer staging a driver is the case nobody declares), the dossier has a driver-trust row, and
396
+ `New-IntuneTrustedCertPolicy.ps1` is now fully self-contained like the firewall script.
397
+ - New guide **Appendix Q** with the decision tree, pnputil exit codes and the installer-bundled-driver
398
+ case. Suite 251 → 307 tests.
399
+
400
+ ### 0.21.0 - 04.09.2026
401
+ - **Every package gets a manifest** (`psadt-package.json`): identity, gate decisions, research findings,
402
+ every phase result and the produced artifacts, in one file per app. The generators write it, every phase
403
+ script reads and updates it — so an app's version, name and test evidence stop living in someone's head.
404
+ - **The `.intunewin` is named after the app.** New: `<outputRoot>\<Vendor>_<App>_<Version>_<Arch>\<same
405
+ stem>.intunewin`, produced by the new `scripts/Invoke-PsadtPackage.ps1`. Until now every package came out
406
+ as `Invoke-AppDeployToolkit.intunewin` — that name reached Intune, and concurrent uploads collided in one
407
+ shared temp folder. The new script also verifies the archive before calling it a deliverable and refuses
408
+ an output folder inside the package (which made the tool pack its own previous output).
409
+ - **One PSADT log per run** instead of one ever-growing file:
410
+ `<Vendor>_<App>_<Version>_<Arch>_<Install|Uninstall|Repair>_<timestamp>.log`. PSADT appends to a fixed
411
+ name by default, so by the third attempt a failed install was unreadable.
412
+ - **Pre-flight gained two checks:** a missing or incomplete manifest is RED, a pre-0.21 launcher without a
413
+ per-run log name is a WARN. Report and upload both take their identity from the manifest.
414
+ - Guide: the missing `Phase 6 / 9 / 10` sections now exist, and Appendix E is numbered by phase.
415
+ Suite 173 → 251 tests.
416
+
417
+ ### 0.20.0 - 04.09.2026
418
+ - **Intune access is state, not a 403.** New `scripts/Test-PsadtIntuneAccess.ps1` answers *before* Phase 9
419
+ whether the configured app can actually upload, assign groups or create policies — and for how long the
420
+ credential lives. `TokenOk` and every capability are three-valued: verified / refused / **unknown**,
421
+ because Graph tokens are opaque by contract and "we could not tell" is not "not permitted". An offline run
422
+ never overwrites what was verified earlier.
423
+ - **The scripts assert the role they need before their first write** instead of discovering it from a 403
424
+ mid-upload. Group assignment requires *both* group roles and names the missing half.
425
+ - **`New-PsadtEntraApp.ps1` is re-runnable.** It finds the app by the recorded `clientId`, **merges**
426
+ requested permissions instead of replacing them (a run without `-IncludeConfigurationManagement` used to
427
+ silently revoke that role), never prompts, persists what it learned, and only sets `uploadEnabled` once
428
+ consent is really in place. Older client secrets are counted, never deleted.
429
+ - **Auth failures say what to do:** expired/invalid client secret, unknown app or tenant, Conditional Access
430
+ block — and an undecryptable DPAPI secret now explains that DPAPI is bound to the Windows user profile.
431
+ - `references/app-registration.md` is now the single permission matrix (app roles → capabilities → how to
432
+ grant), referenced from the guide instead of duplicated. Suite 128 → 173 tests.
433
+
434
+ ### 0.19.0 - 04.09.2026
435
+ - **Setup doctor: `scripts/Initialize-PsadtSkill.ps1`.** One idempotent script replaces the Phase 0 prose
436
+ wizard and reports GREEN/YELLOW/RED over 13 prerequisite checks, each with a concrete fix hint. `-Fix`
437
+ installs the modules, downloads the content-prep tool, fills the EN/DE + tool-path defaults and migrates an
438
+ old setup; `-Set @{...}` persists your values first; `-Json` / `-JsonPath` for other tooling. `.Missing`
439
+ lists only the four values a human has to supply (`paths.packageRoot`, `paths.outputRoot`, `author.person`,
440
+ `author.company`) — never a key the doctor could fill itself.
441
+ - **Config, secret and tools moved to a per-user config home** (`%LOCALAPPDATA%\psadt-deploy\`, override
442
+ `$env:PSADT_DEPLOY_HOME`) instead of the skill folder, so a `git pull`, re-clone or re-install no longer
443
+ takes the whole setup with it — and scripts started from an output folder still find their config.
444
+ `Get-PsadtConfig.ps1` is the single resolver and now returns `.Home` / `.DefaultHome` / `.LegacyInUse`. An
445
+ old config beside `scripts/` keeps working read-only until `-Fix` migrates it (originals renamed
446
+ `*.migrated`, nothing deleted).
447
+ - **`Set-PsadtConfig.ps1 -Remove`** deletes dotted keys, so switching credential type can clean up the
448
+ stale one. **Fixed:** `New-PsadtEntraApp.ps1` reported `<skill>\config.json` even when the config lived
449
+ elsewhere. Suite 120 → 128 tests.
450
+
451
+
452
+ <details>
453
+ <summary><strong>Earlier releases (0.18.1 and older)</strong></summary>
454
+
455
+ ### 0.18.1 - 03.09.2026
456
+ - **Upload: `-MaxRunTimeMinutes`.** `Invoke-IntuneWin32Upload.ps1` can now set
457
+ `installExperience.maxRunTimeInMinutes` (1–1440); `0` (default) omits the field and keeps the service default
458
+ of 60 min. Raise it for long-running installs (OS in-place upgrades, large suites) so the IME does not kill them.
459
+
460
+ ### 0.18.0 - 01.07.2026
461
+ - **HanseMerkur corporate design + editorial report redesign.** The dossier/report template is re-themed to the
462
+ HanseMerkur CD (green brand family on a light mint canvas; Metric font stack with Segoe fallback and no
463
+ web-font fetch → no CORS console errors on a local `file://` open) and relaid out as an "editorial
464
+ data-report": flat hairline sections, auto-numbered headings (`01…13`), an at-a-glance KPI band under the hero
465
+ (version · pre-flight · min OS · arch), and a wider 1600px layout. The detection script is folded behind a
466
+ collapsed `<details>` (the rule summary stays visible). German report text now uses real umlauts. Fixed the
467
+ sticky-header flicker (Chrome/Edge scroll-anchoring vs. the condensing hero → `overflow-anchor: none`,
468
+ Playwright-verified) and removed the redundant hero status pill.
469
+
470
+ ### 0.17.0 - 01.07.2026
471
+ - **install4j fingerprint + behavioral silent-switch verification.** Appendix L.1 now recognises install4j
472
+ (Java) installers (`com/install4j/runtime`, `exe4j`, `i4jparams.conf`, bundled `jre\`) and records that `/S`
473
+ is NOT its switch (it hangs on the language dialog) — the unattended switch is `-q`, run elevated. New BINDING
474
+ rule: a single string match is a hint, not proof; confirm the engine by its definitive fingerprint AND run the
475
+ silent switch once (timeout+kill, expect exit 0, no dialog) before packaging. Appendix L.3 adds the
476
+ trademark-sign gotcha (`Name(R)` breaks `-match 'Name'` → tolerant regex); Appendix B adds anti-patterns 13–15.
477
+ (Driven by an Aperio install4j installer misidentified as NSIS, where `/S` hung on the language dialog.)
478
+
479
+ ### 0.16.0 - 29.06.2026
480
+ - **Dossier auto-sync convention (BINDING)** + report header layout fix. Any change to the package scripts
481
+ (launcher, Extensions, detection, version/changelog, return codes, re-packaging) now requires regenerating
482
+ `Intune-Dossier.html` in the same pass; a stale dossier is a defect. The `.pill-lg` status badge caps at 230px
483
+ and wraps so a long status no longer overlaps the hero title.
484
+
485
+ ### 0.15.2 - 15.06.2026
486
+ - **Follow-up doc fix.** A contradiction sweep after 0.15.1 caught one more stale "Phase 7.5" in
487
+ `New-PsadtReport.ps1` help (upload is Phase 9); corrected. No other live stale references remain.
488
+
489
+ ### 0.15.1 - 15.06.2026
490
+ - **Generator hardening from a self-review (correctness + security).** All three generators now single-quote-escape
491
+ values embedded in `$adtSession` literals, so an apostrophe in the App name/vendor/author (e.g. "Bob's App",
492
+ "L'Oreal") no longer produces an unparseable package; the MSI `-AdditionalArgumentList` / `ProcessesToClose`
493
+ literals are escaped too (also closing a SYSTEM code-injection path). Detection exit-code drift fixed:
494
+ `New-MsiPackage.ps1` + the WinGet example now `exit 0` for "not installed" (a non-zero exit reads as a detection
495
+ error), and a new **pre-flight Detection check** WARNs on a non-zero exit in `Detect*.ps1`. The WSUS bypass in
496
+ `New-WindowsFeaturePackage.ps1` now saves all prior state before writing and runs inside the `try/finally`, so a
497
+ partial failure can't leave `UseWUServer=0` permanently. Added input guards (`$Name` path-traversal, `__TOKEN__`
498
+ leak), MSI `-Author` config fallback + `-InstallerPath` validation. Stale refs fixed (SKILL.md "A-M"->"A-P",
499
+ `New-PsadtEntraApp.ps1` "Phase 7.5"->"Phase 9").
500
+
501
+ ### 0.15.0 - 15.06.2026
502
+ - **Windows-feature packages (optional features + capabilities / FoD).** New `scripts/New-WindowsFeaturePackage.ps1`
503
+ — one-call generator that enables Windows **Optional Features** (`Enable-WindowsOptionalFeature`: NetFx3,
504
+ Hyper-V, WSL, TelnetClient, …) and **Capabilities / Features on Demand** (`Add-WindowsCapability`: RSAT.*,
505
+ OpenSSH, …) from one typed list, multiple per package. Uninstall reverts (disable/remove); Repair re-enables
506
+ (idempotent). Reboot surfaces **3010** via `$adtSession.SetExitCode(3010)` (`-NoRestart`); detection treats
507
+ `EnablePending` as not-yet-done. Content comes from a bundled `-Source` (offline SxS) else Windows Update
508
+ behind a **temporary** WSUS bypass (`RepairContentServerSource=2`, `UseWUServer=0`) whose exact prior state is
509
+ restored. Guide **Appendix P**, SKILL.md Gate-1/anti-patterns/ref-lookup. Pre-flight GREEN; helper logic
510
+ verified against an in-memory registry sim; ASCII-clean.
511
+
512
+ ### 0.14.0 - 15.06.2026
513
+ - **Browser-extension force-install packages (Edge / Chrome / Firefox).** New
514
+ `scripts/New-BrowserExtensionPackage.ps1` — one-call generator for force-installing browser extensions via
515
+ enterprise **policy registry keys** (policy-only, no installer, ESP-safe). Multiple extensions per package.
516
+ Chromium helper computes the **next free `ExtensionInstallForcelist` index** (never hard-codes `1`), dedupes by
517
+ ID and removes only its own entry (coexistence); Firefox merges into the single `ExtensionSettings` JSON written
518
+ as **`REG_MULTI_SZ`** (single-line `REG_SZ` is silently ignored — Mozilla bug 1750233). Guide **Appendix O**,
519
+ SKILL.md Gate-1/anti-patterns/ref-lookup. Pre-flight GREEN; helpers validated against a scratch registry hive;
520
+ ASCII-clean.
521
+
522
+ ### 0.13.1 - 15.06.2026
523
+ - **Firewall policy body fixed against the live template (verified 201).** `New-IntuneFirewallPolicy.ps1`
524
+ produced a body Graph rejected (400). Corrected via the **msgraph skill** (not guessed): the group id needs
525
+ the `{firewallrulename}` token, the program path is the direct child `..._app_filepath`, action values are
526
+ `_action_type_1`/`_0`, and a template-based policy requires `settingInstanceTemplateReference` per instance +
527
+ `settingValueTemplateReference` per simple/choice value (profiles collection: instance ref only — a per-value
528
+ ref is a duplicate). Confirmed by a live **201 Create**; tests assert the references. Mirrored into the
529
+ MxManagementCenter Output deliverable.
530
+
531
+ ### 0.13.0 - 15.06.2026
532
+ - **Self-contained firewall deliverable (copy-to-client safe).** `scripts/New-IntuneFirewallPolicy.ps1` is now
533
+ fully self-contained — no dot-sourcing of `_GraphCommon`/`_GraphInteractive`, no skill path; WAM sign-in +
534
+ policy body builder + console helpers are embedded. It runs on a test client that does **not** have the skill
535
+ installed (`-Interactive` WAM, or `-GraphToken`). Fixes the "Skill script not found … -SkillRoot" failure when
536
+ the deliverable was copied to another machine. New **binding SKILL.md convention "Self-contained deliverables"**
537
+ + a Pester test that enforces it (no dot-source / no skill path / embeds WAM). Test-first per writing-skills.
538
+
539
+ ### 0.12.0 - 15.06.2026
540
+ - **Interactive WAM sign-in for the Intune policy scripts.** `New-IntuneFirewallPolicy.ps1` and
541
+ `New-IntuneTrustedCertPolicy.ps1` gain `-Interactive` (+ `-TenantId`): delegated sign-in via **WAM**
542
+ (Windows Web Account Manager) when there is no app registration — **no device code**. The WAM machinery
543
+ (`Initialize-MsalBroker` / `Get-WamToken` / new `Get-InteractiveGraphToken`) was extracted from
544
+ `New-PsadtEntraApp.ps1` into a shared `scripts/_GraphInteractive.ps1` (one implementation, no copy-paste
545
+ drift); the bootstrap now consumes it. The MxManagementCenter firewall deliverable became a thin wrapper
546
+ over the generic script.
547
+
548
+ ### 0.11.0 - 15.06.2026
549
+ - **Intune firewall-rules policy + app config-management permission.** New `scripts/New-IntuneFirewallPolicy.ps1`
550
+ (Endpoint Security "Windows Firewall Rules" policy, one program-scoped rule; dry-run / `-Execute` / manual
551
+ portal fallback) + Pester test. Suppresses the first-run Windows Firewall prompt for apps that listen inbound.
552
+ New `New-PsadtEntraApp.ps1 -IncludeConfigurationManagement` consents `DeviceManagementConfiguration.ReadWrite.All`
553
+ (needed by the firewall and trusted-cert policies for `-Execute`).
554
+
555
+ ### 0.10.0 - 15.06.2026
556
+ - **Certificate store deployment (driver-trust / TrustedPublisher).** New `scripts/New-IntuneTrustedCertPolicy.ps1` —
557
+ a Custom OMA-URI profile that places a certificate into a Windows machine store via the
558
+ `RootCATrustedCertificates` CSP (the policy-based way to suppress the Windows "install device software?"
559
+ driver-trust prompt). Guide Appendix N, dossier "Treiber-Zertifikat" row, SKILL.md convention + Pester test.
560
+
561
+ ### 0.9.2 - 12.06.2026
562
+ - **Reconciled a diverged install copy back into the repo.** Fixed `Invoke-PsadtSystemTest.ps1` crashing at
563
+ param binding under the WinPS 5.1 re-exec (`$SkillRoot` default is now fail-safe, so the SYSTEM Install/
564
+ Uninstall gate works on a pwsh-7 host). Added per-field copy buttons + a `file://`-safe clipboard to the
565
+ HTML dossier (`Report-Template.html`, token set unchanged). Added `scripts/New-MsiPackage.ps1` (reusable
566
+ MSI package generator, `$PSScriptRoot`-relative, ASCII-only) + its Pester test.
567
+
568
+ ### 0.9.1 - 12.06.2026
569
+ - **Applicability/portability drift cleanup** (docs + instructions; no script logic changed). Removed the
570
+ phantom `test.maxIterations` / `test.endState` config keys from SKILL.md Phase 6 (the cap is a hard count of
571
+ 5 the orchestrator owns); fixed stale "Phase 7.5" -> **Phase 9** in SKILL.md + `app-registration.md`; fixed
572
+ the README project tree (report **Phase 8**, added the 3 missing scripts `Invoke-PsadtPreflight`,
573
+ `Invoke-IntuneAppAssignment`, `_GraphCommon`).
574
+ - **`superpowers` downgraded from hard `REQUIRED` to optional/preferred.** The Researcher/Reviewer roles now
575
+ prefer `superpowers:*` if installed and otherwise fall back to the native Agent tool / `/code-review`; the
576
+ Requirements list documents it as an optional (recommended) enhancement. The workflow no longer depends on it.
577
+
578
+ ### 0.9.0 - 11.06.2026
579
+ - **Audit cleanup** (quality / content / applicability+compatibility / tests). Highlights: shared
580
+ `scripts/_GraphCommon.ps1` (de-duplicates the 3 Graph scripts) + **28 new Pester tests** for the previously
581
+ untested upload/assignment/Entra-app/token scripts (suite now 74); fixed an `Invoke-WithRetry` precedence
582
+ bug (retried every error 6x) and PS7-fragile throttling reads; the HTML report no longer shows synthetic
583
+ "passed" rows when no pre-flight/SYSTEM-test results are supplied (neutral "not run") and derives the PSADT
584
+ version from the installed module; GUID `ValidatePattern` on the MSI codes.
585
+ - **Phase numbering unified** across SKILL.md + guide into one integer scheme **0-12**; every Phase/Appendix
586
+ cross-reference re-verified. Anti-pattern list trimmed; SYSTEM-test prerequisites, `/beta` drift caveat,
587
+ rollback step, logging convention, and config help added. Guide `$adtSession` template `1.0.0`->`0.1` +
588
+ author-from-config; intro appendix index -> A-M.
589
+
590
+ ### 0.8.1 - 11.06.2026
591
+ - **Docs consistency.** Fixed a stale cross-reference in SKILL.md (the guide range said **Appendix A-J** but
592
+ the guide now runs through **M** — K/L were added in 0.7.0 and M in 0.8.0 without updating it). Restored the
593
+ **0.5.3** entry that was missing from this README changelog mirror (it was present in `CHANGELOG.md`).
594
+
595
+ ### 0.8.0 - 11.06.2026
596
+ - **Opt-in Entra group assignment, wired end-to-end.** New Phase 10 + `Invoke-IntuneAppAssignment.ps1`:
597
+ create/reuse Entra security groups by a configured naming scheme (`intune.groups`) and assign the uploaded
598
+ app (Required / Available / Uninstall). Read-only dry-run → confirm → execute; idempotent; never deletes a
599
+ group or another app's assignment; ambiguous/duplicate names skipped. Least-privilege roles
600
+ (`Group.Create` + `GroupMember.Read.All`) via `New-PsadtEntraApp.ps1 -IncludeGroupManagement`. Full
601
+ reference in **guide Appendix M** (config schema, naming tokens, version-independent default vs `%version%`
602
+ opt-in, permission model).
603
+ - **Upload min-OS fix.** `-MinWindowsRelease` is now a `ValidateSet` of backend-accepted release IDs
604
+ (`1607..2004`) — `21H2`/`22H2` are server-rejected and used to kill the upload mid-flight with a Graph
605
+ `BadRequest`. Fails fast at param binding instead; set a higher minimum in the portal. Guide **H.11**.
606
+
607
+ ### 0.7.5 - 10.06.2026
608
+ - **Honest exit codes + detection (correctness fix).** Removed the dangerous "always `exit 0`" guidance from
609
+ guide Appendix K — a blanket `exit 0` (or a detection tag written in a `finally`) reports GREEN on failure.
610
+ New **K.7**: the exit code reflects whether the fix could RUN (couldn't-run -> non-zero; the 64-bit relaunch
611
+ propagates the child's exit code), detection reflects the real END-STATE (tag only on success), with a
612
+ per-package decision table; "never block enrollment" is now an explicit ESP-assignment + return-code-mapping
613
+ choice, not a masked exit code. SKILL.md anti-pattern added.
614
+
615
+ ### 0.7.0 - 10.06.2026
616
+ - **Value-adding extensions.** New `scripts/Invoke-PsadtPreflight.ps1` turns the Phase-5 Reviewer gate into one
617
+ deterministic `GREEN/RED` tool (encoding/parse/v3-scan/top-level/structure/GUID-to-`-FilePath`), with a Pester
618
+ suite. New guide **Appendix K** (script-only remediation / fix packages, ESP-safe — the debloat/Cisco pattern)
619
+ and **Appendix L** (installer technologies + silent switches, a lookup consulted before web research). Expanded
620
+ error-code catalogue (MSI 1603/1605/1619/1638/1639…, PSADT 60001/60008 + ranges) in guide Appendix A and the
621
+ SKILL.md troubleshooting table. SKILL.md rewired to point at the pre-flight script and the new appendices.
622
+
623
+ ### 0.6.2 - 10.06.2026
624
+ - **Audit & harden.** Agent-based audit + source-level verification (discarded ~8 false positives). Fixes: the
625
+ guide's broken `New-ADTTemplate` "Extended scaffold" (passing app metadata params that v4.1.x rejects);
626
+ the Graph uploader leaving its extracted work dir (with the AES keys in `Detection.xml`) in `%TEMP%` (now
627
+ `try/finally` cleanup); the report `Notes` default `&middot;` double-escape; the fallback initials-SVG logo
628
+ now XML-escapes + base64-encodes (no markup injection, with a regression test). Robustness: `Invoke-Graph`
629
+ 429/5xx retry with `Retry-After`; malformed-`config.json` safety in four scripts; WinGet 2-byte header read;
630
+ symmetric temp cleanup in self-update. Graph request shapes left untouched.
631
+
632
+ ### 0.6.1 - 10.06.2026
633
+ - **Report header flicker fixed** (`references/Report-Template.html`): at widths where the content height met
634
+ the viewport edge, the vertical scrollbar toggled on/off and the header's `vw`-based `clamp()` padding and
635
+ `h1` font-size reflowed on every toggle — a wild flicker loop. Reserving the scrollbar gutter
636
+ (`html { overflow-y: scroll; scrollbar-gutter: stable; }`) keeps the width constant and breaks the loop.
637
+
638
+ ### 0.6.0 - 10.06.2026
639
+ - **SKILL.md slimmed to a control plane** (733 → 244 lines, ~67% fewer tokens) via progressive disclosure:
640
+ the long inline code moved into the reference guide (new **Appendix I** WinGet + **Appendix J** app-logo),
641
+ loaded on demand. Intake restructured into **4 decision gates** (researchable facts become stated
642
+ assumptions, not questions); explicit **sub-agent roles** (Researcher×3 / Builder / Reviewer) with GREEN
643
+ handoff gates; a single **blockade protocol** for errors. No binding rule or behaviour dropped.
644
+
645
+ ### 0.5.3 - 09.06.2026
646
+ - **Guide code-fences are now English/ASCII.** Anglicized every German comment, string literal and placeholder
647
+ living **inside** PowerShell/text code fences in the guide (and the inline-code placeholders in the Appendix
648
+ F.1 table) — snippets get copied verbatim into deployment scripts, where the binding rule is English + 7-bit
649
+ ASCII. German explanatory **prose** and the **F.2 Company-Portal dossier template** deliberately stay German
650
+ (legitimate `language.dossier` end-user text). No script/tooling code changed.
651
+
652
+ ### 0.5.2 - 08.06.2026
653
+ - **HTML package report is now always generated** (upload or not) by `scripts/New-PsadtReport.ps1` from the
654
+ fixed template `references/Report-Template.html`. One self-contained, **bilingual (DE/EN toggle)** document
655
+ combining the Intune dossier + a technical package report; Fluent-2 styled, sticky shrink header, logo
656
+ embedded as a data URI, description preview rendered from its Markdown source. New Pester test
657
+ `tests/New-PsadtReport.Tests.ps1`.
658
+
659
+ ### 0.5.1 - 06.06.2026
660
+ - Self-update now decides by **commit** (git `HEAD` vs `origin/main`, or the GitHub commits-API sha vs a
661
+ recorded `tooling.skillCommit`) instead of the CHANGELOG version — no more CDN lag / circular version reads.
662
+ - README project-structure tree compacted so it renders without horizontal scroll.
663
+
664
+ ### 0.5.0 - 06.06.2026
665
+ - **Skill self-update** — `scripts/Update-PsadtSkill.ps1` checks GitHub for a newer version, shows what's new,
666
+ and updates in place on confirmation (`git pull` for a clone, else branch-zip overwrite of tracked files
667
+ only; `config.json` / `secret.dpapi` / `tools/` preserved). Triggers: *"update skill"*, *"/update-skill"*,
668
+ *"psadt update"*.
669
+
670
+ ### 0.4.0 - 06.06.2026
671
+ - **WinGet packaging support** (strictly opt-in, never the default) + **certificate-based auth** for the Phase 9
672
+ upload (no secret at rest) + MSI icon-table logo fallback + device-code first-poll fix. Contributed by
673
+ **@joakim-i** (PR #4), reviewed and hardened before merge. See [CHANGELOG.md](CHANGELOG.md).
674
+
675
+ ### 0.3.2 - 06.06.2026
676
+ - **Test-before-upload is now a binding gate:** Install + Uninstall must pass the Phase 6 SYSTEM test before
677
+ any Phase 9 upload. If it can't be run (no elevation / VM), stop before upload and hand back the command.
678
+
679
+ ### 0.3.1 - 06.06.2026
680
+ - `Invoke-IntuneWin32Upload.ps1` gains **`-DetectionScriptPath`** (PowerShell-script detection rule) for
681
+ EXE / non-MSI installers without a ProductCode (e.g. Vivaldi). Verified live by uploading Vivaldi 8.0.4033.44.
682
+ - Lesson: a *detection* script rule accepts only `ruleType,enforceSignatureCheck,runAs32Bit,scriptContent`
683
+ (guide Appendix H.2).
684
+
685
+ ### 0.3.0 - 06.06.2026
686
+ - **Direct upload** (`scripts/Invoke-IntuneWin32Upload.ps1`, Phase 9): self-contained raw-Graph
687
+ `win32LobApp` upload (parse `.intunewin` → token → probe → idempotency → create/update → content → SAS
688
+ block-blob upload via HttpClient → commit → activate → categories → supersedence). Read-only dry-run by
689
+ default; `-Execute` to write.
690
+ - **WAM Entra-app bootstrap** (`scripts/New-PsadtEntraApp.ps1`): interactive Windows-broker sign-in (device
691
+ code fallback), creates the app + admin consent + secret, DPAPI-stored.
692
+ - **App-only token helper** (`scripts/Get-GraphToken.ps1`).
693
+ - **Coexistence-safe versioning:** never deletes an older version; new versions coexist; optional
694
+ supersedence wiring. **Logo guard:** refuses the PSADT default `AppIcon.png`. Fills the full
695
+ App-information tab; never auto-assigns category/notes/groups.
696
+ - Fixed the Repair `-FilePath`→`-ProductCode` example; reference guide gains **Appendix H**.
697
+
698
+ ### 0.2.0 - 05.06.2026
699
+ - **Automated SYSTEM test loop** (`scripts/Invoke-PsadtSystemTest.ps1`, Phase 6): install → uninstall →
700
+ reinstall the package as the SYSTEM account via `Invoke-CommandAs`, with agent-driven auto-fix until
701
+ green or a max-iteration cap. Opt-in; elevated session required.
702
+ - Phase 8 now prefers `Invoke-CommandAs -AsSystem` for SYSTEM-context testing (PsExec kept as a fallback).
703
+
704
+ ### 0.1.0 - 04.06.2026
705
+ - Initial release: guided PSADT v4 → Intune Win32 lifecycle (intake, autonomous research, scaffolding, all
706
+ three deployment types, pre-flight checks, packaging, dossier + logo, guided testing, troubleshooting).
707
+ - First-run setup writing a machine-local `config.json` (paths, language, author).
708
+ - Self-healing prerequisites: PSAppDeployToolkit module (PSGallery) and `IntuneWinAppUtil.exe`
709
+ (auto-download + version check).
710
+ - HTML dossier document with a Markdown app-description block (the Intune description field is
711
+ Markdown-only).
712
+ - English skill + reference guide; MIT licensed.
713
+
714
+ </details>
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * psadt-deploy-skill - installer for the psadt-deploy Claude Code skill.
4
+ *
5
+ * npx psadt-deploy-skill install/update in ~/.claude/skills/psadt-deploy
6
+ * npx psadt-deploy-skill --project install into ./.claude/skills/psadt-deploy instead
7
+ * npx psadt-deploy-skill --dir <path> install into an explicit folder
8
+ * npx psadt-deploy-skill --ref <ref> a branch or tag instead of main
9
+ * npx psadt-deploy-skill --no-setup skip the setup doctor
10
+ *
11
+ * Design notes that matter:
12
+ *
13
+ * - Zero dependencies. Node 18 has global fetch, Windows ships bsdtar as tar.exe, and an installer that
14
+ * pulls in a dependency tree is an installer that can break for reasons unrelated to the skill.
15
+ * - The package ships ONLY bin/. The skill itself comes from GitHub at install time, so a new skill
16
+ * version needs no npm republish - only a change to this file does.
17
+ * - This script never writes the skill config. Resolving the config home is Get-PsadtConfig's job
18
+ * (explicit -SkillRoot > $env:PSADT_DEPLOY_HOME > %LOCALAPPDATA%\psadt-deploy), and a second
19
+ * implementation in JavaScript would drift from it. So it spawns Set-PsadtConfig.ps1 and
20
+ * Initialize-PsadtSkill.ps1 and lets them decide.
21
+ */
22
+
23
+ import { spawnSync } from 'node:child_process';
24
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
25
+ import { homedir, tmpdir } from 'node:os';
26
+ import { join, resolve } from 'node:path';
27
+
28
+ const REPO = 'pt1987/claude-code-psadt-skill';
29
+ const SKILL_FOLDER = 'psadt-deploy';
30
+
31
+ // --- arguments ---------------------------------------------------------------------------------
32
+ const argv = process.argv.slice(2);
33
+ function flagValue(name) {
34
+ const i = argv.indexOf(name);
35
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null;
36
+ }
37
+ const wantHelp = argv.includes('--help') || argv.includes('-h');
38
+ const ref = flagValue('--ref') || 'main';
39
+ const noSetup = argv.includes('--no-setup');
40
+ const explicitDir = flagValue('--dir');
41
+ const projectScope = argv.includes('--project');
42
+
43
+ if (wantHelp) {
44
+ console.log(`psadt-deploy-skill - install the psadt-deploy Claude Code skill
45
+
46
+ npx psadt-deploy-skill [--dir <path>] [--project] [--ref <branch|tag>] [--no-setup]
47
+
48
+ --dir <path> install into this folder
49
+ --project install into ./.claude/skills/${SKILL_FOLDER} (default is the user's ~/.claude/skills)
50
+ --ref <ref> branch or tag to install (default: main)
51
+ --no-setup do not run the setup doctor afterwards
52
+ `);
53
+ process.exit(0);
54
+ }
55
+
56
+ const target = explicitDir
57
+ ? resolve(explicitDir)
58
+ : projectScope
59
+ ? resolve(join(process.cwd(), '.claude', 'skills', SKILL_FOLDER))
60
+ : resolve(join(homedir(), '.claude', 'skills', SKILL_FOLDER));
61
+
62
+ // --- small helpers -----------------------------------------------------------------------------
63
+ function run(exe, args, opts = {}) {
64
+ return spawnSync(exe, args, { stdio: 'inherit', shell: false, ...opts });
65
+ }
66
+ function runCapture(exe, args) {
67
+ return spawnSync(exe, args, { encoding: 'utf8', shell: false });
68
+ }
69
+ function has(exe) {
70
+ const r = runCapture(exe, ['--version']);
71
+ return !r.error && r.status === 0;
72
+ }
73
+ function fail(msg) {
74
+ console.error(`\n ERROR ${msg}\n`);
75
+ process.exit(1);
76
+ }
77
+
78
+ if (process.platform !== 'win32') {
79
+ fail('This skill builds Windows Intune packages and needs Windows (PowerShell, IntuneWinAppUtil, PSADT).');
80
+ }
81
+
82
+ console.log(`\npsadt-deploy-skill\n target : ${target}\n ref : ${ref}`);
83
+
84
+ // --- 1. get the skill --------------------------------------------------------------------------
85
+ // Order of preference: update an existing clone, else clone, else tarball. The tarball route exists
86
+ // because plenty of managed machines have no git at all.
87
+ const gitAvailable = has('git');
88
+ mkdirSync(target, { recursive: true });
89
+
90
+ if (existsSync(join(target, '.git'))) {
91
+ if (!gitAvailable) fail(`${target} is a git clone but git is not available to update it.`);
92
+ console.log('\n updating the existing clone (git pull --ff-only)');
93
+ const r = run('git', ['-C', target, 'pull', '--ff-only', 'origin', ref]);
94
+ if (r.status !== 0) {
95
+ fail('git pull --ff-only failed. Local changes in the skill folder? Commit or discard them, or install into a fresh folder with --dir.');
96
+ }
97
+ } else if (gitAvailable) {
98
+ console.log('\n cloning (git clone --depth 1)');
99
+ // Clone into a temp folder and move the contents, because git refuses a non-empty target.
100
+ const stage = join(tmpdir(), `psadt-skill-${Date.now()}`);
101
+ const r = run('git', ['clone', '--depth', '1', '--branch', ref, `https://github.com/${REPO}.git`, stage]);
102
+ if (r.status !== 0) {
103
+ fail(
104
+ `git clone failed. If ${REPO} is private you need access to it: sign in with the GitHub CLI (gh auth login), ` +
105
+ 'configure a credential helper, or clone it manually and point this installer at the folder with --dir.'
106
+ );
107
+ }
108
+ // robocopy is on every Windows box and handles the merge without a shell. 0-7 are success codes.
109
+ const c = runCapture('robocopy', [stage, target, '/E', '/NFL', '/NDL', '/NJH', '/NJS', '/NP']);
110
+ rmSync(stage, { recursive: true, force: true });
111
+ if ((c.status ?? 8) > 7) fail('Copying the clone into the target folder failed.');
112
+ } else {
113
+ console.log('\n no git found - downloading the tarball');
114
+ const url = `https://codeload.github.com/${REPO}/tar.gz/refs/heads/${ref}`;
115
+ const tgz = join(tmpdir(), `psadt-skill-${Date.now()}.tar.gz`);
116
+ try {
117
+ const res = await fetch(url);
118
+ if (res.status === 404) {
119
+ // GitHub answers 404 - not 403 - for a private repository you cannot see. Without git there is no
120
+ // way to authenticate here, so say what is actually wrong instead of "download failed".
121
+ fail(
122
+ `${REPO} is not publicly readable (HTTP 404), and without git this installer cannot authenticate. ` +
123
+ 'Either install git and sign in to GitHub, or clone the repository manually and re-run with ' +
124
+ '--dir <folder>.'
125
+ );
126
+ }
127
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
128
+ writeFileSync(tgz, Buffer.from(await res.arrayBuffer()));
129
+ } catch (e) {
130
+ fail(`Download failed: ${e.message}`);
131
+ }
132
+ // The archive's top-level directory is <repo>-<ref>/, so strip exactly one component.
133
+ const r = run('tar', ['-xzf', tgz, '-C', target, '--strip-components=1']);
134
+ rmSync(tgz, { force: true });
135
+ if (r.status !== 0) fail('Extracting the tarball failed (tar.exe is expected in C:\\Windows\\System32).');
136
+ }
137
+
138
+ if (!existsSync(join(target, 'SKILL.md'))) {
139
+ fail(`${target} has no SKILL.md - the download did not produce a skill folder.`);
140
+ }
141
+ console.log(' OK skill files in place');
142
+
143
+ // --- 2. PowerShell ------------------------------------------------------------------------------
144
+ // pwsh first; powershell.exe as the fallback. -ExecutionPolicy Bypass on BOTH: 5.1 defaults to
145
+ // Restricted, and a GPO can pin pwsh to AllSigned - either way an unsigned script would not run.
146
+ const psExe = has('pwsh') ? 'pwsh' : 'powershell';
147
+ const psBase = ['-NoProfile', '-ExecutionPolicy', 'Bypass'];
148
+
149
+ // --- 3. record the commit (via the script that owns the config, not from here) -------------------
150
+ try {
151
+ const res = await fetch(`https://api.github.com/repos/${REPO}/commits/${ref}`, {
152
+ headers: { 'User-Agent': 'psadt-deploy-skill', Accept: 'application/vnd.github+json' },
153
+ });
154
+ if (res.ok) {
155
+ const sha = (await res.json()).sha;
156
+ if (sha) {
157
+ const setCfg = join(target, 'scripts', 'Set-PsadtConfig.ps1');
158
+ // -Command, not -File: -File passes every argument as a string and -Updates wants a hashtable.
159
+ run(psExe, [...psBase, '-Command', `& '${setCfg}' -Updates @{'tooling.skillCommit'='${sha}'}`]);
160
+ console.log(` OK recorded commit ${sha.slice(0, 7)}`);
161
+ }
162
+ } else if (res.status === 404) {
163
+ // 404 on a repo you just cloned = the repo is private and this call is unauthenticated. Worth saying
164
+ // once: the skill works, only the sha-based update check falls back to comparing versions.
165
+ console.log(` -- ${REPO} is not publicly readable, so the commit sha was not recorded.`);
166
+ console.log(' The skill works; "update skill" compares CHANGELOG versions instead of shas.');
167
+ } else {
168
+ console.log(` -- could not read the commit sha (HTTP ${res.status}) - not fatal`);
169
+ }
170
+ } catch {
171
+ // A missing commit sha only means the next update check compares versions instead of shas.
172
+ console.log(' -- could not record the commit sha (offline?) - not fatal');
173
+ }
174
+
175
+ // --- 4. the setup doctor ------------------------------------------------------------------------
176
+ if (noSetup) {
177
+ console.log('\n skipped the setup doctor (--no-setup). Run it later:');
178
+ console.log(` ${psExe} -File "${join(target, 'scripts', 'Initialize-PsadtSkill.ps1')}" -Fix\n`);
179
+ process.exit(0);
180
+ }
181
+
182
+ const doctor = join(target, 'scripts', 'Initialize-PsadtSkill.ps1');
183
+ const jsonPath = join(tmpdir(), `psadt-doctor-${Date.now()}.json`);
184
+ console.log('\n running the setup doctor (-Fix)\n');
185
+ // -JsonPath rather than -Json: stdout is inherited so the user sees the doctor's own table live, and the
186
+ // machine-readable copy comes back through the file.
187
+ // -Command with Out-Null rather than -File: the doctor also RETURNS its result object, and with inherited
188
+ // stdout that object gets dumped underneath its own table. Out-Null drops the object; Write-Host output
189
+ // (the table) is unaffected.
190
+ const d = run(psExe, [...psBase, '-Command', `& '${doctor}' -Fix -JsonPath '${jsonPath}' | Out-Null`]);
191
+
192
+ let verdict = null;
193
+ try {
194
+ if (existsSync(jsonPath)) verdict = JSON.parse(readFileSync(jsonPath, 'utf8'));
195
+ } catch {
196
+ /* the doctor printed its table anyway */
197
+ }
198
+ rmSync(jsonPath, { force: true });
199
+
200
+ if (verdict) {
201
+ console.log(`\n setup: ${verdict.Overall}`);
202
+ const missing = Array.isArray(verdict.Missing) ? verdict.Missing : [];
203
+ if (missing.length) {
204
+ console.log(`\n ${missing.length} value(s) still need you: ${missing.join(', ')}`);
205
+ console.log(" Open Claude Code in a package folder and say: \"psadt setup\"");
206
+ } else if (verdict.Overall === 'GREEN') {
207
+ console.log(' Ready. Open Claude Code and say what you want packaged.');
208
+ }
209
+ } else if (d.status !== 0) {
210
+ console.log('\n The setup doctor did not complete. Run it manually:');
211
+ console.log(` ${psExe} -File "${doctor}" -Fix`);
212
+ }
213
+
214
+ console.log('');
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "psadt-deploy-skill",
3
+ "version": "0.23.0",
4
+ "description": "Installer for the psadt-deploy Claude Code skill: build, test and deploy PSADT v4.x Intune Win32 packages.",
5
+ "keywords": [
6
+ "psadt",
7
+ "psappdeploytoolkit",
8
+ "intune",
9
+ "win32",
10
+ "claude-code",
11
+ "claude-skill",
12
+ "packaging"
13
+ ],
14
+ "homepage": "https://github.com/pt1987/claude-code-psadt-skill#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/pt1987/claude-code-psadt-skill/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/pt1987/claude-code-psadt-skill.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Patrick Taubert, PHAT Consulting GmbH",
24
+ "type": "module",
25
+ "bin": {
26
+ "psadt-deploy-skill": "bin/install.mjs"
27
+ },
28
+ "files": [
29
+ "bin"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "os": [
35
+ "win32"
36
+ ]
37
+ }