Skip to content

Commit 2860a44

Browse files
committed
Add Update-Toolkit self-update command, fix 2 remaining CI failures
New Update-Toolkit command: git pull, changelog summary, module re-import. Test-ToolkitUpdate runs silently on startup at a configurable interval (toolkit.updateCheckDays in config.json, default 1 day, 0 to disable). Added to Show-Help, helpme.ps1, COMMANDS.md, and config.example.json. Fix recent-commands test for missing PSReadLine history on CI runners. Fix Use-NppForGit test error capture and module reload on CI.
1 parent d88c3a4 commit 2860a44

11 files changed

Lines changed: 305 additions & 4 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ secrets/
2626
# Profile backups
2727
*PROFILE_BACKUP*
2828

29+
# Toolkit update stamp
30+
.last-update-check
31+
2932
# OS files
3033
.DS_Store
3134
Thumbs.db

PowerShellDevToolkit/PowerShellDevToolkit.psd1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
# Network commands
4848
'Get-IPAddress'
4949
'Clear-DNSCache'
50+
# Toolkit management
51+
'Update-Toolkit'
52+
'Test-ToolkitUpdate'
5053
)
5154

5255
CmdletsToExport = @()

PowerShellDevToolkit/PowerShellDevToolkit.psm1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@ New-Alias -Name grep -Value Select-String -Force -Scope Global
5252
New-Alias -Name ip -Value Get-IPAddress -Force -Scope Global
5353
New-Alias -Name Flush-DNS -Value Clear-DNSCache -Force -Scope Global
5454

55+
5556
# la — list all including hidden (wraps Get-DirectoryListing -Force)
5657
function global:la { Get-DirectoryListing -Force @args }
5758

5859
# o. — open current directory in Explorer
5960
function global:o. { Open-Item . }
61+
62+
# Startup update check (runs once per configured interval, silent on error)
63+
try { Test-ToolkitUpdate } catch { }

