Commit 0774aa9
add hostProject support for Windows-resident worktrees (#20)
* add hostProject schema and validation
Introduces a `type` discriminator on projects[] with two variants: `distro`
(existing default — bare mirror inside the distro) and `host` (new — Windows-
side checkout, sessions live on the host and mount into the distro). The
host variant adds `hostCheckout` (required) and `hostShadows` (list of host
tools to wrap via a per-session PATH shim).
Schema validation rejects cross-type field usage (no `remote` on host,
no `hostCheckout`/`hostShadows` on distro) and disallows the global-scoped
`hostTools` block under a hostProject — wrappers must go through `hostShadows`
so they land in a per-project bin dir instead of /usr/local/bin (which would
shadow distro-installed tools for parallel distroProject sessions).
This commit is scaffolding only: the new fields are validated but not yet
wired into Projects/Sessions/Mounts. Subsequent commits add the resolver,
the worktree + mount lifecycle, and the entry-point wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* add host-shadow resolver and per-project bin-dir helpers
HostShadows.psm1 takes a shadow name (`pwsh`, `git`, ...) and returns the
concrete Windows .exe to wrap into the distro. Resolution order is PATH-first
(via `where.exe`) so the user's day-to-day install wins, with a built-in
catalog of well-known install paths as a backup for headless / pruned-PATH
environments. A mismatch between PATH and catalog yields a warning so users
can pin via the explicit `{ name, windowsExe }` form.
Install-HostShadowsForProject writes the resolved wrappers into a per-project
bin dir at /home/claude/host-projects/<project>/bin/, which open-claudearium
prepends to PATH only for sessions of that hostProject. This keeps the host
wrappers strictly scoped to their project — distro-installed `git`/`pwsh`
remain visible to every other session.
Also tightens the test-description regex in pure/Gotchas.Tests.ps1: the
original `[^'"]*` class blinded the scanner to a `<word>` placeholder that
sat after a nested opposite-quote (`It "... '<name>.exe'"`). I stepped on
this writing the new HostShadows tests, so the fix lands in the same commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* wire hostProject through project / session / mount lifecycle
End-to-end plumbing for the new hostProject type:
* `project add -HostProject -HostCheckout <path>` registers a host-resident
project (no bare mirror, no clone), resolves hostShadows via where.exe
with built-in fallbacks, and installs wrappers into a per-project bin
dir at /home/claude/host-projects/<project>/bin/.
* `session new` and `session remove` branch on the profile-recorded
project type. For hostProjects the worktree is created on the Windows
side at `<hostCheckout>-sessions\<name>` and auto-mounted into the
distro at `/host/<project>/<name>` via Mounts.Get-MergedDesiredMounts
(profile.hostMounts ∪ session mounts).
* `project remove` for a hostProject tears down every session (host
worktrees + mounts) and the per-project bin dir, but leaves
hostCheckout itself untouched.
* `open-claudearium.ps1` sources a per-project init.sh that prepends the
bin dir to PATH. The init.sh lives at
/home/claude/host-projects/<project>/init.sh and is read by bash from
disk — avoiding wsl2-gotchas.md #1, where putting `$PATH` in the
wsl.exe argv would mangle it to empty and break every host session.
Parallel distroProject sessions are entirely unaffected: PATH overrides
are bash-local to the host session, and the distro's /usr/bin tools stay
authoritative for every other shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* document hostProject support
* Cookbook recipe: "Work on a Windows-specific project as a hostProject",
including the dogfood case (using Claudearium itself as a hostProject).
* usage.md: project add gets a -HostProject branch with the example
layout; session new/remove get the type-aware behavior described.
* design-decisions.md §22: rationale for per-session PATH shadowing
(no global hostTools cross-talk between hostProject and distroProject
sessions sharing the same distro).
* wsl2-gotchas.md #20: routing PATH prepend through a per-project
init.sh sourced by the launcher, after we hit gotcha #1 again with
literal `$PATH` in the wsl.exe argv.
* host-tool-notes/pwsh.md + git.md: per-tool argv / path-translation
caveats Claude will see when working in a hostProject session.
* README: one-line mention under features.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* add hostProject end-to-end distro test
Covers the full hostProject lifecycle against the ephemeral test distro:
* `project add` records `type=host`, `hostCheckout`, and `hostShadows` in
the profile, deploys the per-project bin dir + init.sh, and the init.sh
carries a literal `:$PATH` (verifying gotcha #20's fix survives a real
base64-transport round-trip into the distro).
* `session new` creates the sibling host worktree at
`<checkout>-sessions/<name>` and registers an fstab managed-block entry
for `/host/<project>/<session>`; the seed file is readable through the
mount.
* `session remove -Force` tears down the worktree and the mount while
leaving the project-scoped bin dir intact for any other sessions.
* `project remove -Force` finally drops the bin dir, but the user's
hostCheckout itself is never touched.
The test stands up a real one-commit git checkout in `%TEMP%` and uses
that as `hostCheckout`, so it exercises the same git worktree path the
production code uses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix reconcile + mount verbs to handle hostProjects without trampling
Three issues surfaced by code review and the first distro-lane run:
1. reconcile crashed on any profile containing a hostProject. Get-ProjectsActualFromDistro
only enumerated bare mirrors under /home/claude/mirrors/, so every hostProject
diffed as 'add' and Invoke-ProjectsApply unconditionally called New-ProjectMirror
-Remote (which is empty for hostProjects). Both sides now branch on type:
the actual list also enumerates /home/claude/host-projects/<name>/, and the
apply path dispatches to Invoke-HostProjectApply / Remove-HostShadowsForProject.
2. mount add/remove/sync called Set-HostMountsInDistro directly with just
profile.hostMounts, which wiped session-derived fstab entries — any
running hostProject session would lose its mount. They now route through
Invoke-MergedMountsApply (profile ∪ session-derived). Same fix is applied
to reconcile's mountsDiff computation.
3. Invoke-ProjectAdd's hostProject branch crashed when the host checkout had
no `origin` remote: Resolve-SmartProjectName has a mandatory non-empty
parameter and the unconditional `-Remote (Resolve-SmartRemote ...)` pass-
through fed it $null. Guarded.
Bonus: Invoke-SessionNew's hostProject path now wraps the worktree +
state + mount + shadow sequence in try/finally, rolling back the host
worktree and state if a downstream step throws. Distro test:
HostProjects.Tests.ps1 had a literal `$PATH` inside a double-quoted It
description that crashed Pester discovery under StrictMode (same trap as
the gotchas regex catches) — switched to single quotes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fall back to --detach when a hostProject session would collide with another worktree
Every hostCheckout is itself a worktree, so `git worktree add ... <branch>`
for a session refuses with exit 128 ("'branch' is already used by worktree
at ...") whenever the user's main checkout sits on the same branch they
asked the session to use. CI distro lane surfaced this on the first
end-to-end run.
New-HostSession now reads `git worktree list --porcelain` and detects
when the requested branch is already claimed by any existing worktree.
On collision it adds `--detach` to the worktree-add invocation and notes
the fallback in the dashboard output — the session lands at the branch
tip in detached HEAD, and the user can `git switch -c <name>` inside if
they want to start committing. Distroguarded with `-NewBranch`: passing
that flag still creates a fresh branch off `-BaseBranch` as before.
The end-to-end test now exercises both paths: the existing-branch case
(which hits the auto-detach fallback because the seed checkout is on
master) and a `-NewBranch -BaseBranch master` case that verifies the
fresh-branch happy path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix host-session lookup that throws "Index outside bounds" under StrictMode
The HostProjects distro test ("tears down the worktree and the mount,
leaving the bin dir intact") fails on the `session remove` path with
"Index was outside the bounds of the array." Root cause is that
Get-Sessions returns `,$all` to preserve array shape across the function
boundary, but Remove-HostSession then does:
$session = @(Get-Sessions ... | Where-Object { ... })[0]
Piping the comma-wrapped result without parens delivers the entire
array as a single `$_` to Where-Object. The filter's `.name` test then
runs against the array (not its elements), the predicate fails for any
real session, `@()` collects nothing, and `[0]` on the empty result
throws under StrictMode.
Same root cause hits two more call sites:
- Invoke-ProjectRemove iterated over @(Get-Sessions ...) — the bare
function call wrapped in @() ends up as a 1-element array containing
the inner sessions array, so the foreach binds `$s` to the whole
array and `[string]$s.name` broadcasts member-access enumeration
into a space-joined string of every session name. The cascade
delete then called Remove-HostSession with that joined-string name,
which threw before any session was actually torn down.
- open-claudearium.ps1 dashboard built its session table the same way
— `$sessions.Count` was always 1 when any sessions existed, and the
loop displayed broadcast-joined `.project` / `.name` / `.branch`
fields instead of one row per session.
All three sites now read Get-Sessions into a plain variable and iterate
explicitly. Test-SessionExists is unchanged: it pipes through parens
(`(Get-Sessions ...) | Where ...`), which forces the wrapper to unroll
before reaching the predicate, so it was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent ea40fa4 commit 0774aa9
20 files changed
Lines changed: 1931 additions & 88 deletions
File tree
- docs
- modules
- templates
- host-tool-notes
- tests
- distro
- lib
- pure
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
168 | 233 | | |
169 | 234 | | |
170 | 235 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
456 | 456 | | |
457 | 457 | | |
458 | 458 | | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
| 56 | + | |
55 | 57 | | |
56 | 58 | | |
57 | | - | |
| 59 | + | |
58 | 60 | | |
59 | 61 | | |
60 | | - | |
| 62 | + | |
61 | 63 | | |
62 | 64 | | |
63 | | - | |
| 65 | + | |
64 | 66 | | |
65 | 67 | | |
66 | 68 | | |
67 | 69 | | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
68 | 76 | | |
69 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
70 | 82 | | |
71 | 83 | | |
72 | 84 | | |
73 | 85 | | |
74 | | - | |
| 86 | + | |
75 | 87 | | |
76 | 88 | | |
77 | 89 | | |
78 | 90 | | |
79 | 91 | | |
80 | | - | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
81 | 96 | | |
82 | 97 | | |
83 | | - | |
| 98 | + | |
84 | 99 | | |
85 | 100 | | |
86 | | - | |
| 101 | + | |
87 | 102 | | |
88 | | - | |
89 | 103 | | |
90 | | - | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
91 | 107 | | |
92 | 108 | | |
93 | 109 | | |
94 | | - | |
| 110 | + | |
95 | 111 | | |
96 | 112 | | |
97 | 113 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
528 | 528 | | |
529 | 529 | | |
530 | 530 | | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
531 | 570 | | |
532 | 571 | | |
533 | 572 | | |
| |||
551 | 590 | | |
552 | 591 | | |
553 | 592 | | |
| 593 | + | |
0 commit comments