diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 27fb0ad..45e5843 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,36 +5,43 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
+ # The release workflow calls this, so a tag is only ever published off a green run.
+ workflow_call:
jobs:
test:
strategy:
fail-fast: false
matrix:
- os: [ windows-latest, ubuntu-latest ]
+ include:
+ - { os: windows-latest, shell: pwsh } # PowerShell 7
+ - { os: windows-latest, shell: powershell } # Windows PowerShell 5.1 - the manifest's floor
+ - { os: ubuntu-latest, shell: pwsh }
runs-on: ${{ matrix.os }}
+ defaults:
+ run:
+ shell: ${{ matrix.shell }}
steps:
- uses: actions/checkout@v4
+ # -SkipPublisherCheck: Windows ships Pester 3.4 signed with a different
+ # certificate, and without it the 5.x install refuses to replace it.
- name: Install PowerShell modules
- shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
- Install-Module Pester -MinimumVersion 5.0.0 -Force -Scope CurrentUser
+ Install-Module Pester -MinimumVersion 5.0.0 -Force -Scope CurrentUser -SkipPublisherCheck
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
- name: PSScriptAnalyzer
- shell: pwsh
run: ./build.ps1 -Task Analyze
- name: Pester (fixtures only - no tenant needed)
- shell: pwsh
run: ./build.ps1 -Task Test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
- name: test-results-${{ matrix.os }}
+ name: test-results-${{ matrix.os }}-${{ matrix.shell }}
path: testResults.xml
if-no-files-found: ignore
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..54d61a8
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,44 @@
+name: Release
+
+# Pushing a v* tag publishes that version to the PowerShell Gallery - but only after
+# the full CI matrix passes again on the tagged commit, and only if the tag matches the
+# manifest. A Gallery version can be unlisted but its number can never be reused, so
+# everything that can be checked is checked before the one step that cannot be undone.
+
+on:
+ push:
+ tags: [ 'v*' ]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: release
+ cancel-in-progress: false
+
+jobs:
+ ci:
+ uses: ./.github/workflows/ci.yml
+
+ publish:
+ needs: ci
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # The tag reaches the script through env, never interpolated into it.
+ - name: Check the tag matches ModuleVersion
+ shell: pwsh
+ env:
+ TAG: ${{ github.ref_name }}
+ run: |
+ $manifest = Import-PowerShellDataFile ./src/IntuneGraphKit/IntuneGraphKit.psd1
+ if ($env:TAG -ne "v$($manifest.ModuleVersion)") {
+ throw "Tag '$env:TAG' does not match ModuleVersion $($manifest.ModuleVersion). Nothing was published."
+ }
+
+ - name: Publish to the PowerShell Gallery
+ shell: pwsh
+ env:
+ PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
+ run: ./build.ps1 -Task Publish
diff --git a/.gitignore b/.gitignore
index a4789e3..eca4b3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,11 +13,11 @@ site/.astro/
out/
IntuneGraph-*/
*.html
-!src/IntuneGraph/Assets/template.html
+!src/IntuneGraphKit/Assets/template.html
!site/public/**/*.html
graph.json
!tests/Fixtures/**/*.json
-!src/IntuneGraph/DemoData/**/*.json
+!src/IntuneGraphKit/DemoData/**/*.json
# Test output
TestResults/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f01490..e7529db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@ All notable changes to IntuneGraph are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/); versions follow
[SemVer](https://semver.org/).
+## [Unreleased]
+
+### Changed
+- **The module is now `IntuneGraphKit`**: `Install-Module IntuneGraphKit`,
+ `Import-Module IntuneGraphKit`. The name `IntuneGraph` on the PowerShell Gallery
+ belongs to an unrelated module by another author, and a Gallery package name has to
+ match the module name. Cmdlet names, the `graph.json` format, the MCP server name and
+ the project name are unchanged. If you import from a clone, the path is now
+ `src/IntuneGraphKit/IntuneGraphKit.psd1`.
+- The README no longer tells visitors to run `Install-Module IntuneGraph`, which
+ installed that unrelated module instead of this one.
+- The manifest `Description` is now the same text as the repository description, and
+ the module has a freshly generated `GUID` ahead of its first Gallery publish.
+
+### Added
+- Release pipeline: pushing a `v*` tag re-runs the full CI matrix and, when it is green
+ and the tag matches `ModuleVersion`, publishes to the PowerShell Gallery
+ (`.github/workflows/release.yml`, `build.ps1 -Task Publish`).
+- Gallery listing metadata: `CompatiblePSEditions` plus the `PSEdition_Desktop` and
+ `PSEdition_Core` tags, so the module appears under both edition filters, and an icon.
+
## [0.2.0] - 2026-09-06
### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 20b4849..2505ae2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,7 +23,7 @@ live tenant. Nothing in the test suite loads it.
## Repository layout
```
-src/IntuneGraph/
+src/IntuneGraphKit/
├── Public/ one file per exported cmdlet — this is the supported surface
├── Private/ internals: Fetch, Normalize, GraphModel, Query, Emit, Mcp, Util
├── DemoData/ the Contoso tenant shipped to users via -DemoData
@@ -50,15 +50,17 @@ them aware of where the data came from, it belongs somewhere else.
.\build.ps1 -Task Test # Pester only
.\build.ps1 -Task Analyze # PSScriptAnalyzer only
.\build.ps1 -Task Fixtures # regenerate the Contoso dataset
+.\build.ps1 -Task Publish # release workflow only — see Releasing
```
-CI runs `Analyze` then `Test` on `windows-latest` and `ubuntu-latest` for every
-push and pull request, so run both locally before you open one.
+CI runs `Analyze` then `Test` for every push and pull request on Windows
+PowerShell 5.1 and PowerShell 7 on `windows-latest`, and PowerShell 7 on
+`ubuntu-latest`, so run both locally before you open one.
### PSScriptAnalyzer
Settings live in [`PSScriptAnalyzerSettings.psd1`](PSScriptAnalyzerSettings.psd1)
-and apply to `src/IntuneGraph` recursively. Severity is `Error` and `Warning`;
+and apply to `src/IntuneGraphKit` recursively. Severity is `Error` and `Warning`;
**errors fail the build, warnings do not.** Three rules are excluded, each with
its reason written next to it — `PSAvoidUsingWriteHost` because the report
cmdlets write formatted output to the host on purpose,
@@ -78,7 +80,7 @@ demo data cannot drift apart. To add a scenario:
1. Edit the identity tables and workload definitions at the top of
[`tests/New-ContosoFixtures.ps1`](tests/New-ContosoFixtures.ps1).
2. Run `.\build.ps1 -Task Fixtures`. It rewrites both `tests/Fixtures/contoso`
- and `src/IntuneGraph/DemoData`.
+ and `src/IntuneGraphKit/DemoData`.
3. Update the expected counts in `tests/IntuneGraph.Tests.ps1` — they are
asserted, and a scenario nobody counted is a scenario nobody tested.
4. Commit the regenerated JSON along with your change.
@@ -125,6 +127,27 @@ A pull request wants:
versions follow [SemVer](https://semver.org/),
- documentation updated in the same pull request when behaviour changes.
+## Releasing
+
+Releases go out from CI, never from a workstation. The module is published to the
+PowerShell Gallery as **IntuneGraphKit** — `IntuneGraph` on the Gallery is an
+unrelated module by another author, which is why the two names differ (see
+[docs/discoverability.md](docs/discoverability.md#the-gallery-name-intunegraphkit)).
+
+1. Move the *Unreleased* entries in `CHANGELOG.md` under the new version and date.
+2. Set `ModuleVersion` in `src/IntuneGraphKit/IntuneGraphKit.psd1` to that version.
+3. Merge to `main`, then tag the merge commit and push the tag —
+ `git tag v0.3.0`, then `git push origin v0.3.0`.
+
+The Release workflow re-runs the whole CI matrix on the tagged commit, checks that the
+tag matches `ModuleVersion`, and only then publishes. A Gallery version can be unlisted
+but its number can never be reused, so that check is the last point at which a mistake
+costs nothing.
+
+The API key exists only as the `PSGALLERY_API_KEY` repository secret.
+`build.ps1 -Task Publish` reads it from the environment and refuses to run without
+it; there is deliberately no parameter for it.
+
## Where to start
Issues labelled
diff --git a/README.md b/README.md
index 61ecff0..6e8eb90 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
**Turn your Microsoft Intune tenant into an interactive relationship graph.** See what actually applies to a device and *why*, preview the blast radius before you touch a group, and find the orphaned junk, all from a local, read-only snapshot.
+[](https://www.powershellgallery.com/packages/IntuneGraphKit)
[](#requirements)
[](LICENSE)
[](docs/permissions.md)
@@ -19,11 +20,13 @@ https://juandresrodca.github.io/IntuneGraph/demo/
```powershell
-Install-Module IntuneGraph -Scope CurrentUser # (coming to PSGallery)
-Import-Module IntuneGraph
+Install-Module IntuneGraphKit -Scope CurrentUser
+Import-Module IntuneGraphKit
Export-IntuneGraph -DemoData -PassThru | Show-IntuneGraph -Open
```
+> The Gallery package is **IntuneGraphKit**. The `IntuneGraph` package on the Gallery is an unrelated module by another author, and unlike this one it writes to Intune — check the name before you install.
+
That builds the bundled **Contoso** demo tenant and opens the interactive graph in your browser — zero Graph auth, zero setup. The whole tool works offline against demo data, which means you can evaluate it (and contribute to it) without ever touching a real tenant.

diff --git a/SECURITY.md b/SECURITY.md
index 78daec3..a3e0b16 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -14,7 +14,7 @@ If private reporting is unavailable to you, email **juandresrodca@gmail.com** wi
Please include:
-- the module version (`(Get-Module IntuneGraph).Version`) and PowerShell version
+- the module version (`(Get-Module IntuneGraphKit).Version`) and PowerShell version
(`$PSVersionTable.PSVersion`),
- what you did, what happened, and what you expected,
- a minimal reproduction — **against `-DemoData` wherever possible**, never against a
@@ -93,7 +93,7 @@ export the way you would treat any tenant extract:
To reproduce almost anything, `-DemoData` is enough:
```powershell
-Import-Module IntuneGraph
+Import-Module IntuneGraphKit
Export-IntuneGraph -DemoData -PassThru | Show-IntuneGraph
```
diff --git a/build.ps1 b/build.ps1
index de70c79..c6fb466 100644
--- a/build.ps1
+++ b/build.ps1
@@ -5,16 +5,17 @@
.\build.ps1 -Task Test
.\build.ps1 -Task Analyze
.\build.ps1 -Task Fixtures
+ .\build.ps1 -Task Publish # release workflow only; reads $env:PSGALLERY_API_KEY
#>
[CmdletBinding()]
param(
- [ValidateSet('Test', 'Analyze', 'Fixtures', 'All')]
+ [ValidateSet('Test', 'Analyze', 'Fixtures', 'Publish', 'All')]
[string]$Task = 'All'
)
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
-$moduleDir = Join-Path $root 'src\IntuneGraph'
+$moduleDir = Join-Path $root 'src\IntuneGraphKit'
function Invoke-Fixtures {
Write-Host '== Regenerating Contoso fixtures ==' -ForegroundColor Cyan
@@ -48,9 +49,26 @@ function Invoke-Test {
Invoke-Pester -Configuration $conf
}
+function Invoke-Publish {
+ Write-Host '== Publish to the PowerShell Gallery ==' -ForegroundColor Cyan
+ # The key is read from the environment only - there is deliberately no parameter
+ # for it - so it never lands in a script, a default value or shell history. The
+ # release workflow sets it from the PSGALLERY_API_KEY repository secret.
+ if ([string]::IsNullOrWhiteSpace($env:PSGALLERY_API_KEY)) {
+ throw 'PSGALLERY_API_KEY is not set. Releases publish from .github/workflows/release.yml - see CONTRIBUTING.md.'
+ }
+ if (-not (Get-Command Publish-PSResource -ErrorAction SilentlyContinue)) {
+ throw 'Publish-PSResource not found. It ships with PowerShell 7.4+ (Microsoft.PowerShell.PSResourceGet).'
+ }
+ $manifest = Test-ModuleManifest -Path (Join-Path $moduleDir 'IntuneGraphKit.psd1')
+ Write-Host "Publishing $($manifest.Name) $($manifest.Version)"
+ Publish-PSResource -Path $moduleDir -Repository PSGallery -ApiKey $env:PSGALLERY_API_KEY
+}
+
switch ($Task) {
'Fixtures' { Invoke-Fixtures }
'Analyze' { Invoke-Analyze }
'Test' { Invoke-Test }
+ 'Publish' { Invoke-Publish }
'All' { Invoke-Analyze; Invoke-Test }
}
diff --git a/docs/discoverability.md b/docs/discoverability.md
index dec4a78..846a341 100644
--- a/docs/discoverability.md
+++ b/docs/discoverability.md
@@ -14,7 +14,7 @@ maintenance task — should change this file in the same commit, with the reason
|---|---|---|
| **GitHub search and topic pages** | Someone types `intune assignments` into GitHub, or lands on `github.com/topics/intune`. The topics decide whether this repo is in that result set at all. | Repository settings → Topics |
| **The live demo** | `https://juandresrodca.github.io/IntuneGraph/demo/`, published by `.github/workflows/pages.yml`. Set as the repository homepage so it appears as a link in search results and in the sidebar. | Repository settings → Website |
-| **PowerShell Gallery** | The intended install route, `Install-Module IntuneGraph`. Gallery search reads the `Tags` in `src/IntuneGraph/IntuneGraph.psd1`, which are a separate list from GitHub topics and should stay in step with them. | `IntuneGraph.psd1` |
+| **PowerShell Gallery** | The install route, `Install-Module IntuneGraphKit` — see [*The Gallery name*](#the-gallery-name-intunegraphkit) for why it is not `IntuneGraph`. Gallery search reads the `Tags` in `src/IntuneGraphKit/IntuneGraphKit.psd1`, which are a separate list from GitHub topics and should stay in step with them. | `IntuneGraphKit.psd1` |
The README's first screen is the fourth surface, and the only one that converts a
visitor into a user. It leads with the demo command that needs no tenant, because the
@@ -22,6 +22,24 @@ cost of trying this tool is the thing most likely to stop someone.
---
+## The Gallery name: IntuneGraphKit
+
+The project is IntuneGraph; the module on the Gallery is **IntuneGraphKit**. That was
+not a choice between two names. `IntuneGraph` on the PowerShell Gallery was already
+taken — by an unrelated module from another author that uploads, assigns and removes
+Intune apps — and a Gallery package name has to equal the module name.
+
+The difference matters in one direction. Anyone who types the project name into
+`Install-Module` gets the other module: one with write cmdlets, installed on the
+strength of a README whose whole pitch is *read-only*. So the README's install block
+carries a one-line note naming the other package, and that note stays for as long as
+the other package exists.
+
+Everything a user does not install kept the project name: the repository, the site,
+the cmdlet names, the MCP server name (`intunegraph`) and the report title.
+
+---
+
## Topics
GitHub allows twenty. The rule for adding one: **a real practitioner would type it into
@@ -122,6 +140,10 @@ visitor who has already clicked: the three questions the tool answers, then the
objections an admin raises before installing anything that touches their tenant
(*does it write?* and *do I need a tenant to try it?*), answered before they are asked.
+The manifest `Description` in `src/IntuneGraphKit/IntuneGraphKit.psd1` carries the same
+text, so the Gallery listing and the repository make the same pitch. Change both in the
+same commit.
+
---
## Sibling repositories
diff --git a/docs/fixtures.md b/docs/fixtures.md
index 935601b..4b337de 100644
--- a/docs/fixtures.md
+++ b/docs/fixtures.md
@@ -31,7 +31,7 @@ A missing fixture is a hard error (silent empties would hide bugs).
## The Contoso dataset
-`tests/Fixtures/contoso` (mirrored to `src/IntuneGraph/DemoData`) is designed so
+`tests/Fixtures/contoso` (mirrored to `src/IntuneGraphKit/DemoData`) is designed so
**each hygiene check fires exactly once** and every query scenario is covered:
- **Nesting:** `SG-AllStaff` ⊃ `SG-Finance`, `SG-IT` → tests the `Via` path.
diff --git a/docs/img/icon.png b/docs/img/icon.png
new file mode 100644
index 0000000..c0fcfd2
Binary files /dev/null and b/docs/img/icon.png differ
diff --git a/docs/mcp.md b/docs/mcp.md
index eafa126..4213c69 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -100,15 +100,16 @@ claude mcp add intunegraph -- powershell.exe -NoLogo -NoProfile -NonInteractive
}
```
-### If the module is installed from PSGallery
+### If the module is installed from the PowerShell Gallery
-You can skip the launcher script and start the server from the installed module:
+You can skip the launcher script and start the server from the installed module
+(the Gallery package is `IntuneGraphKit`):
```json
{
"command": "pwsh",
"args": ["-NoLogo", "-NoProfile", "-NonInteractive", "-InputFormat", "Text",
- "-Command", "Import-Module IntuneGraph; Start-IntuneGraphMcp -Path 'D:/snapshots/contoso/graph.json'"]
+ "-Command", "Import-Module IntuneGraphKit; Start-IntuneGraphMcp -Path 'D:/snapshots/contoso/graph.json'"]
}
```
diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro
index c294923..bb8ac74 100644
--- a/site/src/pages/index.astro
+++ b/site/src/pages/index.astro
@@ -208,8 +208,8 @@ const mcpChat = `> why does Win11 Security Baseline apply to DEV-FIN-01?
1 · Zero-setup demo
-
Install-Module IntuneGraph -Scope CurrentUser
-Import-Module IntuneGraph
+ Install-Module IntuneGraphKit -Scope CurrentUser
+Import-Module IntuneGraphKit
Export-IntuneGraph -DemoData -PassThru | Show-IntuneGraph -Open
diff --git a/src/IntuneGraph/Assets/template.html b/src/IntuneGraphKit/Assets/template.html
similarity index 100%
rename from src/IntuneGraph/Assets/template.html
rename to src/IntuneGraphKit/Assets/template.html
diff --git a/src/IntuneGraph/DemoData/beta/deviceManagement/assignmentFilters.json b/src/IntuneGraphKit/DemoData/beta/deviceManagement/assignmentFilters.json
similarity index 100%
rename from src/IntuneGraph/DemoData/beta/deviceManagement/assignmentFilters.json
rename to src/IntuneGraphKit/DemoData/beta/deviceManagement/assignmentFilters.json
diff --git a/src/IntuneGraph/DemoData/beta/deviceManagement/configurationPolicies.json b/src/IntuneGraphKit/DemoData/beta/deviceManagement/configurationPolicies.json
similarity index 100%
rename from src/IntuneGraph/DemoData/beta/deviceManagement/configurationPolicies.json
rename to src/IntuneGraphKit/DemoData/beta/deviceManagement/configurationPolicies.json
diff --git a/src/IntuneGraph/DemoData/beta/deviceManagement/deviceHealthScripts.json b/src/IntuneGraphKit/DemoData/beta/deviceManagement/deviceHealthScripts.json
similarity index 100%
rename from src/IntuneGraph/DemoData/beta/deviceManagement/deviceHealthScripts.json
rename to src/IntuneGraphKit/DemoData/beta/deviceManagement/deviceHealthScripts.json
diff --git a/src/IntuneGraph/DemoData/beta/deviceManagement/deviceManagementScripts.json b/src/IntuneGraphKit/DemoData/beta/deviceManagement/deviceManagementScripts.json
similarity index 100%
rename from src/IntuneGraph/DemoData/beta/deviceManagement/deviceManagementScripts.json
rename to src/IntuneGraphKit/DemoData/beta/deviceManagement/deviceManagementScripts.json
diff --git a/src/IntuneGraph/DemoData/manifest.json b/src/IntuneGraphKit/DemoData/manifest.json
similarity index 100%
rename from src/IntuneGraph/DemoData/manifest.json
rename to src/IntuneGraphKit/DemoData/manifest.json
diff --git a/src/IntuneGraph/DemoData/v1.0/deviceAppManagement/mobileApps.json b/src/IntuneGraphKit/DemoData/v1.0/deviceAppManagement/mobileApps.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/deviceAppManagement/mobileApps.json
rename to src/IntuneGraphKit/DemoData/v1.0/deviceAppManagement/mobileApps.json
diff --git a/src/IntuneGraph/DemoData/v1.0/deviceManagement/deviceCompliancePolicies.json b/src/IntuneGraphKit/DemoData/v1.0/deviceManagement/deviceCompliancePolicies.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/deviceManagement/deviceCompliancePolicies.json
rename to src/IntuneGraphKit/DemoData/v1.0/deviceManagement/deviceCompliancePolicies.json
diff --git a/src/IntuneGraph/DemoData/v1.0/deviceManagement/deviceConfigurations.json b/src/IntuneGraphKit/DemoData/v1.0/deviceManagement/deviceConfigurations.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/deviceManagement/deviceConfigurations.json
rename to src/IntuneGraphKit/DemoData/v1.0/deviceManagement/deviceConfigurations.json
diff --git a/src/IntuneGraph/DemoData/v1.0/deviceManagement/managedDevices.json b/src/IntuneGraphKit/DemoData/v1.0/deviceManagement/managedDevices.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/deviceManagement/managedDevices.json
rename to src/IntuneGraphKit/DemoData/v1.0/deviceManagement/managedDevices.json
diff --git a/src/IntuneGraph/DemoData/v1.0/groups.json b/src/IntuneGraphKit/DemoData/v1.0/groups.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/groups.json
rename to src/IntuneGraphKit/DemoData/v1.0/groups.json
diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000001/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000001/members.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000001/members.json
rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000001/members.json
diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000002/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000002/members.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000002/members.json
rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000002/members.json
diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000003/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000003/members.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000003/members.json
rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000003/members.json
diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000004/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000004/members.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000004/members.json
rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000004/members.json
diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000005/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000005/members.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000005/members.json
rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000005/members.json
diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000006/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000006/members.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000006/members.json
rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000006/members.json
diff --git a/src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000007/members.json b/src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000007/members.json
similarity index 100%
rename from src/IntuneGraph/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000007/members.json
rename to src/IntuneGraphKit/DemoData/v1.0/groups/00000000-0000-0000-0000-000000000007/members.json
diff --git a/src/IntuneGraph/IntuneGraph.psd1 b/src/IntuneGraphKit/IntuneGraphKit.psd1
similarity index 67%
rename from src/IntuneGraph/IntuneGraph.psd1
rename to src/IntuneGraphKit/IntuneGraphKit.psd1
index a428a12..eb3f295 100644
--- a/src/IntuneGraph/IntuneGraph.psd1
+++ b/src/IntuneGraphKit/IntuneGraphKit.psd1
@@ -1,16 +1,20 @@
@{
- RootModule = 'IntuneGraph.psm1'
+ RootModule = 'IntuneGraphKit.psm1'
ModuleVersion = '0.2.0'
- GUID = 'b7e4b0a2-4c9e-4a1f-9c3d-1a2b3c4d5e6f'
+ GUID = '2fd9d683-0ede-4a50-a4b5-60a39b1a93be'
Author = 'Juan Andres Rodriguez'
CompanyName = 'IntuneGraph'
Copyright = '(c) 2026 Juan Andres Rodriguez. MIT License.'
- Description = 'Turn your Microsoft Intune tenant into an interactive, queryable relationship graph. Target resolution, blast-radius impact preview, and assignment hygiene checks. Read-only, offline-capable, zero write scopes.'
+ # Same text as the GitHub repository description - change both together.
+ Description = 'Turn your Microsoft Intune tenant into an interactive relationship graph. See what applies to a device or user and why, preview the blast radius before you touch a group, and find orphaned or broken assignments. Read-only Graph, offline-capable demo, self-contained HTML viewer. Zero write scopes.'
# 5.1 floor broadens adoption (many Intune admins are still on Windows PowerShell).
# PowerShell 7+ is recommended but not required.
PowerShellVersion = '5.1'
+ # Drives the Gallery's edition filter together with the PSEdition_* tags below.
+ CompatiblePSEditions = @('Desktop', 'Core')
+
# Microsoft.Graph.Authentication is loaded lazily by Connect-IntuneGraph so that
# demo/fixture mode works with zero dependencies installed. It is intentionally
# NOT a hard RequiredModules entry.
@@ -35,9 +39,11 @@
PrivateData = @{
PSData = @{
Tags = @('Intune', 'MicrosoftGraph', 'MEM', 'Endpoint', 'MDM', 'Graph', 'Visualization',
- 'MCP', 'ModelContextProtocol', 'AI', 'Copilot', 'Windows', 'macOS', 'Linux')
+ 'MCP', 'ModelContextProtocol', 'AI', 'Copilot', 'Windows', 'macOS', 'Linux',
+ 'PSEdition_Desktop', 'PSEdition_Core')
LicenseUri = 'https://github.com/juandresrodca/IntuneGraph/blob/main/LICENSE'
ProjectUri = 'https://github.com/juandresrodca/IntuneGraph'
+ IconUri = 'https://raw.githubusercontent.com/juandresrodca/IntuneGraph/main/docs/img/icon.png'
ReleaseNotes = 'https://github.com/juandresrodca/IntuneGraph/blob/main/CHANGELOG.md'
}
}
diff --git a/src/IntuneGraph/IntuneGraph.psm1 b/src/IntuneGraphKit/IntuneGraphKit.psm1
similarity index 100%
rename from src/IntuneGraph/IntuneGraph.psm1
rename to src/IntuneGraphKit/IntuneGraphKit.psm1
diff --git a/src/IntuneGraph/Private/DataSource.ps1 b/src/IntuneGraphKit/Private/DataSource.ps1
similarity index 100%
rename from src/IntuneGraph/Private/DataSource.ps1
rename to src/IntuneGraphKit/Private/DataSource.ps1
diff --git a/src/IntuneGraph/Private/Emit.ps1 b/src/IntuneGraphKit/Private/Emit.ps1
similarity index 100%
rename from src/IntuneGraph/Private/Emit.ps1
rename to src/IntuneGraphKit/Private/Emit.ps1
diff --git a/src/IntuneGraph/Private/Fetch.ps1 b/src/IntuneGraphKit/Private/Fetch.ps1
similarity index 100%
rename from src/IntuneGraph/Private/Fetch.ps1
rename to src/IntuneGraphKit/Private/Fetch.ps1
diff --git a/src/IntuneGraph/Private/GraphModel.ps1 b/src/IntuneGraphKit/Private/GraphModel.ps1
similarity index 100%
rename from src/IntuneGraph/Private/GraphModel.ps1
rename to src/IntuneGraphKit/Private/GraphModel.ps1
diff --git a/src/IntuneGraph/Private/Mcp.ps1 b/src/IntuneGraphKit/Private/Mcp.ps1
similarity index 100%
rename from src/IntuneGraph/Private/Mcp.ps1
rename to src/IntuneGraphKit/Private/Mcp.ps1
diff --git a/src/IntuneGraph/Private/Normalize.ps1 b/src/IntuneGraphKit/Private/Normalize.ps1
similarity index 100%
rename from src/IntuneGraph/Private/Normalize.ps1
rename to src/IntuneGraphKit/Private/Normalize.ps1
diff --git a/src/IntuneGraph/Private/Query.ps1 b/src/IntuneGraphKit/Private/Query.ps1
similarity index 100%
rename from src/IntuneGraph/Private/Query.ps1
rename to src/IntuneGraphKit/Private/Query.ps1
diff --git a/src/IntuneGraph/Private/Util.ps1 b/src/IntuneGraphKit/Private/Util.ps1
similarity index 100%
rename from src/IntuneGraph/Private/Util.ps1
rename to src/IntuneGraphKit/Private/Util.ps1
diff --git a/src/IntuneGraph/Public/Connect-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Connect-IntuneGraph.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Connect-IntuneGraph.ps1
rename to src/IntuneGraphKit/Public/Connect-IntuneGraph.ps1
diff --git a/src/IntuneGraph/Public/Disconnect-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Disconnect-IntuneGraph.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Disconnect-IntuneGraph.ps1
rename to src/IntuneGraphKit/Public/Disconnect-IntuneGraph.ps1
diff --git a/src/IntuneGraph/Public/Export-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Export-IntuneGraph.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Export-IntuneGraph.ps1
rename to src/IntuneGraphKit/Public/Export-IntuneGraph.ps1
diff --git a/src/IntuneGraph/Public/Find-IntuneOrphan.ps1 b/src/IntuneGraphKit/Public/Find-IntuneOrphan.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Find-IntuneOrphan.ps1
rename to src/IntuneGraphKit/Public/Find-IntuneOrphan.ps1
diff --git a/src/IntuneGraph/Public/Get-IntuneBlastRadius.ps1 b/src/IntuneGraphKit/Public/Get-IntuneBlastRadius.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Get-IntuneBlastRadius.ps1
rename to src/IntuneGraphKit/Public/Get-IntuneBlastRadius.ps1
diff --git a/src/IntuneGraph/Public/Get-IntuneGraphNode.ps1 b/src/IntuneGraphKit/Public/Get-IntuneGraphNode.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Get-IntuneGraphNode.ps1
rename to src/IntuneGraphKit/Public/Get-IntuneGraphNode.ps1
diff --git a/src/IntuneGraph/Public/Get-IntuneTarget.ps1 b/src/IntuneGraphKit/Public/Get-IntuneTarget.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Get-IntuneTarget.ps1
rename to src/IntuneGraphKit/Public/Get-IntuneTarget.ps1
diff --git a/src/IntuneGraph/Public/Import-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Import-IntuneGraph.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Import-IntuneGraph.ps1
rename to src/IntuneGraphKit/Public/Import-IntuneGraph.ps1
diff --git a/src/IntuneGraph/Public/Show-IntuneGraph.ps1 b/src/IntuneGraphKit/Public/Show-IntuneGraph.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Show-IntuneGraph.ps1
rename to src/IntuneGraphKit/Public/Show-IntuneGraph.ps1
diff --git a/src/IntuneGraph/Public/Start-IntuneGraphMcp.ps1 b/src/IntuneGraphKit/Public/Start-IntuneGraphMcp.ps1
similarity index 100%
rename from src/IntuneGraph/Public/Start-IntuneGraphMcp.ps1
rename to src/IntuneGraphKit/Public/Start-IntuneGraphMcp.ps1
diff --git a/tests/IntuneGraph.Tests.ps1 b/tests/IntuneGraph.Tests.ps1
index 614e4d2..79ab66b 100644
--- a/tests/IntuneGraph.Tests.ps1
+++ b/tests/IntuneGraph.Tests.ps1
@@ -3,7 +3,7 @@
#requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }
BeforeAll {
- $script:ModulePath = Join-Path $PSScriptRoot '..\src\IntuneGraph\IntuneGraph.psd1'
+ $script:ModulePath = Join-Path $PSScriptRoot '..\src\IntuneGraphKit\IntuneGraphKit.psd1'
Import-Module $script:ModulePath -Force
$script:FixtureRoot = Join-Path $PSScriptRoot 'Fixtures\contoso'
$script:GraphPath = Join-Path $TestDrive 'graph.json'
@@ -16,7 +16,7 @@ BeforeAll {
$script:McpSend = {
param($Graph, [string[]]$Lines)
$payload = ($Lines -join "`n")
- $raw = & (Get-Module IntuneGraph) {
+ $raw = & (Get-Module IntuneGraphKit) {
param($g, $text)
$sw = New-Object System.IO.StringWriter
Invoke-IgMcpLoop -Reader (New-Object System.IO.StringReader($text)) -Writer $sw -State (New-IgMcpState -Graph $g -Path $null)
@@ -44,9 +44,21 @@ Describe 'Module' {
$expected = 'Connect-IntuneGraph', 'Disconnect-IntuneGraph', 'Export-IntuneGraph', 'Import-IntuneGraph',
'Get-IntuneGraphNode', 'Get-IntuneTarget', 'Get-IntuneBlastRadius', 'Find-IntuneOrphan', 'Show-IntuneGraph',
'Start-IntuneGraphMcp'
- $actual = (Get-Command -Module IntuneGraph -CommandType Function).Name
+ $actual = (Get-Command -Module IntuneGraphKit -CommandType Function).Name
($expected | Sort-Object) | Should -Be ($actual | Sort-Object)
}
+ It 'carries the metadata the Gallery listing needs' {
+ # A published version's listing can never be corrected in place; only a new
+ # version fixes it. Catch a missing field here instead.
+ $m = Import-PowerShellDataFile $script:ModulePath
+ $m.CompatiblePSEditions | Should -Contain 'Desktop'
+ $m.CompatiblePSEditions | Should -Contain 'Core'
+ $m.PrivateData.PSData.Tags | Should -Contain 'PSEdition_Desktop'
+ $m.PrivateData.PSData.Tags | Should -Contain 'PSEdition_Core'
+ foreach ($uri in 'ProjectUri', 'LicenseUri', 'IconUri', 'ReleaseNotes') {
+ $m.PrivateData.PSData[$uri] | Should -Match '^https://' -Because "$uri is shown on the listing"
+ }
+ }
}
Describe 'Export / build' {
@@ -304,7 +316,7 @@ Describe 'MCP transport (feature 4)' {
$out.Raw | Should -Match '\\n' # the rendered table's newlines, escaped
}
It 'serves every tool with the session disconnected, so no live tenant call is reachable' {
- (InModuleScope IntuneGraph { $script:IgSession.Mode }) | Should -Be 'None'
+ (InModuleScope IntuneGraphKit { $script:IgSession.Mode }) | Should -Be 'None'
foreach ($tool in 'intune_summary', 'intune_orphans', 'intune_node') {
$out = & $script:McpSend $script:G @(& $script:McpToolRequest $tool @{})
$out.Responses[0].result.isError | Should -BeFalse
diff --git a/tests/New-ContosoFixtures.ps1 b/tests/New-ContosoFixtures.ps1
index ef61625..51c73e3 100644
--- a/tests/New-ContosoFixtures.ps1
+++ b/tests/New-ContosoFixtures.ps1
@@ -11,7 +11,7 @@
param(
[string[]]$TargetRoot = @(
(Join-Path $PSScriptRoot 'Fixtures\contoso'),
- (Join-Path $PSScriptRoot '..\src\IntuneGraph\DemoData')
+ (Join-Path $PSScriptRoot '..\src\IntuneGraphKit\DemoData')
)
)
diff --git a/tools/icon/New-IntuneGraphIcon.ps1 b/tools/icon/New-IntuneGraphIcon.ps1
new file mode 100644
index 0000000..0bebd25
--- /dev/null
+++ b/tools/icon/New-IntuneGraphIcon.ps1
@@ -0,0 +1,75 @@
+<#
+ Draws the module icon the PowerShell Gallery listing shows (the manifest's IconUri).
+ Run: .\tools\icon\New-IntuneGraphIcon.ps1 (writes docs\img\icon.png)
+
+ Windows only: it uses System.Drawing, which ships with Windows PowerShell, so the
+ icon needs no package install. Colours are the node palette from the HTML viewer,
+ and the two bright edges are the "via" path - device -> group -> policy - which is
+ the one thing this tool shows that a flat list does not.
+#>
+[CmdletBinding()]
+param(
+ [string]$OutputPath,
+ [int]$Size = 256
+)
+$ErrorActionPreference = 'Stop'
+# Resolved here rather than as a parameter default: Windows PowerShell leaves
+# $PSScriptRoot empty in parameter defaults when the script is started with -File.
+if (-not $OutputPath) { $OutputPath = Join-Path $PSScriptRoot '..\..\docs\img\icon.png' }
+Add-Type -AssemblyName System.Drawing
+
+function New-IgColor([string]$Hex) { [System.Drawing.ColorTranslator]::FromHtml($Hex) }
+
+$s = $Size / 256.0 # drawn on a 256 grid, scaled to $Size
+$bmp = New-Object System.Drawing.Bitmap $Size, $Size
+$g = [System.Drawing.Graphics]::FromImage($bmp)
+$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
+$g.Clear([System.Drawing.Color]::Transparent)
+
+# Rounded-square tile, inset so the border stroke is not clipped.
+$inset = 3 * $s; $d = 2 * 52 * $s; $far = $Size - $inset - $d
+$tile = New-Object System.Drawing.Drawing2D.GraphicsPath
+$tile.AddArc($inset, $inset, $d, $d, 180, 90)
+$tile.AddArc($far, $inset, $d, $d, 270, 90)
+$tile.AddArc($far, $far, $d, $d, 0, 90)
+$tile.AddArc($inset, $far, $d, $d, 90, 90)
+$tile.CloseFigure()
+$bg = New-IgColor '#0d1320'
+$g.FillPath((New-Object System.Drawing.SolidBrush $bg), $tile)
+$g.DrawPath((New-Object System.Drawing.Pen (New-IgColor '#27395a'), (4 * $s)), $tile)
+
+# x, y, radius, colour - group hub in the middle, one of each workload kind around it.
+$nodes = [ordered]@{
+ group = @(128, 128, 30, '#9fef00')
+ config = @(194, 64, 19, '#ffb454')
+ device = @(196, 192, 19, '#58a6ff')
+ user = @( 60, 192, 19, '#c792ea')
+ app = @( 62, 64, 19, '#ff7b72')
+}
+$edges = @(
+ @('app', 'group', '#34496e'),
+ @('user', 'group', '#34496e'),
+ @('device', 'group', '#9fef00'),
+ @('group', 'config', '#9fef00')
+)
+
+foreach ($e in $edges) {
+ $a = $nodes[$e[0]]; $b = $nodes[$e[1]]
+ $pen = New-Object System.Drawing.Pen (New-IgColor $e[2]), (8 * $s)
+ $pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
+ $pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
+ $g.DrawLine($pen, $a[0] * $s, $a[1] * $s, $b[0] * $s, $b[1] * $s)
+ $pen.Dispose()
+}
+
+# Each node sits in a ring of tile colour so edges stop short of it.
+foreach ($n in $nodes.Values) {
+ $x = $n[0] * $s; $y = $n[1] * $s; $r = $n[2] * $s; $ring = 7 * $s
+ $g.FillEllipse((New-Object System.Drawing.SolidBrush $bg), ($x - $r - $ring), ($y - $r - $ring), (2 * ($r + $ring)), (2 * ($r + $ring)))
+ $g.FillEllipse((New-Object System.Drawing.SolidBrush (New-IgColor $n[3])), ($x - $r), ($y - $r), (2 * $r), (2 * $r))
+}
+
+$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
+$bmp.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
+$g.Dispose(); $bmp.Dispose()
+Write-Host "Wrote $OutputPath ($Size x $Size)"
diff --git a/tools/mcp/mcp-server.ps1 b/tools/mcp/mcp-server.ps1
index 2493710..2078d39 100644
--- a/tools/mcp/mcp-server.ps1
+++ b/tools/mcp/mcp-server.ps1
@@ -9,5 +9,5 @@ param(
[switch]$LogRequests
)
$ErrorActionPreference = 'Stop'
-Import-Module (Join-Path $PSScriptRoot '..\..\src\IntuneGraph\IntuneGraph.psd1') -Force
+Import-Module (Join-Path $PSScriptRoot '..\..\src\IntuneGraphKit\IntuneGraphKit.psd1') -Force
Start-IntuneGraphMcp -Path $Path -LogRequests:$LogRequests