28 KiB
alt-tab-free — Master Paywall Audit
Purpose. This is the authoritative, read-this-first overview of the codebase, the paywall grafted onto it, and the complete surface that must be removed to ship a fully-free build. It summarizes nine deep-dive sections under
docs/audit/and the completeness review; follow the links for line-by-line detail.Audit date / commit basis: repo at
master(HEAD9fadf36b, v11.3.0). Allfile:linereferences are against this snapshot.Status legend: KEEP = legitimate app, must survive · CUT = paywall, remove · REWRITE = touched by both, hand-edit · FLAG = owner decision.
1. What this project is, and the licensing position
alt-tab-free (github.com/KofTwentyTwo/CommandTabFree) is a re-host of the popular open-source macOS window switcher AltTab (github.com/lwouis/alt-tab-macos). It is a pure Swift 5.8 AppKit application — no SwiftUI, no Interface Builder (per AGENTS.md).
It is not a diverged fork — it is upstream + a paywall commit
Git history shows every recent commit is authored by upstream author lwouis (34/40) or semantic-release-bot (6/40); zero commits by the alt-tab-free owner. The entire paywall was introduced upstream in a single commit 9147a4a8 "feat: introducing alt-tab pro!" (v11.0.0, 2026-04-05), a 1869-file change that also did a large repo-wide folder reorg. src/pro/ did not exist before it and has only ever been touched by 3 commits. All branding is still upstream's: bundle id com.lwouis.alt-tab-macos, name AltTab, domain alt-tab.app, repo lwouis/alt-tab-macos, Sparkle SUPublicEDKey, AppCenter secret. (See 09-telemetry-and-upstream.md.)
Licensing: republishing a free build is permitted, with one obligation
The project is GPL-3.0 (LICENCE.md:1 — "GNU GENERAL PUBLIC LICENSE Version 3"). Under GPL-3 you may modify and redistribute, including a build with all paywall code removed. The obligation: you must offer the complete corresponding source of your modified version under GPL-3 to your users (typically by publishing the fork's repo). The GPL-3 copyright string in Info.plist:20-21 is correct and must stay. The paywall itself was GPL-3 code upstream, so its removal carries no additional license burden.
Strategic alternative (owner decision). Because upstream is alive and GPL-3, rebasing the free build on the pre-paywall upstream tag v10.12.0 (commit
317a485b) yields the same unlocked feature set with no paywall to extract, at the cost of losing post-v11 fixes unless cherry-picked. The rest of this document assumes in-place removal from the current v11.x snapshot.
2. High-level architecture (the legitimate app)
This is the app that survives removal. Deep dive: 01-core-architecture.md, 02-switcher-ui.md.
Startup (two-phase, permission-gated)
- Entry point is top-level code in
src/main.swift(not@main/@NSApplicationMain): CLI fast-path → signal/exception handlers →App.shared.run()(main.swift:23).main.swiftis paywall-free. App(src/App.swift:7) subclassesAppCenterApplication(this is why removing AppCenter touches the principal class — see §5) and is its ownNSApplicationDelegate.- Phase A —
applicationDidFinishLaunching(App.swift:435-463): wires logging, Preferences, the license callbacks, and starts a permission-polling timer. - Phase B —
App.continueAppLaunchAfterPermissionsAreGranted(App.swift:383-431): invoked once fromSystemPermissions.checkPermissionsPreStartup(SystemPermissions.swift:77) only after Accessibility + Screen-Recording permissions pass. It creates the background threads/queues, switcher panels, menubar, main menu, and installs every macOS hook. - The phase-A → phase-B handoff is the critical launch path and is paywall-free.
macOS integration hooks (all paywall-free)
- Keyboard:
cghidCGEvent tap + CarbonRegisterEventHotKey(KeyboardEvents.swift:104-152). - Windows: AX observers with refcon-encoded
(pid,wid)identity (AccessibilityEvents.swift), dispatched through a bounded retry executor (AXCallScheduler.swift). - App lifecycle: KVO on
NSWorkspace.runningApplications(RunningApplicationsEvents.swift:6-10). - Threading owned by
BackgroundWork(dedicated CFRunLoop threads + bounded operation queues, ~45-thread budget).src/macos/andBackgroundWorkcontain zero License/Pro references (grep-verified).
Switcher UI
Event-driven: each Window subscribes to AX notifications → Windows.findOrCreate (Windows.swift:390), gated by WindowDiscriminator.isActualWindow, into the canonical Windows.list. Pure unit-tested kernels (SelectionResolver, SearchModeResolver, WindowFilterResolver, WindowOrderResolver) are separated from imperative wrappers and never call pro code — gating is passed in as a Bool. Rendering is a static TilesView engine with a 20-TileView recycled pool (TilesView.swift:35); focus uses private SkyLight APIs (Window.swift:245-288).
Preferences
Everything stored as strings in UserDefaults.standard, macro (enum) prefs as stringified CaseIterable index, read through CachedUserDefaults, written through Preferences.set firing one PreferencesEvents.preferenceChanged hook. The core preferences plumbing is paywall-independent — only the feature-gating layer on top is paywall (see §3).
Build / release / update
Raw xcodebuild over alt-tab-macos.xcodeproj (3 schemes), settings layered via config/*.xcconfig. Releases are fully automated by semantic-release on push to master. Signed with upstream Developer ID Louis Pontoise (QXD7GW8FHY), notarized via bundled notarytool. Auto-update via embedded Sparkle 2.9.1, feed https://alt-tab.app/appcast.xml, EdDSA-signed. Crash reporting via Microsoft AppCenter (Crashes module only; opt-in; EoL). Deep dive: 08-build-release-distribution.md, 09-telemetry-and-upstream.md.
3. The paywall, anatomized
The paywall lives almost entirely under src/pro/ and is grafted onto the spine at a small, well-defined set of call sites. Four subsystems:
3a. License subsystem — src/pro/license/ (04-license-subsystem.md)
LicenseManager.shared is the single source of truth for entitlement. It computes a flat 4-case LicenseState enum — .trial(daysRemaining:), .pro, .proExpired, .trialExpired (LicenseState.swift:14-19); there is no .lifetime case (lifetime is .pro + variant slug "pro_lifetime", LicenseManager.swift:25). A 14-day trial starts on first launch (LicenseManager.swift:17,192-200).
- The master gate
isProLocked(LicenseManager.swift:67-72) istruefor.proExpired/.trialExpired,falsefor.pro/.trial. Every settings intercept, the write-bounce, the shortcut cap, andProFeature.attemptUse()consult it. activate(_:)(LicenseManager.swift:104-140) does all-or-nothing Keychain writes with rollback, then flipsstate = .pro.RemoteLicenseClient(RemoteLicenseClient.swift) POSTs activate/validate/deactivate tohttps://alt-tab.app/api/v1/license(Endpoints.swift:11), sending a machine fingerprint (MachineFingerprint.swift:9-20, IOPlatformUUID) + trial-start timestamp. Revalidation is throttled to 30 days.- Keychain service:
com.lwouis.alt-tab-macos.license(LicenseManager.swift:4); a parallel UserDefaults suite of the same name holdstrialStartDate,lastValidation,customerEmail. LicenseCookie.swift:5-22writes a per-tierlicensecookie on.alt-tab.appso the Sparkle appcast can be tier-tailored.
3b. Trial-nag "Pro Transition" scheduler — src/pro/scheduling/ (05-trial-nag-scheduling.md)
A self-contained Day1→Day35 upsell-escalation engine. Coordinator ProTransitionManager (singleton) + timed ProTransitionScheduler (persisted DispatchWorkItem) + persisted ProTransitionState (all hasSeen* flags + remembered* Pro indices, in the .license UserDefaults suite) + pure, 100%-unit-tested ProTransitionManagerTestable state machine. The coordinator emits abstract ProPromptAction cases through onAction; ProPromptHost maps each to a concrete Day-X window/popover so the coordinator stays AppKit-free.
- Soft nags (never block):
[A]Day1 Welcome (fires immediately on first launch),[H]Day4 Tour,[B]Day12 Heads-Up,[D]Day15 Proactive,[F]Day21,[G]Day35, plus a Days13-14 menubar badge dot. - Hard gate arms at Day15 expiry:
onProLockEngaged()(ProTransitionState.swift:73-79) snapshots degradable Pro prefs toremembered*and downgrades stored values to free equivalents; the three hard-gated features then route through a free-pass ladder (ProTransitionManager.swift:226-242): first post-expiry attempt =.freePass(allow once, queue[C]Full Upgrade) →.showFullUpgrade→.showHardGatePopover[E]forever. - Prompts only fire 10:00-11:30 or 15:30-17:00; give-up at Day49.
3c. Feature gating — ProFeature.swift + PreferenceDefinition.swift (03-preferences-and-gating.md, 06-profeature-and-copy.md)
Two complementary mechanisms:
- Degradable preferences — declared as 6
ProGatedPreferencesdefinitions (PreferenceDefinition.swift:103-164): 3 globals (appearanceStyle,appearanceSize,shortcutStyle) + their 3 shortcut-0 overrides. Each carries afreeEquivalent, arememberedKey, and anisProValuepredicate.PreferenceDefinition.read()(:32-44) is the hot-path getter; while locked it returns the free equivalent. Pro getters inPreferences.swiftroute through these (:129,130,155,313,319,330). - Hard-gated runtime actions — no backing preference, gated at use-time via
ProFeature.attemptUse()(ProFeature.swift:74-83):extraShortcut(index:),searchInSwitcher,lockSearchInSwitcher. Call sites:ShortcutAction.swift:56,TilesView.swift:79,92.
Write-side enforcement bounces any setter that lands a Pro value into a gated key while locked, to the Upgrade tab (PreferencesEvents.swift:62-64). Free users are hard-capped at 1 shortcut (ControlsTab.swift:589-592). The registered defaults already hold the Pro values (e.g. appearanceSize=.auto), so removing the gate auto-unlocks everything with no default changes.
3d. Upsell UI — src/pro/ui/ + Settings/Menubar entanglement (07-pro-ui-and-menubar.md)
Reusable AppKit upsell primitives (none contain license logic; they only render): ProPromptHost (dispatch hub), ProPromptPopover/ProPromptWindow (shared chrome), ProPromptHeader, ProGradientButton (gradient buy CTA), UsageStatHeroView (a "your usage so far" stat block), and ProBadgeView.swift (misleadingly named — holds 5 types incl. the ProGradient utility enum, NotAdvisedButton, and the "Pro" pill). These are also consumed by Settings (the persistent Get-Pro gradient pill UpgradeButton, the in-window Upgrade view, ProBadgeView badges on gated rows, and the "Pro" search-index keyword), which is why src/pro/ui/ can only be deleted after Settings consumers are cut.
4. The complete set of Pro-gated features (what becomes free)
There are two degradable groups + three hard-gated actions, marketed as four headline features. After removal, all are unconditionally available.
| Feature (marketing copy) | Kind | Free (locked) value | Pro value | Gate mechanism | Definition |
|---|---|---|---|---|---|
App Icons & Titles styles (appearanceStyle) |
Degradable + hard-gated | thumbnails (0) |
appIcons (1) / titles (2) |
read() downgrade + first-summon [C] |
PreferenceDefinition.swift:103-109; Preferences.swift:129 |
Auto Size (appearanceSize) |
Degradable | medium (1) |
auto (3) |
read() downgrade only |
PreferenceDefinition.swift:111-117; Preferences.swift:130 |
Search on release (shortcutStyle) |
Degradable + hard-gated | doNothingOnRelease/focusOnRelease |
searchOnRelease (2) |
read() downgrade + first-summon [C] |
PreferenceDefinition.swift:119-125; Preferences.swift:155 |
| ↳ same three as shortcut-0 overrides | Degradable | free equivalents | Pro values | read() downgrade |
PreferenceDefinition.swift:130-164; Preferences.swift:313,319,330 |
| Up to 9 keyboard shortcuts (extra slots ≥ 2) | Hard-gated | capped at 1 shortcut | up to 9 | ProFeature.attemptUse() + addShortcutSlot cap |
ProFeature.swift:14; ShortcutAction.swift:56; ControlsTab.swift:589-592 |
| Search — find a window by typing | Hard-gated | blocked | available | ProFeature.attemptUse() |
ProFeature.swift:15; TilesView.swift:92 |
| Lock-search in switcher | Hard-gated | blocked | available | ProFeature.attemptUse() |
ProFeature.swift:16; TilesView.swift:79 |
Marketing copy: ProFeatureCopy.swift (4 strings); the comparison table that advertises them is Day1WelcomeLetterWindow.swift:77-80. Note preview-selected-window is NOT actually Pro-gated (Preferences.effectivePreviewSelectedWindow reads the raw bool, gated only by Screen-Recording permission) — leave it alone.
5. The complete removal surface
Removal strategy (consensus across all sections): delete src/pro/ as a unit, then fix the consumers that reference it. The lowest-risk staged approach is to first force LicenseManager.isProLocked = false / isProAvailable = true (every gate becomes a pass-through with no other edits), then progressively excise. The list below — grouped by file — is the full set of edits required for a clean build, incorporating the completeness review's additional items and build-break risks. Bold flags mark items the deep sections under-weighted.
src/pro/ — delete wholesale (CUT)
src/pro/license/—LicenseManager,LicenseState,LicenseAPI,RemoteLicenseClient,LicenseCookie,Keychain,MachineFingerprint,Clock,LicenseManagerSpecs.md,LicenseManagerTests.swift.src/pro/scheduling/—ProTransitionManager,ProTransitionScheduler,ProTransitionState,ProTransitionManagerTestable, allDay*.swiftwindows/popovers,ProTransitionTests.swift,ProTransitionSpecs.md.src/pro/ui/—ProPromptHost,ProPromptPopover,ProPromptWindow,ProPromptHeader,ProGradientButton,UsageStatHeroView,ProBadgeView.swift(all 5 types). Delete only AFTER the Settings consumers below are removed.src/pro/root —ProFeature.swift,ProFeatureCopy.swift,ProConversionCopy.swift,UsageStats.swift/UsageStatsTestable.swift(local-only; feeds only upsell copy).
src/App.swift (REWRITE — the spine wiring)
- Selectors
upgradeToProAction/openAccountAction/supportProjectAction(:25-27) and methodsupgradeToPro/openAccount/supportProject(:111-121) — remove (Menubar-only consumers). hideUionSwitcherDismissed()(:78) andshowUiOrCycleSelectiononSwitcherShown()(:328) — remove the single Pro line each (switcher hot path).- First-launch welcome entanglement (
:189-233):willShowDay1WelcomeOnAppLaunch(:205-208) + deferral observer referencingDay1WelcomeLetterWindow(:194-218). Simplify to always callshowAndCenterSettingsWindowOnFirstLaunch; the welcome window and itswillCloseNotificationobserver must be removed together or Settings never shows on first launch. - Phase-B Pro wiring
onAction/onAppLaunchComplete(:428-429) — remove (all other phase-B wiring stays). - License callback block (
:448-460):onBeforeProUnlock, theonStateChangedfan-out (refreshLicenseMenuItems,syncLicenseCookie,onLicenseStateChanged,UpgradeTab.refreshStatus,refreshUpgradeButton,proLockStateDidChangepost),LicenseManager.initialize(). The cleanest 13-line cut point. KeepresetPreferencesDependentComponents(:51-53) — it's used elsewhere; just drop the caller. - Custom-URL handler
application(_:open:)+handleCustomUrl(:465-490) — remove wholesale (exists only for the<bundleid>://activatedeep link; the registered scheme is$(PRODUCT_BUNDLE_IDENTIFIER)=com.lwouis.alt-tab-macos://, NOT a literalalt-tab://). Also drop the URL scheme fromInfo.plist(CFBundleURLSchemes, :53-56).
src/Menubar.swift (REWRITE)
- License item vars (
:8-10), the Get Pro / My Account / Support adds + view assignment (:46-50),refreshLicenseMenuItems(:84-107),toggleUpgradeMenuItem(:109-118). UpgradeMenuItemViewgradient pill incl. trial "days remaining" copy (:235-390; copy at:370-375) — delete; only consumer is:47.- Badge dot:
badgeDotLayer(:187),updateBadgeDotOverlay(:202-227) gated onProTransitionManager.shouldShowBadgeDot(:205), and its call insideloadPreferredIcon(:196). menuWillOpen(:392-399): dropLicenseManager.shared.refreshState()(:396); keeprefreshPermissionCallout()(:397).updateContent(_ state: LicenseState)(:357) hard-depends on theLicenseStateenum type — goes away withUpgradeMenuItemView. KeepmenubarIconCallback(:155-164), used by non-pro callers.
src/preferences/ (REWRITE)
PreferenceDefinition.swift— delete entirely; it is also a unit-test-target member (project.pbxproj:1979), so a broken getter fails app AND test compile.Preferences.swift— rewrite the 3 gated getters (:129,130,155) and 3 index-0 override branches (:313,319,330) to read raw values; removeoverrideRememberedKey+ProTransitionStatecleanup inremoveOverride(:282-298).PreferencesEvents.swift:62-64— delete theisProLocked && isStoredValueProbounce.PreferencesMigrations.swift:20— remove theProTransitionState.markFreshInstallIfUnknowncall (the only paywall coupling in migrations; the real symbol, distinct from the self-contained stub inPreferencesMigrationsTests.swift:356).LabelAndControl.swift— stripproGatedIndicesparam + intercept (:74-116, esp.:86-92).AppearanceTab.swift—proLockStateDidChangeobserver (:414-419),proGatedAppearanceStyleIndices(:462,700-703),ProBadge.attach(:463,481,575,688-698),wrapShortcutStyleProLockIntercept(:585-598),wrapAppearanceSizeProLockIntercept(:662-679),refreshProLockUi(:705+).ShortcutEditor.swift—proGatedIndices(:429,437,449,459), segmented + radios locked intercepts (:599-607,705-711).ControlsTab.swift—proLockStateDidChangeobserver (:141),row.setProBadgefor index≥1 (:515), theaddShortcutSlotshortcut cap (:589-592).ShortcutsWhenActiveSheet.swift—ProBadgeon search/lock-search rows (:25,51,54,63-69).
src/preferences/settings-window/SettingsWindow.swift — the most license-entangled non-pro file outside Menubar/UpgradeTab (REWRITE — under-weighted by §7)
UpgradeButtonclass (:133-214, aProGradientButtonsubclass readingLicenseManager.state/.isLifetimeVariant/.customerEmail/.trial/.proExpired); instance:303; state:308-311;setupUpgradeButton+upgradeButtonClicked(:401,507-526).showUpgradeView/hideUpgradeViewembeddingUpgradeTab.initTab(:1066,1122-1161);refreshUpgradeButton(:1163-1165, called fromApp.swift:454andUpgradeTab:214).windowDidBecomeKey→LicenseManager.refreshState+UpgradeTab.refreshStatus+ shine animation (:1243-1254);UpgradeTab.cleanupinwindowWillClose(:1266).- Settings-search registers
ProBadgeView → "Pro"(:679-680);refreshEmailTooltip+hasSecondLineread.pro/customerEmail(:196,206-207).
Other Settings / secondary windows (REWRITE)
SidebarList.swift—private var proBadge: ProBadgeView?(:93) andsetProBadgeinstantiatingProBadgeView(:244-258).UpgradeTab.swift— delete the whole tab;ProHeroButton: ProGradientButton(:493) +usageHero/heroButton/makeHeroButton(:5-6,37-38,160-161);activateLicense/deactivateLicense/deactivateInstance+LicenseAPIError.seatLimitExceededseat-management UI (:322-405) — full activation UI, §4 listed onlyLicenseManager/RemoteLicenseClient.DebugProfile.swift:17—LicenseManager.shared.state.debugProfileLabel(extension atLicenseState.swift:14). This feedsFeedbackWindow, which is being kept — hand-edit (drop the License line), do not delete the file.
src/debug/QAMenu.swift (REWRITE)
- The DEBUG manual triggers/resets (
:170-241) directly instantiate every Day-X window and pokeLicenseManager/manager flags — must be removed/updated in lockstep or the DEBUG build won't compile.
Build / distribution / telemetry (REWRITE / FLAG — see 08, 09)
Endpoints.swift—licenseApiBaseUrl(:11),checkoutUrl(:9),accountUrl(:10) become dead.feedbackUrl(:12) andsupportUrl(:8) are not paywall — owner decides.- Identity (mandatory regardless of removal): the upstream Developer ID cannot be used. Change bundle id (
config/base.xcconfig:4; also hardcodedMocks.swift:133and unit-test idsproject.pbxproj:2477/2542), signing cert (release.xcconfig:5), generate a NEW Sparkle EdDSA keypair (Info.plist:62 SUPublicEDKey+ CI$SPARKLE_ED_PRIVATE_KEY), repoint feed/download/website URLs (base.xcconfig:20-21,update_appcast.sh:16,18,App.repositoryApp.swift:16), truncate the upstream-signedappcast.xml. - AppCenter removal touches the app's principal class:
AppsubclassesAppCenterApplication(App.swift:7),Info.plist:36-37 NSPrincipalClass=AppCenterApplication,alt-tab-macos-Bridging-Header.h:1. Removing AppCenter requires choosing a replacement principal class / reverting toNSApplication, or the app won't launch and won't compile. Also:src/vendors/AppCenterCrashes.swift,Secrets.appCenterSecret,scripts/upload_symbols_to_appcenter.sh,ci_cd.yml:56. .github/FUNDING.ymland the website repository-dispatch (update_website.sh:5 → lwouis/alt-tab-website) point at upstream — repoint or remove.
alt-tab-macos.xcodeproj/project.pbxproj — mechanical but build-fatal (REWRITE)
Every src/pro file (and Endpoints/Secrets/AppCenter) has a PBXBuildFile + PBXFileReference + PBXGroup child + PBXSourcesBuildPhase membership (~102 in-Sources pro entries; app phase ~1830, test phase ~1902; incl. PreferenceDefinition.swift:1979). Clock/Keychain/LicenseAPI/MachineFingerprint/ProTransitionManagerTestable each have TWO build-file UUIDs (app + test) — both must be removed. Any file deleted on disk but left referenced breaks at project-load/link.
Test target (REWRITE)
LicenseManagerTests, ProTransitionTests, ProTransitionManagerTestable, UsageStatsMessageTests, ProBadgeViewSegmentTests, and _test-support/Mocks.swift (ProBadgeView stub + ShortcutStylePreference mock + bundle id :133) all break if pro symbols vanish — update/remove in lockstep.
Build-break risk summary (the hard dependencies)
| Symbol being deleted | Non-pro / under-weighted consumers that break first |
|---|---|
LicenseState enum |
Menubar.swift:357 (updateContent param), DebugProfile.swift:17 (.debugProfileLabel) |
LicenseManager.shared |
SettingsWindow.swift:162,164,167,196,206-207,1246; UpgradeTab.swift:220-405 |
ProGradientButton (superclass) |
UpgradeButton (SettingsWindow.swift:133), ProHeroButton (UpgradeTab.swift:493), ProGradient.makeLayer (Menubar.swift:247) |
ProBadgeView |
SidebarList.swift:93,254, SettingsWindow.swift:679, Appearance/Controls/ShortcutsWhenActiveSheet tabs |
ProGatedPreferences / PreferenceDefinition |
Preferences.swift:129,130,155; also a test-target member → fails app + test compile |
AppCenterApplication |
App.swift:7 superclass + Bridging-Header + Info.plist NSPrincipalClass |
Day1WelcomeLetterWindow |
App.swift:213 first-launch deferral observer |
ProTransitionState.markFreshInstallIfUnknown |
PreferencesMigrations.swift:20 (real symbol) |
6. Notable findings, risks, and open questions
Findings
- The paywall is well-isolated. Outside
src/pro/, the entire spine coupling is ~13 lines inApp.swift(:448-460) plus discrete blocks inMenubar,Settings, and the gating layer. The launch path, switcher kernels, macOS hooks, threading, and core preferences are all paywall-free. - Defaults already hold Pro values. Removing the gate auto-unlocks features with no default changes (
appearanceSize=.auto, etc.). .proExpiredis effectively dead code in this build:versionLimitedVariantsis empty (LicenseManager.swift:28), so no.provariant ever expires by version.- No third-party analytics SDK. Only telemetry is AppCenter Crashes (opt-in, EoL);
UsageStatsis local-only and never networked.
Risks
- In-place upgrade data loss. Users who passed trial expiry on the paywalled build have stored prefs already downgraded to free equivalents, with the original Pro index parked in
proTransition.remembered*keys (in the.licenseUserDefaults suite). After removal,read()no longer restores them → silent loss of their prior Pro selection. A new bundle id (clean UserDefaults domain) sidesteps this; otherwise add a one-time migration copyingremembered*indices back to base keys before deletingproTransition.*. state.didSetfiresonStateChanged. If you hardcodestate = .proat init, ensure the hook tolerates being invoked (or unset it) — otherwise it can re-enter paywall code you intend to delete.- Keychain/signing invariant is MOOT for a free build. Removing
src/proremoves all Keychain license usage, so changing bundle id / Developer ID has nothing to orphan — but the identity change is still mandatory because the upstream Developer ID cannot be used. (PerAGENTS.md, do not rotate identity while licensing is retained.) - Do NOT
git revert 9147a4a8. It mixes the paywall with a 1869-file folder reorg; removal must be by deletingsrc/pro/and untangling the wiring, then confirming the build links (ai/build.sh).
Open questions (owner decisions)
- New bundle id vs in-place? Determines whether the
remembered*migration above is needed. - Keep "Support this project" (donation link, upstream precedent) and the feedback feature? Both are non-paywall but currently route through paywall-adjacent code / upstream endpoints (
Endpoints.supportUrl,Endpoints.feedbackUrl→alt-tab.app, whose server side is not in this repo). - Auto-update story. A fork cannot ship updates against the upstream
SUPublicEDKey/alt-tab.app/appcast.xmlwithout owning that domain and lwouis's private key. Stand up own host + EdDSA key, or strip Sparkle. - Keep crash diagnostics? Remove AppCenter entirely (most privacy-aligned, but requires a replacement principal class) vs repoint to a self-owned AppCenter app (EoL).
- Rebase on pre-paywall v10.12.0 vs in-place removal — both yield the same unlocked feature set.
Deep-dive sections: 01-core-architecture · 02-switcher-ui · 03-preferences-and-gating · 04-license-subsystem · 05-trial-nag-scheduling · 06-profeature-and-copy · 07-pro-ui-and-menubar · 08-build-release-distribution · 09-telemetry-and-upstream