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
120 changes: 120 additions & 0 deletions claudearium.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ param(
[switch]$DiscardDirty,
[string]$Scope,
[switch]$DryRun,
[switch]$IncludeTodos,
[switch]$IncludePlans,
[string]$HostPath,
[string]$Guest,
[string]$Mode,
Expand Down Expand Up @@ -90,6 +92,7 @@ Import-Module (Join-Path $Script:ModulesDir 'HostToolNotes.psm1') -Force
Import-Module (Join-Path $Script:ModulesDir 'SelfUpdate.psm1') -Force
Import-Module (Join-Path $Script:ModulesDir 'ToolUpdates.psm1') -Force
Import-Module (Join-Path $Script:ModulesDir 'Prune.psm1') -Force
Import-Module (Join-Path $Script:ModulesDir 'Temp.psm1') -Force
Set-VpnPayloadRoot -Path $Script:PayloadDir

# Snapshot the wt tab title so we can prefix it with '*' when tool updates are
Expand All @@ -111,6 +114,11 @@ Verbs:
reconcile Diff profile against state; prompt; apply.
prune [-Scope <a>] Drift detection + repair. -Scope sessions|worktrees|
mounts|artifacts|all. -DryRun to report only.
temp Print scratch sizes (/tmp, ~/.cache, ~/.claude).
temp size Same as bare.
temp clean -Scope <s> Wipe a scratch scope. -Scope tmp|cache|claude|all.
claude scope keeps todos/plans by default; pass
-IncludeTodos / -IncludePlans to wipe those too.

profile validate <path> Validate a profile (or the default profile if omitted).
profile export -Out <p> Write current state to a profile file at <p>.
Expand Down Expand Up @@ -1305,6 +1313,105 @@ function Invoke-Prune {
}
}

function Invoke-Temp {
# Scratch-space management. Three scopes — tmp / cache / claude — each
# owns a chunk of disk that's safe to reclaim under different rules.
# Subverbs:
# bare / (no subverb) → print sizes (cheap, no mutation)
# size → same as bare
# clean -Scope <s> → wipe the scope, prompt unless -Force
#
# claude-scope cleans transcripts + shell-snapshots by default; pass
# -IncludeTodos / -IncludePlans to widen the wipe to those preserve dirs.
$distro = Resolve-DistroForOps
if (-not (Test-DistroExists -Name $distro)) {
throw "Distro '$distro' does not exist; nothing to size or wipe."
}
$sub = if ($SubVerb) { $SubVerb.ToLowerInvariant() } else { 'size' }
switch ($sub) {
'size' { Invoke-TempSize -DistroName $distro }
'clean' { Invoke-TempClean -DistroName $distro }
default {
Write-Host "Unknown temp subverb: $SubVerb" -ForegroundColor Red
Write-Host "Subverbs: size | clean (or bare 'temp' for sizes)"
exit 64
}
}
}

function Invoke-TempSize {
[CmdletBinding()] param([Parameter(Mandatory)][string]$DistroName)
$s = Get-ScratchSizes -DistroName $DistroName
Write-Host ''
Write-Host '=== Claudearium scratch sizes ===' -ForegroundColor Cyan
Write-Host (' {0,-9} {1,10} {2}' -f 'scope','size','path')
Write-Host (' {0,-9} {1,10} {2}' -f '-----','----','----')
Write-Host (' {0,-9} {1,10} {2}' -f 'tmp', (Format-Bytes -Bytes $s.tmp), '/tmp')
Write-Host (' {0,-9} {1,10} {2}' -f 'cache', (Format-Bytes -Bytes $s.cache), '/home/claude/.cache')
Write-Host (' {0,-9} {1,10} {2}' -f 'claude', (Format-Bytes -Bytes $s.claude), '/home/claude/.claude')
Write-Host (' {0,-9} {1,10}' -f 'total', (Format-Bytes -Bytes $s.total))
}

