param(
|
[string]$SkillsRoot,
|
[switch]$InstallAllDetected,
|
[switch]$UseWorkspaceLock,
|
[switch]$WhatIf
|
)
|
|
$ErrorActionPreference = "Stop"
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
$WorkspaceRoot = Resolve-Path (Join-Path $ScriptDir "..\..")
|
|
$SourceSkill = Join-Path $ScriptDir "SKILL.md"
|
$SourceSpecItem = Get-ChildItem -LiteralPath $ScriptDir -File -Filter "AI*.md" |
|
Where-Object { $_.Name -ne "SKILL.md" } |
|
Select-Object -First 1
|
if (-not $SourceSpecItem) {
|
throw "Missing source spec: expected an AI*.md file beside install-fp-skill.ps1"
|
}
|
$SourceSpec = $SourceSpecItem.FullName
|
|
if (-not (Test-Path -LiteralPath $SourceSkill)) {
|
throw "Missing source SKILL.md: $SourceSkill"
|
}
|
if (-not (Test-Path -LiteralPath $SourceSpec)) {
|
throw "Missing source spec: $SourceSpec"
|
}
|
|
function Get-LockedSkillsRoot {
|
$SkillLock = Join-Path $WorkspaceRoot.Path ".mbx\skills.lock.yaml"
|
if (-not (Test-Path -LiteralPath $SkillLock)) {
|
return $null
|
}
|
$LockedTargetLine = Get-Content -LiteralPath $SkillLock -Encoding UTF8 |
|
Where-Object { $_ -match "^\s*target_dir:\s*(.+?)\s*$" } |
|
Select-Object -First 1
|
if ($LockedTargetLine -and $LockedTargetLine -match "^\s*target_dir:\s*(.+?)\s*$") {
|
return $Matches[1].Trim().Trim("'").Trim('"')
|
}
|
return $null
|
}
|
|
function Test-CurrentUserPath {
|
param([string]$Path)
|
if (-not $Path -or $Path.Trim() -eq "") {
|
return $false
|
}
|
$homePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($HOME)
|
return $Path.StartsWith($homePath, [System.StringComparison]::OrdinalIgnoreCase)
|
}
|
|
function Add-Target {
|
param(
|
[System.Collections.ArrayList]$Targets,
|
[string]$Path,
|
[string]$Reason,
|
[bool]$AllowForeign = $false
|
)
|
if (-not $Path -or $Path.Trim() -eq "") {
|
return
|
}
|
$resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
|
if (-not $AllowForeign -and -not (Test-CurrentUserPath -Path $resolved)) {
|
[void]$SkippedTargets.Add([ordered]@{
|
skills_root = $resolved
|
reason = $Reason
|
skipped_reason = "foreign_user_absolute_path"
|
})
|
return
|
}
|
foreach ($target in $Targets) {
|
if ($target.skills_root -eq $resolved) {
|
return
|
}
|
}
|
[void]$Targets.Add([ordered]@{
|
skills_root = $resolved
|
reason = $Reason
|
})
|
}
|
|
$Targets = New-Object System.Collections.ArrayList
|
$SkippedTargets = New-Object System.Collections.ArrayList
|
$LockedTarget = Get-LockedSkillsRoot
|
|
if ($SkillsRoot -and $SkillsRoot.Trim() -ne "") {
|
Add-Target -Targets $Targets -Path $SkillsRoot -Reason "explicit -SkillsRoot" -AllowForeign $true
|
} elseif ($InstallAllDetected) {
|
if ($env:CODEX_HOME -and $env:CODEX_HOME.Trim() -ne "") {
|
Add-Target -Targets $Targets -Path (Join-Path $env:CODEX_HOME "skills") -Reason "CODEX_HOME"
|
}
|
if ($LockedTarget) {
|
Add-Target -Targets $Targets -Path $LockedTarget -Reason ".mbx skills.lock target_dir" -AllowForeign ([bool]$UseWorkspaceLock)
|
}
|
Add-Target -Targets $Targets -Path (Join-Path $HOME ".codex\skills") -Reason "current user .codex"
|
Add-Target -Targets $Targets -Path (Join-Path $HOME ".codex3\skills") -Reason "current user .codex3"
|
Get-ChildItem -LiteralPath $HOME -Directory -Force -ErrorAction SilentlyContinue |
|
Where-Object { $_.Name -like ".codex*" -and (Test-Path -LiteralPath (Join-Path $_.FullName "skills")) } |
|
ForEach-Object { Add-Target -Targets $Targets -Path (Join-Path $_.FullName "skills") -Reason "detected existing Codex home" }
|
} else {
|
if ($env:CODEX_HOME -and $env:CODEX_HOME.Trim() -ne "") {
|
Add-Target -Targets $Targets -Path (Join-Path $env:CODEX_HOME "skills") -Reason "CODEX_HOME"
|
if ($LockedTarget -and (Test-CurrentUserPath -Path $LockedTarget)) {
|
Add-Target -Targets $Targets -Path $LockedTarget -Reason ".mbx skills.lock target_dir"
|
}
|
} elseif ($LockedTarget -and (Test-CurrentUserPath -Path $LockedTarget)) {
|
Add-Target -Targets $Targets -Path $LockedTarget -Reason ".mbx skills.lock target_dir"
|
} elseif (Test-Path -LiteralPath (Join-Path $HOME ".codex\skills")) {
|
Add-Target -Targets $Targets -Path (Join-Path $HOME ".codex\skills") -Reason "current user existing .codex"
|
} elseif (Test-Path -LiteralPath (Join-Path $HOME ".codex3\skills")) {
|
Add-Target -Targets $Targets -Path (Join-Path $HOME ".codex3\skills") -Reason "current user existing .codex3"
|
} else {
|
Add-Target -Targets $Targets -Path (Join-Path $HOME ".codex\skills") -Reason "default current user .codex"
|
}
|
}
|
|
if ($Targets.Count -eq 0) {
|
throw "No safe target skills directory resolved. Pass -SkillsRoot explicitly, or use -UseWorkspaceLock if you intentionally want the workspace lock target."
|
}
|
|
$result = [ordered]@{
|
workspace_root = $WorkspaceRoot.Path
|
source_skill = $SourceSkill
|
source_spec = $SourceSpec
|
codex_home = $env:CODEX_HOME
|
install_all_detected = [bool]$InstallAllDetected
|
use_workspace_lock = [bool]$UseWorkspaceLock
|
what_if = [bool]$WhatIf
|
targets = $Targets
|
skipped_targets = $SkippedTargets
|
}
|
|
if ($WhatIf) {
|
$result.status = "DRY_RUN"
|
$result.note = "No files copied. Review targets before installing."
|
$result | ConvertTo-Json -Depth 6
|
exit 0
|
}
|
|
$installed = New-Object System.Collections.ArrayList
|
foreach ($target in $Targets) {
|
$targetRoot = $target.skills_root
|
$TargetDir = Join-Path $targetRoot "fp"
|
New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null
|
Copy-Item -LiteralPath $SourceSkill -Destination (Join-Path $TargetDir "SKILL.md") -Force
|
Copy-Item -LiteralPath $SourceSpec -Destination (Join-Path $TargetDir $SourceSpecItem.Name) -Force
|
|
$installedSkill = Join-Path $TargetDir "SKILL.md"
|
$installedSpec = Join-Path $TargetDir $SourceSpecItem.Name
|
$frontmatter = Get-Content -LiteralPath $installedSkill -Encoding UTF8 | Select-Object -First 4
|
if ($frontmatter[0] -ne "---" -or $frontmatter[1] -notmatch "^name:\s*fp\s*$" -or $frontmatter[2] -notmatch "^description:\s+") {
|
throw "Installed SKILL.md frontmatter is invalid: $installedSkill"
|
}
|
[void]$installed.Add([ordered]@{
|
skills_root = $targetRoot
|
target_dir = $TargetDir
|
reason = $target.reason
|
files = @($installedSkill, $installedSpec)
|
})
|
}
|
|
$result.status = "INSTALLED"
|
$result.installed = $installed
|
$result.note = "Restart or reopen target Codex sessions before expecting fp to appear in the skill list. Existing threads may still need explicit local skill routing."
|
$result | ConvertTo-Json -Depth 6
|