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 +21 -0
- package/README.md +714 -0
- package/bin/install.mjs +214 -0
- package/package.json +37 -0
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 `·` 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>
|
package/bin/install.mjs
ADDED
|
@@ -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
|
+
}
|