function Invoke-TempClean {
[CmdletBinding()] param([Parameter(Mandatory)][string]$DistroName)
$scope = if ($Scope) { $Scope.ToLowerInvariant() } else { 'all' }
$validScopes = @('tmp', 'cache', 'claude', 'all')
if ($scope -notin $validScopes) {
throw "temp clean: -Scope must be one of: $($validScopes -join ', ') (got '$Scope')."
}
# Always size first so the user sees what's about to go.
$sizes = Get-ScratchSizes -DistroName $DistroName
$targets = if ($scope -eq 'all') { @('tmp','cache','claude') } else { @($scope) }

Write-Host ''
Write-Host "=== Claudearium temp clean (scope: $scope) ===" -ForegroundColor Cyan
foreach ($t in $targets) {
$sz = switch ($t) { 'tmp' { $sizes.tmp } 'cache' { $sizes.cache } 'claude' { $sizes.claude } }
Write-Host (" {0,-9} {1,10}" -f $t, (Format-Bytes -Bytes $sz))
}
if ($scope -in @('claude','all')) {
$wipe = @('projects','shell-snapshots')
if ($IncludeTodos) { $wipe += 'todos' }
if ($IncludePlans) { $wipe += 'plans' }
$preserved = @('todos','plans','host-tools') | Where-Object { $wipe -notcontains $_ }
Write-Host ''
Write-Host ' claude scope:'
Write-Host (" wipe: " + (($wipe | ForEach-Object { "~/.claude/$_/" }) -join ', '))
if ($preserved) {
Write-Host (" preserved: " + (($preserved | ForEach-Object { "~/.claude/$_/" }) -join ', '))
}
}

if (-not $Force) {
$ok = Read-YesNo -Prompt "Wipe the above?" -Default $false -NonInteractive:$NonInteractive
if (-not $ok) { Write-Host 'Aborted.' -ForegroundColor Yellow; return }
}

foreach ($t in $targets) {
$extra = @{}
if ($t -eq 'claude') {
if ($IncludeTodos) { $extra.IncludeTodos = $true }
if ($IncludePlans) { $extra.IncludePlans = $true }
}
$r = Clear-Scratch -DistroName $DistroName -Scope $t @extra
Write-Host (" [$t] removed $($r.Removed) — $($r.PreservedNote)") -ForegroundColor Green
Comment on lines +1372 to +1397
}

# Re-size after so the user gets the before/after delta in one go.
$after = Get-ScratchSizes -DistroName $DistroName
$reclaimed = 0L
foreach ($t in $targets) {
$reclaimed += switch ($t) {
'tmp' { $sizes.tmp - $after.tmp }
'cache' { $sizes.cache - $after.cache }
'claude' { $sizes.claude - $after.claude }
}
}
if ($reclaimed -lt 0) { $reclaimed = 0 }
Write-Host ''
Write-Host "Reclaimed: $((Format-Bytes -Bytes $reclaimed))" -ForegroundColor Green
}