PowerShellDevToolkit/Public/Show-Help.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@ function Show-Help {
128128
Write-Option " art make:model User -m # Create model with migration"
129129
Write-Option " art tinker # Interactive REPL"
130130

131+
Write-Header "TOOLKIT MANAGEMENT"
132+
Write-Cmd "Update-Toolkit" "Self-update the toolkit from git"
133+
Write-Option " Update-Toolkit # Pull latest and reload"
134+
Write-Option " Update-Toolkit -CheckOnly # Check without applying"
135+
Write-Option " Update-Toolkit -Force # Skip confirmation prompt"
136+
Write-Option " Auto-checks on startup (set toolkit.updateCheckDays in config.json)"
137+
131138
Write-Header "KEYBOARD SHORTCUTS"
132139
Write-Cmd "$([char]0x2191) / $([char]0x2193)" "Search history by prefix"
133140
Write-Cmd "Ctrl+R" "Reverse search history"
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
function Update-Toolkit {
2+
<#
3+
.SYNOPSIS
4+
Self-update the PowerShell Dev Toolkit from its git remote.
5+
6+
.DESCRIPTION
7+
Pulls the latest changes from the toolkit's git repository, shows a
8+
summary of what changed, and re-imports the module so new commands and
9+
aliases take effect immediately.
10+
11+
.PARAMETER CheckOnly
12+
Only check whether updates are available without applying them.
13+
14+
.PARAMETER Force
15+
Skip the confirmation prompt and apply updates immediately.
16+
17+
.EXAMPLE
18+
Update-Toolkit
19+
.EXAMPLE
20+
Update-Toolkit -CheckOnly
21+
.EXAMPLE
22+
Update-Toolkit -Force
23+
#>
24+
[CmdletBinding()]
25+
param(
26+
[switch]$CheckOnly,
27+
[switch]$Force
28+
)
29+
30+
$toolkitDir = $script:ToolkitRoot
31+
32+
if (-not (Test-Path (Join-Path $toolkitDir ".git"))) {
33+
Write-Error "Toolkit directory is not a git repository: $toolkitDir"
34+
return
35+
}
36+
37+
$git = Get-Command git -ErrorAction SilentlyContinue
38+
if (-not $git) {
39+
Write-Error "Git is not installed or not in PATH."
40+
return
41+
}
42+
43+
Push-Location $toolkitDir
44+
try {
45+
$currentBranch = git rev-parse --abbrev-ref HEAD 2>$null
46+
if (-not $currentBranch) {
47+
Write-Error "Failed to determine current git branch."
48+
return
49+
}
50+
51+
Write-Host "Checking for updates..." -ForegroundColor Cyan
52+
git fetch origin $currentBranch --quiet 2>$null
53+
54+
$localHead = git rev-parse HEAD 2>$null
55+
$remoteHead = git rev-parse "origin/$currentBranch" 2>$null
56+
57+
if ($localHead -eq $remoteHead) {
58+
Write-Host "Already up to date." -ForegroundColor Green
59+
$manifest = Import-PowerShellDataFile (Join-Path $toolkitDir "PowerShellDevToolkit\PowerShellDevToolkit.psd1")
60+
Write-Host " Version: $($manifest.ModuleVersion)" -ForegroundColor Gray
61+
Write-Host " Branch: $currentBranch" -ForegroundColor Gray
62+
return
63+
}
64+
65+
$behind = git rev-list --count "HEAD..origin/$currentBranch" 2>$null
66+
Write-Host "$behind new commit(s) available on $currentBranch" -ForegroundColor Yellow
67+
Write-Host ""
68+
69+
$log = git log --oneline "HEAD..origin/$currentBranch" 2>$null
70+
if ($log) {
71+
Write-Host "Changes:" -ForegroundColor Cyan
72+
$log | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
73+
Write-Host ""
74+
}
75+
76+
$diffStat = git diff --stat "HEAD..origin/$currentBranch" 2>$null
77+
if ($diffStat) {
78+
Write-Host "Files changed:" -ForegroundColor Cyan
79+
$diffStat | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
80+
Write-Host ""
81+
}
82+
83+
if ($CheckOnly) { return }
84+
85+
if (-not $Force) {
86+
Write-Host "Apply update? (Y/N): " -NoNewline -ForegroundColor Yellow
87+
$response = Read-Host
88+
if ($response -ne 'Y' -and $response -ne 'y') {
89+
Write-Host "Update cancelled." -ForegroundColor Gray
90+
return
91+
}
92+
}
93+
94+
Write-Host "Pulling changes..." -ForegroundColor Cyan
95+
$pullOutput = git pull origin $currentBranch 2>&1
96+
$pullExitCode = $LASTEXITCODE
97+
98+
if ($pullExitCode -ne 0) {
99+
Write-Host "Git pull failed:" -ForegroundColor Red
100+
$pullOutput | ForEach-Object { Write-Host " $_" -ForegroundColor Red }
101+
Write-Host ""
102+
Write-Host "You may need to resolve conflicts manually." -ForegroundColor Yellow
103+
return
104+
}
105+
106+
$pullOutput | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
107+
108+
Write-Host ""
109+
Write-Host "Re-importing module..." -ForegroundColor Cyan
110+
Import-Module (Join-Path $toolkitDir "PowerShellDevToolkit") -Force -Global -DisableNameChecking
111+
112+
$manifest = Import-PowerShellDataFile (Join-Path $toolkitDir "PowerShellDevToolkit\PowerShellDevToolkit.psd1")
113+
Write-Host ""
114+
Write-Host "Updated to version $($manifest.ModuleVersion)" -ForegroundColor Green
115+
Write-Host "All commands and aliases are now current." -ForegroundColor Green
116+
117+
Set-ToolkitUpdateTimestamp
118+
} finally {
119+
Pop-Location
120+
}
121+
}
122+
123+
function Test-ToolkitUpdate {
124+
<#
125+
.SYNOPSIS
126+
Silently check if toolkit updates are available (used on shell startup).
127+
128+
.DESCRIPTION
129+
Compares the local HEAD against the remote. Returns $true if there are
130+
commits to pull. Respects the updateCheckDays setting in config.json
131+
so it only hits the network at the configured frequency.
132+
#>
133+
[CmdletBinding()]
134+
param()
135+
136+
$toolkitDir = $script:ToolkitRoot
137+
138+
if (-not (Test-Path (Join-Path $toolkitDir ".git"))) { return }
139+
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { return }
140+
141+
$stampFile = Join-Path $toolkitDir ".last-update-check"
142+
$config = Get-ScriptConfig -ErrorAction SilentlyContinue
143+
$intervalDays = 1
144+
if ($config -and $config.toolkit -and $null -ne $config.toolkit.updateCheckDays) {
145+
$intervalDays = [int]$config.toolkit.updateCheckDays
146+
}
147+
148+
if ($intervalDays -le 0) { return }
149+
150+
if (Test-Path $stampFile) {
151+
$lastCheck = (Get-Item $stampFile).LastWriteTime
152+
if (([datetime]::Now - $lastCheck).TotalDays -lt $intervalDays) { return }
153+
}
154+
155+
Push-Location $toolkitDir
156+
try {
157+
$branch = git rev-parse --abbrev-ref HEAD 2>$null
158+
if (-not $branch) { return }
159+
160+
git fetch origin $branch --quiet 2>$null
161+
$local = git rev-parse HEAD 2>$null
162+
$remote = git rev-parse "origin/$branch" 2>$null
163+
164+
Set-ToolkitUpdateTimestamp
165+
166+
if ($local -ne $remote) {
167+
$behind = git rev-list --count "HEAD..origin/$branch" 2>$null
168+
Write-Host ""
169+
Write-Host "PowerShell Dev Toolkit: $behind update(s) available. Run " -NoNewline -ForegroundColor Yellow
170+
Write-Host "Update-Toolkit" -NoNewline -ForegroundColor Cyan
171+
Write-Host " to update." -ForegroundColor Yellow
172+
}
173+
} finally {
174+
Pop-Location
175+
}
176+
}
177+
178+
function Set-ToolkitUpdateTimestamp {
179+
<# Touches the .last-update-check stamp file. #>
180+
[CmdletBinding()]
181+
param()
182+
$stampFile = Join-Path $script:ToolkitRoot ".last-update-check"
183+
[IO.File]::WriteAllText($stampFile, (Get-Date -Format 'o'))
184+
}

config.example.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,8 @@
3131
},
3232
"editor": {
3333
"notepadPlusPlus": "C:\\Program Files\\Notepad++\\notepad++.exe"
34+
},
35+
"toolkit": {
36+
"updateCheckDays": 1
3437
}
3538
}

