Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/advanced/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,13 @@ Staging is performed by:

- `tools/New-IdleModulePackage.ps1`

This script copies the `IdLE` meta-module and required nested modules into a local `Modules/` folder and patches the staged
This script copies the `IdLE` meta-module and baseline nested modules (`IdLE.Core`, `IdLE.Steps.Common`) into a local `Modules/` folder and patches the staged
`IdLE.psd1` so `NestedModules` use in-package relative paths (e.g. `./Modules/IdLE.Core/IdLE.Core.psd1`).

For details on baseline vs optional modules and the non-blocking import policy, see **[Installation Guide](../getting-started/installation.md#what-gets-imported)**.

> This approach avoids repository restructuring while ensuring that `Install-Module IdLE` + `Import-Module IdLE` works
> after installation.
> reliably on any clean PowerShell 7 environment without external dependencies.

## Versioning and naming

Expand Down
27 changes: 18 additions & 9 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Install-Module -Name IdLE -Scope CurrentUser
Import-Module IdLE
```

> Note: The `IdLE` meta-module loads the bundled nested modules (e.g. `IdLE.Core`, built-in steps, and the mock provider
> used by examples) from within the installed package.
> Note: The `IdLE` module automatically imports the baseline modules (`IdLE.Core` and `IdLE.Steps.Common`).
> Optional modules (providers, additional step modules) are shipped with the package but not auto-imported.

### Verify installation

Expand Down Expand Up @@ -61,17 +61,19 @@ Get-Command -Module IdLE.Core

## What gets imported

### Default: `IdLE` meta-module (batteries included)
### Default: `IdLE` meta-module (baseline)

`IdLE` is the **batteries-included** entrypoint. Importing it loads:
`IdLE` is the **baseline** entrypoint. Importing it automatically loads:

- **IdLE.Core** — the workflow engine (step-agnostic)
- **IdLE.Steps.Common** — first-party built-in steps (e.g. `IdLE.Step.EmitEvent`, `IdLE.Step.EnsureAttribute`)

Built-in steps are **available to the engine by default**, but are intentionally **not exported into the global session state**.
This keeps your PowerShell session clean while still allowing workflows to reference built-in steps by `Step.Type`.

**When to use:** Most users and production scenarios.
**Non-blocking guarantee:** `Import-Module IdLE` always succeeds on a clean PowerShell 7 environment without any external dependencies (RSAT, AD tools, third-party modules, etc.).

**When to use:** All users and production scenarios.

### Advanced: Engine-only import

Expand All @@ -85,14 +87,21 @@ Import-Module ./src/IdLE.Core/IdLE.Core.psd1 -Force

---

## Provider modules (optional)
## Optional modules (shipped but not auto-imported)

The `IdLE` package ships additional modules that are **not automatically imported**. These modules may have system-specific or tool-specific dependencies and are imported explicitly when needed:

The core engine is provider-agnostic. Provider modules are **packaged with IdLE** but must be **imported separately** when needed.
- **Provider modules:** `IdLE.Provider.AD`, `IdLE.Provider.EntraID`, `IdLE.Provider.ExchangeOnline`, `IdLE.Provider.DirectorySync.EntraConnect`
- **Optional step modules:** `IdLE.Steps.DirectorySync`, `IdLE.Steps.Mailbox`
- **Development/testing modules:** `IdLE.Provider.Mock`

Example:
Example (from source):

```powershell
# From source
# Import baseline (auto-imports Core and Steps.Common)
Import-Module ./src/IdLE/IdLE.psd1 -Force

# Import optional provider when needed
Import-Module ./src/IdLE.Provider.AD/IdLE.Provider.AD.psd1 -Force
```

Expand Down
2 changes: 1 addition & 1 deletion examples/Invoke-IdleDemo.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ function Select-DemoWorkflows {

# Import modules from the repo (path-based import, no global installation required).
Import-Module (Join-Path $PSScriptRoot '..\src\IdLE\IdLE.psd1') -Force -ErrorAction Stop
Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Steps.Common\IdLE.Steps.Common.psd1') -Force -ErrorAction Stop
# Mock provider is optional and must be imported explicitly (not auto-imported by IdLE)
Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Provider.Mock\IdLE.Provider.Mock.psd1') -Force -ErrorAction Stop

$available = @(Get-DemoWorkflows -Category $Category)
Expand Down
37 changes: 36 additions & 1 deletion src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,42 @@ function Invoke-IdlePlanObject {
)
}

$cmd = Get-Command -Name ([string]$handlerName) -CommandType Function -ErrorAction Stop
# Resolve the handler command.
# The handler name can be:
# 1) A simple function name (e.g. "Invoke-IdleStepEmitEvent") - globally available
# 2) A module-qualified name (e.g. "IdLE.Steps.Common\Invoke-IdleStepEmitEvent") - from a nested module
#
# Module-qualified names are used for built-in steps that are loaded as nested modules
# and not exported globally to keep the session clean.

$cmd = $null

# Try simple lookup first (globally available commands)
$cmd = Get-Command -Name ([string]$handlerName) -CommandType Function -ErrorAction SilentlyContinue

# If not found and name contains backslash, try module-qualified lookup
if ($null -eq $cmd -and ([string]$handlerName).Contains('\')) {
$parts = ([string]$handlerName).Split('\', 2)
if ($parts.Count -eq 2) {
$moduleName = $parts[0]
$commandName = $parts[1]

# Get-Module -All returns loaded modules (including nested/hidden modules)
# We use -All to find modules that are loaded but not in the global session state
$modules = @(Get-Module -Name $moduleName -All)
if ($modules.Count -gt 0) {
$module = $modules[0]
if ($null -ne $module.ExportedCommands -and $module.ExportedCommands.ContainsKey($commandName)) {
$cmd = $module.ExportedCommands[$commandName]
}
}
}
}

if ($null -eq $cmd) {
throw [System.ArgumentException]::new("Step handler '$handlerName' for step type '$StepType' could not be resolved to a valid command.", 'Providers')
}

return $cmd
}

Expand Down
5 changes: 1 addition & 4 deletions src/IdLE/IdLE.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@

NestedModules = @(
'..\IdLE.Core\IdLE.Core.psd1',
'..\IdLE.Steps.Common\IdLE.Steps.Common.psd1',
'..\IdLE.Steps.DirectorySync\IdLE.Steps.DirectorySync.psd1',
'..\IdLE.Provider.DirectorySync.EntraConnect\IdLE.Provider.DirectorySync.EntraConnect.psd1',
'..\IdLE.Provider.AD\IdLE.Provider.AD.psd1'
'..\IdLE.Steps.Common\IdLE.Steps.Common.psd1'
)

FunctionsToExport = @(
Expand Down
28 changes: 28 additions & 0 deletions tests/ModuleSurface.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,34 @@ Describe 'Module manifests and public surface' {
($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.Common') | Should -Not -BeNullOrEmpty
}

It 'IdLE auto-imports only baseline modules (Core and Steps.Common), not optional modules' {
# Clean test state - remove baseline modules to ensure fresh import
# We don't remove optional modules to maintain test isolation (other tests may have them loaded)
Remove-Module IdLE, IdLE.Core, IdLE.Steps.Common -Force -ErrorAction SilentlyContinue
Comment thread
blindzero marked this conversation as resolved.
Import-Module $idlePsd1 -Force -ErrorAction Stop

$idle = Get-Module IdLE
$idle | Should -Not -BeNullOrEmpty

# Define expected baseline modules in one place
$baselineModules = @('IdLE.Core', 'IdLE.Steps.Common')

# Baseline modules should be auto-imported (explicit positive check)
Comment thread
blindzero marked this conversation as resolved.
foreach ($moduleName in $baselineModules) {
($idle.NestedModules | Where-Object Name -eq $moduleName) | Should -Not -BeNullOrEmpty -Because "$moduleName should be auto-imported"
}

# Only baseline modules should be nested (count check ensures no extras)
@($idle.NestedModules).Count | Should -Be $baselineModules.Count

# Verify no optional modules are nested (generalized negative check using pattern)
# This pattern matches: IdLE.Provider.* or IdLE.Steps.* (except Steps.Common)
$nestedNames = @($idle.NestedModules | Select-Object -ExpandProperty Name)
$optionalModulePattern = '^IdLE\.(Provider\.|Steps\.(?!Common$))'
$unexpectedModules = $nestedNames | Where-Object { $_ -match $optionalModulePattern }
$unexpectedModules | Should -BeNullOrEmpty -Because "Optional modules should not be auto-imported"
}

It 'Steps module exports the intended step functions' {
Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue
Import-Module $stepsPsd1 -Force -ErrorAction Stop
Expand Down