function Invoke-ProfileValidate {
$path = if ($Arg) { $Arg } else { Resolve-ProfilePath }
Write-Host ''
Expand Down Expand Up @@ -3607,9 +3714,21 @@ function Invoke-CentralDashboard {
} else {
$vpnText = '-'
}
# Scratch sizes are one short `du -sb` in-distro — fast on a
# running distro, skipped when it's stopped to avoid waking it.
$scratchLine = '-'
if ($state -eq 'Running') {
try {
$sz = Get-ScratchSizes -DistroName $distro
$scratchLine = ("{0} (tmp {1}, cache {2}, claude {3})" -f `
(Format-Bytes -Bytes $sz.total), (Format-Bytes -Bytes $sz.tmp),
(Format-Bytes -Bytes $sz.cache), (Format-Bytes -Bytes $sz.claude))
} catch { $scratchLine = '?' }
Comment on lines +3717 to +3726
}
Write-Host (" Distro: {0,-20} ({1})" -f $distro, $state)
Write-Host (" VPN: {0}" -f $vpnText)
Write-Host (" Sessions: {0}" -f $sessionCount)
Write-Host (" Scratch: {0}" -f $scratchLine)
Write-Host (" Profile: {0}" -f (Resolve-ProfilePath))
}
Write-Host ''
Expand Down Expand Up @@ -3697,6 +3816,7 @@ try {
'nuke' { Invoke-Nuke }
'reconcile' { Invoke-Reconcile }
'prune' { Invoke-Prune }
'temp' { Invoke-Temp }
'profile' { Invoke-Profile }
'project' { Invoke-Project }
'session' { Invoke-Session }
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ sidestep argv mangling — see [wsl2-gotchas.md](./wsl2-gotchas.md#1-wslexe-argv
│ ├── Sessions.psm1 # git-worktree-per-session + Get-RecentBranches
│ ├── Mounts.psm1 # drvfs host mounts via /etc/fstab managed block
│ ├── Prune.psm1 # drift detection (orphan sessions, stale worktrees, dangling mounts, heavy artifacts)
│ ├── Temp.psm1 # scratch / cache sizing + scope-aware wipe (/tmp, ~/.cache, ~/.claude)
│ ├── Tools.psm1 # tool catalog (handler registry)
│ ├── ToolUpdates.psm1 # latest-upstream-version cache + background refresh
│ ├── HostTools.psm1 # WSL-interop wrappers for Windows .exe
Expand Down Expand Up @@ -159,7 +160,7 @@ exit 0 # avoid $LASTEXITCODE leak from internal `command -v` probes
| Category | Verbs |
|---|---|
| Lifecycle | `setup`, `status`, `nuke`, `update {check\|apply\|status}`, `diagnostics` |
| Declarative | `reconcile`, `prune {sessions\|worktrees\|mounts\|artifacts\|all}`, `profile {validate\|export\|edit\|show}` |
| Declarative | `reconcile`, `prune {sessions\|worktrees\|mounts\|artifacts\|all}`, `temp {size\|clean -Scope tmp\|cache\|claude\|all}`, `profile {validate\|export\|edit\|show}` |
| Repo work | `project {add\|list\|remove\|move\|show}` (+ bare dashboard), `session {new\|list\|remove}` (+ bare dashboard) |
| Distro plumbing | `mount {add\|list\|remove\|sync}` (+ bare dashboard) |
| Toolchain | `tools {list\|install\|enable\|disable\|sync\|attach}` (+ bare dashboard) |
Expand Down
21 changes: 21 additions & 0 deletions docs/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,27 @@ Cleanup:
# Per-project bin dir is gone. C:\GitHub\Claudearium itself is untouched.
```

### Get a quick read on scratch sizes — and reclaim them

The central dashboard's `Scratch:` row gives a one-line summary; the verb
prints the detailed table:

```powershell
.\claudearium.ps1 temp # size table only, no mutation
.\claudearium.ps1 temp clean -Scope tmp -Force # safe — tmpfs
.\claudearium.ps1 temp clean -Scope cache -Force # safe — slow first build
.\claudearium.ps1 temp clean -Scope claude -Force # transcripts + shell snapshots only
.\claudearium.ps1 temp clean -Scope claude -IncludeTodos -IncludePlans -Force # also wipes in-flight work
.\claudearium.ps1 temp clean -Scope all -Force # everything except ~/.claude/host-tools (we manage that)
```

The `claude` scope is selectively destructive: by default it wipes
`~/.claude/projects/` (transcripts) and `~/.claude/shell-snapshots/`, but
preserves `~/.claude/todos/`, `~/.claude/plans/`, and `~/.claude/host-tools/`.
`-IncludeTodos` and `-IncludePlans` opt into wiping the active-work
directories too; `host-tools/` is always preserved because the tool owns
that tree.

### Reclaim disk and resync after manual edits with `prune`

When you've been editing things outside the tool — `rm -rf` on a worktree,
Expand Down
4 changes: 2 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ on any failure — that's the form the GitHub Actions workflow uses.
Headline numbers as of this writing — Pester `It`-block counts for
the auto lanes (the manifest entries are coarser; each entry is a
test *file* that typically contains 3–10 individual assertions):
**366 pure** + **43 distro** = ~409 auto checks. The 4 **manual** entries
**373 pure** + **46 distro** = ~419 auto checks. The 4 **manual** entries
in the manifest aren't Pester `It` blocks — they're y/n prompts wired
through `Invoke-ManualTest` — bringing the suite total to ~413 checks. CI runs parse-check + pure on
through `Invoke-ManualTest` — bringing the suite total to ~423 checks. CI runs parse-check + pure on
every push to any branch; the distro lane runs on PRs and on `master`.
Manual is opt-in (never in CI); diag is on-demand.

Expand Down
10 changes: 10 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ Smart defaults pull from `-HostCheckout`'s `origin` URL (or the current working

Refuses if any session has uncommitted work, unless `-DiscardDirty` (or `-Force`) is set. A timestamped profile snapshot (`claudearium.profile.json.bak-<stamp>`) is written next to the profile before any mutation, for hand recovery.

## `temp [size | clean -Scope <area> [-IncludeTodos] [-IncludePlans] [-Force]]`

Runtime scratch / cache size + cleanup. Three scopes: `tmp` (`/tmp`), `cache` (`/home/claude/.cache`), and `claude` (`/home/claude/.claude`).

- Bare `temp` (or `temp size`) prints a per-scope size table.
- `temp clean -Scope <tmp|cache|claude|all>` wipes the named scope. `tmp` is always safe (tmpfs, wiped on reboot). `cache` is safe but a first-build penalty applies. `claude` defaults to wiping `~/.claude/projects/` (transcripts) and `~/.claude/shell-snapshots/` — `~/.claude/todos/`, `~/.claude/plans/`, and `~/.claude/host-tools/` are **preserved**. Pass `-IncludeTodos` / `-IncludePlans` to widen the wipe. `host-tools/` is never wiped (the tool owns that tree).
- `-Force` skips the confirmation prompt; useful for scripted cleanup.

The central dashboard's status block also surfaces the sizes so you can see disk pressure at a glance.

## `prune [-Scope <area>] [-DryRun] [-Force]`

Detect drift between state, profile, and the distro filesystem; repair (or, with `-DryRun`, just report). `-Scope` narrows the run to one area — defaults to `all`:
Expand Down
112 changes: 112 additions & 0 deletions modules/Temp.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Temp.psm1
# Runtime scratch / cache cleanup. Three scopes, each independently sized
# and wipeable:
#
# tmp — /tmp (tmpfs). Fully safe to wipe; reboot does it anyway.
# cache — /home/claude/.cache (xdg cache). Safe but slow first-rebuild
# (npm/pip/cargo redownloads).
# claude — /home/claude/.claude (Claude Code state). Destructive by
# default for transcripts + shell-snapshots; preserves todos
# + plans + host-tools unless -IncludeTodos / -IncludePlans
# is set.
#
# Sizes come from a single in-distro `du -sb` invocation so the dashboard
# render budget stays sub-second.
#
# Public surface:
# Get-ScratchSizes -DistroName — @{ tmp, cache, claude, total }, bytes
# Clear-Scratch -DistroName -Scope [-IncludeTodos -IncludePlans]
# — wipe the named scope, return @{ Removed; PreservedNote }
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Import-Module (Join-Path $PSScriptRoot 'Wsl.psm1')

# What `~/.claude/` paths the 'claude' scope wipes by default vs. preserves.
# The wipe set covers ephemeral stuff Claude Code regenerates on next run;
# the preserve set is in-flight user work (todos, plans) and our own managed
# tree (host-tools/). Both are arrays so the verb can render them in the
# preview before destruction.
$Script:ClaudeStateWipeDirs = @('projects', 'shell-snapshots')
$Script:ClaudeStatePreservedDirs = @('todos', 'plans', 'host-tools')

function Get-ScratchSizes {
# Batched single in-distro call: one `du -sb` per dir, output parsed by
# path. Missing dirs produce zero (so the cache score is honest even on
# a fresh distro that hasn't run npm/pip yet). Each `du` is capped with
# `timeout 3` so a cold page cache or a months-old ~/.claude transcript
# tree can't block the dashboard render — on timeout we treat the size
# as 0 and the caller is free to retry from the verb (which has no
# such latency budget).
[CmdletBinding()]
param([Parameter(Mandatory)][string]$DistroName)
$cmd = @'
for d in /tmp /home/claude/.cache /home/claude/.claude; do
if [ -d "$d" ]; then
sz=$(timeout 3 du -sb "$d" 2>/dev/null | cut -f1)
if [ -z "$sz" ]; then sz=0; fi
printf '%s\t%s\n' "$d" "$sz"
else
printf '%s\t0\n' "$d"
fi
done
'@
$r = Invoke-InDistroScript -Name $DistroName -User 'claude' -Script $cmd -AllowFail -CaptureOutput
$sizes = @{ tmp = 0L; cache = 0L; claude = 0L; total = 0L }
if ($r.ExitCode -ne 0) { return $sizes }
foreach ($line in @($r.Output)) {
$s = [string]$line
if ($s -match '^/tmp\s+(\d+)$') { $sizes.tmp = [long]$Matches[1] }
elseif ($s -match '^/home/claude/\.cache\s+(\d+)$') { $sizes.cache = [long]$Matches[1] }
elseif ($s -match '^/home/claude/\.claude\s+(\d+)$') { $sizes.claude = [long]$Matches[1] }
}
$sizes.total = $sizes.tmp + $sizes.cache + $sizes.claude
return $sizes
}

function Clear-Scratch {
# Wipe one scope and return what was done. Single in-distro script per
# call so failures atomic (or at least scoped to one shell pipeline).
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$DistroName,
[Parameter(Mandatory)][ValidateSet('tmp','cache','claude')][string]$Scope,
[switch]$IncludeTodos,
[switch]$IncludePlans
)
switch ($Scope) {
'tmp' {
# mindepth 1 keeps the /tmp mountpoint itself.
$script = 'find /tmp -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null; echo done'
Invoke-InDistroScript -Name $DistroName -User 'claude' -Script $script -AllowFail | Out-Null
return @{ Removed = '/tmp/*'; PreservedNote = 'reboot does this too — safe to wipe anytime' }
}
'cache' {
$script = '[ -d /home/claude/.cache ] && find /home/claude/.cache -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null; echo done'
Invoke-InDistroScript -Name $DistroName -User 'claude' -Script $script -AllowFail | Out-Null
return @{ Removed = '~/.cache/*'; PreservedNote = 'first-build penalty applies as npm/pip/cargo redownload' }
}
'claude' {
# Build the wipe list dynamically so -IncludeTodos / -IncludePlans
# add to it without us hard-coding two extra paths in the bash side.
$wipeDirs = @($Script:ClaudeStateWipeDirs)
if ($IncludeTodos) { $wipeDirs += 'todos' }
if ($IncludePlans) { $wipeDirs += 'plans' }
$removed = @()
$body = ''
foreach ($d in $wipeDirs) {
$body += "[ -d /home/claude/.claude/$d ] && find /home/claude/.claude/$d -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null`n"
$removed += "~/.claude/$d/*"
}
$body += "echo done`n"
Invoke-InDistroScript -Name $DistroName -User 'claude' -Script $body -AllowFail | Out-Null
$preservedDirs = $Script:ClaudeStatePreservedDirs | Where-Object { $_ -ne $null -and ($wipeDirs -notcontains $_) }
$note = if ($preservedDirs) { "preserved: " + (($preservedDirs | ForEach-Object { "~/.claude/$_/" }) -join ', ') } else { 'every subdir wiped' }
return @{ Removed = ($removed -join ', '); PreservedNote = $note }
}
}
}

Export-ModuleMember -Function `
Get-ScratchSizes, `
Clear-Scratch
Loading
Loading