docs/COMMANDS.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
| [SSH](#ssh-commands) | `cssh`, `tunnel`, `tssh` |
1414
| [AI Integration](#ai-integration-commands) | `ai-rules`, `context` |
1515
| [Development](#development-commands) | `port`, `proj`, `serve`, `gs`, `search`, `http`, `services`, `useenv`, `tail`, `clip`, `art` |
16+
| [Toolkit Management](#toolkit-management) | `Update-Toolkit` |
1617

1718
---
1819

@@ -362,6 +363,35 @@ art cache:clear # Clear application cache
362363

363364
---
364365

366+
## Toolkit Management
367+
368+
### `Update-Toolkit`
369+
Self-update the toolkit by pulling the latest changes from git.
370+
371+
```powershell
372+
Update-Toolkit # Pull latest, show changes, reload module
373+
Update-Toolkit -CheckOnly # Check for updates without applying
374+
Update-Toolkit -Force # Skip confirmation prompt
375+
```
376+
377+
After updating, the module is re-imported automatically so new commands and aliases are available immediately.
378+
379+
**Automatic Update Checks:**
380+
381+
The toolkit checks for available updates once per day on shell startup (configurable). To change the frequency, set `toolkit.updateCheckDays` in `config.json`:
382+
383+
```json
384+
{
385+
"toolkit": {
386+
"updateCheckDays": 7
387+
}
388+
}
389+
```
390+
391+
Set to `0` to disable automatic checks entirely.
392+
393+
---
394+
365395
## Keyboard Shortcuts
366396

367397
| Shortcut | Action |

helpme.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ if (-not $ScriptsOnly) {
111111
Write-Option " art make:model User -m # Create model with migration"
112112
Write-Option " art tinker # Interactive REPL"
113113

114+
Write-Header "TOOLKIT MANAGEMENT"
115+
Write-Cmd "Update-Toolkit" "Self-update the toolkit from git"
116+
Write-Option " Update-Toolkit # Pull latest and reload"
117+
Write-Option " Update-Toolkit -CheckOnly # Check without applying"
118+
Write-Option " Update-Toolkit -Force # Skip confirmation prompt"
119+
Write-Option " Auto-checks on startup (set toolkit.updateCheckDays in config.json)"
120+
114121
Write-Header "KEYBOARD SHORTCUTS"
115122
Write-Cmd "↑ / ↓" "Search history by prefix"
116123
Write-Cmd "Ctrl+R" "Reverse search history"

tests/Update-Toolkit.Tests.ps1

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
BeforeAll {
2+
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
3+
Import-Module (Join-Path $repoRoot "PowerShellDevToolkit") -Force -DisableNameChecking
4+
}
5+
6+
Describe "Update-Toolkit" {
7+
It "Should be exported from the module" {
8+
$cmd = Get-Command Update-Toolkit -Module PowerShellDevToolkit -ErrorAction SilentlyContinue
9+
($null -ne $cmd) | Should -Be $true
10+
}
11+
12+
It "Should expose -CheckOnly and -Force parameters" {
13+
$info = Get-Command Update-Toolkit -Module PowerShellDevToolkit
14+
($info.Parameters.ContainsKey('CheckOnly')) | Should -Be $true
15+
($info.Parameters.ContainsKey('Force')) | Should -Be $true
16+
}
17+
18+
It "Should report status when run with -CheckOnly" {
19+
$output = Update-Toolkit -CheckOnly *>&1 | Out-String
20+
$hasStatus = ($output -match 'up to date') -or ($output -match 'available') -or ($output -match 'not a git')
21+
$hasStatus | Should -Be $true
22+
}
23+
24+
It "Should show current version when up to date" {
25+
$output = Update-Toolkit -CheckOnly *>&1 | Out-String
26+
if ($output -match 'up to date') {
27+
($output -match 'Version') | Should -Be $true
28+
}
29+
}
30+
}
31+
32+
Describe "Test-ToolkitUpdate" {
33+
It "Should be exported from the module" {
34+
$cmd = Get-Command Test-ToolkitUpdate -Module PowerShellDevToolkit -ErrorAction SilentlyContinue
35+
($null -ne $cmd) | Should -Be $true
36+
}
37+
38+
It "Should not throw on a valid git repo" {
39+
{ Test-ToolkitUpdate } | Should -Not -Throw
40+
}
41+
}
42+
43+
Describe "Set-ToolkitUpdateTimestamp" {
44+
It "Should create the stamp file" {
45+
$stampFile = Join-Path $repoRoot ".last-update-check"
46+
if (Test-Path $stampFile) { Remove-Item $stampFile }
47+
& (Get-Module PowerShellDevToolkit) { Set-ToolkitUpdateTimestamp }
48+
(Test-Path $stampFile) | Should -Be $true
49+
Remove-Item $stampFile -ErrorAction SilentlyContinue
50+
}
51+
52+
It "Should write a valid ISO 8601 timestamp" {
53+
& (Get-Module PowerShellDevToolkit) { Set-ToolkitUpdateTimestamp }
54+
$stampFile = Join-Path $repoRoot ".last-update-check"
55+
$content = Get-Content $stampFile -Raw
56+
{ [datetime]::Parse($content) } | Should -Not -Throw
57+
Remove-Item $stampFile -ErrorAction SilentlyContinue
58+
}
59+
}

tests/Use-NppForGit.Tests.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ Describe "Use-NppForGit" {
3131

3232
try {
3333
'{"editor":{"notepadPlusPlus":"C:\\does_not_exist\\notepad++.exe"}}' | Set-Content $configPath
34+
Import-Module (Join-Path $repoRoot "PowerShellDevToolkit") -Force -DisableNameChecking
3435
Use-NppForGit -ErrorAction SilentlyContinue -ErrorVariable err
3536
($err.Count -gt 0) | Should -Be $true
36-
($err[0].Exception.Message -match 'not found|cannot find|does not exist') | Should -Be $true
37+
($err[0].ToString() -match 'not found|cannot find|does not exist|Notepad') | Should -Be $true
3738
} finally {
3839
if ($savedConfig) {
3940
Set-Content $configPath $savedConfig

0 commit comments

Comments
 (0)