Skip to content

feat(host-prep): wire --force-main-vhost to allow a deliberate main-vhost swap (static->node cutover) - #191

Merged
samo-agent merged 2 commits into
mainfrom
feat/force-main-vhost-wiring
Jul 21, 2026
Merged

feat(host-prep): wire --force-main-vhost to allow a deliberate main-vhost swap (static->node cutover)#191
samo-agent merged 2 commits into
mainfrom
feat/force-main-vhost-wiring

Conversation

@samo-agent

Copy link
Copy Markdown
Collaborator

Root-cause analysis

runEnvPlan (src/commands/env.ts ~340) calls buildHostPrepScript(r.app, r.vm.sshUser) with no firewallOpts argument, so bakedForce is hardcoded false. Additionally, no --force-main-vhost token was parsed in src/cli.ts. The result: the PR-0 landmine guard in samohost_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_server vhost differs from the staged node reverse_proxy block → guard refuses → conversion stuck.

Fix

Three surgical changes, no behavioral change to the default-refuse path:

  1. src/cli.ts parseEnvPlan: parse --force-main-vhost boolean flag. Defaults false. Added to HELP text under env options.
  2. src/commands/env.ts EnvPlanInput: add forceMainVhost?: boolean field with JSDoc documenting the static->node use-case and the safe default.
  3. src/commands/env.ts runEnvPlan: thread forceMainVhost into buildHostPrepScript as { forceMainVhost: true } when the flag is set; passes undefined (hardcoded-false path) otherwise.

Safety preserved

  • Default (no flag): bakedForce="false" → guard refuses to overwrite a differing live file. Behavior unchanged.
  • With --force-main-vhost: bakedForce="true" → guard runs the existing backup + caddy validate + rollback-on-failure path (already tested in test/host-prep-landmine-guard.test.ts tests (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:

# Scenario Result
1 CLI: --force-main-vhost sets forceMainVhost=true in parsed input was RED, now GREEN
2 CLI: without the flag, forceMainVhost is falsy (safe default) GREEN throughout
3 CLI: flag accepted with --branch too (orthogonal to --host-prep) was RED, now GREEN
4 runEnvPlan + forceMainVhost=true → emitted bash has force=true baked in was RED, now GREEN
5 runEnvPlan without flag → emitted bash has force=false (guard preserved) GREEN throughout
6 E2E bash: force=true + differing live vhost → overwrite, backup, validate, reload GREEN throughout (guard already implemented)
7 E2E bash: force=false + differing live vhost → REFUSE, file unchanged GREEN throughout
7b E2E bash: force=false + custom multi-service vhost → REFUSE (samograph landmine) GREEN throughout
8 E2E bash: force=true + caddy validate failure → RESTORE backup (rollback intact) GREEN throughout
bonus Full CLI→runEnvPlan→script: --force-main-vhost produces force=true end-to-end was RED, now GREEN

Checklist

  • tsc --noEmit: clean
  • Local suite: 2326 pass (pg.test.ts requires CI Postgres service — green in CI)
  • RED commit: b5885e8
  • GREEN commit: 7c9d929
  • NOT merged (per instructions)

samo-agent and others added 2 commits July 21, 2026 16:38
…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>
@samo-agent
samo-agent merged commit cececdf into main Jul 21, 2026
1 check passed
@samo-agent
samo-agent deleted the feat/force-main-vhost-wiring branch July 21, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant