feat(host-prep): wire --force-main-vhost to allow a deliberate main-vhost swap (static->node cutover) - #191
Merged
Merged
Conversation
…an->buildHostPrepScript) Tests pin the three broken points: - CLI: --force-main-vhost throws UsageError (not yet parsed) - EnvPlanInput: forceMainVhost field not yet threaded into buildHostPrepScript - runEnvPlan: always passes firewallOpts=undefined → bakedForce hardcoded false 4 tests RED, 6 pass (default-refuse guard and bash-level guard already work). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…host swap (static->node cutover)
Root cause: runEnvPlan called buildHostPrepScript(r.app, r.vm.sshUser) with no
firewallOpts, so bakedForce was hardcoded false. No --force-main-vhost token
existed in src/cli.ts, making a static->node vhost conversion impossible via
the automated host-prep path (the PR-0 landmine guard always refused).
Fix:
1. src/cli.ts parseEnvPlan: parse --force-main-vhost boolean flag. Default false.
Added to HELP text under env options.
2. src/commands/env.ts EnvPlanInput: add optional forceMainVhost?: boolean field
with full JSDoc describing the static->node use-case and the safety default.
3. src/commands/env.ts runEnvPlan: thread forceMainVhost into buildHostPrepScript
as { forceMainVhost: true } when the flag is set; undefined (default-refuse)
otherwise.
Safety preserved:
- Default (no flag): bakedForce="false" → guard refuses to overwrite. Unchanged.
- With flag: bakedForce="true" → guard runs the existing backup+caddy validate+
rollback path (already tested in host-prep-landmine-guard.test.ts tests (c)/(d)).
- A bad node vhost cannot break routing: caddy validate gate + rollback-on-failure
is the same path, now reachable via --force-main-vhost.
Tests: 10 scenarios across force/no-force/rollback covering CLI->runEnvPlan->bash.
tsc --noEmit clean. Full local suite: 2326 pass (pg.test.ts needs CI Postgres).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root-cause analysis
runEnvPlan(src/commands/env.ts ~340) callsbuildHostPrepScript(r.app, r.vm.sshUser)with nofirewallOptsargument, sobakedForceis hardcodedfalse. Additionally, no--force-main-vhosttoken was parsed insrc/cli.ts. The result: the PR-0 landmine guard insamohost_apply_main_vhost()always refuses to overwrite a differing live main vhost, with no supported automated escape path.This is the #1 blocker for a push-button static->node cutover (e.g. game-changers): the live static
file_servervhost differs from the staged nodereverse_proxyblock → guard refuses → conversion stuck.Fix
Three surgical changes, no behavioral change to the default-refuse path:
src/cli.tsparseEnvPlan: parse--force-main-vhostboolean flag. Defaultsfalse. Added to HELP text under env options.src/commands/env.tsEnvPlanInput: addforceMainVhost?: booleanfield with JSDoc documenting the static->node use-case and the safe default.src/commands/env.tsrunEnvPlan: threadforceMainVhostintobuildHostPrepScriptas{ forceMainVhost: true }when the flag is set; passesundefined(hardcoded-false path) otherwise.Safety preserved
bakedForce="false"→ guard refuses to overwrite a differing live file. Behavior unchanged.--force-main-vhost:bakedForce="true"→ guard runs the existing backup +caddy validate+ rollback-on-failure path (already tested intest/host-prep-landmine-guard.test.tstests (c)/(d)). A bad node vhost cannot break routing: the validate gate + rollback is the same code path, now reachable via the explicit opt-in flag.Tests (
test/force-main-vhost-wiring.test.ts)10 scenarios, strict TDD RED→GREEN:
--force-main-vhostsetsforceMainVhost=truein parsed inputforceMainVhostis falsy (safe default)--branchtoo (orthogonal to--host-prep)runEnvPlan+forceMainVhost=true→ emitted bash hasforce=truebaked inrunEnvPlanwithout flag → emitted bash hasforce=false(guard preserved)force=true+ differing live vhost → overwrite, backup, validate, reloadforce=false+ differing live vhost → REFUSE, file unchangedforce=false+ custom multi-service vhost → REFUSE (samograph landmine)force=true+ caddy validate failure → RESTORE backup (rollback intact)runEnvPlan→script:--force-main-vhostproducesforce=trueend-to-endChecklist