feat(setup): two-step wizard - #52
Open
Behnam-RK wants to merge 2 commits into
Open
Conversation
This was referenced Aug 21, 2026
uninstall.sh removed only root-owned state, so everything belonging to the logged-in user survived it: the preference domain, the login-keychain control token, and the login-item registration. That is also why the setup wizard stopped appearing after a reinstall — FirstRunDecision.offer reads dezhban.firstRunCompleted out of a preference domain no uninstaller ever cleared, so a machine with an empty /etc/dezhban still answered "already done". The missing feature and the reported bug are the same defect from two ends. Root cannot do this work: a login keychain item's ACL is bound to the user's session and a login item is registered per user. So the app owns the per-user half. Settings gains Remove Dezhban…, which clears the keychain token and its capability probe, both login-item registrations, the saved window state, and the preference domains (current and the dead com.dezhban.DezhbanMenu), then opens Terminal on the root uninstaller and quits. Terminal rather than an in-app sheet because uninstall.sh quits Dezhban and deletes its bundle partway through: a progress sheet dies mid-teardown and cannot distinguish a finished uninstall from one that stopped after panic removed the rules. A terminal window outlives the app and shows the rule teardown, which for a kill switch is the step you most want to watch succeed. Preference domains are cleared last, immediately before terminate, because AppKit writes defaults as the app winds down; uninstall.sh repeats the deletion for $SUDO_USER for the same reason, and names the two per-user items it cannot reach rather than failing at them silently. Refs docs/adr/0015-complete-purge-semantics.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Countries, then one "Use automatic VPN detection?" tickbox with the manual fields — tunnel interfaces, self-hosted config files, endpoints — hanging off it. The opening "Configure your VPN now?" question is gone, and both wizards read the same question set, so the CLI changes with the app. Dropping configureVPN removes the thing that stood between a re-run and a working config, so two guards replace it. autoMode's default is seeded from the config: a config with pinned vpn.tunnelInterfaces starts on manual, because Apply clears TunnelInterfaces under AutoMode on purpose and a wizard defaulting to automatic would silently unpin them. Input.Endpoints becomes *[]string, nil when the question was never shown. On macOS the endpoint question is gated behind "not automatic", so a re-run that leaves detection on reaches Apply with no answer — writing that as an empty list would delete the user's server. Same convention, and the same reasoning, as the existing AutoDiscover *bool. The Swift side already had this property via shouldAsk. Off macOS the question is ungated instead: there is no live discovery, so an endpoint is required whichever mode is chosen. The CLI now asks a group in waves. A huh form binds every field before any is answered, so a question gated on another question in the same group would be decided by that question's seeded default. Rather than split the shared question set to suit one renderer, the terminal asks the ungated questions, re-evaluates, and asks whatever that opened up — the app shows step 2 as one screen because it re-evaluates gates as answers change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Behnam-RK
force-pushed
the
feat/complete-purge
branch
from
August 22, 2026 07:11
f448e31 to
f75954c
Compare
Behnam-RK
force-pushed
the
feat/two-step-wizard
branch
from
August 22, 2026 07:11
76b09e9 to
496be74
Compare
Behnam-RK
force-pushed
the
feat/complete-purge
branch
2 times, most recently
from
August 24, 2026 03:55
9c0c4f3 to
f834f8f
Compare
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.
Implements the two-step shape you described: step 1 countries, step 2 an automatic-detection tickbox that reveals the self-hosted options when unticked, with
configureVPNdropped.The shape
dezhban setup --questionsnow emits exactly:Both wizards read this one set, so
dezhban setupchanges with the app.Dropping
configureVPNneeded two guardsThat question was what kept a re-run from touching a working VPN config (
Applyreturned early when it was false). Removing it means a run always writes the VPN keys, so:1.
autoMode's default is seeded from the config. A config with pinnedvpn.tunnelInterfacesstarts on manual. Without this, a re-run would default to automatic, andApplyclearsTunnelInterfacesunderAutoModeon purpose — so pressing Enter through the wizard would have silently unpinned interfaces someone chose deliberately. Pinned byTestAutoModeSeedsFalseWhenInterfacesArePinnedon both sides.2.
Input.Endpointsbecomes*[]string, nil when the question was never shown. On macOS the endpoint question is gated behind "not automatic", so someone re-running setup to change their blocked-country list — leaving detection on, as recommended — reachesApplywith no endpoint answer at all. Writing that as an empty list would delete their server. Nil rather than empty for exactly the reason the existingAutoDiscover *boolis a pointer, and its doc comment already says so: an empty slice cannot be told apart from "asked, and cleared on purpose". The Swift side already had this property for free viashouldAsk.One thing worth flagging
Off macOS the endpoint question is ungated. There is no live discovery on Linux or Windows, so an endpoint is required whichever detection mode is chosen — gating it would let a Linux user finish the wizard with a config that cannot enforce.
TestEndpointsAreUngatedWhereThereIsNoDiscoverypins it.The CLI renders step 2 as two prompts
A
huhform binds every field before any of them is answered, so a question gated on another question in the same group would be decided by that question's seeded default rather than by what the user just ticked. The app has no such problem — it re-evaluates gates as answers change, which is what makes step 2 a single screen there.Rather than split the shared question set to suit one renderer, the terminal now asks a group in waves: the ungated questions first, then re-evaluate, then whatever that opened up. Documented in
cli.mdso the difference is not a surprise.Verification
go build ./...,go vet ./...,go test ./...— pass.internal/setupgains five tests: the two-group shape across all three platforms, the pinned-interface seed both ways, the unasked-endpoint rule, the gating, and the off-macOS ungating.swift test— 199 tests pass. TheSetupQuestionsTestsJSON fixture is updated to the real emitted shape, and gainstheWizardIsTwoSteps,anUnaskedQuestionWritesNoKey, andaSeededManualModeReWritesItsPins.dezhban setup --questionsoutput inspected directly (above).On-host checks added to docs/contribute/testing.md, the important two being: a re-run on a pinned config keeps its pins, and a re-run under automatic detection keeps configured endpoints. Also fixes a stale
defaults delete com.dezhban.menuin the first-run checklist — that domain has beencom.behnam-rk.dezhban.appfor a while.🤖 Generated with Claude Code