From 394fa0bad7db5b854315231bf27bd99f3b712df7 Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Tue, 24 Feb 2026 10:08:51 -0500 Subject: [PATCH 1/2] create intellij plugin --- lib/wt-common | 10 +- wt-intellij-plugin/.gitignore | 4 + wt-intellij-plugin/README.md | 181 +++++ wt-intellij-plugin/build.gradle.kts | 72 ++ wt-intellij-plugin/gradle.properties | 12 + .../gradle/wrapper/gradle-wrapper.properties | 7 + wt-intellij-plugin/gradlew | 251 +++++++ wt-intellij-plugin/gradlew.bat | 94 +++ .../install-plugin-to-intellij.sh | 147 ++++ wt-intellij-plugin/settings.gradle.kts | 5 + .../kotlin/com/block/wt/actions/WtAction.kt | 73 ++ .../wt/actions/context/AddContextAction.kt | 95 +++ .../wt/actions/context/DeleteContextAction.kt | 51 ++ .../context/ReprovisionContextAction.kt | 123 ++++ .../context/ShowContextConfigAction.kt | 47 ++ .../actions/metadata/ExportMetadataAction.kt | 36 + .../actions/metadata/ImportMetadataAction.kt | 36 + .../metadata/RefreshBazelTargetsAction.kt | 37 + .../wt/actions/util/CopyWorktreePathAction.kt | 14 + .../wt/actions/util/OpenInTerminalAction.kt | 47 ++ .../actions/util/RefreshWorktreeListAction.kt | 13 + .../wt/actions/util/RevealInFinderAction.kt | 18 + .../block/wt/actions/util/WelcomeAction.kt | 19 + .../actions/worktree/CreateWorktreeAction.kt | 74 ++ .../worktree/ProvisionWorktreeAction.kt | 59 ++ .../wt/actions/worktree/RemoveMergedAction.kt | 104 +++ .../actions/worktree/RemoveWorktreeAction.kt | 128 ++++ .../actions/worktree/SwitchWorktreeAction.kt | 59 ++ .../com/block/wt/agent/AgentDetection.kt | 13 + .../com/block/wt/agent/AgentDetector.kt | 146 ++++ .../com/block/wt/git/GitBranchHelper.kt | 19 + .../com/block/wt/git/GitConfigHelper.kt | 131 ++++ .../kotlin/com/block/wt/git/GitDirResolver.kt | 50 ++ .../main/kotlin/com/block/wt/git/GitParser.kt | 57 ++ .../com/block/wt/model/ContextConfig.kt | 13 + .../com/block/wt/model/MetadataPattern.kt | 28 + .../com/block/wt/model/ProvisionMarker.kt | 14 + .../kotlin/com/block/wt/model/WorktreeInfo.kt | 48 ++ .../com/block/wt/progress/ProgressScope.kt | 29 + .../com/block/wt/progress/RemovalProgress.kt | 61 ++ .../com/block/wt/provision/ProvisionHelper.kt | 81 +++ .../wt/provision/ProvisionMarkerService.kt | 147 ++++ .../wt/provision/ProvisionSwitchHelper.kt | 137 ++++ .../com/block/wt/services/BazelService.kt | 157 +++++ .../com/block/wt/services/ContextService.kt | 114 +++ .../wt/services/CreateWorktreeUseCase.kt | 90 +++ .../wt/services/ExternalChangeWatcher.kt | 246 +++++++ .../kotlin/com/block/wt/services/GitClient.kt | 142 ++++ .../com/block/wt/services/MetadataService.kt | 195 ++++++ .../block/wt/services/SymlinkSwitchService.kt | 142 ++++ .../com/block/wt/services/WorktreeEnricher.kt | 46 ++ .../wt/services/WorktreeRefreshScheduler.kt | 33 + .../com/block/wt/services/WorktreeService.kt | 260 +++++++ .../block/wt/services/WtStartupActivity.kt | 51 ++ .../com/block/wt/settings/WtPluginSettings.kt | 43 ++ .../block/wt/settings/WtSettingsComponent.kt | 91 +++ .../wt/settings/WtSettingsConfigurable.kt | 30 + .../com/block/wt/ui/AddContextDialog.kt | 254 +++++++ .../com/block/wt/ui/ContextSetupDialog.kt | 209 ++++++ .../com/block/wt/ui/CreateWorktreeDialog.kt | 66 ++ .../kotlin/com/block/wt/ui/Notifications.kt | 29 + .../com/block/wt/ui/WelcomePageHelper.kt | 123 ++++ .../kotlin/com/block/wt/ui/WorktreePanel.kt | 340 +++++++++ .../wt/ui/WorktreeStatusBarWidgetFactory.kt | 110 +++ .../com/block/wt/ui/WorktreeTableModel.kt | 79 +++ .../block/wt/ui/WorktreeToolWindowFactory.kt | 38 + .../com/block/wt/util/ConfigFileHelper.kt | 79 +++ .../com/block/wt/util/PathExtensions.kt | 12 + .../kotlin/com/block/wt/util/PathHelper.kt | 58 ++ .../kotlin/com/block/wt/util/ProcessHelper.kt | 96 +++ .../kotlin/com/block/wt/util/ProcessRunner.kt | 12 + .../src/main/resources/META-INF/plugin.xml | 210 ++++++ .../src/main/resources/icons/worktree.svg | 15 + wt-intellij-plugin/src/main/resources/ui.png | Bin 0 -> 326578 bytes .../src/main/resources/welcome.html | 647 ++++++++++++++++++ .../com/block/wt/git/GitConfigHelperTest.kt | 538 +++++++++++++++ .../com/block/wt/git/GitDirResolverTest.kt | 172 +++++ .../com/block/wt/model/WorktreeInfoTest.kt | 230 +++++++ .../provision/ProvisionMarkerServiceTest.kt | 247 +++++++ .../wt/services/ExternalChangeWatcherTest.kt | 145 ++++ .../wt/services/MetadataServiceStaticTest.kt | 101 +++ .../block/wt/services/MetadataServiceTest.kt | 97 +++ .../block/wt/services/WorktreeServiceTest.kt | 141 ++++ .../block/wt/testutil/FakeAgentDetection.kt | 15 + .../block/wt/testutil/FakeProcessRunner.kt | 20 + .../com/block/wt/testutil/TestFileHelper.kt | 16 + .../com/block/wt/util/ConfigFileHelperTest.kt | 230 +++++++ .../com/block/wt/util/PathHelperTest.kt | 147 ++++ 88 files changed, 8904 insertions(+), 2 deletions(-) create mode 100644 wt-intellij-plugin/.gitignore create mode 100644 wt-intellij-plugin/README.md create mode 100644 wt-intellij-plugin/build.gradle.kts create mode 100644 wt-intellij-plugin/gradle.properties create mode 100644 wt-intellij-plugin/gradle/wrapper/gradle-wrapper.properties create mode 100755 wt-intellij-plugin/gradlew create mode 100644 wt-intellij-plugin/gradlew.bat create mode 100755 wt-intellij-plugin/install-plugin-to-intellij.sh create mode 100644 wt-intellij-plugin/settings.gradle.kts create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/WtAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetection.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetector.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitDirResolver.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitParser.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ContextConfig.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/model/MetadataPattern.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/ProgressScope.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/BazelService.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ContextService.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/GitClient.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/MetadataService.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeService.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/Notifications.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathExtensions.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathHelper.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessHelper.kt create mode 100644 wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessRunner.kt create mode 100644 wt-intellij-plugin/src/main/resources/META-INF/plugin.xml create mode 100644 wt-intellij-plugin/src/main/resources/icons/worktree.svg create mode 100644 wt-intellij-plugin/src/main/resources/ui.png create mode 100644 wt-intellij-plugin/src/main/resources/welcome.html create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt create mode 100644 wt-intellij-plugin/src/test/kotlin/com/block/wt/util/PathHelperTest.kt diff --git a/lib/wt-common b/lib/wt-common index 6ee31bf..19a2bc5 100644 --- a/lib/wt-common +++ b/lib/wt-common @@ -105,7 +105,7 @@ wt_read_git_config() { # Read all wt.* keys into local variables local gc_worktrees_base="" gc_idea_files_base="" - local gc_active_worktree="" gc_base_branch="" gc_metadata_patterns="" + local gc_active_worktree="" gc_base_branch="" gc_metadata_patterns="" gc_context_name="" local has_any=false local key value line lkey @@ -124,6 +124,7 @@ wt_read_git_config() { wt.activeworktree) gc_active_worktree="$value" ;; wt.basebranch) gc_base_branch="$value" ;; wt.metadatapatterns) gc_metadata_patterns="$value" ;; + wt.contextname) gc_context_name="$value" ;; esac done < <(git config --local --get-regexp '^wt\.' 2>/dev/null) @@ -155,6 +156,7 @@ wt_read_git_config() { # Optional keys: apply if present [[ -n "$gc_active_worktree" ]] && WT_ACTIVE_WORKTREE="$gc_active_worktree" [[ -n "$gc_metadata_patterns" ]] && WT_METADATA_PATTERNS="$gc_metadata_patterns" + [[ -n "$gc_context_name" ]] && WT_CONTEXT_NAME="$gc_context_name" return 0 } @@ -370,7 +372,11 @@ wt_show_context_banner() { fi if [[ -n "$current_context" ]]; then - printf "${BLUE}[Context: %s]${NC} ${YELLOW}(wt context to switch)${NC}\n" "$current_context" >&2 + if [[ "$(git config --local --get wt.enabled 2>/dev/null)" == "true" ]]; then + printf "${BLUE}[Context: %s 📌]${NC}\n" "$current_context" >&2 + else + printf "${BLUE}[Context: %s]${NC} ${YELLOW}(wt context to switch)${NC}\n" "$current_context" >&2 + fi else printf "%s[No context set]%s %s(wt context to switch)%s\n" "$YELLOW" "$NC" "$YELLOW" "$NC" >&2 fi diff --git a/wt-intellij-plugin/.gitignore b/wt-intellij-plugin/.gitignore new file mode 100644 index 0000000..1b8490b --- /dev/null +++ b/wt-intellij-plugin/.gitignore @@ -0,0 +1,4 @@ +build/ +.gradle/ +.intellijPlatform/ +gradle/wrapper/gradle-wrapper.jar diff --git a/wt-intellij-plugin/README.md b/wt-intellij-plugin/README.md new file mode 100644 index 0000000..13845a6 --- /dev/null +++ b/wt-intellij-plugin/README.md @@ -0,0 +1,181 @@ +# Worktree Manager - IntelliJ Plugin + +Native git worktree management for all JetBrains IDEs, using atomic symlink switching for sub-second context switches. This is the IDE companion to the [wt CLI](http://go/wt). + +## Requirements + +- JetBrains IDE **2025.3+** (IntelliJ IDEA, Android Studio, CLion, WebStorm, etc.) +- Git on PATH +- macOS or Linux +- [wt CLI](http://go/wt) setup recommended + +## Installation + +```bash +./gradlew buildPlugin +``` + +Then **Settings > Plugins > gear icon > Install Plugin from Disk...** and select `build/distributions/wt-intellij-plugin-0.1.0.zip`. + +To update, rebuild and reinstall. The old version is replaced automatically. + +## Screenshot + +![Tool Window](src/main/resources/ui.png) + +## Features + +| wt CLI command | Plugin equivalent | +|---|---| +| `wt list [-v]` | **Worktrees** tool window with async status indicators | +| `wt add [-b] ` | **Create Worktree** dialog (stash/pull/create/restore) | +| `wt switch ` | **Switch Worktree** with atomic symlink swap | +| `wt remove [--merged]` | **Remove Worktree** / **Remove Merged** with safety checks | +| `wt context [name]` | **Context selector** in status bar + popup | +| `wt metadata-export` | **Export Metadata to Vault** | +| `wt metadata-import` | **Import Metadata from Vault** | +| `wt-metadata-refresh` | **Refresh Bazel Targets** | +| `wt cd` | **Open in Terminal** | + +The plugin reads the same `~/.wt/` config files as the CLI. Both work side by side. A file watcher auto-refreshes the tool window on external changes. + +--- + +## Usage + +### Tool Window + +**View > Tool Windows > Worktrees** shows all worktrees: + +| Column | Description | +|---|---| +| `*` | Currently linked worktree | +| Path | Directory name | +| Branch | Checked-out branch | +| Status | `⚠`conflicts `●`staged `✱`modified `…`untracked `↑`ahead `↓`behind | +| Agent | Active Claude Code session IDs (truncated; hover for full) | +| Provisioned | `✓` current context, `~` other context | + +Double-click a row to switch. Hover Status or Agent cells for details. + +### Shortcuts + +| Action | Shortcut | +|---|---| +| Switch Worktree | `Ctrl+Alt+W` | +| Create Worktree | `Ctrl+Alt+Shift+W` | + +Also available under **VCS > Worktrees**. + +### Status Bar + +Shows current context (e.g. `wt: java`). Click to switch. + +### Settings + +**Settings > Tools > Worktree Manager**: + +- Auto-refresh interval +- Status indicator loading +- Auto-export metadata on shutdown (default: off) +- Switch/remove confirmation dialogs +- Provision prompt on switch + +--- + +## How It Works + +### Symlink Swap + +``` +1. Create temp symlink: .active..tmp -> /new/worktree +2. Atomic rename: rename(.tmp, active) -- single syscall, zero gap +3. VFS refresh: save docs -> swap -> reload editors -> refresh VFS -> update git +``` + +### Metadata Vault + +`~/.wt/repos//idea-files/` stores symlinks to worktree metadata: + +- **Export**: vault symlinks point to current worktree's `.idea/`, `.ijwb/`, `.run/`, etc. +- **Import**: copies from vault (following symlinks) into target worktree + +### Agent Detection + +Two complementary methods: + +- **Process** (`/usr/sbin/lsof`): finds running `claude` processes by cwd +- **Session files** (`~/.claude/projects/`): checks for recently-modified `.jsonl` transcripts (30 min window) + +A worktree shows the agent indicator if either method detects activity. + +--- + +## Development + +JDK 25 toolchain is auto-provisioned by Gradle (via [foojay](https://github.com/gradle/foojay-toolchains)). Just needs JDK 17+ to run Gradle itself. + +```bash +./gradlew buildPlugin # Build ZIP +./gradlew test # 65 tests +./gradlew runIde # Launch sandbox IDE +./gradlew clean buildPlugin test # Full rebuild +``` + +Debug: **Gradle tool window > Tasks > intellij platform > runIde > right-click > Debug**. + +### Project Structure + +``` +src/main/kotlin/com/block/wt/ + model/ WorktreeInfo, WorktreeStatus, ContextConfig, ProvisionMarker + git/ GitParser, GitBranchHelper, GitDirResolver + provision/ ProvisionMarkerService, ProvisionHelper, ProvisionSwitchHelper + agent/ AgentDetection (interface), AgentDetector (lsof + session files) + util/ PathHelper, PathExtensions, ConfigFileHelper, ProcessHelper/Runner + services/ WorktreeService (facade), GitClient, WorktreeEnricher, + WorktreeRefreshScheduler, CreateWorktreeUseCase, + SymlinkSwitchService, ContextService, MetadataService, + BazelService, ExternalChangeWatcher + actions/ 14 actions: worktree/, context/, metadata/, util/ + ui/ WorktreePanel, WorktreeTableModel, ContextPopupHelper, + ContextStatusBarWidgetFactory, dialogs, Notifications + settings/ WtPluginSettings, WtSettingsComponent, WtSettingsConfigurable +src/test/kotlin/ 65 tests across 8 classes + test fakes +``` + +### Tests + +| Test class | Coverage | +|---|---| +| `PathHelperTest` | Atomic symlink, tilde expansion, normalization | +| `ConfigFileHelperTest` | Config parse/write, missing files, quoted paths | +| `WorktreeInfoTest` | Porcelain parsing, WorktreeStatus sealed class, isDirty | +| `WorktreeServiceTest` | Parsing + agent enrichment with test fakes | +| `MetadataServiceTest` | Path deduplication, directory copy | +| `MetadataServiceStaticTest` | Static export/import | +| `ProvisionMarkerServiceTest` | Markers: write/read/remove, multi-context, linked worktrees | +| `GitDirResolverTest` | Git dir resolution (main + linked) | + +--- + +## Architecture + +| Decision | Rationale | +|---|---| +| Atomic symlink swap | `create-temp + Files.move(ATOMIC_MOVE)` -- `rename(2)` syscall, zero gap | +| Git CLI over Git4Idea | `git worktree list --porcelain` is more reliable for all worktree states | +| Direct `~/.wt/` file I/O | Ensures CLI interop (no `PersistentStateComponent`) | +| Coroutines + StateFlow | Reactive UI from background loading | +| Facade pattern | `WorktreeService` delegates to `GitClient`, `WorktreeEnricher`, `WorktreeRefreshScheduler` internally; 18 callers unchanged | +| Dual agent detection | `lsof` for running processes + session files for recently active; path encoding matches Claude Code (`[^a-zA-Z0-9]` -> `-`) | +| `WorktreeStatus` sealed class | Replaces 6 nullable `Int?` with `NotLoaded` / `Loaded` states | +| `Result` mutations | `ProvisionMarkerService` preserves exceptions instead of bare `Boolean` | + +## Compatibility + +| | | +|---|---| +| IDE versions | 2025.3 (253) through 2026.1 (261.*) | +| Platforms | macOS, Linux | +| Build | Gradle 9.3.1, Kotlin 2.3.0, IntelliJ Platform Gradle Plugin 2.11.0, JDK 25 toolchain → JVM 21 bytecode | diff --git a/wt-intellij-plugin/build.gradle.kts b/wt-intellij-plugin/build.gradle.kts new file mode 100644 index 0000000..c0c9eeb --- /dev/null +++ b/wt-intellij-plugin/build.gradle.kts @@ -0,0 +1,72 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + +plugins { + id("org.jetbrains.kotlin.jvm") version "2.3.0" + id("org.jetbrains.intellij.platform") version "2.11.0" +} + +group = providers.gradleProperty("pluginGroup").get() +version = providers.gradleProperty("pluginVersion").get() + +kotlin { + jvmToolchain(25) + compilerOptions { + // JDK 25 toolchain for compilation, JVM 21 bytecode for IDE compatibility. + // IntelliJ 2025.3 bundles JBR 21; bump to JVM_25 when targeting 2026.1+ (JBR 25). + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + } +} + +repositories { + maven(url = "https://maven.global.square/artifactory/square-public") + intellijPlatform { + defaultRepositories() + } +} + +dependencies { + intellijPlatform { + intellijIdea(providers.gradleProperty("platformVersion")) + + bundledPlugin("Git4Idea") + bundledPlugin("org.jetbrains.plugins.terminal") + + testFramework(TestFrameworkType.Platform) + } + + testImplementation("junit:junit:4.13.2") +} + +intellijPlatform { + pluginConfiguration { + name = providers.gradleProperty("pluginName") + version = providers.gradleProperty("pluginVersion") + + ideaVersion { + sinceBuild = providers.gradleProperty("pluginSinceBuild") + untilBuild = providers.gradleProperty("pluginUntilBuild") + } + } + + pluginVerification { + ides { + recommended() + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +tasks { + runIde { + jvmArgs("-Xmx2g") + systemProperty("idea.is.internal", "true") + } + + test { + systemProperty("idea.home.path", layout.buildDirectory.dir("idea-sandbox").get().asFile.absolutePath) + } +} diff --git a/wt-intellij-plugin/gradle.properties b/wt-intellij-plugin/gradle.properties new file mode 100644 index 0000000..87003ac --- /dev/null +++ b/wt-intellij-plugin/gradle.properties @@ -0,0 +1,12 @@ +pluginGroup = com.block.wt +pluginName = Worktree Manager +pluginVersion = 0.1.0 + +pluginSinceBuild = 253 +pluginUntilBuild = 261.* + +platformVersion = 2025.3.3 + +kotlin.stdlib.default.dependency = false +org.gradle.configuration-cache = true +org.gradle.caching = true diff --git a/wt-intellij-plugin/gradle/wrapper/gradle-wrapper.properties b/wt-intellij-plugin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f78a6 --- /dev/null +++ b/wt-intellij-plugin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/wt-intellij-plugin/gradlew b/wt-intellij-plugin/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/wt-intellij-plugin/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/wt-intellij-plugin/gradlew.bat b/wt-intellij-plugin/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/wt-intellij-plugin/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/wt-intellij-plugin/install-plugin-to-intellij.sh b/wt-intellij-plugin/install-plugin-to-intellij.sh new file mode 100755 index 0000000..6a03b5c --- /dev/null +++ b/wt-intellij-plugin/install-plugin-to-intellij.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +PLUGIN_DIR_NAME="wt-intellij-plugin" + +# Detect OS +OS="$(uname -s)" +case "$OS" in + Darwin) PLATFORM="macos" ;; + Linux) PLATFORM="linux" ;; + MINGW*|MSYS*|CYGWIN*) PLATFORM="windows" ;; + *) + echo "Error: Unsupported OS: $OS" >&2 + exit 1 + ;; +esac + +# Build the plugin +echo "Building plugin..." +if [[ "$PLATFORM" == "windows" ]]; then + ./gradlew.bat buildPlugin +else + ./gradlew buildPlugin +fi + +# Find the built ZIP +ZIP_FILE=$(ls build/distributions/wt-intellij-plugin-*.zip 2>/dev/null | head -1) +if [[ -z "$ZIP_FILE" ]]; then + echo "Error: No plugin ZIP found in build/distributions/" >&2 + exit 1 +fi +echo "Built: $ZIP_FILE" + +# Find JetBrains config directory based on OS +case "$PLATFORM" in + macos) + JETBRAINS_DIR="$HOME/Library/Application Support/JetBrains" + ;; + linux) + JETBRAINS_DIR="$HOME/.config/JetBrains" + ;; + windows) + JETBRAINS_DIR="$APPDATA/JetBrains" + ;; +esac + +if [[ ! -d "$JETBRAINS_DIR" ]]; then + echo "Error: JetBrains directory not found at $JETBRAINS_DIR" >&2 + exit 1 +fi + +# Find the most recent IntelliJ IDEA directory +IDEA_DIR=$(ls -dt "$JETBRAINS_DIR"/IntelliJIdea* 2>/dev/null | head -1) +if [[ -z "$IDEA_DIR" ]]; then + echo "Error: No IntelliJ IDEA installation found in $JETBRAINS_DIR" >&2 + exit 1 +fi + +PLUGINS_DIR="$IDEA_DIR/plugins" +mkdir -p "$PLUGINS_DIR" + +echo "IntelliJ plugins dir: $PLUGINS_DIR" + +# Remove existing version of the plugin +if [[ -d "$PLUGINS_DIR/$PLUGIN_DIR_NAME" ]]; then + echo "Removing existing plugin..." + rm -rf "$PLUGINS_DIR/$PLUGIN_DIR_NAME" +fi + +# Extract the new version +echo "Installing plugin..." +unzip -qo "$ZIP_FILE" -d "$PLUGINS_DIR" + +echo "" +echo "Plugin installed successfully!" + +# Restart IntelliJ IDEA +case "$PLATFORM" in + macos) + IDEA_APP_PATH=$(ps aux | grep -o '/[^[:space:]]*IntelliJ IDEA[^/]*.app' | head -1 || true) + IDEA_PID=$(pgrep -f "IntelliJ IDEA.*/MacOS/idea" | head -1 || true) + if [[ -n "$IDEA_PID" && -n "$IDEA_APP_PATH" ]]; then + echo "Restarting IntelliJ IDEA (pid $IDEA_PID)..." + kill "$IDEA_PID" + while kill -0 "$IDEA_PID" 2>/dev/null; do sleep 1; done + open -a "$IDEA_APP_PATH" + echo "IntelliJ IDEA restarted." + else + echo "IntelliJ IDEA is not running. Launch it manually to use the plugin." + fi + ;; + linux) + IDEA_PID=$(pgrep -f "idea" 2>/dev/null | head -1) + if [[ -n "$IDEA_PID" ]]; then + # Find the binary path before killing + IDEA_BIN=$(readlink -f "/proc/$IDEA_PID/exe" 2>/dev/null || true) + echo "Restarting IntelliJ IDEA..." + kill "$IDEA_PID" + while kill -0 "$IDEA_PID" 2>/dev/null; do sleep 1; done + if [[ -n "$IDEA_BIN" && -x "$IDEA_BIN" ]]; then + nohup "$IDEA_BIN" &>/dev/null & + else + # Fall back to common install locations + for candidate in \ + /snap/intellij-idea-ultimate/current/bin/idea.sh \ + /snap/intellij-idea-community/current/bin/idea.sh \ + "$HOME/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/*/bin/idea.sh" \ + "$HOME/.local/share/JetBrains/Toolbox/apps/IDEA-C/ch-0/*/bin/idea.sh" \ + /opt/idea/bin/idea.sh; do + # shellcheck disable=SC2086 + FOUND=$(ls -t $candidate 2>/dev/null | head -1) + if [[ -n "$FOUND" && -x "$FOUND" ]]; then + nohup "$FOUND" &>/dev/null & + break + fi + done + fi + echo "IntelliJ IDEA restarted." + else + echo "IntelliJ IDEA is not running. Launch it manually to use the plugin." + fi + ;; + windows) + IDEA_PID=$(tasklist 2>/dev/null | grep -i "idea" | awk '{print $2}' | head -1) + if [[ -n "$IDEA_PID" ]]; then + echo "Restarting IntelliJ IDEA..." + taskkill //PID "$IDEA_PID" //F 2>/dev/null || true + sleep 3 + # Try common install locations + for candidate in \ + "$LOCALAPPDATA/Programs/IntelliJ IDEA Ultimate/bin/idea64.exe" \ + "$LOCALAPPDATA/Programs/IntelliJ IDEA Community/bin/idea64.exe" \ + "C:/Program Files/JetBrains/IntelliJ IDEA/bin/idea64.exe"; do + if [[ -f "$candidate" ]]; then + start "" "$candidate" & + break + fi + done + echo "IntelliJ IDEA restarted." + else + echo "IntelliJ IDEA is not running. Launch it manually to use the plugin." + fi + ;; +esac diff --git a/wt-intellij-plugin/settings.gradle.kts b/wt-intellij-plugin/settings.gradle.kts new file mode 100644 index 0000000..1caa1bd --- /dev/null +++ b/wt-intellij-plugin/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "wt-intellij-plugin" + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/WtAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/WtAction.kt new file mode 100644 index 0000000..98b6818 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/WtAction.kt @@ -0,0 +1,73 @@ +package com.block.wt.actions + +import com.block.wt.services.ContextService +import com.block.wt.model.ContextConfig +import com.block.wt.ui.WorktreePanel +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.progress.runBlockingCancellable + +/** + * Base action for all wt plugin actions. Provides DumbAware, BGT update thread, + * and a `runInBackground` template method for background work with coroutine bridging. + */ +abstract class WtAction : AnAction(), DumbAware { + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = isAvailable(e) + } + + protected open fun isAvailable(e: AnActionEvent): Boolean = + e.project != null + + protected fun runInBackground( + project: Project, + title: String, + cancellable: Boolean = true, + action: suspend (ProgressIndicator) -> Unit, + ) { + ProgressManager.getInstance().run(object : Task.Backgroundable(project, title, cancellable) { + override fun run(indicator: ProgressIndicator) { + runBlockingCancellable { action(indicator) } + } + }) + } +} + +/** + * Base action for actions that require a configured wt context. + * Greyed out when no context is auto-detected for the current project. + */ +abstract class WtConfigAction : WtAction() { + override fun isAvailable(e: AnActionEvent): Boolean { + val project = e.project ?: return false + return ContextService.getInstance(project).getCurrentConfig() != null + } + + protected fun requireConfig(e: AnActionEvent): ContextConfig? { + val project = e.project ?: return null + return ContextService.getInstance(project).getCurrentConfig() + } +} + +/** + * Base action for actions that operate on a selected table row. + * Uses EDT for update() since reading table selection requires Swing thread. + */ +abstract class WtTableAction : AnAction(), DumbAware { + override fun getActionUpdateThread() = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = getSelectedPanel(e)?.getSelectedWorktree() != null + } + + protected fun getSelectedPanel(e: AnActionEvent): WorktreePanel? = + e.getData(WorktreePanel.DATA_KEY) +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt new file mode 100644 index 0000000..e378134 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt @@ -0,0 +1,95 @@ +package com.block.wt.actions.context + +import com.block.wt.actions.WtAction +import com.block.wt.model.ContextConfig +import com.block.wt.services.ContextService +import com.block.wt.services.MetadataService +import com.block.wt.services.WorktreeService +import com.block.wt.ui.AddContextDialog +import com.block.wt.ui.Notifications +import com.block.wt.util.PathHelper +import com.intellij.openapi.actionSystem.AnActionEvent +import java.nio.file.Files + +class AddContextAction : WtAction() { + + override fun isAvailable(e: AnActionEvent): Boolean = true + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project + + val dialog = AddContextDialog(project) + if (!dialog.showAndGet()) return + + val repoPath = dialog.repoPath ?: return + val contextName = dialog.contextName + val baseBranch = dialog.baseBranch + val activeWorktree = dialog.activeWorktree ?: return + val mainRepoRoot = dialog.mainRepoRoot ?: return + val worktreesBase = dialog.worktreesBase ?: return + val ideaFilesBase = dialog.ideaFilesBase ?: return + val patterns = dialog.selectedPatterns + + runInBackground(project ?: return, "Creating Context", cancellable = false) { indicator -> + try { + indicator.text = "Creating directories..." + Files.createDirectories(worktreesBase) + Files.createDirectories(ideaFilesBase) + + // Migration: move repo to mainRepoRoot, create symlink at activeWorktree + // Matches shell's _wt_migrate_repo() + if (PathHelper.isSymlink(activeWorktree)) { + // Already a symlink — previously set up, skip migration + } else if (Files.isDirectory(activeWorktree)) { + if (Files.exists(mainRepoRoot)) { + error("${mainRepoRoot} already exists; cannot migrate") + } + Files.createDirectories(mainRepoRoot.parent) + + // Use temp dir for safety (handles nested paths like ~/java -> ~/java/.wt/...) + val tempDir = activeWorktree.resolveSibling( + ".${activeWorktree.fileName}.wt-migrate-${System.currentTimeMillis()}" + ) + indicator.text = "Moving repository to temp location..." + Files.move(activeWorktree, tempDir) + + indicator.text = "Moving to ${mainRepoRoot}..." + Files.move(tempDir, mainRepoRoot) + + indicator.text = "Creating symlink..." + Files.createSymbolicLink(activeWorktree, mainRepoRoot) + } else if (!Files.exists(activeWorktree)) { + // Neither exists — create symlink if mainRepoRoot exists + if (Files.isDirectory(mainRepoRoot)) { + Files.createSymbolicLink(activeWorktree, mainRepoRoot) + } + } + + indicator.text = "Writing configuration..." + val config = ContextConfig( + name = contextName, + mainRepoRoot = mainRepoRoot, + worktreesBase = worktreesBase, + activeWorktree = activeWorktree, + ideaFilesBase = ideaFilesBase, + baseBranch = baseBranch, + metadataPatterns = patterns, + ) + ContextService.getInstance(project).addContext(config) + + if (patterns.isNotEmpty()) { + indicator.text = "Exporting metadata..." + MetadataService.exportMetadataStatic(mainRepoRoot, ideaFilesBase, patterns) + .onFailure { ex -> + Notifications.warning(project, "Metadata Export Failed", ex.message ?: "Unknown error") + } + } + + WorktreeService.getInstance(project).refreshWorktreeList() + Notifications.info(project, "Context Created", "Context '$contextName' created") + } catch (ex: Exception) { + Notifications.error(project, "Context Creation Failed", ex.message ?: "Unknown error") + } + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt new file mode 100644 index 0000000..25d4216 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt @@ -0,0 +1,51 @@ +package com.block.wt.actions.context + +import com.block.wt.actions.WtConfigAction +import com.block.wt.git.GitConfigHelper +import com.block.wt.services.ContextService +import com.block.wt.ui.Notifications +import com.block.wt.util.PathHelper +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.ui.Messages +import java.nio.file.Files + +class DeleteContextAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val config = requireConfig(e) ?: return + val project = e.project ?: return + + val answer = Messages.showYesNoDialog( + project, + "Delete context '${config.name}'?\n\n" + + "Your repository lives at ${config.mainRepoRoot}.\n" + + "The symlink at ${config.activeWorktree} will be removed.\n" + + "You may need to move the repo back manually.\n\n" + + "Existing worktree directories will be kept.", + "Delete Context", + Messages.getWarningIcon(), + ) + if (answer != Messages.YES) return + + runInBackground(project, "Deleting Context", cancellable = false) { indicator -> + try { + indicator.text = "Removing git config..." + GitConfigHelper.removeAllConfig(config.mainRepoRoot) + + indicator.text = "Removing .conf file..." + val confFile = PathHelper.reposDir.resolve("${config.name}.conf") + Files.deleteIfExists(confFile) + + indicator.text = "Removing symlink..." + if (PathHelper.isSymlink(config.activeWorktree)) { + Files.delete(config.activeWorktree) + } + + ContextService.getInstance(project).reload() + Notifications.info(project, "Context Deleted", "Context '${config.name}' deleted") + } catch (ex: Exception) { + Notifications.error(project, "Delete Failed", ex.message ?: "Unknown error") + } + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt new file mode 100644 index 0000000..de78d63 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt @@ -0,0 +1,123 @@ +package com.block.wt.actions.context + +import com.block.wt.actions.WtConfigAction +import com.block.wt.git.GitConfigHelper +import com.block.wt.services.ContextService +import com.block.wt.services.MetadataService +import com.block.wt.services.WorktreeService +import com.block.wt.ui.AddContextDialog +import com.block.wt.ui.Notifications +import com.block.wt.util.PathHelper +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.ui.Messages +import java.nio.file.Files + +class ReprovisionContextAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val config = requireConfig(e) ?: return + val project = e.project ?: return + + val answer = Messages.showYesNoDialog( + project, + "This will remove the current wt configuration for '${config.name}' and let you re-create it.\n" + + "Existing worktree directories will be kept.\n\n" + + "Continue?", + "Re-provision Context", + Messages.getQuestionIcon(), + ) + if (answer != Messages.YES) return + + runInBackground(project, "Re-provisioning Context", cancellable = false) { indicator -> + try { + indicator.text = "Removing git config..." + GitConfigHelper.removeAllConfig(config.mainRepoRoot) + + indicator.text = "Removing .conf file..." + val confFile = PathHelper.reposDir.resolve("${config.name}.conf") + Files.deleteIfExists(confFile) + + indicator.text = "Removing symlink..." + if (PathHelper.isSymlink(config.activeWorktree)) { + Files.delete(config.activeWorktree) + } + + ContextService.getInstance(project).reload() + } catch (ex: Exception) { + Notifications.error(project, "Re-provision Failed", ex.message ?: "Unknown error") + return@runInBackground + } + + // Open AddContextDialog on EDT for the user to re-create + com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater { + val dialog = AddContextDialog(project) + if (!dialog.showAndGet()) return@invokeLater + + val repoPath = dialog.repoPath ?: return@invokeLater + val contextName = dialog.contextName + val baseBranch = dialog.baseBranch + val activeWorktree = dialog.activeWorktree ?: return@invokeLater + val mainRepoRoot = dialog.mainRepoRoot ?: return@invokeLater + val worktreesBase = dialog.worktreesBase ?: return@invokeLater + val ideaFilesBase = dialog.ideaFilesBase ?: return@invokeLater + val patterns = dialog.selectedPatterns + + runInBackground(project, "Creating Context", cancellable = false) { indicator2 -> + try { + indicator2.text = "Creating directories..." + Files.createDirectories(worktreesBase) + Files.createDirectories(ideaFilesBase) + + // Migration logic (same as AddContextAction) + if (PathHelper.isSymlink(activeWorktree)) { + // Already a symlink — skip + } else if (Files.isDirectory(activeWorktree)) { + if (Files.exists(mainRepoRoot)) { + error("${mainRepoRoot} already exists; cannot migrate") + } + Files.createDirectories(mainRepoRoot.parent) + val tempDir = activeWorktree.resolveSibling( + ".${activeWorktree.fileName}.wt-migrate-${System.currentTimeMillis()}" + ) + indicator2.text = "Moving repository to temp location..." + Files.move(activeWorktree, tempDir) + indicator2.text = "Moving to ${mainRepoRoot}..." + Files.move(tempDir, mainRepoRoot) + indicator2.text = "Creating symlink..." + Files.createSymbolicLink(activeWorktree, mainRepoRoot) + } else if (!Files.exists(activeWorktree)) { + if (Files.isDirectory(mainRepoRoot)) { + Files.createSymbolicLink(activeWorktree, mainRepoRoot) + } + } + + indicator2.text = "Writing configuration..." + val newConfig = com.block.wt.model.ContextConfig( + name = contextName, + mainRepoRoot = mainRepoRoot, + worktreesBase = worktreesBase, + activeWorktree = activeWorktree, + ideaFilesBase = ideaFilesBase, + baseBranch = baseBranch, + metadataPatterns = patterns, + ) + ContextService.getInstance(project).addContext(newConfig) + + if (patterns.isNotEmpty()) { + indicator2.text = "Exporting metadata..." + MetadataService.exportMetadataStatic(mainRepoRoot, ideaFilesBase, patterns) + .onFailure { ex -> + Notifications.warning(project, "Metadata Export Failed", ex.message ?: "Unknown error") + } + } + + WorktreeService.getInstance(project).refreshWorktreeList() + Notifications.info(project, "Context Re-provisioned", "Context '$contextName' re-provisioned") + } catch (ex: Exception) { + Notifications.error(project, "Context Creation Failed", ex.message ?: "Unknown error") + } + } + } + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt new file mode 100644 index 0000000..fbacec5 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt @@ -0,0 +1,47 @@ +package com.block.wt.actions.context + +import com.block.wt.actions.WtConfigAction +import com.block.wt.util.PathHelper +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.ui.components.JBLabel +import com.intellij.ui.dsl.builder.panel +import javax.swing.SwingConstants + +class ShowContextConfigAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val config = requireConfig(e) ?: return + val project = e.project ?: return + + val symlinkTarget = PathHelper.readSymlink(config.activeWorktree) + + val content = panel { + row("Name:") { cell(JBLabel(config.name)) } + row("Main repo root:") { cell(JBLabel(config.mainRepoRoot.toString())) } + row("Active worktree:") { + val text = if (symlinkTarget != null) { + "${config.activeWorktree} -> $symlinkTarget" + } else { + config.activeWorktree.toString() + } + cell(JBLabel(text)) + } + row("Worktrees base:") { cell(JBLabel(config.worktreesBase.toString())) } + row("Metadata vault:") { cell(JBLabel(config.ideaFilesBase.toString())) } + row("Base branch:") { cell(JBLabel(config.baseBranch)) } + if (config.metadataPatterns.isNotEmpty()) { + row("Metadata patterns:") { cell(JBLabel(config.metadataPatterns.joinToString(", "))) } + } + } + + JBPopupFactory.getInstance() + .createComponentPopupBuilder(content, null) + .setTitle("Context: ${config.name}") + .setFocusable(true) + .setRequestFocus(true) + .setMovable(true) + .createPopup() + .showCenteredInCurrentWindow(project) + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt new file mode 100644 index 0000000..b43c469 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt @@ -0,0 +1,36 @@ +package com.block.wt.actions.metadata + +import com.block.wt.actions.WtConfigAction +import com.block.wt.services.MetadataService +import com.block.wt.ui.Notifications +import com.block.wt.util.PathHelper +import com.intellij.openapi.actionSystem.AnActionEvent + +class ExportMetadataAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val config = requireConfig(e) ?: return + + val source = if (PathHelper.isSymlink(config.activeWorktree)) { + val raw = PathHelper.readSymlink(config.activeWorktree) ?: config.mainRepoRoot + if (raw.isAbsolute) raw else config.activeWorktree.parent.resolve(raw).normalize() + } else { + config.mainRepoRoot + } + + runInBackground(project, "Exporting Metadata", cancellable = false) { + it.text = "Exporting metadata to vault..." + MetadataService.getInstance(project) + .exportMetadata(source, config.ideaFilesBase, config.metadataPatterns) + .fold( + onSuccess = { count -> + Notifications.info(project, "Metadata Exported", "Exported $count metadata directories to vault") + }, + onFailure = { ex -> + Notifications.error(project, "Export Failed", ex.message ?: "Unknown error") + } + ) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt new file mode 100644 index 0000000..a50a6a2 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt @@ -0,0 +1,36 @@ +package com.block.wt.actions.metadata + +import com.block.wt.actions.WtConfigAction +import com.block.wt.services.MetadataService +import com.block.wt.ui.Notifications +import com.block.wt.util.PathHelper +import com.intellij.openapi.actionSystem.AnActionEvent + +class ImportMetadataAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val config = requireConfig(e) ?: return + + val target = if (PathHelper.isSymlink(config.activeWorktree)) { + val raw = PathHelper.readSymlink(config.activeWorktree) ?: config.activeWorktree + if (raw.isAbsolute) raw else config.activeWorktree.parent.resolve(raw).normalize() + } else { + config.activeWorktree + } + + runInBackground(project, "Importing Metadata", cancellable = false) { + it.text = "Importing metadata from vault..." + MetadataService.getInstance(project) + .importMetadata(config.ideaFilesBase, target) + .fold( + onSuccess = { count -> + Notifications.info(project, "Metadata Imported", "Imported $count metadata directories") + }, + onFailure = { ex -> + Notifications.error(project, "Import Failed", ex.message ?: "Unknown error") + } + ) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt new file mode 100644 index 0000000..31c4903 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt @@ -0,0 +1,37 @@ +package com.block.wt.actions.metadata + +import com.block.wt.actions.WtConfigAction +import com.block.wt.services.BazelService +import com.block.wt.services.MetadataService +import com.block.wt.ui.Notifications +import com.intellij.openapi.actionSystem.AnActionEvent + +class RefreshBazelTargetsAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val config = requireConfig(e) ?: return + + runInBackground(project, "Refreshing Bazel Targets") { indicator -> + indicator.text = "Refreshing Bazel targets..." + BazelService.getInstance(project) + .refreshAllBazelMetadata(config.mainRepoRoot) + .fold( + onSuccess = { count -> + if (count > 0) { + indicator.text = "Re-exporting metadata..." + MetadataService.getInstance(project) + .exportMetadata(config.mainRepoRoot, config.ideaFilesBase, config.metadataPatterns) + .onFailure { ex -> + Notifications.warning(project, "Metadata Export Failed", ex.message ?: "Unknown error") + } + } + Notifications.info(project, "Bazel Targets Refreshed", "Refreshed $count Bazel IDE directories") + }, + onFailure = { ex -> + Notifications.error(project, "Refresh Failed", ex.message ?: "Unknown error") + } + ) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt new file mode 100644 index 0000000..e1f4c22 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt @@ -0,0 +1,14 @@ +package com.block.wt.actions.util + +import com.block.wt.actions.WtTableAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.ide.CopyPasteManager +import java.awt.datatransfer.StringSelection + +class CopyWorktreePathAction : WtTableAction() { + + override fun actionPerformed(e: AnActionEvent) { + val wt = getSelectedPanel(e)?.getSelectedWorktree() ?: return + CopyPasteManager.getInstance().setContents(StringSelection(wt.path.toString())) + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt new file mode 100644 index 0000000..f697f19 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt @@ -0,0 +1,47 @@ +package com.block.wt.actions.util + +import com.block.wt.actions.WtConfigAction +import com.block.wt.services.WorktreeService +import com.block.wt.ui.Notifications +import com.block.wt.ui.WorktreePanel +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.wm.ToolWindowManager +import org.jetbrains.plugins.terminal.TerminalToolWindowManager + +class OpenInTerminalAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + + val selectedPath = e.getData(WorktreePanel.DATA_KEY)?.getSelectedWorktree()?.path + ?: run { + val worktreeService = WorktreeService.getInstance(project) + val worktrees = worktreeService.worktrees.value + worktrees.firstOrNull { it.isLinked }?.path + ?: project.basePath?.let { java.nio.file.Path.of(it) } + } + ?: return + + try { + val terminalWindow = ToolWindowManager.getInstance(project).getToolWindow("Terminal") + if (terminalWindow != null) { + terminalWindow.activate { + val projectRoot = project.basePath + if (projectRoot != null && selectedPath.toString() != projectRoot) { + try { + val manager = TerminalToolWindowManager.getInstance(project) + @Suppress("DEPRECATION") + manager.createLocalShellWidget(selectedPath.toString(), selectedPath.fileName?.toString() ?: "Terminal") + } catch (_: Throwable) { + // Fallback: just activate the terminal window (user can cd manually) + } + } + } + } else { + Notifications.warning(project, "Terminal", "Terminal tool window is not available") + } + } catch (ex: Exception) { + Notifications.error(project, "Open Terminal", "Failed to open terminal: ${ex.message}") + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt new file mode 100644 index 0000000..5d64307 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt @@ -0,0 +1,13 @@ +package com.block.wt.actions.util + +import com.block.wt.actions.WtConfigAction +import com.block.wt.services.WorktreeService +import com.intellij.openapi.actionSystem.AnActionEvent + +class RefreshWorktreeListAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + WorktreeService.getInstance(project).refreshWorktreeList() + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt new file mode 100644 index 0000000..cbdc100 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt @@ -0,0 +1,18 @@ +package com.block.wt.actions.util + +import com.block.wt.actions.WtTableAction +import com.intellij.ide.actions.RevealFileAction +import com.intellij.openapi.actionSystem.AnActionEvent + +class RevealInFinderAction : WtTableAction() { + + override fun update(e: AnActionEvent) { + super.update(e) + e.presentation.text = RevealFileAction.getActionName() + } + + override fun actionPerformed(e: AnActionEvent) { + val wt = getSelectedPanel(e)?.getSelectedWorktree() ?: return + RevealFileAction.openDirectory(wt.path) + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt new file mode 100644 index 0000000..0fd52e6 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt @@ -0,0 +1,19 @@ +package com.block.wt.actions.util + +import com.block.wt.actions.WtAction +import com.block.wt.ui.WelcomePageHelper +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider +import com.intellij.ui.jcef.JBCefApp + +class WelcomeAction : WtAction() { + + override fun isAvailable(e: AnActionEvent): Boolean = + e.project != null && JBCefApp.isSupported() + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val html = WelcomePageHelper.buildThemedHtml() ?: return + HTMLEditorProvider.openEditor(project, "Worktree Manager Welcome", html) + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt new file mode 100644 index 0000000..1974317 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt @@ -0,0 +1,74 @@ +package com.block.wt.actions.worktree + +import com.block.wt.actions.WtConfigAction +import com.block.wt.progress.asScope +import com.block.wt.provision.ProvisionHelper +import com.block.wt.git.GitBranchHelper +import com.block.wt.services.ContextService +import com.block.wt.services.CreateWorktreeUseCase +import com.block.wt.services.WorktreeService +import com.block.wt.ui.CreateWorktreeDialog +import com.block.wt.ui.Notifications +import com.intellij.openapi.actionSystem.AnActionEvent +import java.nio.file.Path + +class CreateWorktreeAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + + val dialog = CreateWorktreeDialog(project) + if (!dialog.showAndGet()) return + + val branchName = GitBranchHelper.sanitizeBranchName(dialog.branchName) + val worktreePath = Path.of(dialog.worktreePath) + val createNewBranch = dialog.createNewBranch + + runInBackground(project, "Creating Worktree") { indicator -> + val worktreeService = WorktreeService.getInstance(project) + val contextService = ContextService.getInstance(project) + val config = contextService.getCurrentConfig() + + if (createNewBranch && config != null) { + val useCase = CreateWorktreeUseCase(worktreeService, project) + useCase.runCreateNewBranchFlow( + indicator, config.mainRepoRoot, + config.baseBranch, branchName, worktreePath, config, + ) + } else { + indicator.isIndeterminate = false + val scope = indicator.asScope() + + // Step 1: Create worktree (0%–85%) — no progress signal + scope.fraction(0.0) + scope.text("Creating worktree...") + val result = worktreeService.createWorktree( + worktreePath, branchName, createNewBranch, + ) + + result.fold( + onSuccess = { + // Step 2: Provision (85%–95%) + scope.fraction(0.85) + if (config != null) { + ProvisionHelper.provisionWorktree( + project, worktreePath, config, + scope = scope.sub(0.85, 0.10), + ) + } + // Step 3: Refresh (95%–100%) + scope.fraction(0.95) + scope.text("Refreshing worktree list...") + scope.text2("") + worktreeService.refreshWorktreeList() + scope.fraction(1.0) + Notifications.info(project, "Worktree Created", "Created worktree at $worktreePath") + }, + onFailure = { + Notifications.error(project, "Create Failed", it.message ?: "Unknown error") + } + ) + } + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt new file mode 100644 index 0000000..ce02e01 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt @@ -0,0 +1,59 @@ +package com.block.wt.actions.worktree + +import com.block.wt.actions.WtTableAction +import com.block.wt.progress.asScope +import com.block.wt.provision.ProvisionHelper +import com.block.wt.services.ContextService +import com.block.wt.services.WorktreeService +import com.block.wt.ui.Notifications +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.progress.runBlockingCancellable + +class ProvisionWorktreeAction : WtTableAction() { + + override fun update(e: AnActionEvent) { + val project = e.project + val wt = getSelectedPanel(e)?.getSelectedWorktree() + + if (project == null || wt == null) { + e.presentation.isEnabledAndVisible = false + return + } + + e.presentation.isEnabled = !wt.isProvisionedByCurrentContext + e.presentation.isVisible = true + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val wt = getSelectedPanel(e)?.getSelectedWorktree() ?: return + + val config = ContextService.getInstance(project).getCurrentConfig() + val currentContextName = config?.name + + if (currentContextName == null) { + Notifications.error(project, "No Context", "No wt context is configured. Add a context first.") + return + } + + if (wt.isProvisionedByCurrentContext) { + Notifications.info(project, "Already Provisioned", "This worktree is already provisioned by '$currentContextName'") + return + } + + ProgressManager.getInstance().run(object : Task.Backgroundable( + project, "Provisioning Worktree", true + ) { + override fun run(indicator: com.intellij.openapi.progress.ProgressIndicator) { + indicator.isIndeterminate = false + runBlockingCancellable { + ProvisionHelper.provisionWorktree(project, wt.path, config, scope = indicator.asScope()) + WorktreeService.getInstance(project).refreshWorktreeList() + Notifications.info(project, "Worktree Provisioned", "Provisioned ${wt.displayName} for context '$currentContextName'") + } + } + }) + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt new file mode 100644 index 0000000..8a55c41 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt @@ -0,0 +1,104 @@ +package com.block.wt.actions.worktree + +import com.block.wt.actions.WtConfigAction +import com.block.wt.progress.RemovalProgress +import com.block.wt.progress.asScope +import com.block.wt.services.WorktreeService +import com.block.wt.ui.Notifications +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.progress.runBlockingCancellable + +class RemoveMergedAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val worktreeService = WorktreeService.getInstance(project) + + runInBackground(project, "Finding Merged Worktrees") { indicator -> + indicator.text = "Checking merged branches..." + + val worktrees = worktreeService.listWorktrees() + val mergedBranches = worktreeService.getMergedBranches().toSet() + + val mergedWorktrees = worktrees.filter { wt -> + !wt.isMain && !wt.isLinked && wt.branch in mergedBranches + } + + if (mergedWorktrees.isEmpty()) { + Notifications.info(project, "No Merged Worktrees", "No worktrees with merged branches found") + return@runInBackground + } + + val dirtyWorktrees = mergedWorktrees.filter { wt -> + worktreeService.hasUncommittedChanges(wt.path) + } + + val cleanWorktrees = mergedWorktrees - dirtyWorktrees.toSet() + + val message = buildString { + append("Remove ${cleanWorktrees.size} merged worktree(s)?") + for (wt in cleanWorktrees) { + append("\n - ${wt.displayName} (${wt.shortPath})") + } + if (dirtyWorktrees.isNotEmpty()) { + append("\n\nSkipping ${dirtyWorktrees.size} dirty worktree(s):") + for (wt in dirtyWorktrees) { + append("\n - ${wt.displayName} (${wt.shortPath}) [dirty]") + } + } + } + + com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater { + val answer = Messages.showYesNoDialog( + project, message, "Remove Merged Worktrees", Messages.getWarningIcon() + ) + + if (answer == Messages.YES) { + ProgressManager.getInstance().run(object : Task.Backgroundable( + project, "Removing Merged Worktrees", false + ) { + override fun run(indicator: ProgressIndicator) { + runBlockingCancellable { + indicator.isIndeterminate = false + val scope = indicator.asScope() + var removed = 0 + var failed = 0 + + for ((i, wt) in cleanWorktrees.withIndex()) { + val wtStart = i.toDouble() / cleanWorktrees.size * 0.95 + val wtSize = 0.95 / cleanWorktrees.size + val wtScope = scope.sub(wtStart, wtSize) + + scope.text("Removing ${wt.displayName}...") + val result = RemovalProgress.removeWithProgress( + wtScope, wt.path, worktreeService, + ) + if (result.isSuccess) removed++ else failed++ + } + + scope.fraction(0.95) + scope.text("Refreshing worktree list...") + scope.text2("") + worktreeService.refreshWorktreeList() + scope.fraction(1.0) + + val msg = buildString { + append("Removed $removed worktree(s)") + if (failed > 0) append(", $failed failed") + if (dirtyWorktrees.isNotEmpty()) { + append(", ${dirtyWorktrees.size} skipped (dirty)") + } + } + Notifications.info(project, "Merged Worktrees Removed", msg) + } + } + }) + } + } + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt new file mode 100644 index 0000000..ccd961e --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt @@ -0,0 +1,128 @@ +package com.block.wt.actions.worktree + +import com.block.wt.actions.WtConfigAction +import com.block.wt.model.WorktreeInfo +import com.block.wt.progress.RemovalProgress +import com.block.wt.progress.asScope +import com.block.wt.services.ContextService +import com.block.wt.services.SymlinkSwitchService +import com.block.wt.services.WorktreeService +import com.block.wt.settings.WtPluginSettings +import com.block.wt.ui.Notifications +import com.block.wt.ui.WorktreePanel +import com.block.wt.util.normalizeSafe +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep + +class RemoveWorktreeAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val worktreeService = WorktreeService.getInstance(project) + + // If invoked from the table with a selected row, use that directly + val selected = e.getData(WorktreePanel.DATA_KEY)?.getSelectedWorktree() + if (selected != null && !selected.isMain) { + confirmAndRemove(project, selected, worktreeService) + return + } + + runInBackground(project, "Loading Worktrees") { + val worktrees = worktreeService.listWorktrees() + val removable = worktrees.filter { !it.isMain } + if (removable.isEmpty()) { + Notifications.info(project, "No Worktrees", "No removable worktrees found (main worktree cannot be removed)") + return@runInBackground + } + + val displayNames = removable.map { wt -> + buildString { + if (wt.isLinked) append("* ") + append(wt.displayName) + append(" (${wt.shortPath})") + } + } + + ApplicationManager.getApplication().invokeLater { + val step = object : BaseListPopupStep("Remove Worktree", displayNames) { + override fun onChosen(selectedValue: String, finalChoice: Boolean): PopupStep<*>? { + if (finalChoice) { + val index = displayNames.indexOf(selectedValue) + val wt = removable.getOrNull(index) ?: return PopupStep.FINAL_CHOICE + confirmAndRemove(project, wt, worktreeService) + } + return PopupStep.FINAL_CHOICE + } + } + + JBPopupFactory.getInstance() + .createListPopup(step) + .showCenteredInCurrentWindow(project) + } + } + } + + private fun confirmAndRemove(project: Project, wt: WorktreeInfo, worktreeService: WorktreeService) { + val config = ContextService.getInstance(project).getCurrentConfig() + if (config != null && wt.path.normalizeSafe() == config.mainRepoRoot.normalizeSafe()) { + Notifications.error(project, "Cannot Remove", "Cannot remove the main repository worktree") + return + } + + val needsConfirmation = WtPluginSettings.getInstance().state.confirmBeforeRemove || wt.isDirty == true + + if (needsConfirmation) { + val message = buildString { + append("Remove worktree '${wt.displayName}' at ${wt.path}?") + if (wt.isDirty == true) { + append("\n\nWARNING: This worktree has uncommitted changes!") + } + if (wt.isLinked) { + append("\n\nThis is the currently linked worktree. The symlink will be switched to main.") + } + } + + val answer = Messages.showYesNoDialog(project, message, "Remove Worktree", Messages.getWarningIcon()) + if (answer != Messages.YES) return + } + + runInBackground(project, "Removing Worktree", cancellable = false) { indicator -> + indicator.isIndeterminate = false + val scope = indicator.asScope() + + if (wt.isLinked && config != null) { + scope.fraction(0.0) + scope.text("Switching to main worktree...") + SymlinkSwitchService.getInstance(project).doSwitch( + config.mainRepoRoot, indicator, + scope = scope.sub(0.0, 0.05), + ) + } + + scope.fraction(0.05) + scope.text("Removing worktree...") + val force = wt.isDirty == true + val result = RemovalProgress.removeWithProgress( + scope.sub(0.05, 0.90), wt.path, worktreeService, force, + ) + + result.fold( + onSuccess = { + scope.fraction(0.95) + scope.text("Refreshing worktree list...") + worktreeService.refreshWorktreeList() + scope.fraction(1.0) + Notifications.info(project, "Worktree Removed", "Removed ${wt.displayName}") + }, + onFailure = { ex -> + Notifications.error(project, "Remove Failed", ex.message ?: "Unknown error") + }, + ) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt new file mode 100644 index 0000000..6ab1adb --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt @@ -0,0 +1,59 @@ +package com.block.wt.actions.worktree + +import com.block.wt.actions.WtConfigAction +import com.block.wt.provision.ProvisionSwitchHelper +import com.block.wt.services.WorktreeService +import com.block.wt.ui.WorktreePanel +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep + +class SwitchWorktreeAction : WtConfigAction() { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + + // If invoked from the table with a selected row, use that directly + val selected = e.getData(WorktreePanel.DATA_KEY)?.getSelectedWorktree() + if (selected != null && !selected.isLinked) { + ProvisionSwitchHelper.switchWithProvisionPrompt(project, selected) + return + } + + val worktreeService = WorktreeService.getInstance(project) + + runInBackground(project, "Loading Worktrees") { + val worktrees = worktreeService.listWorktrees() + if (worktrees.isEmpty()) return@runInBackground + + val displayNames = worktrees.map { wt -> + buildString { + if (wt.isLinked) append("* ") + append(wt.displayName) + if (wt.isMain) append(" [main]") + } + } + + ApplicationManager.getApplication().invokeLater { + val step = object : BaseListPopupStep("Switch Worktree", displayNames) { + override fun onChosen(selectedValue: String, finalChoice: Boolean): PopupStep<*>? { + if (finalChoice) { + val index = displayNames.indexOf(selectedValue) + val wt = worktrees.getOrNull(index) ?: return PopupStep.FINAL_CHOICE + if (!wt.isLinked) { + ProvisionSwitchHelper.switchWithProvisionPrompt(project, wt) + } + } + return PopupStep.FINAL_CHOICE + } + } + + JBPopupFactory.getInstance() + .createListPopup(step) + .showCenteredInCurrentWindow(project) + } + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetection.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetection.kt new file mode 100644 index 0000000..bff522a --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetection.kt @@ -0,0 +1,13 @@ +package com.block.wt.agent + +import java.nio.file.Path + +/** + * Interface for detecting active Claude agent sessions. + * Production implementation uses lsof and filesystem scanning; + * test implementation returns canned responses. + */ +interface AgentDetection { + fun detectActiveAgentDirs(): Set + fun detectActiveSessionIds(worktreePath: Path): List +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetector.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetector.kt new file mode 100644 index 0000000..72f9552 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetector.kt @@ -0,0 +1,146 @@ +package com.block.wt.agent + +import com.intellij.openapi.diagnostic.Logger +import java.nio.file.Files +import java.nio.file.Path + +object AgentDetector : AgentDetection { + + private val log = Logger.getInstance(AgentDetector::class.java) + + const val CLAUDE_PROJECTS_DIR = ".claude/projects" + const val SESSION_ACTIVE_THRESHOLD_MS = 30L * 60 * 1000 // 30 minutes + private val NON_ALNUM = Regex("[^a-zA-Z0-9]") + + /** + * Detects working directories that have active Claude processes. + * Uses `/usr/sbin/lsof` to find running `claude` processes and their cwds. + * Falls back to scanning `~/.claude/projects/` for recent session files. + */ + override fun detectActiveAgentDirs(): Set { + val fromProcess = detectViaLsof() + val fromSessions = detectViaSessions() + return fromProcess + fromSessions + } + + /** + * Finds active Claude session IDs for a worktree by checking `.jsonl` transcripts + * in `~/.claude/projects//` modified within the threshold. + */ + override fun detectActiveSessionIds(worktreePath: Path): List { + return try { + val projectDir = resolveProjectDir(worktreePath) ?: return emptyList() + val cutoff = System.currentTimeMillis() - SESSION_ACTIVE_THRESHOLD_MS + + Files.list(projectDir).use { stream -> + stream + .filter { it.fileName.toString().endsWith(".jsonl") } + .filter { Files.getLastModifiedTime(it).toMillis() > cutoff } + .map { it.fileName.toString().removeSuffix(".jsonl") } + .sorted(Comparator.reverseOrder()) + .toList() + } + } catch (_: Exception) { + emptyList() + } + } + + /** + * Detects active claude process cwds via lsof. + * Uses full path `/usr/sbin/lsof` to avoid PATH issues in IntelliJ. + * Returns empty set on failure (Windows, permission denied, lsof not found, etc.) + */ + private fun detectViaLsof(): Set { + if (isWindows()) return emptySet() + + return try { + val lsofPath = if (Files.exists(Path.of("/usr/sbin/lsof"))) "/usr/sbin/lsof" + else if (Files.exists(Path.of("/usr/bin/lsof"))) "/usr/bin/lsof" + else "lsof" + + val process = ProcessBuilder(lsofPath, "-a", "-d", "cwd", "-c", "claude", "-Fn") + .redirectErrorStream(true) + .start() + + val output = process.inputStream.bufferedReader().readText() + val exited = process.waitFor() + if (exited != 0) return emptySet() + + output.lines() + .filter { it.startsWith("n") && it.length > 1 } + .mapNotNull { line -> + val pathStr = line.removePrefix("n") + if (pathStr == "/") return@mapNotNull null + try { Path.of(pathStr) } catch (_: Exception) { null } + } + .toSet() + } catch (e: Exception) { + log.debug("lsof detection failed, falling back to session files", e) + emptySet() + } + } + + /** + * Scans `~/.claude/projects/` for dirs with recently-modified session files. + * Uses the correct encoding (all non-alphanumeric → `-`). + */ + private fun detectViaSessions(): Set { + return try { + val projectsDir = claudeProjectsDir() ?: return emptySet() + val cutoff = System.currentTimeMillis() - SESSION_ACTIVE_THRESHOLD_MS + + Files.list(projectsDir).use { stream -> + stream.toList() + .filter { Files.isDirectory(it) } + .filter { dir -> hasRecentSession(dir, cutoff) } + .mapNotNull { dir -> decodeDirToPath(dir.fileName.toString()) } + .toSet() + } + } catch (e: Exception) { + log.debug("Session-based detection failed", e) + emptySet() + } + } + + private fun resolveProjectDir(worktreePath: Path): Path? { + val projectsDir = claudeProjectsDir() ?: return null + val encoded = encodePath(worktreePath) + val dir = projectsDir.resolve(encoded) + return if (Files.isDirectory(dir)) dir else null + } + + private fun claudeProjectsDir(): Path? { + val dir = Path.of(System.getProperty("user.home")).resolve(CLAUDE_PROJECTS_DIR) + return if (Files.isDirectory(dir)) dir else null + } + + private fun hasRecentSession(projectDir: Path, cutoff: Long): Boolean { + return try { + Files.list(projectDir).use { stream -> + stream.anyMatch { file -> + file.fileName.toString().endsWith(".jsonl") && + Files.getLastModifiedTime(file).toMillis() > cutoff + } + } + } catch (_: Exception) { + false + } + } + + /** Claude Code encodes paths by replacing all non-alphanumeric chars with `-`. */ + private fun encodePath(path: Path): String = NON_ALNUM.replace(path.toString(), "-") + + /** Best-effort reverse of encodePath. Lossy, so we validate via round-trip. */ + private fun decodeDirToPath(encoded: String): Path? { + if (isWindows()) return null + val candidate = "/" + encoded.replace("-", "/") + return try { + val path = Path.of(candidate).normalize() + if (Files.isDirectory(path) && encodePath(path) == encoded) path else null + } catch (_: Exception) { + null + } + } + + private fun isWindows(): Boolean = System.getProperty("os.name").lowercase().contains("win") +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt new file mode 100644 index 0000000..879899b --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt @@ -0,0 +1,19 @@ +package com.block.wt.git + +import java.nio.file.Path + +object GitBranchHelper { + + fun sanitizeBranchName(name: String): String { + val trimmed = name.trim() + require(!trimmed.contains("..")) { "Branch name cannot contain '..' (path traversal)" } + require(trimmed.isNotBlank()) { "Branch name cannot be blank" } + require(!trimmed.startsWith("-")) { "Branch name cannot start with '-'" } + return trimmed + } + + fun worktreePathForBranch(worktreesBase: Path, branchName: String): Path { + val safeName = branchName.replace("/", "-") + return worktreesBase.resolve(safeName) + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt new file mode 100644 index 0000000..879b247 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt @@ -0,0 +1,131 @@ +package com.block.wt.git + +import com.block.wt.model.ContextConfig +import com.block.wt.util.PathHelper +import com.block.wt.util.ProcessHelper +import java.nio.file.Path + +object GitConfigHelper { + + /** + * Reads wt.* config from git local config (.git/config). + * Uses `git config --local --get-regexp '^wt\.'` — the same command the shell CLI + * uses in `wt_read_git_config()` (PR #23). + * Returns null if wt.enabled is not true or any required key is missing (all-or-nothing). + * For linked worktrees, reads from the main repo's .git/config (shared via gitdir pointer). + */ + fun readConfig(repoOrWorktreePath: Path): ContextConfig? { + val mainGitDir = GitDirResolver.resolveMainGitDir(repoOrWorktreePath) ?: return null + val mainRepoRoot = mainGitDir.parent + + val result = ProcessHelper.runGit( + listOf("config", "--local", "--get-regexp", "^wt\\."), + workingDir = mainRepoRoot, + ) + if (!result.isSuccess) return null + + // git config lowercases keys: "wt.worktreesbase" not "wt.worktreesBase" + val values = result.stdout.lines() + .filter { it.isNotBlank() } + .associate { line -> + val spaceIdx = line.indexOf(' ') + if (spaceIdx < 0) return@associate line.lowercase() to "" + line.substring(0, spaceIdx).lowercase() to line.substring(spaceIdx + 1) + } + + if (values["wt.enabled"]?.equals("true", ignoreCase = true) != true) return null + + // All 3 required keys must be present (all-or-nothing, matching shell behavior) + val worktreesBase = values["wt.worktreesbase"] ?: return null + val ideaFilesBase = values["wt.ideafilesbase"] ?: return null + val baseBranch = values["wt.basebranch"] ?: return null + + // Context name: prefer wt.contextName from git config; fall back to dirname derivation + val name = values["wt.contextname"] + ?: mainRepoRoot.fileName?.toString() + ?.removeSuffix("-master")?.removeSuffix("-main") + ?: return null + + // Optional keys + val activeWorktreeStr = values["wt.activeworktree"] + val metadataPatternsStr = values["wt.metadatapatterns"] + + return ContextConfig( + name = name, + mainRepoRoot = mainRepoRoot, + worktreesBase = PathHelper.expandTilde(worktreesBase), + ideaFilesBase = PathHelper.expandTilde(ideaFilesBase), + baseBranch = baseBranch, + activeWorktree = activeWorktreeStr?.let { PathHelper.expandTilde(it) } + ?: mainRepoRoot, + metadataPatterns = metadataPatternsStr + ?.split(" ")?.filter { it.isNotBlank() } + ?: emptyList(), + ) + } + + /** + * Writes wt.* config to git local config (.git/config). + * Uses individual `git config --local wt. ` calls. + * Note: the shell CLI never writes to git config (read-only); only the plugin writes. + */ + fun writeConfig(repoPath: Path, config: ContextConfig) { + val mainGitDir = GitDirResolver.resolveMainGitDir(repoPath) + ?: error("Not a git repo: $repoPath") + val repoRoot = mainGitDir.parent + + fun gitSet(key: String, value: String) { + ProcessHelper.runGit(listOf("config", "--local", key, value), workingDir = repoRoot) + } + + gitSet("wt.enabled", "true") + gitSet("wt.contextName", config.name) + gitSet("wt.worktreesBase", config.worktreesBase.toString()) + gitSet("wt.ideaFilesBase", config.ideaFilesBase.toString()) + gitSet("wt.baseBranch", config.baseBranch) + gitSet("wt.activeWorktree", config.activeWorktree.toString()) + if (config.metadataPatterns.isNotEmpty()) { + gitSet("wt.metadataPatterns", config.metadataPatterns.joinToString(" ")) + } else { + // --unset may fail if key doesn't exist — that's fine + ProcessHelper.runGit( + listOf("config", "--local", "--unset", "wt.metadataPatterns"), + workingDir = repoRoot, + ) + } + } + + /** + * Removes all wt.* config keys from git local config. + * Used by re-provision and delete context actions. + */ + fun removeAllConfig(repoPath: Path) { + val mainGitDir = GitDirResolver.resolveMainGitDir(repoPath) ?: return + val repoRoot = mainGitDir.parent + val keys = listOf( + "wt.enabled", "wt.contextName", "wt.worktreesBase", "wt.ideaFilesBase", + "wt.baseBranch", "wt.activeWorktree", "wt.metadataPatterns", + ) + for (key in keys) { + // --unset-all may fail if key doesn't exist — that's fine + ProcessHelper.runGit(listOf("config", "--local", "--unset-all", key), workingDir = repoRoot) + } + } + + /** + * Quick check: is wt.enabled=true in git local config? + * Uses `git config --local --get wt.enabled`. + */ + fun isEnabled(repoOrWorktreePath: Path): Boolean { + val mainGitDir = GitDirResolver.resolveMainGitDir(repoOrWorktreePath) ?: return false + return try { + val result = ProcessHelper.runGit( + listOf("config", "--local", "--get", "wt.enabled"), + workingDir = mainGitDir.parent, + ) + result.isSuccess && result.stdout.trim().equals("true", ignoreCase = true) + } catch (_: Exception) { + false + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitDirResolver.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitDirResolver.kt new file mode 100644 index 0000000..ebc28ac --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitDirResolver.kt @@ -0,0 +1,50 @@ +package com.block.wt.git + +import java.nio.file.Files +import java.nio.file.Path + +object GitDirResolver { + + /** + * Resolves the git directory for a worktree path. + * For main worktrees this is `/.git/`, for linked worktrees it follows + * the `.git` file pointer (e.g. `gitdir: /path/to/.git/worktrees/`). + */ + fun resolveGitDir(worktreePath: Path): Path? { + val dotGit = worktreePath.resolve(".git") + return when { + Files.isDirectory(dotGit) -> dotGit + Files.isRegularFile(dotGit) -> try { + // Linked worktree: .git is a file containing "gitdir: " + val content = Files.readString(dotGit).trim() + if (!content.startsWith("gitdir: ")) return null + val gitDirStr = content.removePrefix("gitdir: ") + val gitDir = Path.of(gitDirStr) + if (gitDir.isAbsolute) gitDir else worktreePath.resolve(gitDir).normalize() + } catch (_: Exception) { + null + } + else -> null + } + } + + /** + * Resolves the main `.git` directory for a path (repo or linked worktree). + * For a main repo, returns `/.git`. + * For a linked worktree (where gitdir points to `.git/worktrees/`), + * walks up from the worktree-specific git dir to find the `.git` parent. + */ + fun resolveMainGitDir(path: Path): Path? { + val gitDir = resolveGitDir(path) ?: return null + // If it's already the main .git directory, return it + if (gitDir.fileName?.toString() == ".git") return gitDir + // For linked worktrees, gitDir is like .git/worktrees/ + // Walk up to find the .git directory + var dir = gitDir + while (dir.parent != null) { + if (dir.fileName?.toString() == ".git") return dir + dir = dir.parent + } + return null + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitParser.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitParser.kt new file mode 100644 index 0000000..336218f --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitParser.kt @@ -0,0 +1,57 @@ +package com.block.wt.git + +import com.block.wt.model.WorktreeInfo +import com.block.wt.util.normalizeSafe +import java.nio.file.Path + +object GitParser { + + fun parsePorcelainOutput(output: String, linkedWorktreePath: Path? = null): List { + if (output.isBlank()) return emptyList() + + val worktrees = mutableListOf() + val blocks = output.trim().split("\n\n") + + for ((index, block) in blocks.withIndex()) { + val lines = block.lines().filter { it.isNotBlank() } + var path: Path? = null + var head: String? = null + var branch: String? = null + var isPrunable = false + + for (line in lines) { + when { + line.startsWith("worktree ") -> path = Path.of(line.removePrefix("worktree ")) + line.startsWith("HEAD ") -> head = line.removePrefix("HEAD ") + line.startsWith("branch ") -> { + branch = line.removePrefix("branch ") + if (branch.startsWith("refs/heads/")) { + branch = branch.removePrefix("refs/heads/") + } + } + line == "detached" -> branch = null + line == "prunable" -> isPrunable = true + } + } + + if (path != null && head != null) { + val isMain = index == 0 + val isLinked = linkedWorktreePath != null && + path.normalizeSafe() == linkedWorktreePath.normalizeSafe() + + worktrees.add( + WorktreeInfo( + path = path, + branch = branch, + head = head, + isMain = isMain, + isLinked = isLinked, + isPrunable = isPrunable, + ) + ) + } + } + + return worktrees + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ContextConfig.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ContextConfig.kt new file mode 100644 index 0000000..63e4af7 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ContextConfig.kt @@ -0,0 +1,13 @@ +package com.block.wt.model + +import java.nio.file.Path + +data class ContextConfig( + val name: String, + val mainRepoRoot: Path, + val worktreesBase: Path, + val activeWorktree: Path, + val ideaFilesBase: Path, + val baseBranch: String, + val metadataPatterns: List, +) diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/MetadataPattern.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/MetadataPattern.kt new file mode 100644 index 0000000..604b0e7 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/MetadataPattern.kt @@ -0,0 +1,28 @@ +package com.block.wt.model + +data class MetadataPattern( + val name: String, + val description: String, +) { + companion object { + val KNOWN_PATTERNS: List = listOf( + MetadataPattern(".idea", "IntelliJ IDEA project settings"), + MetadataPattern(".ijwb", "IntelliJ with Bazel plugin settings"), + MetadataPattern(".aswb", "Android Studio with Bazel plugin settings"), + MetadataPattern(".clwb", "CLion with Bazel plugin settings"), + MetadataPattern(".bazelproject", "Bazel project view file"), + MetadataPattern(".xcodeproj", "Xcode project"), + MetadataPattern(".xcworkspace", "Xcode workspace"), + MetadataPattern(".swiftpm", "Swift Package Manager settings"), + MetadataPattern(".vscode", "VS Code settings"), + MetadataPattern(".bsp", "Build Server Protocol settings"), + MetadataPattern(".metals", "Metals (Scala) settings"), + MetadataPattern(".eclipse", "Eclipse project settings"), + MetadataPattern(".classpath", "Eclipse classpath file"), + MetadataPattern(".project", "Eclipse project file"), + MetadataPattern(".settings", "Eclipse workspace settings"), + ) + + val BAZEL_IDE_PATTERNS: Set = setOf(".ijwb", ".aswb", ".clwb") + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt new file mode 100644 index 0000000..2e64356 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt @@ -0,0 +1,14 @@ +package com.block.wt.model + +import com.google.gson.annotations.SerializedName + +data class ProvisionMarker( + val current: String, + val provisions: List, +) + +data class ProvisionEntry( + val context: String, + @SerializedName("provisioned_at") val provisionedAt: String, + @SerializedName("provisioned_by") val provisionedBy: String, +) diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt new file mode 100644 index 0000000..2818cbe --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt @@ -0,0 +1,48 @@ +package com.block.wt.model + +import com.block.wt.util.relativizeAgainst +import java.nio.file.Path + +sealed class WorktreeStatus { + data object NotLoaded : WorktreeStatus() + data class Loaded( + val staged: Int, + val modified: Int, + val untracked: Int, + val conflicts: Int, + val ahead: Int?, + val behind: Int?, + ) : WorktreeStatus() { + val isDirty: Boolean get() = staged + modified + untracked + conflicts > 0 + } +} + +data class WorktreeInfo( + val path: Path, + val branch: String?, + val head: String, + val isMain: Boolean = false, + val isLinked: Boolean = false, + val isPrunable: Boolean = false, + val status: WorktreeStatus = WorktreeStatus.NotLoaded, + val isProvisioned: Boolean = false, + val isProvisionedByCurrentContext: Boolean = false, + val activeAgentSessionIds: List = emptyList(), +) { + val hasActiveAgent: Boolean get() = activeAgentSessionIds.isNotEmpty() + + /** True if the worktree has any uncommitted changes. Null if status not loaded yet. */ + val isDirty: Boolean? + get() = when (status) { + is WorktreeStatus.NotLoaded -> null + is WorktreeStatus.Loaded -> status.isDirty + } + + val displayName: String + get() = branch ?: head.take(8) + + val shortPath: String + get() = path.fileName?.toString() ?: path.toString() + + fun relativePath(worktreesBase: Path?): String = path.relativizeAgainst(worktreesBase) +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/ProgressScope.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/ProgressScope.kt new file mode 100644 index 0000000..9430dee --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/ProgressScope.kt @@ -0,0 +1,29 @@ +package com.block.wt.progress + +import com.intellij.openapi.progress.ProgressIndicator + +/** + * Maps a sub-range of a [ProgressIndicator]'s fraction to 0.0..1.0. + * + * Example: if the overall bar is at 0.40..0.80 for "Creating worktree", + * `ProgressScope(indicator, start=0.40, size=0.40)` lets you call + * `fraction(0.5)` to set the bar to 0.60 (the midpoint of that range). + */ +class ProgressScope( + private val indicator: ProgressIndicator, + private val start: Double, + private val size: Double, +) { + fun fraction(progress: Double) { + indicator.fraction = start + progress.coerceIn(0.0, 1.0) * size + } + + fun text(value: String) { indicator.text = value } + fun text2(value: String) { indicator.text2 = value } + fun checkCanceled() { indicator.checkCanceled() } + + fun sub(subStart: Double, subSize: Double) = + ProgressScope(indicator, start + subStart * size, subSize * size) +} + +fun ProgressIndicator.asScope() = ProgressScope(this, 0.0, 1.0) diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt new file mode 100644 index 0000000..d68896e --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt @@ -0,0 +1,61 @@ +package com.block.wt.progress + +import com.block.wt.services.WorktreeService +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.nio.file.Files +import java.nio.file.Path + +object RemovalProgress { + + /** + * Removes a worktree while polling file count for progress. + * + * Counts files before removal, launches a polling coroutine that updates + * `scope.fraction` based on (deleted / total), then invokes `git worktree remove`. + * Poll interval is 250ms -- a compromise between responsiveness and I/O cost. + */ + suspend fun removeWithProgress( + scope: ProgressScope, + path: Path, + worktreeService: WorktreeService, + force: Boolean = false, + ): Result { + val totalFiles = countFiles(path) + if (totalFiles == 0L) { + return worktreeService.removeWorktree(path, force = force) + } + + return coroutineScope { + val monitorJob = launch { + while (isActive) { + delay(250) + val remaining = countFiles(path) + val deleted = totalFiles - remaining + if (deleted > 0) { + scope.fraction(deleted.toDouble() / totalFiles) + scope.text2("$deleted / $totalFiles files removed") + } + if (remaining == 0L) break + } + } + + val result = worktreeService.removeWorktree(path, force = force) + monitorJob.cancel() + scope.text2("") + scope.fraction(1.0) + result + } + } + + fun countFiles(dir: Path): Long { + if (!Files.isDirectory(dir)) return 0 + return try { + Files.walk(dir).use { stream -> stream.count() } + } catch (_: Exception) { + 0 + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt new file mode 100644 index 0000000..c0472b7 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt @@ -0,0 +1,81 @@ +package com.block.wt.provision + +import com.block.wt.model.ContextConfig +import com.block.wt.progress.ProgressScope +import com.block.wt.services.BazelService +import com.block.wt.services.MetadataService +import com.block.wt.ui.Notifications +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import java.nio.file.Path + +/** + * Consolidates the provision flow: write marker -> import metadata -> install Bazel symlinks. + * Collects errors and reports them instead of silently swallowing. + */ +object ProvisionHelper { + + private val log = Logger.getInstance(ProvisionHelper::class.java) + + /** + * Performs the full provision sequence for a single worktree. + * + * @param keepExistingFiles If true, only writes the provision marker without importing metadata. + * @param scope Optional progress scope for UI feedback. + */ + suspend fun provisionWorktree( + project: Project, + worktreePath: Path, + config: ContextConfig, + keepExistingFiles: Boolean = false, + scope: ProgressScope? = null, + ) { + val errors = mutableListOf() + + // Step 1: Write marker (0%–5%) + scope?.checkCanceled() + scope?.text("Writing provision marker...") + scope?.fraction(0.0) + ProvisionMarkerService.writeProvisionMarker(worktreePath, config.name).onFailure { + log.warn("Failed to write provision marker for $worktreePath", it) + errors.add("Failed to write provision marker: ${it.message}") + } + + if (!keepExistingFiles) { + // Step 2: Import metadata (5%–85%) + scope?.checkCanceled() + scope?.fraction(0.05) + scope?.text("Importing metadata...") + runCatching { + MetadataService.getInstance(project).importMetadata( + config.ideaFilesBase, worktreePath, + scope = scope?.sub(0.05, 0.80), + ) + }.onFailure { + log.warn("Metadata import failed for $worktreePath", it) + errors.add("Metadata import: ${it.message}") + } + + // Step 3: Bazel symlinks (85%–100%) + scope?.checkCanceled() + scope?.fraction(0.85) + scope?.text("Installing Bazel symlinks...") + runCatching { + BazelService.getInstance(project).installBazelSymlinks(config.mainRepoRoot, worktreePath) + }.onFailure { + log.warn("Bazel symlink install failed for $worktreePath", it) + errors.add("Bazel symlinks: ${it.message}") + } + } + + scope?.fraction(1.0) + + if (errors.isNotEmpty()) { + Notifications.warning( + project, + "Provisioning Warnings", + "Provisioned with issues:\n${errors.joinToString("\n• ", prefix = "• ")}", + ) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt new file mode 100644 index 0000000..4977a2a --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt @@ -0,0 +1,147 @@ +package com.block.wt.provision + +import com.block.wt.git.GitDirResolver +import com.block.wt.model.ProvisionEntry +import com.block.wt.model.ProvisionMarker +import com.google.gson.GsonBuilder +import com.intellij.openapi.diagnostic.Logger +import java.nio.file.Files +import java.nio.file.Path +import java.time.Instant + +object ProvisionMarkerService { + + private val log = Logger.getInstance(ProvisionMarkerService::class.java) + + const val MARKER_FILE = "wt-provisioned" + + val IDE_METADATA_DIRS = listOf(".idea", ".ijwb", ".aswb", ".clwb", ".vscode") + + private val gson = GsonBuilder().setPrettyPrinting().create() + + /** + * Fast check whether a worktree has been provisioned by any context. + * No subprocess — reads the filesystem directly. + */ + fun isProvisioned(worktreePath: Path): Boolean { + val gitDir = GitDirResolver.resolveGitDir(worktreePath) ?: return false + return Files.exists(gitDir.resolve(MARKER_FILE)) + } + + /** + * Checks whether a worktree is currently provisioned by a specific context + * (i.e. that context is the `current` provisioner). + */ + fun isProvisionedByContext(worktreePath: Path, contextName: String): Boolean { + val marker = readProvisionMarker(worktreePath) ?: return false + return marker.current == contextName + } + + /** + * Reads and parses the provision marker file, returning null if not provisioned. + */ + fun readProvisionMarker(worktreePath: Path): ProvisionMarker? { + val gitDir = GitDirResolver.resolveGitDir(worktreePath) ?: return null + val markerPath = gitDir.resolve(MARKER_FILE) + if (!Files.exists(markerPath)) return null + + return try { + val marker = gson.fromJson(Files.readString(markerPath), ProvisionMarker::class.java) + // Gson can produce non-null Kotlin types with null values when JSON fields + // are missing or null. The null checks below are runtime-necessary despite + // Kotlin's type system saying they're redundant. + @Suppress("SENSELESS_COMPARISON") + if (marker == null || marker.current == null || marker.provisions == null) { + log.warn("Incomplete provision marker for $worktreePath") + null + } else { + marker + } + } catch (e: Exception) { + log.warn("Failed to parse provision marker for $worktreePath", e) + null + } + } + + /** + * Writes (or updates) the provision marker for a worktree. + * Sets the given context as `current` and adds/updates its entry in the provisions array. + */ + fun writeProvisionMarker(worktreePath: Path, contextName: String): Result { + val gitDir = GitDirResolver.resolveGitDir(worktreePath) + ?: return Result.failure(IllegalStateException("Cannot resolve git dir for $worktreePath")) + val markerPath = gitDir.resolve(MARKER_FILE) + + val existing = readProvisionMarker(worktreePath) + val now = Instant.now().toString() + + val provisions = (existing?.provisions?.filter { it.context != contextName } ?: emptyList()) + + ProvisionEntry(context = contextName, provisionedAt = now, provisionedBy = "intellij-plugin") + + val marker = ProvisionMarker(current = contextName, provisions = provisions) + return try { + Files.writeString(markerPath, markerToJson(marker)) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** + * Removes the provision marker for a worktree. + * If contextName is null, deletes the entire marker file. + * If non-null, removes just that context's entry; clears current if it matched; + * if the array becomes empty, deletes the file. + */ + fun removeProvisionMarker(worktreePath: Path, contextName: String? = null): Result { + val gitDir = GitDirResolver.resolveGitDir(worktreePath) + ?: return Result.failure(IllegalStateException("Cannot resolve git dir for $worktreePath")) + val markerPath = gitDir.resolve(MARKER_FILE) + if (!Files.exists(markerPath)) return Result.success(Unit) + + if (contextName == null) { + return try { + Files.deleteIfExists(markerPath) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + val existing = readProvisionMarker(worktreePath) ?: return Result.success(Unit) + val remaining = existing.provisions.filter { it.context != contextName } + + if (remaining.isEmpty()) { + return try { + Files.deleteIfExists(markerPath) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + val newCurrent = if (existing.current == contextName) remaining.last().context else existing.current + val marker = ProvisionMarker(current = newCurrent, provisions = remaining) + + return try { + Files.writeString(markerPath, markerToJson(marker)) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** + * Checks whether a worktree already has IDE metadata directories (e.g. .idea/, .ijwb/). + * Used to detect worktrees that were set up outside the provision flow and don't need + * a full metadata import — just the provision marker. + */ + fun hasExistingMetadata(worktreePath: Path): Boolean { + for (pattern in IDE_METADATA_DIRS) { + if (Files.isDirectory(worktreePath.resolve(pattern))) return true + } + return false + } + + private fun markerToJson(marker: ProvisionMarker): String = gson.toJson(marker) +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt new file mode 100644 index 0000000..767ab6a --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt @@ -0,0 +1,137 @@ +package com.block.wt.provision + +import com.block.wt.model.WorktreeInfo +import com.block.wt.progress.asScope +import com.block.wt.services.ContextService +import com.block.wt.services.SymlinkSwitchService +import com.block.wt.settings.WtPluginSettings +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages + +/** + * Handles the "should we provision before switching?" flow. + * Extracted from SwitchWorktreeAction.Companion to give it a proper home + * in the provision/ package. Called from SwitchWorktreeAction and WorktreePanel. + */ +object ProvisionSwitchHelper { + + fun switchWithProvisionPrompt(project: Project, wt: WorktreeInfo) { + val settings = WtPluginSettings.getInstance().state + + // Standard confirm-before-switch check + if (settings.confirmBeforeSwitch) { + val answer = Messages.showYesNoDialog( + project, + "Switch to '${wt.displayName}'?", + "Confirm Switch", + Messages.getQuestionIcon(), + ) + if (answer != Messages.YES) return + } + + // Provision prompt: only when the worktree is not provisioned by the current context + val contextService = ContextService.getInstance(project) + val config = contextService.getCurrentConfig() + val currentContextName = config?.name + + if (settings.promptProvisionOnSwitch && currentContextName != null && !wt.isProvisionedByCurrentContext) { + val hasMetadata = ProvisionMarkerService.hasExistingMetadata(wt.path) + + if (hasMetadata) { + // Worktree already has project files — ask whether to claim them or overwrite + val options = arrayOf( + "Provision (keep files)", + "Provision (overwrite from vault)", + "Switch Only", + "Cancel", + ) + val answer = Messages.showDialog( + project, + "Worktree '${wt.displayName}' has existing project files but hasn't been " + + "provisioned by context '$currentContextName'.\n\n" + + "Keep files: mark as provisioned without changing anything.\n" + + "Overwrite: replace project files with this context's vault.", + "Provision Worktree?", + options, + 0, // default: keep files + Messages.getQuestionIcon(), + ) + + when (answer) { + 0 -> { + // Provision (keep files) — marker only, no import + ProvisionMarkerService.writeProvisionMarker(wt.path, currentContextName) + .onFailure { /* best-effort: switch anyway */ } + SymlinkSwitchService.getInstance(project).switchWorktree(wt.path) + return + } + 1 -> { + // Provision (overwrite from vault) — full import then switch + provisionAndSwitch(project, wt) + return + } + 2 -> { + // Switch Only — fall through + } + else -> return // Cancel or closed + } + } else { + // No existing metadata — standard provision prompt + val answer = Messages.showYesNoCancelDialog( + project, + "Worktree '${wt.displayName}' is not provisioned by context '$currentContextName'.\n\n" + + "Provisioning will import IDE metadata and install Bazel symlinks.", + "Provision Worktree?", + "Provision && Switch", + "Switch Only", + "Cancel", + Messages.getQuestionIcon(), + ) + + when (answer) { + Messages.YES -> { + provisionAndSwitch(project, wt) + return + } + Messages.NO -> { + // Switch Only — fall through + } + else -> return // Cancel + } + } + } + + SymlinkSwitchService.getInstance(project).switchWorktree(wt.path) + } + + private fun provisionAndSwitch(project: Project, wt: WorktreeInfo) { + val config = ContextService.getInstance(project).getCurrentConfig() ?: return + + ProgressManager.getInstance().run(object : Task.Backgroundable( + project, "Provisioning & Switching Worktree", true + ) { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = false + val scope = indicator.asScope() + + runBlockingCancellable { + ProvisionHelper.provisionWorktree( + project, wt.path, config, + scope = scope.sub(0.0, 0.40), + ) + + scope.fraction(0.40) + SymlinkSwitchService.getInstance(project).doSwitch( + wt.path, indicator, + scope = scope.sub(0.40, 0.60), + ) + scope.fraction(1.0) + } + } + }) + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/BazelService.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/BazelService.kt new file mode 100644 index 0000000..a13b6b2 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/BazelService.kt @@ -0,0 +1,157 @@ +package com.block.wt.services + +import com.block.wt.model.MetadataPattern +import com.block.wt.util.ProcessHelper +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.readText + +@Service(Service.Level.PROJECT) +class BazelService( + private val project: Project, + private val cs: CoroutineScope, +) { + private val log = Logger.getInstance(BazelService::class.java) + + companion object { + val BAZEL_SYMLINKS = listOf("bazel-out", "bazel-bin", "bazel-testlogs", "bazel-genfiles") + + fun getInstance(project: Project): BazelService = project.service() + } + + suspend fun installBazelSymlinks( + mainRepo: Path, + worktree: Path, + ): Result = withContext(Dispatchers.IO) { + try { + var count = 0 + for (name in BAZEL_SYMLINKS) { + val mainLink = mainRepo.resolve(name) + if (!Files.isSymbolicLink(mainLink)) continue + + val target = Files.readSymbolicLink(mainLink) + val worktreeLink = worktree.resolve(name) + + if (Files.exists(worktreeLink) || Files.isSymbolicLink(worktreeLink)) { + Files.delete(worktreeLink) + } + + Files.createSymbolicLink(worktreeLink, target) + count++ + log.info("Installed Bazel symlink: $name -> $target") + } + Result.success(count) + } catch (e: Exception) { + log.error("Failed to install Bazel symlinks", e) + Result.failure(e) + } + } + + suspend fun refreshTargets(bazelDir: Path): Result = withContext(Dispatchers.IO) { + try { + val projectViewFile = findProjectViewFile(bazelDir) ?: run { + log.info("No .bazelproject file found in $bazelDir") + return@withContext Result.success(null) + } + + val directories = parseDirectoriesSection(projectViewFile) + if (directories.isEmpty()) { + log.info("No directories found in $projectViewFile") + return@withContext Result.success(null) + } + + val queryExpr = directories.joinToString(" + ") { "//\$it/..." } + val fullQuery = "kind('.*', $queryExpr)" + + val workingDir = bazelDir.parent ?: bazelDir + val result = ProcessHelper.run( + listOf("bazel", "query", fullQuery, "--output=label", "--keep_going"), + workingDir = workingDir, + timeoutSeconds = 300, + ) + + if (!result.isSuccess && result.stdout.isBlank()) { + return@withContext Result.failure(RuntimeException(result.stderr)) + } + + // Write targets file + val targetsDir = bazelDir.resolve("targets") + Files.createDirectories(targetsDir) + + val existingTargetsFile = Files.list(targetsDir).use { stream -> + stream.filter { it.fileName.toString().startsWith("targets-") } + .findFirst() + .orElse(null) + } + + val outputFile = existingTargetsFile ?: targetsDir.resolve("targets-${bazelDir.fileName}") + val sortedTargets = result.stdout.lines() + .filter { it.isNotBlank() } + .sorted() + .joinToString("\n") + + Files.writeString(outputFile, sortedTargets + "\n") + log.info("Wrote ${sortedTargets.lines().size} targets to $outputFile") + + Result.success(outputFile) + } catch (e: Exception) { + log.error("Failed to refresh Bazel targets", e) + Result.failure(e) + } + } + + suspend fun refreshAllBazelMetadata(repoPath: Path): Result = withContext(Dispatchers.IO) { + try { + var count = 0 + for (pattern in MetadataPattern.BAZEL_IDE_PATTERNS) { + val bazelDir = repoPath.resolve(pattern) + if (bazelDir.exists()) { + val result = refreshTargets(bazelDir) + result.fold( + onSuccess = { path -> if (path != null) count++ }, + onFailure = { log.warn("Failed to refresh targets in $bazelDir", it) }, + ) + } + } + Result.success(count) + } catch (e: Exception) { + Result.failure(e) + } + } + + private fun findProjectViewFile(bazelDir: Path): Path? { + val projectView = bazelDir.resolve(".bazelproject") + return if (projectView.exists()) projectView else null + } + + internal fun parseDirectoriesSection(projectViewFile: Path): List { + val lines = projectViewFile.readText().lines() + val directories = mutableListOf() + var inDirectoriesSection = false + + for (line in lines) { + val trimmed = line.trim() + when { + trimmed == "directories:" -> inDirectoriesSection = true + inDirectoriesSection && trimmed.isBlank() -> continue + inDirectoriesSection && !trimmed.startsWith("#") && !trimmed.startsWith("-") -> { + if (trimmed.endsWith(":")) { + // New section started + break + } + directories.add(trimmed) + } + } + } + + return directories + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ContextService.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ContextService.kt new file mode 100644 index 0000000..35c2ec7 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ContextService.kt @@ -0,0 +1,114 @@ +package com.block.wt.services + +import com.block.wt.git.GitConfigHelper +import com.block.wt.git.GitDirResolver +import com.block.wt.model.ContextConfig +import com.block.wt.util.ConfigFileHelper +import com.block.wt.util.PathHelper +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.nio.file.Path + +@Service(Service.Level.PROJECT) +class ContextService( + private val project: Project, + private val cs: CoroutineScope, +) { + private val log = Logger.getInstance(ContextService::class.java) + + private val _contexts = MutableStateFlow>(emptyList()) + val contexts: StateFlow> = _contexts.asStateFlow() + + private val _config = MutableStateFlow(null) + val config: StateFlow = _config.asStateFlow() + + fun initialize() { + reload() + } + + fun reload() { + _contexts.value = ConfigFileHelper.listConfigFiles().mapNotNull { ConfigFileHelper.readConfig(it) } + val detected = detectContext() + _config.value = detected + } + + fun getCurrentConfig(): ContextConfig? = _config.value + + fun addContext(config: ContextConfig) { + // Primary: write to git local config + runCatching { GitConfigHelper.writeConfig(config.mainRepoRoot, config) } + .onFailure { log.warn("Failed to write git local config: ${it.message}") } + + // Backward compat: write .conf file + val confFile = PathHelper.reposDir.resolve("${config.name}.conf") + ConfigFileHelper.writeConfig(confFile, config) + + // Write ~/.wt/current only when creating a new active context (not re-provisioning a different one) + val currentActive = getCurrentConfig()?.name + if (currentActive == null || currentActive == config.name) { + runCatching { ConfigFileHelper.writeCurrentContext(config.name) } + .onFailure { log.warn("Failed to write ~/.wt/current: ${it.message}") } + } + + reload() + } + + /** + * Detects context: tries git local config first, falls back to .conf file matching. + */ + private fun detectContext(): ContextConfig? { + val projectPath = project.basePath?.let { Path.of(it) } ?: return null + + // Try git local config first (from PR #23 convention) + val gitConfig = GitConfigHelper.readConfig(projectPath) + if (gitConfig != null) { + log.info("Auto-detected context '${gitConfig.name}' from git config for $projectPath") + return gitConfig + } + + // Fallback: match against .conf files + val repoRoot = findGitRepoRoot(projectPath) ?: return null + return detectFromConfFiles(repoRoot) + } + + /** + * Matches repo root against loaded .conf file configs. + */ + private fun detectFromConfFiles(repoRoot: Path): ContextConfig? { + val repoRootPaths = resolvePaths(repoRoot) + + return _contexts.value.firstOrNull { config -> + val mainRootPaths = resolvePaths(config.mainRepoRoot) + repoRootPaths.any { it in mainRootPaths } + }?.also { log.info("Auto-detected context '${it.name}' from .conf for project") } + } + + /** + * Walks up from the given path to find the git repo root. + * Delegates to GitDirResolver for .git resolution. + */ + private fun findGitRepoRoot(startPath: Path): Path? { + var current = startPath.normalize() + while (true) { + val mainGitDir = GitDirResolver.resolveMainGitDir(current) + if (mainGitDir != null) return mainGitDir.parent + current = current.parent ?: break + } + return null + } + + private fun resolvePaths(path: Path): Set = buildSet { + add(path.normalize()) + runCatching { path.toRealPath() }.onSuccess { add(it) } + } + + companion object { + fun getInstance(project: Project): ContextService = project.service() + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt new file mode 100644 index 0000000..d3f0d56 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt @@ -0,0 +1,90 @@ +package com.block.wt.services + +import com.block.wt.progress.asScope +import com.block.wt.provision.ProvisionHelper +import com.block.wt.model.ContextConfig +import com.block.wt.ui.Notifications +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.Project +import java.nio.file.Path + +class CreateWorktreeUseCase( + private val worktreeService: WorktreeService, + private val project: Project, +) { + suspend fun runCreateNewBranchFlow( + indicator: ProgressIndicator, + mainRepoRoot: Path, + baseBranch: String, + branchName: String, + worktreePath: Path, + config: ContextConfig, + ) { + val scope = indicator.asScope() + val stashName = "wta-${System.currentTimeMillis()}-${ProcessHandle.current().pid()}" + + val origBranch = worktreeService.getCurrentBranch(mainRepoRoot) + ?: worktreeService.getCurrentRevision(mainRepoRoot) + ?: "HEAD" + + try { + indicator.isIndeterminate = false + + // Step 1: Stash (0%–2%) + scope.checkCanceled() + scope.fraction(0.0) + scope.text("Stashing uncommitted changes...") + if (worktreeService.hasUncommittedChanges(mainRepoRoot)) { + worktreeService.stashSave(mainRepoRoot, stashName) + } + + // Step 2: Checkout (2%–5%) + scope.checkCanceled() + scope.fraction(0.02) + scope.text("Checking out $baseBranch...") + worktreeService.checkout(mainRepoRoot, baseBranch) + + // Step 3: Pull (5%–45%) — git streams progress via stderr + scope.checkCanceled() + scope.fraction(0.05) + scope.text("Pulling latest changes...") + val pullScope = scope.sub(0.05, 0.40) + worktreeService.pullFfOnly(mainRepoRoot) { gitPct -> + pullScope.fraction(gitPct) + } + + // Step 4: Create worktree (45%–85%) — no progress signal, bar sits at 45% + scope.checkCanceled() + scope.fraction(0.45) + scope.text("Creating worktree...") + val result = worktreeService.createWorktree( + worktreePath, branchName, createNewBranch = true, + ) + result.getOrThrow() + + // Step 5: Provision (85%–95%) + scope.fraction(0.85) + ProvisionHelper.provisionWorktree( + project, worktreePath, config, + scope = scope.sub(0.85, 0.10), + ) + + // Step 6: Refresh (95%–100%) + scope.fraction(0.95) + scope.text("Refreshing worktree list...") + scope.text2("") + worktreeService.refreshWorktreeList() + scope.fraction(1.0) + Notifications.info(project, "Worktree Created", "Created worktree '$branchName' at $worktreePath") + } catch (e: Exception) { + Notifications.error(project, "Create Failed", e.message ?: "Unknown error") + } finally { + scope.text2("") + scope.text("Restoring original state...") + val checkoutOk = runCatching { worktreeService.checkout(mainRepoRoot, origBranch) }.isSuccess + if (checkoutOk) { + runCatching { worktreeService.stashPop(mainRepoRoot, stashName) } + } + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt new file mode 100644 index 0000000..f1ac19e --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt @@ -0,0 +1,246 @@ +package com.block.wt.services + +import com.block.wt.model.ContextConfig +import com.block.wt.settings.WtPluginSettings +import com.block.wt.util.PathHelper +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds +import java.nio.file.WatchService + +@Service(Service.Level.PROJECT) +class ExternalChangeWatcher( + private val project: Project, + private val cs: CoroutineScope, +) : Disposable { + private val log = Logger.getInstance(ExternalChangeWatcher::class.java) + private var watchService: WatchService? = null + private var watchJob: Job? = null + private var debounceJob: Job? = null + private var lastWatchState: WatchState = WatchState.EMPTY + + fun startWatching() { + stopWatching() + + watchJob = cs.launch(Dispatchers.IO) { + try { + val ws = FileSystems.getDefault().newWatchService() + watchService = ws + + val pathsToWatch = mutableListOf() + + // Watch ~/.wt/ for `current` file changes (shell context switch) + val wtRoot = PathHelper.wtRoot + if (Files.isDirectory(wtRoot)) { + wtRoot.register( + ws, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, + ) + pathsToWatch.add(wtRoot) + } + + // Watch ~/.wt/repos/ for .conf file changes (NIO watches are not recursive) + val reposDir = PathHelper.reposDir + if (Files.isDirectory(reposDir)) { + reposDir.register( + ws, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, + ) + pathsToWatch.add(reposDir) + } + + // Watch the symlink's parent directory for symlink changes + val config = ContextService.getInstance(project).getCurrentConfig() + val symlinkParent = config?.activeWorktree?.parent + if (symlinkParent != null && Files.isDirectory(symlinkParent)) { + symlinkParent.register( + ws, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, + ) + pathsToWatch.add(symlinkParent) + } + + // Watch .git/worktrees/ for new/deleted linked worktrees + val mainRepoRoot = config?.mainRepoRoot + val gitWorktreesDir = mainRepoRoot?.resolve(".git/worktrees") + if (gitWorktreesDir != null && Files.isDirectory(gitWorktreesDir)) { + gitWorktreesDir.register( + ws, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, + ) + pathsToWatch.add(gitWorktreesDir) + } + + // Watch .git/ for config file changes (external git config edits to wt.* keys) + val gitDir = mainRepoRoot?.resolve(".git") + if (gitDir != null && Files.isDirectory(gitDir)) { + gitDir.register( + ws, + StandardWatchEventKinds.ENTRY_MODIFY, + ) + pathsToWatch.add(gitDir) + } + + lastWatchState = buildWatchState(config, pathsToWatch.toSet()) + log.info("Watching for external changes: $pathsToWatch") + + // Note: `config` is intentionally captured once per watch session. Context-changing + // events always go through `.git/config` or `~/.wt/current` writes, which are detected + // without referencing `config`. When state changes, `debouncedRefresh()` detects the + // WatchState mismatch and re-registers watches with the updated config. + while (true) { + val key = ws.take() + var relevant = false + + val watchedPath = key.watchable() as? Path + + for (event in key.pollEvents()) { + val context = event.context() as? Path ?: continue + val fileName = context.fileName?.toString() ?: continue + + if (isRelevantEvent(fileName, watchedPath, config)) { + relevant = true + } + } + + if (!key.reset()) { + log.info("Watch key invalidated for ${key.watchable()}, re-registering watches") + startWatching() + return@launch + } + + if (relevant) { + debouncedRefresh() + } + } + } catch (_: java.nio.file.ClosedWatchServiceException) { + // Normal shutdown + } catch (e: Exception) { + log.warn("File watcher error", e) + } + } + } + + private fun debouncedRefresh() { + debounceJob?.cancel() + debounceJob = cs.launch { + delay(WtPluginSettings.getInstance().state.debounceDelayMs) + log.info("External change detected, refreshing") + ContextService.getInstance(project).reload() + WorktreeService.getInstance(project).refreshWorktreeList() + + // Re-register watches if context-derived state changed + val config = ContextService.getInstance(project).getCurrentConfig() + val currentState = buildWatchState(config) + if (currentState != lastWatchState) { + log.info("Watch state changed, re-registering watches") + startWatching() + } + } + } + + fun stopWatching() { + watchJob?.cancel() + watchJob = null + debounceJob?.cancel() + debounceJob = null + runCatching { watchService?.close() } + watchService = null + } + + override fun dispose() { + stopWatching() + } + + /** + * Immutable snapshot of the state that determines which paths we watch and which + * events we filter. When any field changes, watches must be re-registered. + */ + data class WatchState( + val paths: Set, + val activeWorktreeFileName: String?, + ) { + companion object { + val EMPTY = WatchState(emptySet(), null) + } + } + + companion object { + fun getInstance(project: Project): ExternalChangeWatcher = project.service() + + /** + * Determines whether a file-system event is relevant to the wt plugin. + * Pure function — extracted for testability. + * + * @param fileName the name of the changed file (event context) + * @param watchedPath the directory that emitted the event + * @param config the current context config (may be null) + */ + internal fun isRelevantEvent( + fileName: String, + watchedPath: Path?, + config: ContextConfig?, + ): Boolean { + // .conf file changes in ~/.wt/repos/ (scoped to reposDir to avoid spurious matches in .git/) + if (fileName.endsWith(".conf") && watchedPath == PathHelper.reposDir) return true + + // ~/.wt/current changed — shell context switch + if (fileName == "current" && watchedPath == PathHelper.wtRoot) return true + + // Active worktree symlink changed (scoped to its parent directory) + if (fileName == config?.activeWorktree?.fileName?.toString() + && watchedPath == config?.activeWorktree?.parent) return true + + // .git/worktrees/ changes — worktree created/removed + val gitWorktreesDir = config?.mainRepoRoot?.resolve(".git/worktrees") + if (watchedPath != null && gitWorktreesDir != null && watchedPath == gitWorktreesDir) { + return true + } + + // .git/config modified — may contain wt.* key changes + val gitDir = config?.mainRepoRoot?.resolve(".git") + if (watchedPath != null && gitDir != null && watchedPath == gitDir && fileName == "config") { + return true + } + + return false + } + + /** + * Builds the watch state from the current config. + * When called from debouncedRefresh(), applies the same existence guards + * used by startWatching() so the path sets are comparable. + */ + internal fun buildWatchState( + config: ContextConfig?, + existingPaths: Set? = null, + ): WatchState { + val paths = existingPaths ?: buildSet { + if (Files.isDirectory(PathHelper.wtRoot)) add(PathHelper.wtRoot) + if (Files.isDirectory(PathHelper.reposDir)) add(PathHelper.reposDir) + config?.activeWorktree?.parent?.let { if (Files.isDirectory(it)) add(it) } + config?.mainRepoRoot?.resolve(".git/worktrees")?.let { if (Files.isDirectory(it)) add(it) } + config?.mainRepoRoot?.resolve(".git")?.let { if (Files.isDirectory(it)) add(it) } + } + return WatchState( + paths = paths, + activeWorktreeFileName = config?.activeWorktree?.fileName?.toString(), + ) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/GitClient.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/GitClient.kt new file mode 100644 index 0000000..09ceced --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/GitClient.kt @@ -0,0 +1,142 @@ +package com.block.wt.services + +import com.block.wt.util.ProcessHelper +import com.block.wt.util.ProcessRunner +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Path + +class GitClient(private val processRunner: ProcessRunner) { + + suspend fun createWorktree( + repoRoot: Path, + path: Path, + branch: String, + createNewBranch: Boolean = false, + onProgress: ((Double) -> Unit)? = null, + ): Result = withContext(Dispatchers.IO) { + val args = mutableListOf("worktree", "add") + if (createNewBranch) { + args.add("-b") + args.add(branch) + args.add(path.toString()) + } else { + args.add(path.toString()) + args.add(branch) + } + + val result = if (onProgress != null && processRunner is ProcessHelper) { + processRunner.runGitWithProgress(args, workingDir = repoRoot, onProgress = onProgress) + } else { + processRunner.runGit(args, workingDir = repoRoot) + } + if (result.isSuccess) { + Result.success(Unit) + } else { + Result.failure(RuntimeException(result.stderr.ifBlank { "Failed to create worktree" })) + } + } + + suspend fun removeWorktree(repoRoot: Path, path: Path, force: Boolean = false): Result = + withContext(Dispatchers.IO) { + val args = mutableListOf("worktree", "remove") + if (force) args.add("--force") + args.add(path.toString()) + + val result = processRunner.runGit(args, workingDir = repoRoot) + if (result.isSuccess) { + Result.success(Unit) + } else { + Result.failure(RuntimeException(result.stderr.ifBlank { "Failed to remove worktree" })) + } + } + + suspend fun getMergedBranches(repoRoot: Path, baseBranch: String): List = + withContext(Dispatchers.IO) { + val result = processRunner.runGit( + listOf("branch", "--merged", baseBranch), + workingDir = repoRoot, + ) + if (!result.isSuccess) return@withContext emptyList() + + result.stdout.lines() + .map { it.trim().removePrefix("* ") } + .filter { it.isNotBlank() && it != baseBranch } + } + + suspend fun hasUncommittedChanges(worktreePath: Path): Boolean = withContext(Dispatchers.IO) { + val result = processRunner.runGit( + listOf("status", "--porcelain"), + workingDir = worktreePath, + ) + result.isSuccess && result.stdout.isNotBlank() + } + + suspend fun stashSave(worktreePath: Path, name: String): Result = withContext(Dispatchers.IO) { + val result = processRunner.runGit( + listOf("stash", "push", "-m", name), + workingDir = worktreePath, + ) + if (result.isSuccess) Result.success(Unit) + else Result.failure(RuntimeException(result.stderr)) + } + + suspend fun stashPop(worktreePath: Path, name: String): Result = withContext(Dispatchers.IO) { + val listResult = processRunner.runGit( + listOf("stash", "list"), + workingDir = worktreePath, + ) + if (!listResult.isSuccess) return@withContext Result.failure(RuntimeException(listResult.stderr)) + + val stashIndex = listResult.stdout.lines() + .indexOfFirst { it.contains(name) } + + if (stashIndex < 0) return@withContext Result.success(Unit) + + val result = processRunner.runGit( + listOf("stash", "pop", "stash@{$stashIndex}"), + workingDir = worktreePath, + ) + if (result.isSuccess) Result.success(Unit) + else Result.failure(RuntimeException(result.stderr)) + } + + suspend fun checkout(worktreePath: Path, branchOrRev: String): Result = withContext(Dispatchers.IO) { + val result = processRunner.runGit( + listOf("checkout", branchOrRev), + workingDir = worktreePath, + ) + if (result.isSuccess) Result.success(Unit) + else Result.failure(RuntimeException(result.stderr)) + } + + suspend fun pullFfOnly( + worktreePath: Path, + onProgress: ((Double) -> Unit)? = null, + ): Result = withContext(Dispatchers.IO) { + val args = listOf("pull", "--ff-only", "--progress") + val result = if (onProgress != null && processRunner is ProcessHelper) { + processRunner.runGitWithProgress(args, workingDir = worktreePath, onProgress = onProgress) + } else { + processRunner.runGit(args, workingDir = worktreePath) + } + if (result.isSuccess) Result.success(Unit) + else Result.failure(RuntimeException(result.stderr)) + } + + suspend fun getCurrentBranch(worktreePath: Path): String? = withContext(Dispatchers.IO) { + val result = processRunner.runGit( + listOf("rev-parse", "--abbrev-ref", "HEAD"), + workingDir = worktreePath, + ) + if (result.isSuccess) result.stdout.trim().ifBlank { null } else null + } + + suspend fun getCurrentRevision(worktreePath: Path): String? = withContext(Dispatchers.IO) { + val result = processRunner.runGit( + listOf("rev-parse", "HEAD"), + workingDir = worktreePath, + ) + if (result.isSuccess) result.stdout.trim().ifBlank { null } else null + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/MetadataService.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/MetadataService.kt new file mode 100644 index 0000000..df2ab75 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/MetadataService.kt @@ -0,0 +1,195 @@ +package com.block.wt.services + +import com.block.wt.model.MetadataPattern +import com.block.wt.progress.ProgressScope +import com.block.wt.util.PathHelper +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.FileVisitOption +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.StandardCopyOption +import java.nio.file.attribute.BasicFileAttributes +import java.util.EnumSet + +@Service(Service.Level.PROJECT) +class MetadataService( + private val project: Project, + private val cs: CoroutineScope, +) { + private val log = Logger.getInstance(MetadataService::class.java) + + suspend fun exportMetadata( + source: Path, + vault: Path, + patterns: List, + ): Result = withContext(Dispatchers.IO) { + exportMetadataStatic(source, vault, patterns) + } + + suspend fun importMetadata( + vault: Path, + target: Path, + scope: ProgressScope? = null, + ): Result = withContext(Dispatchers.IO) { + try { + if (!Files.isDirectory(vault)) { + return@withContext Result.failure( + IllegalArgumentException("Vault directory does not exist: $vault") + ) + } + + val entries = Files.list(vault).use { it.toList() } + val total = entries.size + var count = 0 + + for ((i, entry) in entries.withIndex()) { + scope?.fraction(i.toDouble() / total.coerceAtLeast(1)) + scope?.text2("${i + 1} / $total directories") + + val realPath = if (Files.isSymbolicLink(entry)) { + try { + entry.toRealPath() + } catch (_: Exception) { + log.warn("Broken symlink in vault: $entry, cleaning up") + Files.deleteIfExists(entry) + continue + } + } else { + entry + } + + if (!Files.isDirectory(realPath)) continue + + val targetDir = target.resolve(entry.fileName) + copyDirectory(realPath, targetDir) + count++ + log.info("Imported metadata: ${entry.fileName}") + } + + scope?.text2("") + scope?.fraction(1.0) + Result.success(count) + } catch (e: Exception) { + log.error("Failed to import metadata", e) + Result.failure(e) + } + } + + fun detectPatterns(repoPath: Path): List { + val found = mutableListOf() + for (pattern in MetadataPattern.KNOWN_PATTERNS) { + val candidate = repoPath.resolve(pattern.name) + if (Files.exists(candidate)) { + found.add(pattern.name) + } + } + return found + } + + internal fun deduplicateNested(paths: List): List = deduplicateNestedStatic(paths) + + private fun copyDirectory(source: Path, target: Path) { + // FOLLOW_LINKS so vault symlinks are resolved to actual content + Files.walkFileTree(source, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Int.MAX_VALUE, + object : SimpleFileVisitor() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + val targetDir = target.resolve(source.relativize(dir)) + Files.createDirectories(targetDir) + return FileVisitResult.CONTINUE + } + + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + val targetFile = target.resolve(source.relativize(file)) + try { + Files.copy(file, targetFile, StandardCopyOption.REPLACE_EXISTING) + } catch (e: java.nio.file.NoSuchFileException) { + log.warn("Skipping broken symlink during copy: $file") + } + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(file: Path, exc: java.io.IOException): FileVisitResult { + log.warn("Failed to access file during copy, skipping: $file (${exc.message})") + return FileVisitResult.CONTINUE + } + }) + } + + companion object { + private val staticLog = Logger.getInstance(MetadataService::class.java) + + fun getInstance(project: Project): MetadataService = project.service() + + fun exportMetadataStatic( + source: Path, + vault: Path, + patterns: List, + ): Result { + return try { + Files.createDirectories(vault) + + val foundPaths = findMetadataDirsStatic(source, patterns) + val deduplicated = deduplicateNestedStatic(foundPaths) + + var count = 0 + for (metaPath in deduplicated) { + val relative = source.relativize(metaPath) + val vaultLink = vault.resolve(relative) + Files.createDirectories(vaultLink.parent) + + if (Files.isSymbolicLink(vaultLink)) { + Files.delete(vaultLink) + } + + Files.createSymbolicLink(vaultLink, metaPath) + count++ + staticLog.info("Exported metadata: vault/$relative -> $metaPath") + } + + Result.success(count) + } catch (e: Exception) { + staticLog.error("Failed to export metadata", e) + Result.failure(e) + } + } + + private fun findMetadataDirsStatic( + source: Path, + patterns: List, + maxDepth: Int = 5, + ): List { + val found = mutableListOf() + Files.walkFileTree(source, emptySet(), maxDepth, object : SimpleFileVisitor() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + val name = dir.fileName?.toString() ?: return FileVisitResult.CONTINUE + if (name in patterns) { + found.add(dir) + return FileVisitResult.SKIP_SUBTREE + } + return FileVisitResult.CONTINUE + } + }) + return found + } + + internal fun deduplicateNestedStatic(paths: List): List { + val sorted = paths.sortedBy { it.nameCount } + val kept = mutableListOf() + for (path in sorted) { + val isNested = kept.any { path.startsWith(it) } + if (!isNested) { + kept.add(path) + } + } + return kept + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt new file mode 100644 index 0000000..a6eff61 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt @@ -0,0 +1,142 @@ +package com.block.wt.services + +import com.block.wt.progress.ProgressScope +import com.block.wt.progress.asScope +import com.block.wt.ui.Notifications +import com.block.wt.util.PathHelper +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFileManager +import git4idea.repo.GitRepositoryManager +import com.intellij.openapi.progress.runBlockingCancellable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Path + +@Service(Service.Level.PROJECT) +class SymlinkSwitchService( + private val project: Project, + private val cs: CoroutineScope, +) { + fun switchWorktree(newTarget: Path) { + ProgressManager.getInstance().run(object : Task.Backgroundable( + project, "Switching Worktree", false + ) { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = false + val scope = indicator.asScope() + runBlockingCancellable { doSwitch(newTarget, indicator, scope) } + } + }) + } + + suspend fun doSwitch( + newTarget: Path, + indicator: ProgressIndicator? = null, + scope: ProgressScope? = null, + ) { + val contextService = ContextService.getInstance(project) + val config = contextService.getCurrentConfig() + if (config == null) { + Notifications.error(project, "No Context", "No active wt context detected for this project") + return + } + + val symlinkPath = config.activeWorktree + + val app = ApplicationManager.getApplication() + + try { + // Phase 1: Save documents (0%–10%) + scope?.fraction(0.0) + scope?.text("Saving documents...") + indicator?.text = "Saving documents..." + app.invokeAndWait({ + WriteAction.run { + FileDocumentManager.getInstance().saveAllDocuments() + } + }, ModalityState.defaultModalityState()) + + // Phase 2: Atomic symlink swap (10%–20%) + scope?.fraction(0.10) + scope?.text("Swapping symlink...") + indicator?.text = "Swapping symlink..." + withContext(Dispatchers.IO) { + PathHelper.atomicSetSymlink(symlinkPath, newTarget) + } + + // Phase 3: Reload editors (20%–40%) + scope?.fraction(0.20) + scope?.text("Reloading editors...") + indicator?.text = "Reloading editors..." + app.invokeAndWait({ + WriteAction.run { + val fdm = FileDocumentManager.getInstance() + for (openFile in FileEditorManager.getInstance(project).openFiles) { + val doc = fdm.getCachedDocument(openFile) ?: continue + fdm.reloadFromDisk(doc) + } + } + }, ModalityState.defaultModalityState()) + + // Phase 4: VFS refresh (40%–65%) + // Re-resolve projectRoot AFTER symlink swap so VFS picks up the new target + scope?.fraction(0.40) + scope?.text("Refreshing file system...") + indicator?.text = "Refreshing file system..." + val projectRoot = project.basePath?.let { VfsUtil.findFileByIoFile(java.io.File(it), true) } + if (projectRoot != null) { + projectRoot.refresh(false, true) + VfsUtil.markDirtyAndRefresh(false, true, true, projectRoot) + } + VirtualFileManager.getInstance().asyncRefresh {} + + // Phase 5: Git state (65%–85%) + scope?.fraction(0.65) + scope?.text("Updating git state...") + indicator?.text = "Updating git state..." + withContext(Dispatchers.IO) { + val repos = GitRepositoryManager.getInstance(project).repositories + for (repo in repos) { + repo.update() + } + } + VcsDirtyScopeManager.getInstance(project).markEverythingDirty() + + // Phase 6: Refresh list (85%–100%) + scope?.fraction(0.85) + scope?.text("Refreshing worktree list...") + indicator?.text = "Refreshing worktree list..." + WorktreeService.getInstance(project).refreshWorktreeList() + scope?.fraction(1.0) + + Notifications.info( + project, + "Worktree Switched", + "Switched to ${newTarget.fileName}", + ) + } catch (e: Exception) { + Notifications.error( + project, + "Switch Failed", + "Failed to switch worktree: ${e.message}", + ) + } + } + + companion object { + fun getInstance(project: Project): SymlinkSwitchService = project.service() + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt new file mode 100644 index 0000000..334df73 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt @@ -0,0 +1,46 @@ +package com.block.wt.services + +import com.block.wt.provision.ProvisionMarkerService +import com.block.wt.agent.AgentDetection +import com.block.wt.model.WorktreeInfo +import com.block.wt.util.normalizeSafe + +interface WorktreeEnricher { + fun enrich(worktrees: List): List +} + +class ProvisionStatusEnricher(private val contextService: ContextService) : WorktreeEnricher { + override fun enrich(worktrees: List): List { + val currentContextName = contextService.getCurrentConfig()?.name + return worktrees.map { wt -> + val provisioned = ProvisionMarkerService.isProvisioned(wt.path) + val provisionedByCtx = if (provisioned && currentContextName != null) { + ProvisionMarkerService.isProvisionedByContext(wt.path, currentContextName) + } else false + wt.copy(isProvisioned = provisioned, isProvisionedByCurrentContext = provisionedByCtx) + } + } +} + +class AgentStatusEnricher(private val agentDetection: AgentDetection) : WorktreeEnricher { + override fun enrich(worktrees: List): List { + // Process detection (lsof) catches running-but-idle agents; + // session file check catches recently active agents. + // detectActiveAgentDirs() returns the union of both. + val activeDirs = agentDetection.detectActiveAgentDirs() + return worktrees.map { wt -> + val hasRunningProcess = activeDirs.any { it.normalizeSafe() == wt.path.normalizeSafe() } + val sessionIds = agentDetection.detectActiveSessionIds(wt.path) + // Show 🤖 if either a process is running or sessions are recent. + // For idle processes with no recent sessions, use a placeholder ID. + val effectiveIds = if (sessionIds.isNotEmpty()) { + sessionIds + } else if (hasRunningProcess) { + listOf("(running)") + } else { + emptyList() + } + wt.copy(activeAgentSessionIds = effectiveIds) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt new file mode 100644 index 0000000..63d6072 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt @@ -0,0 +1,33 @@ +package com.block.wt.services + +import com.block.wt.settings.WtPluginSettings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class WorktreeRefreshScheduler( + private val cs: CoroutineScope, + private val onRefresh: () -> Unit, +) { + @Volatile + private var periodicRefreshJob: Job? = null + + fun start() { + periodicRefreshJob?.cancel() + val intervalSeconds = WtPluginSettings.getInstance().state.autoRefreshIntervalSeconds + if (intervalSeconds <= 0) return + + periodicRefreshJob = cs.launch { + while (true) { + delay(intervalSeconds * 1000L) + onRefresh() + } + } + } + + fun stop() { + periodicRefreshJob?.cancel() + periodicRefreshJob = null + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeService.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeService.kt new file mode 100644 index 0000000..cc5fb06 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeService.kt @@ -0,0 +1,260 @@ +package com.block.wt.services + +import com.block.wt.agent.AgentDetection +import com.block.wt.agent.AgentDetector +import com.block.wt.git.GitParser +import com.block.wt.model.WorktreeInfo +import com.block.wt.model.WorktreeStatus +import com.block.wt.settings.WtPluginSettings +import com.block.wt.util.PathHelper +import com.block.wt.util.ProcessHelper +import com.block.wt.util.ProcessRunner +import com.block.wt.util.normalizeSafe +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import git4idea.repo.GitRepositoryManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.nio.file.Path + +@Service(Service.Level.PROJECT) +class WorktreeService( + private val project: Project, + private val cs: CoroutineScope, +) { + private val log = Logger.getInstance(WorktreeService::class.java) + + // Injectable for testing — default to production singletons + internal var processRunner: ProcessRunner = ProcessHelper + internal var agentDetection: AgentDetection = AgentDetector + + private val gitClient by lazy { GitClient(processRunner) } + private val enrichers: List by lazy { + listOf( + ProvisionStatusEnricher(ContextService.getInstance(project)), + AgentStatusEnricher(agentDetection), + ) + } + private val refreshScheduler = WorktreeRefreshScheduler(cs) { refreshWorktreeList() } + + private val _worktrees = MutableStateFlow>(emptyList()) + val worktrees: StateFlow> = _worktrees.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val statusMutex = Mutex() + + fun refreshWorktreeList() { + _isLoading.value = true + cs.launch { + try { + val list = listWorktrees() + _worktrees.value = list + lastRefreshTime = System.currentTimeMillis() + + // Async-load status indicators (skip if disabled in settings) + if (!WtPluginSettings.getInstance().state.statusLoadingEnabled) return@launch + val statusJobs = list.withIndex().map { (index, wt) -> + async { + val loadedStatus = loadStatusIndicators(wt).status + statusMutex.withLock { + val current = _worktrees.value.toMutableList() + if (index < current.size && current[index].path == wt.path) { + current[index] = current[index].copy(status = loadedStatus) + _worktrees.value = current + } + } + } + } + statusJobs.awaitAll() + } finally { + _isLoading.value = false + } + } + } + + suspend fun listWorktrees(): List = withContext(Dispatchers.IO) { + val repoRoot = getMainRepoRoot() ?: return@withContext emptyList() + + // worktree list has no programmatic API — use subprocess + val result = processRunner.runGit( + listOf("worktree", "list", "--porcelain"), + workingDir = repoRoot, + ) + + if (!result.isSuccess) { + log.warn("git worktree list failed (exit=${result.exitCode}): ${result.stderr.trim()}") + return@withContext emptyList() + } + + val linkedPath = getLinkedWorktreePath() + parseAndEnrich(result.stdout, linkedPath) + } + + internal fun parseAndEnrich(porcelainOutput: String, linkedPath: Path?): List { + val parsed = GitParser.parsePorcelainOutput(porcelainOutput, linkedPath) + return enrichers.fold(parsed) { list, enricher -> enricher.enrich(list) } + } + + /** + * Loads status via `git status --porcelain=v1 -b` subprocess. + * Runs in a separate process — zero JVM heap impact + * for DirCache, pack indices, and working tree scanning. + */ + private suspend fun loadStatusIndicators(wt: WorktreeInfo): WorktreeInfo = withContext(Dispatchers.IO) { + val result = processRunner.runGit( + listOf("status", "--porcelain=v1", "-b"), + workingDir = wt.path, + ) + if (!result.isSuccess) { + log.warn("git status failed for ${wt.path}: ${result.stderr.trim()}") + return@withContext wt + } + parseGitStatusOutput(wt, result.stdout) + } + + /** + * Parses `git status --porcelain=v1 -b` output. + * + * Format: + * ``` + * ## branch...origin/branch [ahead 1, behind 2] + * M staged-file.txt + * M unstaged-file.txt + * ?? untracked.txt + * UU conflicting.txt + * ``` + */ + internal fun parseGitStatusOutput(wt: WorktreeInfo, output: String): WorktreeInfo { + var staged = 0; var modified = 0; var untracked = 0; var conflicts = 0 + var ahead: Int? = null; var behind: Int? = null + + for (line in output.lines()) { + if (line.startsWith("## ")) { + // Parse branch line: "## branch...origin/branch [ahead 1, behind 2]" + val bracketContent = line.substringAfter("[", "").substringBefore("]", "") + if (bracketContent.isNotEmpty()) { + for (part in bracketContent.split(",")) { + val trimmed = part.trim() + if (trimmed.startsWith("ahead ")) { + ahead = trimmed.removePrefix("ahead ").trim().toIntOrNull() + } else if (trimmed.startsWith("behind ")) { + behind = trimmed.removePrefix("behind ").trim().toIntOrNull() + } + } + } + continue + } + if (line.length < 2) continue + val x = line[0] // staged status + val y = line[1] // unstaged status + + // Conflicts: UU, AA, DD, AU, UA, DU, UD + if ((x == 'U' || y == 'U') || (x == 'A' && y == 'A') || (x == 'D' && y == 'D')) { + conflicts++; continue + } + // Untracked + if (x == '?' && y == '?') { untracked++; continue } + // Staged changes (X column) + if (x in "MADRC") staged++ + // Unstaged changes (Y column) + if (y in "MD") modified++ + } + + return wt.copy( + status = WorktreeStatus.Loaded(staged, modified, untracked, conflicts, ahead, behind), + ) + } + + // --- Facade delegates to GitClient --- + + suspend fun createWorktree( + path: Path, + branch: String, + createNewBranch: Boolean = false, + onProgress: ((Double) -> Unit)? = null, + ): Result { + val repoRoot = getMainRepoRoot() + ?: return Result.failure(IllegalStateException("No git repository found")) + return gitClient.createWorktree(repoRoot, path, branch, createNewBranch, onProgress) + } + + suspend fun removeWorktree(path: Path, force: Boolean = false): Result { + val repoRoot = getMainRepoRoot() + ?: return Result.failure(IllegalStateException("No git repository found")) + return gitClient.removeWorktree(repoRoot, path, force) + } + + suspend fun getMergedBranches(): List { + val repoRoot = getMainRepoRoot() ?: return emptyList() + val contextService = ContextService.getInstance(project) + val baseBranch = contextService.getCurrentConfig()?.baseBranch ?: "main" + return gitClient.getMergedBranches(repoRoot, baseBranch) + } + + suspend fun hasUncommittedChanges(worktreePath: Path): Boolean = + gitClient.hasUncommittedChanges(worktreePath) + + suspend fun stashSave(worktreePath: Path, name: String): Result = + gitClient.stashSave(worktreePath, name) + + suspend fun stashPop(worktreePath: Path, name: String): Result = + gitClient.stashPop(worktreePath, name) + + suspend fun checkout(worktreePath: Path, branchOrRev: String): Result = + gitClient.checkout(worktreePath, branchOrRev) + + suspend fun pullFfOnly(worktreePath: Path, onProgress: ((Double) -> Unit)? = null): Result = + gitClient.pullFfOnly(worktreePath, onProgress) + + suspend fun getCurrentBranch(worktreePath: Path): String? = + gitClient.getCurrentBranch(worktreePath) + + suspend fun getCurrentRevision(worktreePath: Path): String? = + gitClient.getCurrentRevision(worktreePath) + + // --- Repo root resolution --- + + fun getMainRepoRoot(): Path? { + val contextService = ContextService.getInstance(project) + val config = contextService.getCurrentConfig() + if (config != null) return config.mainRepoRoot + + // Fallback: use the project's git root + val repos = GitRepositoryManager.getInstance(project).repositories + return repos.firstOrNull()?.root?.toNioPath() + } + + private fun getLinkedWorktreePath(): Path? { + val contextService = ContextService.getInstance(project) + val config = contextService.getCurrentConfig() ?: return null + val symlinkPath = config.activeWorktree + return PathHelper.readSymlink(symlinkPath)?.normalizeSafe() + } + + // --- Periodic refresh --- + + @Volatile + var lastRefreshTime: Long = 0L + private set + + fun startPeriodicRefresh() = refreshScheduler.start() + + fun stopPeriodicRefresh() = refreshScheduler.stop() + + companion object { + fun getInstance(project: Project): WorktreeService = project.service() + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt new file mode 100644 index 0000000..e4bc42c --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt @@ -0,0 +1,51 @@ +package com.block.wt.services + +import com.block.wt.settings.WtPluginSettings +import com.block.wt.ui.ContextSetupDialog +import com.block.wt.ui.WelcomePageHelper +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.ui.jcef.JBCefApp +class WtStartupActivity : ProjectActivity { + override suspend fun execute(project: Project) { + // Initialize context service (project-level, auto-detects context) + ContextService.getInstance(project).initialize() + + // Start watching for external changes + ExternalChangeWatcher.getInstance(project).startWatching() + + // Initial worktree list load + val worktreeService = WorktreeService.getInstance(project) + worktreeService.refreshWorktreeList() + + // Show context setup dialog if this context hasn't been set up yet + val config = ContextService.getInstance(project).getCurrentConfig() + if (config != null) { + val worktrees = worktreeService.listWorktrees() + ApplicationManager.getApplication().invokeLater { + ContextSetupDialog.showIfNeeded(project, config, worktrees) + } + } + + // Show welcome tab on first install or version update + showWelcomeTabIfNeeded(project) + } + + private fun showWelcomeTabIfNeeded(project: Project) { + val currentVersion = PluginManagerCore.getPlugin(PluginId.getId("com.block.wt"))?.version ?: return + val settings = WtPluginSettings.getInstance() + if (settings.state.lastWelcomeVersion == currentVersion) return + if (!JBCefApp.isSupported()) return + + settings.state.lastWelcomeVersion = currentVersion + + ApplicationManager.getApplication().invokeLater { + val html = WelcomePageHelper.buildThemedHtml() ?: return@invokeLater + HTMLEditorProvider.openEditor(project, "Worktree Manager Welcome", html) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt new file mode 100644 index 0000000..c06ed91 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt @@ -0,0 +1,43 @@ +package com.block.wt.settings + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service + +@Service(Service.Level.APP) +@State( + name = "com.block.wt.settings.WtPluginSettings", + storages = [Storage("WtPluginSettings.xml")] +) +class WtPluginSettings : PersistentStateComponent { + + data class State( + var showStatusBarWidget: Boolean = true, + var autoRefreshOnExternalChange: Boolean = true, + var confirmBeforeSwitch: Boolean = false, + var confirmBeforeRemove: Boolean = true, + var statusLoadingEnabled: Boolean = true, + var debounceDelayMs: Long = 500, + var promptProvisionOnSwitch: Boolean = true, + var autoRefreshIntervalSeconds: Int = 30, + var setupCompletedContexts: MutableList = mutableListOf(), + var lastWelcomeVersion: String = "", + ) + + @Volatile + private var state = State() + + override fun getState(): State = state + + override fun loadState(state: State) { + this.state = state + } + + companion object { + fun getInstance(): WtPluginSettings = + ApplicationManager.getApplication().service() + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt new file mode 100644 index 0000000..efad20e --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt @@ -0,0 +1,91 @@ +package com.block.wt.settings + +import com.intellij.ui.dsl.builder.bindIntValue +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent +import javax.swing.JPanel + +class WtSettingsComponent { + + private val settings = WtPluginSettings.getInstance() + private var showStatusBar = settings.state.showStatusBarWidget + private var autoRefresh = settings.state.autoRefreshOnExternalChange + private var confirmSwitch = settings.state.confirmBeforeSwitch + private var confirmRemove = settings.state.confirmBeforeRemove + private var statusLoading = settings.state.statusLoadingEnabled + private var promptProvision = settings.state.promptProvisionOnSwitch + private var autoRefreshInterval = settings.state.autoRefreshIntervalSeconds + + val panel: JPanel = panel { + group("General") { + row { + checkBox("Show context widget in status bar") + .bindSelected(::showStatusBar) + } + row { + checkBox("Auto-refresh on external changes (CLI usage)") + .bindSelected(::autoRefresh) + } + row { + checkBox("Load status indicators (dirty, ahead/behind) asynchronously") + .bindSelected(::statusLoading) + .comment("Disable to speed up worktree list loading for repos with many worktrees") + } + row { + checkBox("Prompt to provision when switching to non-provisioned worktrees") + .bindSelected(::promptProvision) + .comment("Shows Provision & Switch / Switch Only / Cancel dialog") + } + row("Auto-refresh interval (seconds, 0 to disable):") { + spinner(0..600, 5) + .bindIntValue(::autoRefreshInterval) + .comment("Periodically refreshes the worktree list to detect external changes") + } + } + group("Confirmations") { + row { + checkBox("Confirm before switching worktrees") + .bindSelected(::confirmSwitch) + } + row { + checkBox("Confirm before removing worktrees") + .bindSelected(::confirmRemove) + } + } + } + + fun getComponent(): JComponent = panel + + fun isModified(): Boolean { + return showStatusBar != settings.state.showStatusBarWidget || + autoRefresh != settings.state.autoRefreshOnExternalChange || + confirmSwitch != settings.state.confirmBeforeSwitch || + confirmRemove != settings.state.confirmBeforeRemove || + statusLoading != settings.state.statusLoadingEnabled || + promptProvision != settings.state.promptProvisionOnSwitch || + autoRefreshInterval != settings.state.autoRefreshIntervalSeconds + } + + fun apply() { + settings.loadState(settings.state.copy( + showStatusBarWidget = showStatusBar, + autoRefreshOnExternalChange = autoRefresh, + confirmBeforeSwitch = confirmSwitch, + confirmBeforeRemove = confirmRemove, + statusLoadingEnabled = statusLoading, + promptProvisionOnSwitch = promptProvision, + autoRefreshIntervalSeconds = autoRefreshInterval, + )) + } + + fun reset() { + showStatusBar = settings.state.showStatusBarWidget + autoRefresh = settings.state.autoRefreshOnExternalChange + confirmSwitch = settings.state.confirmBeforeSwitch + confirmRemove = settings.state.confirmBeforeRemove + statusLoading = settings.state.statusLoadingEnabled + promptProvision = settings.state.promptProvisionOnSwitch + autoRefreshInterval = settings.state.autoRefreshIntervalSeconds + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt new file mode 100644 index 0000000..2c4c511 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt @@ -0,0 +1,30 @@ +package com.block.wt.settings + +import com.intellij.openapi.options.Configurable +import javax.swing.JComponent + +class WtSettingsConfigurable : Configurable { + + private var component: WtSettingsComponent? = null + + override fun getDisplayName(): String = "Worktree Manager" + + override fun createComponent(): JComponent { + component = WtSettingsComponent() + return component!!.getComponent() + } + + override fun isModified(): Boolean = component?.isModified() == true + + override fun apply() { + component?.apply() + } + + override fun reset() { + component?.reset() + } + + override fun disposeUIResources() { + component = null + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt new file mode 100644 index 0000000..22be88e --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt @@ -0,0 +1,254 @@ +package com.block.wt.ui + +import com.block.wt.git.GitConfigHelper +import com.block.wt.git.GitDirResolver +import com.block.wt.model.MetadataPattern +import com.block.wt.util.PathHelper +import com.block.wt.util.ProcessHelper +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.panel +import java.nio.file.Files +import java.nio.file.Path +import javax.swing.JComponent +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener + +class AddContextDialog(private val project: Project?) : DialogWrapper(project) { + + private val repoPathField = JBTextField().apply { isEditable = false } + private val contextNameField = JBTextField() + private val baseBranchField = JBTextField() + private val mainRepoRootLabel = JBLabel() + private val worktreesBaseLabel = JBLabel() + private val ideaFilesBaseLabel = JBLabel() + private val migrationInfoLabel = JBLabel() + private val patternCheckboxes = mutableListOf>() + + private var isGitRepo: Boolean = false + private var hasExistingGitConfig: Boolean = false + + var repoPath: Path? = null + private set + var contextName: String = "" + private set + var baseBranch: String = "main" + private set + var mainRepoRoot: Path? = null + private set + var activeWorktree: Path? = null + private set + var worktreesBase: Path? = null + private set + var ideaFilesBase: Path? = null + private set + var selectedPatterns: List = emptyList() + private set + + init { + title = "Add Context" + init() + prefillFromProject() + setupAutoDerivation() + } + + private fun prefillFromProject() { + val basePath = project?.basePath ?: return + // Resolve to git root (follow symlinks, find actual repo root) + val projectPath = Path.of(basePath) + val resolved = runCatching { projectPath.toRealPath() }.getOrElse { projectPath.normalize() } + repoPathField.text = resolved.toString() + rederive() + } + + private fun setupAutoDerivation() { + contextNameField.document.addDocumentListener(object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) = rederivePaths() + override fun removeUpdate(e: DocumentEvent) = rederivePaths() + override fun changedUpdate(e: DocumentEvent) = rederivePaths() + }) + } + + private fun rederive() { + val pathStr = repoPathField.text.trim() + if (pathStr.isBlank()) return + + val path = Path.of(pathStr) + if (!Files.isDirectory(path)) return + + // Auto-derive context name from repo basename + val basename = path.fileName?.toString() ?: return + val name = basename + .removeSuffix("-master") + .removeSuffix("-main") + contextNameField.text = name + + // Auto-detect base branch, validate git repo, and check existing config (runs off EDT) + data class DeriveResult(val branch: String, val isGit: Boolean, val hasGitConfig: Boolean) + val result = ProgressManager.getInstance().runProcessWithProgressSynchronously( + { + val isGit = GitDirResolver.resolveGitDir(path) != null + val branch = if (isGit) detectBaseBranch(path) else "main" + val hasGitConfig = if (isGit) GitConfigHelper.isEnabled(path) else false + DeriveResult(branch, isGit, hasGitConfig) + }, + "Detecting Repository Info", + false, + project, + ) + isGitRepo = result.isGit + hasExistingGitConfig = result.hasGitConfig + baseBranchField.text = result.branch + + // Detect metadata patterns + detectAndShowPatterns(path) + + rederivePaths() + } + + private fun rederivePaths() { + val name = contextNameField.text.trim() + if (name.isBlank()) return + + val wBase = PathHelper.reposDir.resolve(name).resolve("worktrees") + val iBase = PathHelper.reposDir.resolve(name).resolve("idea-files") + val mainRepo = PathHelper.reposDir.resolve(name).resolve("base") + + worktreesBaseLabel.text = wBase.toString() + ideaFilesBaseLabel.text = iBase.toString() + mainRepoRootLabel.text = mainRepo.toString() + + val repoPathStr = repoPathField.text.trim() + if (repoPathStr.isNotBlank()) { + migrationInfoLabel.text = "Repo will be moved to $mainRepo and a symlink created at $repoPathStr" + } + } + + private fun detectBaseBranch(repoPath: Path): String { + try { + val result = ProcessHelper.runGit( + listOf("symbolic-ref", "refs/remotes/origin/HEAD"), + workingDir = repoPath, + ) + if (result.isSuccess) { + val ref = result.stdout.trim() + return ref.substringAfterLast("/") + } + } catch (_: Exception) {} + + // Fallback: check if main or master exists + for (branch in listOf("main", "master")) { + try { + val result = ProcessHelper.runGit( + listOf("rev-parse", "--verify", "refs/heads/$branch"), + workingDir = repoPath, + ) + if (result.isSuccess) return branch + } catch (_: Exception) {} + } + + return "main" + } + + private fun detectAndShowPatterns(repoPath: Path) { + patternCheckboxes.clear() + for (pattern in MetadataPattern.KNOWN_PATTERNS) { + val candidate = repoPath.resolve(pattern.name) + if (Files.exists(candidate)) { + val checkbox = JBCheckBox("${pattern.name} - ${pattern.description}", true) + patternCheckboxes.add(Pair(checkbox, pattern.name)) + } + } + } + + override fun createCenterPanel(): JComponent { + return panel { + row("Repository path:") { + cell(repoPathField).resizableColumn() + } + row("Context name:") { + cell(contextNameField).resizableColumn() + } + row("Base branch:") { + cell(baseBranchField).resizableColumn() + } + row("Main repo root:") { + cell(mainRepoRootLabel) + } + row("Worktrees base:") { + cell(worktreesBaseLabel) + } + row("Metadata vault:") { + cell(ideaFilesBaseLabel) + } + row("") { + cell(migrationInfoLabel) + } + if (patternCheckboxes.isNotEmpty()) { + group("Metadata Patterns") { + for ((checkbox, _) in patternCheckboxes) { + row { cell(checkbox) } + } + } + } + } + } + + override fun doValidate(): ValidationInfo? { + val pathStr = repoPathField.text.trim() + if (pathStr.isBlank()) { + return ValidationInfo("Repository path is required", repoPathField) + } + val path = Path.of(pathStr) + if (!Files.isDirectory(path)) { + return ValidationInfo("Directory does not exist", repoPathField) + } + // Check it's a git repo (computed off-EDT during rederive) + if (!isGitRepo) { + return ValidationInfo("Not a git repository", repoPathField) + } + + val name = contextNameField.text.trim() + if (name.isBlank()) { + return ValidationInfo("Context name is required", contextNameField) + } + if (!name.matches(Regex("[a-zA-Z0-9_-]+"))) { + return ValidationInfo("Context name must contain only letters, digits, hyphens, and underscores", contextNameField) + } + val existingConf = PathHelper.reposDir.resolve("$name.conf") + if (Files.exists(existingConf)) { + return ValidationInfo("A context named '$name' already exists", contextNameField) + } + if (hasExistingGitConfig) { + return ValidationInfo("This repository already has wt config in git config", repoPathField) + } + + if (baseBranchField.text.trim().isBlank()) { + return ValidationInfo("Base branch is required", baseBranchField) + } + + return null + } + + override fun doOKAction() { + val pathStr = repoPathField.text.trim() + repoPath = Path.of(pathStr) + contextName = contextNameField.text.trim() + baseBranch = baseBranchField.text.trim() + // activeWorktree = original repo path (the symlink location) + activeWorktree = repoPath + // mainRepoRoot = where repo will be moved to + mainRepoRoot = PathHelper.reposDir.resolve(contextName).resolve("base") + worktreesBase = PathHelper.reposDir.resolve(contextName).resolve("worktrees") + ideaFilesBase = PathHelper.reposDir.resolve(contextName).resolve("idea-files") + selectedPatterns = patternCheckboxes + .filter { it.first.isSelected } + .map { it.second } + super.doOKAction() + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt new file mode 100644 index 0000000..0e0ced4 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt @@ -0,0 +1,209 @@ +package com.block.wt.ui + +import com.block.wt.progress.asScope +import com.block.wt.provision.ProvisionHelper +import com.block.wt.provision.ProvisionMarkerService +import com.block.wt.model.ContextConfig +import com.block.wt.model.WorktreeInfo +import com.block.wt.services.WorktreeService +import com.block.wt.settings.WtPluginSettings +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.CheckBoxList +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBUI +import java.awt.BorderLayout +import java.awt.Dimension +import javax.swing.BoxLayout +import javax.swing.JComponent +import javax.swing.JPanel + +/** + * One-time setup dialog shown when a context is first used. + * Lists all non-provisioned worktrees and lets the user pick which to provision. + * + * Results: + * - OK ("Provision Selected") → provisions checked worktrees, marks context as set up + * - CANCEL with "Skip" → marks context as set up without provisioning + * - CANCEL with close button → does nothing, will ask again next time + */ +class ContextSetupDialog( + private val project: Project, + private val config: ContextConfig, + private val worktrees: List, +) : DialogWrapper(project) { + + private val checkBoxList = CheckBoxList() + private var skipped = false + + data class WorktreeEntry( + val wt: WorktreeInfo, + val hasMetadata: Boolean, + ) { + override fun toString(): String = buildString { + append(wt.displayName) + append(" (${wt.shortPath})") + if (hasMetadata) append(" — has project files, will keep") + else append(" — no project files, will import from vault") + } + } + + init { + title = "Set Up Context: ${config.name}" + setOKButtonText("Provision Selected") + setCancelButtonText("Remind Me Later") + init() + } + + override fun createCenterPanel(): JComponent { + val panel = JPanel(BorderLayout()) + panel.preferredSize = Dimension(600, 350) + + val header = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + border = JBUI.Borders.emptyBottom(12) + + add(JBLabel("The following worktrees haven't been provisioned by context '${config.name}'.")) + add(JBLabel("Select which ones to provision:").apply { + border = JBUI.Borders.emptyTop(4) + }) + } + panel.add(header, BorderLayout.NORTH) + + // Populate the checkbox list with non-provisioned worktrees + val unprovisioned = worktrees.filter { !it.isProvisionedByCurrentContext } + for (wt in unprovisioned) { + val hasMetadata = ProvisionMarkerService.hasExistingMetadata(wt.path) + val entry = WorktreeEntry(wt, hasMetadata) + checkBoxList.addItem(entry, entry.toString(), true) + } + panel.add(JBScrollPane(checkBoxList), BorderLayout.CENTER) + + val footer = JBLabel("Worktrees with existing project files will be marked as provisioned without changes. " + + "Others will have metadata imported from the vault.").apply { + foreground = JBUI.CurrentTheme.Label.disabledForeground() + border = JBUI.Borders.emptyTop(8) + } + panel.add(footer, BorderLayout.SOUTH) + + return panel + } + + override fun createLeftSideActions(): Array { + val skipAction = object : DialogWrapperAction("Skip Setup") { + override fun doAction(e: java.awt.event.ActionEvent) { + skipped = true + close(CANCEL_EXIT_CODE) + } + } + return arrayOf(skipAction) + } + + /** + * Returns true if the user explicitly chose "Skip Setup" (marks context as done). + * Returns false if the user closed the dialog via X or "Remind Me Later". + */ + fun wasSkipped(): Boolean = skipped + + /** + * Returns the checked worktree entries. Only valid after OK. + */ + fun getSelectedEntries(): List { + val selected = mutableListOf() + for (i in 0 until checkBoxList.itemsCount) { + if (checkBoxList.isItemSelected(i)) { + checkBoxList.getItemAt(i)?.let { selected.add(it) } + } + } + return selected + } + + companion object { + /** + * Shows the context setup dialog if the current context hasn't been set up yet. + * Called from startup activity or context switch. + */ + fun showIfNeeded(project: Project, config: ContextConfig, worktrees: List) { + val settings = WtPluginSettings.getInstance() + if (config.name in settings.state.setupCompletedContexts) return + + // Only show if there are non-provisioned worktrees + val hasUnprovisioned = worktrees.any { !it.isProvisionedByCurrentContext } + if (!hasUnprovisioned) { + // All worktrees are already provisioned — mark as done silently + markSetupComplete(config.name) + return + } + + val dialog = ContextSetupDialog(project, config, worktrees) + if (dialog.showAndGet()) { + // OK — provision selected worktrees + val selected = dialog.getSelectedEntries() + if (selected.isNotEmpty()) { + runProvisioning(project, config, selected) + } + markSetupComplete(config.name) + } else if (dialog.wasSkipped()) { + // Skip — mark as done without provisioning + markSetupComplete(config.name) + } + // else: Remind Me Later / closed — do nothing, will ask again + } + + private fun markSetupComplete(contextName: String) { + val settings = WtPluginSettings.getInstance() + if (contextName !in settings.state.setupCompletedContexts) { + val newState = settings.state.copy( + setupCompletedContexts = (settings.state.setupCompletedContexts + contextName).toMutableList() + ) + settings.loadState(newState) + } + } + + private fun runProvisioning( + project: Project, + config: ContextConfig, + entries: List, + ) { + ProgressManager.getInstance().run(object : Task.Backgroundable( + project, "Provisioning Worktrees", true + ) { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = false + val scope = indicator.asScope() + + runBlockingCancellable { + for ((i, entry) in entries.withIndex()) { + indicator.checkCanceled() + val wtStart = i.toDouble() / entries.size + val wtSize = 1.0 / entries.size + scope.text("Provisioning ${entry.wt.displayName}...") + + ProvisionHelper.provisionWorktree( + project, + entry.wt.path, + config, + keepExistingFiles = entry.hasMetadata, + scope = scope.sub(wtStart, wtSize), + ) + } + + scope.fraction(1.0) + scope.text("Refreshing worktree list...") + WorktreeService.getInstance(project).refreshWorktreeList() + Notifications.info( + project, + "Context Setup Complete", + "Provisioned ${entries.size} worktree(s) for context '${config.name}'", + ) + } + } + }) + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt new file mode 100644 index 0000000..320e358 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt @@ -0,0 +1,66 @@ +package com.block.wt.ui + +import com.block.wt.git.GitBranchHelper +import com.block.wt.services.ContextService +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent + +class CreateWorktreeDialog(private val project: Project) : DialogWrapper(project) { + + var branchName: String = "" + var createNewBranch: Boolean = false + var worktreePath: String = "" + + private lateinit var branchField: JBTextField + private lateinit var pathField: JBTextField + + init { + title = "Create Worktree" + init() + updatePath() + } + + override fun createCenterPanel(): JComponent { + return panel { + row("Branch:") { + textField() + .bindText(::branchName) + .focused() + .onChanged { updatePath() } + .also { branchField = it.component } + } + row("") { + checkBox("Create new branch (-b)") + .bindSelected(::createNewBranch) + } + row("Path:") { + textField() + .bindText(::worktreePath) + .comment("Auto-derived from branch name. Override if needed.") + .also { pathField = it.component } + } + } + } + + private fun updatePath() { + val config = ContextService.getInstance(project).getCurrentConfig() ?: return + if (branchField.text.isNotBlank()) { + pathField.text = GitBranchHelper.worktreePathForBranch(config.worktreesBase, branchField.text).toString() + } + } + + override fun doValidate(): ValidationInfo? { + val branch = branchField.text + if (branch.isBlank()) return ValidationInfo("Branch name is required", branchField) + if (branch.contains("..")) return ValidationInfo("Branch name cannot contain '..'", branchField) + if (branch.startsWith("-")) return ValidationInfo("Branch name cannot start with '-'", branchField) + if (pathField.text.isBlank()) return ValidationInfo("Worktree path is required", pathField) + return null + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/Notifications.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/Notifications.kt new file mode 100644 index 0000000..9fd7049 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/Notifications.kt @@ -0,0 +1,29 @@ +package com.block.wt.ui + +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.project.Project + +object Notifications { + + private const val GROUP_ID = "Worktree Manager" + + fun info(project: Project?, title: String, content: String) { + notify(project, title, content, NotificationType.INFORMATION) + } + + fun warning(project: Project?, title: String, content: String) { + notify(project, title, content, NotificationType.WARNING) + } + + fun error(project: Project?, title: String, content: String) { + notify(project, title, content, NotificationType.ERROR) + } + + private fun notify(project: Project?, title: String, content: String, type: NotificationType) { + NotificationGroupManager.getInstance() + .getNotificationGroup(GROUP_ID) + .createNotification(title, content, type) + .notify(project) + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt new file mode 100644 index 0000000..d0f3ba4 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt @@ -0,0 +1,123 @@ +package com.block.wt.ui + +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.ui.JBColor +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import java.awt.Color +import java.util.Base64 + +/** + * Builds the welcome page HTML with IntelliJ theme colors injected as CSS variables + * and the screenshot embedded as a base64 data URI. + */ +object WelcomePageHelper { + + fun buildThemedHtml(): String? { + val template = javaClass.getResource("/welcome.html")?.readText() ?: return null + val screenshotDataUri = loadScreenshotDataUri() + val themeStyle = buildThemeStyle() + + return template + .replace("/*{{THEME_CSS}}*/", themeStyle) + .replace("{{SCREENSHOT_SRC}}", screenshotDataUri) + } + + private fun loadScreenshotDataUri(): String { + val bytes = javaClass.getResourceAsStream("/ui.png")?.readBytes() + ?: return "" + val encoded = Base64.getEncoder().encodeToString(bytes) + return "data:image/png;base64,$encoded" + } + + private fun buildThemeStyle(): String { + val scheme = EditorColorsManager.getInstance().globalScheme + val bg = scheme.defaultBackground + val fg = scheme.defaultForeground + val panelBg = UIUtil.getPanelBackground() + val linkColor = JBUI.CurrentTheme.Link.Foreground.ENABLED + val separatorColor = JBColor.namedColor("Group.separatorColor", panelBg) + val infoFg = JBColor.namedColor("Component.infoForeground", fg) + + // Derive surface and accent colors from the theme + val isDark = ColorUtil.isDark(bg) + val surface = if (isDark) ColorUtil.brighten(panelBg, 0.06) else Color.WHITE + val surfaceHover = if (isDark) ColorUtil.brighten(panelBg, 0.10) else ColorUtil.darken(Color.WHITE, 0.02) + val border = if (isDark) ColorUtil.brighten(panelBg, 0.15) else ColorUtil.darken(Color.WHITE, 0.10) + val borderStrong = if (isDark) ColorUtil.brighten(panelBg, 0.25) else ColorUtil.darken(Color.WHITE, 0.18) + val muted = infoFg + val subtle = if (isDark) ColorUtil.blend(fg, bg, 0.45) else ColorUtil.blend(fg, bg, 0.55) + val accentBg = if (isDark) ColorUtil.blend(linkColor, bg, 0.15) else ColorUtil.blend(linkColor, Color.WHITE, 0.10) + val accentBorder = if (isDark) ColorUtil.blend(linkColor, bg, 0.35) else ColorUtil.blend(linkColor, Color.WHITE, 0.30) + val kbdBg = if (isDark) ColorUtil.brighten(panelBg, 0.04) else ColorUtil.darken(Color.WHITE, 0.04) + val kbdBorder = borderStrong + val kbdShadow = if (isDark) ColorUtil.darken(bg, 0.15) else ColorUtil.darken(Color.WHITE, 0.25) + val placeholderBg = if (isDark) ColorUtil.brighten(bg, 0.04) else ColorUtil.darken(Color.WHITE, 0.03) + val placeholderBorder = border + val placeholderFg = subtle + + return """ + :root { + --bg: ${bg.css()}; + --fg: ${fg.css()}; + --muted: ${muted.css()}; + --subtle: ${subtle.css()}; + --surface: ${surface.css()}; + --surface-hover: ${surfaceHover.css()}; + --border: ${border.css()}; + --border-strong: ${borderStrong.css()}; + --accent: ${linkColor.css()}; + --accent-fg: ${if (isDark) ColorUtil.darken(linkColor, 0.7).css() else "#ffffff"}; + --accent-muted: ${linkColor.css()}; + --accent-bg: ${accentBg.css()}; + --accent-border: ${accentBorder.css()}; + --kbd-bg: ${kbdBg.css()}; + --kbd-border: ${kbdBorder.css()}; + --kbd-shadow: ${kbdShadow.css()}; + --step-num: ${linkColor.css()}; + --placeholder-bg: ${placeholderBg.css()}; + --placeholder-border: ${placeholderBorder.css()}; + --placeholder-fg: ${placeholderFg.css()}; + --diagram-line: ${border.css()}; + --diagram-node: ${linkColor.css()}; + --diagram-node-fg: ${if (isDark) ColorUtil.darken(linkColor, 0.7).css() else "#ffffff"}; + --diagram-arrow: ${subtle.css()}; + --tag-bg: ${accentBg.css()}; + --tag-border: ${accentBorder.css()}; + --tag-fg: ${linkColor.css()}; + --shadow-card: 0 1px 3px ${ColorUtil.withAlpha(Color.BLACK, if (isDark) 0.20 else 0.04)}, 0 1px 2px ${ColorUtil.withAlpha(Color.BLACK, if (isDark) 0.15 else 0.06)}; + --shadow-card-hover: 0 4px 12px ${ColorUtil.withAlpha(Color.BLACK, if (isDark) 0.25 else 0.06)}, 0 2px 4px ${ColorUtil.withAlpha(Color.BLACK, if (isDark) 0.15 else 0.04)}; + } + """.trimIndent() + } + + private fun Color.css(): String = "rgb($red, $green, $blue)" +} + +private object ColorUtil { + fun isDark(c: Color): Boolean = (c.red * 0.299 + c.green * 0.587 + c.blue * 0.114) < 128 + + fun brighten(c: Color, amount: Double): Color { + val r = (c.red + (255 - c.red) * amount).toInt().coerceIn(0, 255) + val g = (c.green + (255 - c.green) * amount).toInt().coerceIn(0, 255) + val b = (c.blue + (255 - c.blue) * amount).toInt().coerceIn(0, 255) + return Color(r, g, b) + } + + fun darken(c: Color, amount: Double): Color { + val r = (c.red * (1 - amount)).toInt().coerceIn(0, 255) + val g = (c.green * (1 - amount)).toInt().coerceIn(0, 255) + val b = (c.blue * (1 - amount)).toInt().coerceIn(0, 255) + return Color(r, g, b) + } + + fun blend(c1: Color, c2: Color, ratio: Double): Color { + val r = (c1.red * ratio + c2.red * (1 - ratio)).toInt().coerceIn(0, 255) + val g = (c1.green * ratio + c2.green * (1 - ratio)).toInt().coerceIn(0, 255) + val b = (c1.blue * ratio + c2.blue * (1 - ratio)).toInt().coerceIn(0, 255) + return Color(r, g, b) + } + + fun withAlpha(c: Color, alpha: Double): String = + "rgba(${c.red}, ${c.green}, ${c.blue}, $alpha)" +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt new file mode 100644 index 0000000..1d858e7 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt @@ -0,0 +1,340 @@ +package com.block.wt.ui + +import com.block.wt.provision.ProvisionMarkerService +import com.block.wt.provision.ProvisionSwitchHelper +import com.block.wt.model.WorktreeInfo +import com.block.wt.model.WorktreeStatus +import com.block.wt.services.ContextService +import com.block.wt.services.WorktreeService +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.actionSystem.ex.ActionUtil +import com.intellij.openapi.project.Project +import com.intellij.ui.PopupHandler +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.table.JBTable +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import java.awt.BorderLayout +import java.awt.CardLayout +import java.awt.Color +import java.awt.Component +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.SwingConstants +import javax.swing.table.TableCellRenderer + +class WorktreePanel(private val project: Project) : JPanel(BorderLayout()), DataProvider, Disposable { + + private val tableModel = WorktreeTableModel() + private val table = object : JBTable(tableModel) { + override fun getToolTipText(event: MouseEvent): String? { + val row = rowAtPoint(event.point) + val col = columnAtPoint(event.point) + if (row < 0) return super.getToolTipText(event) + + val wt = tableModel.getWorktreeAt(row) ?: return super.getToolTipText(event) + + if (col == WorktreeTableModel.COL_PATH) { + return wt.path.toString() + } + + if (col == WorktreeTableModel.COL_STATUS) { + return buildStatusTooltip(wt) + } + + if (col == WorktreeTableModel.COL_AGENT && wt.hasActiveAgent) { + return buildAgentTooltip(wt) + } + + if (col == WorktreeTableModel.COL_PROVISIONED) { + return buildProvisionTooltip(wt) + } + + return super.getToolTipText(event) + } + + override fun prepareRenderer(renderer: TableCellRenderer, row: Int, column: Int): Component { + val comp = super.prepareRenderer(renderer, row, column) + if (!isRowSelected(row)) { + val wt = tableModel.getWorktreeAt(row) + if (wt != null && wt.isLinked) { + comp.background = linkedRowBackground() + } else { + // Explicitly reset: DefaultTableCellRenderer caches setBackground() calls + // in its unselectedBackground field, so the linked row's green tint leaks + // to subsequent rows without this reset. + comp.background = getBackground() + } + } + return comp + } + } + private val contextLabel = JLabel("", SwingConstants.LEFT) + private val cs = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + private val cardLayout = CardLayout() + private val centerPanel = JPanel(cardLayout) + + companion object { + val DATA_KEY: DataKey = DataKey.create("WtWorktreePanel") + private const val CARD_TABLE = "table" + private const val CARD_EMPTY = "empty" + private const val CARD_LOADING = "loading" + } + + init { + setupTable() + setupLoadingState() + setupEmptyState() + setupToolbar() + setupContextLabel() + setupListeners() + cardLayout.show(centerPanel, CARD_LOADING) + observeState() + // Panel may be created after startup activity completed; re-init to ensure fresh state + ContextService.getInstance(project).initialize() + WorktreeService.getInstance(project).refreshWorktreeList() + } + + override fun getData(dataId: String): Any? { + if (DATA_KEY.`is`(dataId)) return this + return null + } + + private fun setupTable() { + table.columnModel.getColumn(WorktreeTableModel.COL_PATH).apply { + minWidth = 40; preferredWidth = 200 + } + table.columnModel.getColumn(WorktreeTableModel.COL_BRANCH).apply { + minWidth = 40; preferredWidth = 160 + } + table.columnModel.getColumn(WorktreeTableModel.COL_STATUS).apply { + minWidth = 30; preferredWidth = 130 + } + table.columnModel.getColumn(WorktreeTableModel.COL_AGENT).apply { + minWidth = 20; preferredWidth = 30 + } + table.columnModel.getColumn(WorktreeTableModel.COL_PROVISIONED).apply { + minWidth = 20; preferredWidth = 35 + } + + table.autoResizeMode = javax.swing.JTable.AUTO_RESIZE_NEXT_COLUMN + table.setShowGrid(false) + table.rowHeight = 24 + + // Right-click context menu + PopupHandler.installPopupMenu(table, "Wt.WorktreeRowContextMenu", "WtWorktreeTablePopup") + + centerPanel.add(JBScrollPane(table), CARD_TABLE) + add(centerPanel, BorderLayout.CENTER) + } + + private fun setupLoadingState() { + val loadingPanel = panel { + row { + label("Loading worktree context...") + .align(Align.CENTER) + } + }.apply { + border = JBUI.Borders.empty(40, 20) + } + centerPanel.add(loadingPanel, CARD_LOADING) + } + + private fun setupEmptyState() { + val emptyPanel = panel { + row { + label("No wt context configured") + .bold() + .align(Align.CENTER) + } + row { + comment("Set up a context to manage git worktrees from the IDE") + .align(Align.CENTER) + } + row { + button("Add Context...") { + val action = ActionManager.getInstance().getAction("Wt.AddContext") + if (action != null) { + ActionUtil.invokeAction(action, this@WorktreePanel, ActionPlaces.TOOLWINDOW_CONTENT, null, null) + } + }.align(Align.CENTER) + } + row { + comment("Or run 'wt context add' in the terminal") + .align(Align.CENTER) + } + }.apply { + border = JBUI.Borders.empty(40, 20) + } + + centerPanel.add(emptyPanel, CARD_EMPTY) + } + + private fun setupToolbar() { + val actionGroup = ActionManager.getInstance().getAction("Wt.ToolWindowToolbar") as? DefaultActionGroup + ?: return + + val toolbar = ActionManager.getInstance() + .createActionToolbar("WtToolWindow", actionGroup, true) + toolbar.targetComponent = this + + add(toolbar.component, BorderLayout.NORTH) + } + + private fun setupContextLabel() { + val config = ContextService.getInstance(project).getCurrentConfig() + updateContextLabelText(config?.name) + contextLabel.border = javax.swing.BorderFactory.createEmptyBorder(2, 4, 2, 4) + + add(contextLabel, BorderLayout.SOUTH) + } + + private fun updateContextLabelText(name: String?) { + contextLabel.text = if (name != null) " Context: $name" else " No context" + } + + private fun setupListeners() { + // Double-click to switch worktree + table.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (e.clickCount == 2) { + val row = table.rowAtPoint(e.point) + val wt = tableModel.getWorktreeAt(row) ?: return + if (!wt.isLinked) { + switchToWorktree(wt) + } + } + } + }) + } + + private fun observeState() { + val worktreeService = WorktreeService.getInstance(project) + val contextService = ContextService.getInstance(project) + + cs.launch { + combine( + worktreeService.worktrees, + contextService.config, + worktreeService.isLoading, + ) { worktrees, config, isLoading -> + Triple(worktrees, config, isLoading) + }.collectLatest { (worktrees, config, isLoading) -> + // Update worktreesBase for relative paths + tableModel.worktreesBase = config?.worktreesBase + tableModel.setWorktrees(worktrees) + updateContextLabelText(config?.name) + + // Show loading until first refresh completes, then empty or table + when { + isLoading && worktrees.isEmpty() -> cardLayout.show(centerPanel, CARD_LOADING) + config == null && worktrees.isEmpty() -> cardLayout.show(centerPanel, CARD_EMPTY) + else -> cardLayout.show(centerPanel, CARD_TABLE) + } + } + } + } + + private fun switchToWorktree(wt: WorktreeInfo) { + ProvisionSwitchHelper.switchWithProvisionPrompt(project, wt) + } + + fun getSelectedWorktree(): WorktreeInfo? { + val row = table.selectedRow + return if (row >= 0) tableModel.getWorktreeAt(row) else null + } + + private fun buildStatusTooltip(wt: WorktreeInfo): String? { + val status = wt.status + if (status !is WorktreeStatus.Loaded) return null + + val lines = mutableListOf() + if (status.staged > 0) lines.add("Staged: ${status.staged}") + if (status.modified > 0) lines.add("Modified: ${status.modified}") + if (status.untracked > 0) lines.add("Untracked: ${status.untracked}") + if (status.conflicts > 0) lines.add("Conflicts: ${status.conflicts}") + status.ahead?.let { if (it > 0) lines.add("Ahead: $it") } + status.behind?.let { if (it > 0) lines.add("Behind: $it") } + + if (lines.isEmpty()) return "Clean" + return "${lines.joinToString("
")}" + } + + private fun buildAgentTooltip(wt: WorktreeInfo): String { + val ids = wt.activeAgentSessionIds + return when { + ids.size == 1 -> "Claude agent active
Session: ${ids[0]}" + ids.size > 1 -> "${ids.size} active sessions:
${ids.joinToString("
") { "  $it" }}" + else -> "Claude agent active" + } + } + + private fun buildProvisionTooltip(wt: WorktreeInfo): String { + if (!wt.isProvisioned) { + return "Not provisioned \u2014 right-click to provision" + } + + val marker = ProvisionMarkerService.readProvisionMarker(wt.path) ?: return "Provisioned" + val currentContextName = ContextService.getInstance(project).getCurrentConfig()?.name + val otherContexts = marker.provisions + .map { it.context } + .filter { it != marker.current } + + return buildString { + append("Current: ${marker.current}") + if (marker.current == currentContextName) { + append(" (this context)") + } + + if (otherContexts.isNotEmpty()) { + append(" | Also provisioned by: ") + append(otherContexts.joinToString(", ") { name -> + if (name == currentContextName) "$name (this context)" else name + }) + } else if (currentContextName != null && marker.current != currentContextName) { + append(" | Not provisioned by this context") + } + } + } + + private fun linkedRowBackground(): Color { + val bg = table.background + val isDark = (bg.red * 0.299 + bg.green * 0.587 + bg.blue * 0.114) < 128 + return if (isDark) { + Color(bg.red, (bg.green + 30).coerceAtMost(255), bg.blue, bg.alpha) + } else { + Color((bg.red * 0.92).toInt(), (bg.green * 0.98).toInt(), (bg.blue * 0.92).toInt(), bg.alpha) + } + } + + /** + * Refreshes the worktree list if at least 2 seconds have passed since the last refresh. + * Used for tool window focus events to avoid rapid re-refreshes. + */ + fun refreshIfStale() { + val worktreeService = WorktreeService.getInstance(project) + if (System.currentTimeMillis() - worktreeService.lastRefreshTime > 2000) { + worktreeService.refreshWorktreeList() + } + } + + override fun dispose() { + cs.cancel() + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt new file mode 100644 index 0000000..ee1d837 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt @@ -0,0 +1,110 @@ +package com.block.wt.ui + +import com.block.wt.model.WorktreeInfo +import com.block.wt.provision.ProvisionSwitchHelper +import com.block.wt.services.WorktreeService +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.ListPopup +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep +import com.intellij.openapi.wm.StatusBar +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.StatusBarWidgetFactory +import com.intellij.util.Consumer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.awt.event.MouseEvent + +class WorktreeStatusBarWidgetFactory : StatusBarWidgetFactory { + + override fun getId(): String = "WtWorktreeWidget" + + override fun getDisplayName(): String = "Active Worktree" + + override fun isAvailable(project: Project): Boolean = true + + override fun createWidget(project: Project): StatusBarWidget { + return WorktreeStatusBarWidget(project) + } + + override fun canBeEnabledOn(statusBar: StatusBar): Boolean = true +} + +private class WorktreeStatusBarWidget( + private val project: Project, +) : StatusBarWidget, StatusBarWidget.MultipleTextValuesPresentation { + + private var statusBar: StatusBar? = null + private val cs = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + override fun ID(): String = "WtWorktreeWidget" + + override fun install(statusBar: StatusBar) { + this.statusBar = statusBar + + cs.launch { + WorktreeService.getInstance(project).worktrees.collectLatest { + statusBar.updateWidget(ID()) + } + } + } + + override fun dispose() { + cs.cancel() + statusBar = null + } + + override fun getPresentation(): StatusBarWidget.WidgetPresentation = this + + override fun getSelectedValue(): String? { + val linked = WorktreeService.getInstance(project).worktrees.value + .firstOrNull { it.isLinked } + return "active worktree: ${linked?.displayName ?: "(none)"}" + } + + override fun getTooltipText(): String = "Active worktree. Click to switch." + + override fun getPopup(): ListPopup? { + val worktreeService = WorktreeService.getInstance(project) + val worktrees = worktreeService.worktrees.value + if (worktrees.isEmpty()) return null + + val displayNames = worktrees.map { wt -> + buildString { + if (wt.isLinked) append("* ") + append(wt.displayName) + if (wt.isMain) append(" [main]") + } + } + + val step = object : BaseListPopupStep("Switch Worktree", displayNames) { + override fun getDefaultOptionIndex(): Int { + return worktrees.indexOfFirst { it.isLinked }.coerceAtLeast(0) + } + + override fun onChosen(selectedValue: String, finalChoice: Boolean): PopupStep<*>? { + if (finalChoice) { + val index = displayNames.indexOf(selectedValue) + val wt = worktrees.getOrNull(index) ?: return PopupStep.FINAL_CHOICE + if (!wt.isLinked) { + ApplicationManager.getApplication().invokeLater { + ProvisionSwitchHelper.switchWithProvisionPrompt(project, wt) + } + } + } + return PopupStep.FINAL_CHOICE + } + } + + return JBPopupFactory.getInstance().createListPopup(step) + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun getClickConsumer(): Consumer? = null +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt new file mode 100644 index 0000000..9a59f9b --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt @@ -0,0 +1,79 @@ +package com.block.wt.ui + +import com.block.wt.model.WorktreeInfo +import com.block.wt.model.WorktreeStatus +import java.nio.file.Path +import javax.swing.table.AbstractTableModel + +class WorktreeTableModel : AbstractTableModel() { + + private var worktrees: List = emptyList() + var worktreesBase: Path? = null + + companion object { + const val COL_PATH = 0 + const val COL_BRANCH = 1 + const val COL_STATUS = 2 + const val COL_AGENT = 3 + const val COL_PROVISIONED = 4 + + val COLUMN_NAMES = arrayOf("Path", "Branch", "Status", "Agent", "Provisioned") + } + + fun setWorktrees(newWorktrees: List) { + worktrees = newWorktrees + fireTableDataChanged() + } + + fun getWorktreeAt(row: Int): WorktreeInfo? { + return worktrees.getOrNull(row) + } + + override fun getRowCount(): Int = worktrees.size + + override fun getColumnCount(): Int = COLUMN_NAMES.size + + override fun getColumnName(column: Int): String = COLUMN_NAMES[column] + + override fun getColumnClass(columnIndex: Int): Class<*> = String::class.java + + override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? { + val wt = worktrees.getOrNull(rowIndex) ?: return null + return when (columnIndex) { + COL_PATH -> wt.relativePath(worktreesBase) + COL_BRANCH -> buildString { + append(wt.displayName) + if (wt.isMain) append(" [main]") + } + COL_STATUS -> formatStatus(wt) + COL_AGENT -> if (wt.hasActiveAgent) { + "\uD83E\uDD16 " + wt.activeAgentSessionIds.joinToString(", ") { it.take(8) } + } else "" + COL_PROVISIONED -> when { + wt.isProvisionedByCurrentContext -> "âś“" + wt.isProvisioned -> "~" + else -> "" + } + else -> null + } + } + + private fun formatStatus(wt: WorktreeInfo): String { + val status = wt.status + if (status !is WorktreeStatus.Loaded) return "" + + val parts = mutableListOf() + + // Local changes: âš conflicts â—Źstaged âś±modified …untracked + if (status.conflicts > 0) parts.add("\u26A0${status.conflicts}") + if (status.staged > 0) parts.add("\u25CF${status.staged}") + if (status.modified > 0) parts.add("\u2731${status.modified}") + if (status.untracked > 0) parts.add("\u2026${status.untracked}") + + // Remote tracking: ↑ahead ↓behind + status.ahead?.let { if (it > 0) parts.add("↑$it") } + status.behind?.let { if (it > 0) parts.add("↓$it") } + + return parts.joinToString(" ") + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt new file mode 100644 index 0000000..bef6682 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt @@ -0,0 +1,38 @@ +package com.block.wt.ui + +import com.block.wt.services.WorktreeService +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.openapi.wm.ex.ToolWindowManagerListener +import com.intellij.ui.content.ContentFactory + +class WorktreeToolWindowFactory : ToolWindowFactory, DumbAware { + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val panel = WorktreePanel(project) + val content = ContentFactory.getInstance().createContent(panel, "", false) + Disposer.register(content, panel) + toolWindow.contentManager.addContent(content) + + // Start periodic refresh + WorktreeService.getInstance(project).startPeriodicRefresh() + + // Refresh worktree list when the tool window becomes visible + val connection = project.messageBus.connect(content) + connection.subscribe( + ToolWindowManagerListener.TOPIC, + object : ToolWindowManagerListener { + override fun toolWindowShown(tw: ToolWindow) { + if (tw.id == toolWindow.id) { + panel.refreshIfStale() + } + } + }, + ) + } + + override fun shouldBeAvailable(project: Project): Boolean = true +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt new file mode 100644 index 0000000..68273d4 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt @@ -0,0 +1,79 @@ +package com.block.wt.util + +import com.block.wt.model.ContextConfig +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.io.path.writeText + +object ConfigFileHelper { + + private val KEY_PATTERN = Regex("""^(WT_[A-Z_]+)=["']?(.*?)["']?\s*$""") + + fun readConfig(confFile: Path): ContextConfig? { + if (!confFile.exists()) return null + + val values = mutableMapOf() + for (line in Files.readAllLines(confFile)) { + val match = KEY_PATTERN.matchEntire(line) ?: continue + values[match.groupValues[1]] = match.groupValues[2] + } + + val name = confFile.fileName.toString().removeSuffix(".conf") + + return ContextConfig( + name = name, + mainRepoRoot = PathHelper.expandTilde(values["WT_MAIN_REPO_ROOT"] ?: return null), + worktreesBase = PathHelper.expandTilde(values["WT_WORKTREES_BASE"] ?: return null), + activeWorktree = PathHelper.expandTilde(values["WT_ACTIVE_WORKTREE"] ?: return null), + ideaFilesBase = PathHelper.expandTilde(values["WT_IDEA_FILES_BASE"] ?: return null), + baseBranch = values["WT_BASE_BRANCH"] ?: "master", + metadataPatterns = (values["WT_METADATA_PATTERNS"] ?: "") + .split(" ") + .filter { it.isNotBlank() }, + ) + } + + fun writeConfig(confFile: Path, config: ContextConfig) { + Files.createDirectories(confFile.parent) + confFile.writeText( + buildString { + appendLine("""WT_MAIN_REPO_ROOT="${config.mainRepoRoot}"""") + appendLine("""WT_WORKTREES_BASE="${config.worktreesBase}"""") + appendLine("""WT_ACTIVE_WORKTREE="${config.activeWorktree}"""") + appendLine("""WT_IDEA_FILES_BASE="${config.ideaFilesBase}"""") + appendLine("""WT_BASE_BRANCH="${config.baseBranch}"""") + appendLine("""WT_METADATA_PATTERNS="${config.metadataPatterns.joinToString(" ")}"""") + } + ) + } + + /** + * Reads the current context name from ~/.wt/current. + * Returns null if the file doesn't exist or is empty. + * Reads only the first line to match shell semantics (`head -1`). + */ + fun readCurrentContext(file: Path = PathHelper.currentFile): String? { + if (!file.exists()) return null + return Files.newBufferedReader(file).use { it.readLine() }?.trim()?.ifEmpty { null } + } + + /** + * Writes the context name to ~/.wt/current. + * Creates parent directory if needed. + */ + fun writeCurrentContext(name: String, file: Path = PathHelper.currentFile) { + Files.createDirectories(file.parent) + file.writeText(buildString { appendLine(name) }) + } + + fun listConfigFiles(): List { + val reposDir = PathHelper.reposDir + if (!Files.isDirectory(reposDir)) return emptyList() + return Files.list(reposDir).use { stream -> + stream.filter { it.fileName.toString().endsWith(".conf") } + .toList() + } + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathExtensions.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathExtensions.kt new file mode 100644 index 0000000..436dfd9 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathExtensions.kt @@ -0,0 +1,12 @@ +package com.block.wt.util + +import java.nio.file.Path + +fun Path.normalizeSafe(): Path = try { + toRealPath() +} catch (_: Exception) { + toAbsolutePath().normalize() +} + +fun Path.relativizeAgainst(base: Path?): String = + if (base != null && startsWith(base)) base.relativize(this).toString() else toString() diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathHelper.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathHelper.kt new file mode 100644 index 0000000..728c7e0 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathHelper.kt @@ -0,0 +1,58 @@ +package com.block.wt.util + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.UUID + +object PathHelper { + + private val HOME: Path = Path.of(System.getProperty("user.home")) + + fun expandTilde(path: String): Path { + return if (path.startsWith("~/") || path == "~") { + HOME.resolve(path.removePrefix("~/").removePrefix("~")) + } else { + Path.of(path) + } + } + + fun normalize(path: Path): Path { + return path.toRealPath() + } + + fun normalizeSafe(path: Path): Path { + return try { + path.toRealPath() + } catch (_: Exception) { + path.toAbsolutePath().normalize() + } + } + + fun atomicSetSymlink(linkPath: Path, newTarget: Path) { + val parent = linkPath.parent + ?: throw IllegalArgumentException("Link path must have a parent directory: $linkPath") + Files.createDirectories(parent) + + val tempLink = parent.resolve(".${linkPath.fileName}.${UUID.randomUUID()}.tmp") + Files.createSymbolicLink(tempLink, newTarget) + try { + Files.move(tempLink, linkPath, StandardCopyOption.ATOMIC_MOVE) + } catch (e: Exception) { + runCatching { Files.deleteIfExists(tempLink) } + throw e + } + } + + fun isSymlink(path: Path): Boolean = Files.isSymbolicLink(path) + + fun readSymlink(path: Path): Path? { + return if (Files.isSymbolicLink(path)) Files.readSymbolicLink(path) else null + } + + val wtRoot: Path get() = HOME.resolve(".wt") + + val reposDir: Path get() = wtRoot.resolve("repos") + + val currentFile: Path get() = wtRoot.resolve("current") +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessHelper.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessHelper.kt new file mode 100644 index 0000000..b78575a --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessHelper.kt @@ -0,0 +1,96 @@ +package com.block.wt.util + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessListener +import com.intellij.execution.process.ProcessOutputTypes +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.util.Key +import java.nio.file.Path + +object ProcessHelper : ProcessRunner { + + private val log = Logger.getInstance(ProcessHelper::class.java) + + private val GIT_PROGRESS_REGEX = Regex("""\b(\d{1,3})%""") + + data class ProcessResult( + val exitCode: Int, + val stdout: String, + val stderr: String, + ) { + val isSuccess: Boolean get() = exitCode == 0 + } + + override fun run( + command: List, + workingDir: Path?, + timeoutSeconds: Long, + ): ProcessResult { + return runInternal(command, workingDir, timeoutSeconds, onProgress = null) + } + + override fun runGit(args: List, workingDir: Path?): ProcessResult { + return run(listOf("git") + args, workingDir) + } + + /** + * Runs a git command while streaming stderr to parse progress percentages. + * Git outputs lines like "Receiving objects: 45% (123/273)" to stderr. + * The [onProgress] callback receives values in 0.0..1.0. + */ + fun runGitWithProgress( + args: List, + workingDir: Path?, + onProgress: (Double) -> Unit, + ): ProcessResult { + return runInternal(listOf("git") + args, workingDir, timeoutSeconds = 300, onProgress = onProgress) + } + + private fun runInternal( + command: List, + workingDir: Path?, + timeoutSeconds: Long, + onProgress: ((Double) -> Unit)?, + ): ProcessResult { + val cli = GeneralCommandLine(command) + if (workingDir != null) { + cli.workDirectory = workingDir.toFile() + } + cli.charset = Charsets.UTF_8 + + val handler = CapturingProcessHandler(cli) + + if (onProgress != null) { + handler.addProcessListener(object : ProcessListener { + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + if (outputType == ProcessOutputTypes.STDERR) { + val match = GIT_PROGRESS_REGEX.find(event.text) + if (match != null) { + val pct = match.groupValues[1].toIntOrNull() ?: return + onProgress(pct.coerceIn(0, 100) / 100.0) + } + } + } + }) + } + + val output = handler.runProcess((timeoutSeconds * 1000).toInt()) + + if (output.isTimeout) { + log.warn("Command timed out after ${timeoutSeconds}s: ${command.joinToString(" ")}") + return ProcessResult(-1, output.stdout, "Process timed out after ${timeoutSeconds}s") + } + + val result = ProcessResult( + exitCode = output.exitCode, + stdout = output.stdout, + stderr = output.stderr, + ) + if (!result.isSuccess) { + log.debug("Command failed (exit=${result.exitCode}): ${command.joinToString(" ")}${if (result.stderr.isNotBlank()) "\nstderr: ${result.stderr.trim()}" else ""}") + } + return result + } +} diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessRunner.kt b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessRunner.kt new file mode 100644 index 0000000..1477af9 --- /dev/null +++ b/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessRunner.kt @@ -0,0 +1,12 @@ +package com.block.wt.util + +import java.nio.file.Path + +/** + * Interface for running external processes. Production implementation uses IntelliJ's + * OSProcessHandler; test implementation returns canned responses. + */ +interface ProcessRunner { + fun run(command: List, workingDir: Path? = null, timeoutSeconds: Long = 60): ProcessHelper.ProcessResult + fun runGit(args: List, workingDir: Path? = null): ProcessHelper.ProcessResult +} diff --git a/wt-intellij-plugin/src/main/resources/META-INF/plugin.xml b/wt-intellij-plugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..f8bd2a8 --- /dev/null +++ b/wt-intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,210 @@ + + com.block.wt + Worktree Manager + Block + +
+ Features: +
    +
  • List, create, switch, and remove git worktrees from the IDE
  • +
  • Atomic symlink switching — switch worktrees in <1 second with zero IDE restarts
  • +
  • Multi-repo context management — interoperable with the wt CLI
  • +
  • IDE metadata vault — export/import .idea, .ijwb, .vscode dirs across worktrees
  • +
  • Bazel symlink management — shared build cache across worktrees
  • +
+ ]]>
+ + com.intellij.modules.platform + Git4Idea + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/wt-intellij-plugin/src/main/resources/icons/worktree.svg b/wt-intellij-plugin/src/main/resources/icons/worktree.svg new file mode 100644 index 0000000..4de5de6 --- /dev/null +++ b/wt-intellij-plugin/src/main/resources/icons/worktree.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/wt-intellij-plugin/src/main/resources/ui.png b/wt-intellij-plugin/src/main/resources/ui.png new file mode 100644 index 0000000000000000000000000000000000000000..ddca6605ddc93b14b896878d654fcab9bb6e4b4e GIT binary patch literal 326578 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 z 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ÙIX`(z{%0+PYE6d zoE+qAv1+o&>HP=}c4lN`#QEo!8X;2iA_IY7_Z_w7D0^3kB8PFUPCSq-!HIk=468IV zm!RYYon!*412yVO)D`>~7%ZI3L@3iw`XId*exaIM1y6%&_} z&+_-c?VuW}PEZGRxEw8oz7ODb30+Eb>iw`hbVJN_)Up1vtugG6?B{3Pg*4eLTt>ZcOm4bIU)Ww2bmT#+z(=RRxLPgQv%kc8 zPUI}lV&r)sxZCdGc&-=KL$_K@jKdyy-&CAUpmfn_n>rIy{CSN_OK=q=pXj1-#D$?G z15*5|mAl3s7mFyBh6COn>gR(-tfzO40ZpZDp9LmN?f?bK<)V;def~@o*66))F3|pj zP}z6vl8%Zkvb&56IEEWsaT-7 z(eOTAecuTpqZeT_D*gE80RD%0Rc71L4YQT@0DXS^(7Gd6|EI%%&6hn!5CSn_oMKHq zWyuQa4akXt7UJ%P6GST|rIiGsn3>ScCw#nFdS>o`nH*{vIo)@upVLL)L|}%I{JO%p zLdpLu+8FP$`0{*4l;PBBSg&LvH(=WmpLG=kbU!EeQ88oyQc)^3BgTqM_7BEN7m853 z^Ye4TOp`Ds;hq^xtIVI!Jv0yen*VT(IGvP(0*C4i7jXSc5&{;^B zi6aXHdmDEJpt$cG%+X_bsLUqP6W#Y_?~}DU@kWSOT|40 zL9XTJrOpC-i#sX;A#7c)^hu#)bQ`0++4%hB6MBHJoQPh-Lz*s;bG7_NKHHzImxgupdsQj})%j!=U=4jJ!~KejBEAwXl=&9M)KALSoK ztK}nvNIoxxzdRZc$?HcBtQca6tlJDoVq+t<%h33(NVc{r=>#8b_n!c<0#!ygtqUl6 zd*G+B0zeNupDh&Epdvk*Qvrurvm24#zd57$MG43{X1tT`} zjR*)&-r1&f{=wccPj0ycP68`M{rDUnL%eAAD#z!QB*p?u;faM628E zvKa2=u>@?XHRJ>ZuJl=VXup}pahB`U7SJ2y50bhye1_}7$KqmnsL;V=QkxvPkwO9;zNak$l zi+oiD63RNIH||5>b?{qWxZ+KH)w|2)y=CDn;HQjJod1o(gcLJFAgnW{O2!a*vN$l)1=R#w*hM2pRt$0Wee$O+hQ zGgFWM;#Pi?4QQn^At!8OMMyIFer+{QJ{^$swLxU>2);5G_We3d#OvhGqQsT5PnJ{@ z@uJ2Cc}_|8xMt*YNO0pA}6**GPHkrtcw|}7YVS!Wz zJ2%E(WePc`l3n#eXbP1t%O=CAxqo=K_u)3ew*<}esCZpJI(vs6wj0UuP5xR~ico+r zx{d==;;ZGtPt3EtXJLauFKCr`-0s|GNk)J+E`;iti`^`RW_M=d`K2N6^R zZG_UvqWbYCa63NTBQ0sR4Ag7yK#WS;664N&U71jS6_Alh^Z_ z5IZC8W#J^##}BXLMoxFQkIUcgXp)tdwHkeE0_a&V84+rlxVb}{q!>4HnqYw$bxdGVQ#*W4KT)>Btb6S_e2^49G+oC_M$|mCW@h|N4 z|MT{-^`t^_e?~NN_^)TUy_&B9_7D2Ad2H6;Glg|N!ax374#+bYp2@RHfM^k^gmuRY z8*-x$O}B#D4ILNR)VSNwh%b0oAyW?iu2^F2AqqWC!R z@x1$v$hAnxy`xKyGPIl$k7x|ECct1rpB1$6mD0Ctx}vrpo4~6>YLioPVZ{i&lhG1g z&L(}6d9}o_rS_09Vhu=rkH7^fLRQWYW8UF8hu6q2t1jD#xE9c(u(x(74?)`pIcaS5l2^DEiyx=sVM}-n%)bF5e13i4Z{yrZWNh*rdOGEM-#= zim~=8$sl|GPu5v!!vJvs(wlo+%#0n@cKJ6X?5`(x_5m};v7i=~xx=uwEe<=RVaiCo z`E-i3%!%Mg_9&2Nv7_yDiS*qKrpSj;=HCGd8@rR=ICC!0ONp3kJCbAwI-Kr&+_Swl z`2J)bLBE*asotg25 zv4W5WZo`S>-1dbvGY-)%J9?>4*i(0^fP+(<7!90Nv*NeM{vP51zhvper@c0dBFhE; zWjXT>6Kz8|SC6s9WBh8E;GTcdh?cGM!&+d^lgAdo{u#jOV(hpn_K(khPyo<)TE0yz z4)g`w4G&UyCE*EJjLMd4x(*3AjKXD>-$^X4xU{?1&{FoWqJkVB)s_*AUFp_Zsvg92 z=+~HEa6S8=97eO=vH_tQryfY8O(R=mlt2Tq)x&j{bc(UXcTcpwJ3ucjm5!$?46-*A z`S`>r+(;oE^$_ey*^^h3UJSv6_Zt(rr7L~MyPpsyHgcZ2CHY9=w|#F1%6uG%Z=e^IAp_~p-6i(F{47n@ACJWlg zCqU2l+rIsn!>IJJkiGCR5a$2M*yG{btD;&wO0%)VA|kP^U*oPFJvxv^fmst{58NS)@LxPhs8U2fXXYEo9U!8Ri@|m+80yA zL5p`K7RhUO@||YDWysQzF^wmr%%b9wy_l*t50|FCdNvZ(l`U7kDIpE+2uuG{woM$Gzcu5Ks2 ziayt<&EsX7v0sg`r+%L{5V`$9>T(~3dKqWKX_jI1sB+HtsAU+_D%8;lDe~ZMa4*L` zO&BZ_I0c9j=i^5;*>wJ#nCm2!A;Nz}Wt)?8h-hS}%;7 zZ;zGES66#(a^l<9QNVdmq7aKZP7o4C zf+&#ow|NOQ)-FGdwmFscTwIedK*X*o7&wbA)DR4xlASE9=@V?bq~DUB5e3Vd;s-~- zzBDN-EXXVHSZfi1fS&&3FQa+knY7~Fv3W^DRGAVubSE+Kk@hc6tnrcZlJ&onIfCCK zjQ(P03=5`!qpkOA&lV4=t1d-B)3NFtrCbwG!zi(3+8-YoRzIvo8!`rcV~I<_z@ri{ zIrRh7evhVJ#Kn5VY_=ft^RAiokhj9t?c&ITW}dU*h|kI4m*;ZRa4i;us3AAg`!4evX)1M%IQBf-gzi&rr3^+Xdc?odE70b%K3lRoJ^UPe}5 zwv~3QqL{^?;ouR+f!cw*?V@B+a;W3; ze(sDd`yvNmkKG%4&{oKzgYVcgT0Cyf-uX>o7NC*C*oRtT!)`j6p%_io+vqS$H>U(v z%FRF)0J3`)!dqt@#A#?vEcWj%m?wb9_n%%*EP0@PsZhdVLs%;7asLqlLjgB*r z1i;Btdht_}{BR#QT$_u=HHe4`TgHUc(@YO{={IKozw37JKdt^b30^X7$ZD_47ek;${-@Q&ZF5H?G9lF z{lSKW4M+*fMuMvGvaACm(bxu|YqoFrP*mTOog+qufM+J1?V?$4*$X~Qb=`e^(FGV) zN`2PRN)B2avtW~!K+ERTQIzObVLn1le|T<2S4&)(S+@CEX!(8hD5A4;Y_1Un?yi;g z<-?6ZXl7*AgQ6#QBeZfcvT(C~-i;1Ux=>RL2z5%kwev~N{=TLN{he2H18ffvyqrOo}ufO}hP;DbV<=XJ$CS6o`m>n$6j5D!PPl6~~5H z9M4Y3FM=G&BrvB1jxmG3IcwCp&-wlz&`I?uP|R9fvS)?wa+dhg$K zf;NC*?bi#8lw%xB7jO9%YxNVKN=(+(?f}{Bzw9Whmsb13a$8MFiho7AdriQ0WDtt` z^M;*yeNkTNe-M%{j6loxvqq1Ba5L z@i$FiU1D2Rr5bV6Raezpn=y`O%CEd$XgWInepucHWNy~i5Y~+*9~ThuFCg4BOI*2} z=he8OJPi4UBkOhAOpJxuWWy8f4(`~vfRVpkk*ZOa*2+DV?3{<~^Lw53*AH9_F&pch zRQ%W)F%1c`_*{W)Dk6B9_&^OCs=(HnbvQFB63C)GK64;P&~lJnST8umTsQ(*cXyw8zt_eW@QC@*nSTV-5T6;4;qCcNAgcNL1pVcC_dT80y%A!$xy?PC zsWR5&gNe~ETSLe{;fjw;_l}q_afc#_;Qe0F(jA6<4e=u&F;iNdC>(e!VXss5dXzWI zyv-yXgPhGNjqKQJObmPOeTb}2rXl_+mb~P5xh0;^<;_!jfD$_8k4g8L&ux??pqB^S z{Z7u)b)C+83p23K1ho_AXi9hG5pxXO!0nJKCtz3gAREc)VNLW=*6ph=_D@MsLL4^} znO0@|5nPWy*DNrR*)I&FDKuMl!Xa<#{tLH*M1nM<-hPIwK`rc!t=nkb_;kKXoHFs| zn;O?oH&-bWEN%Do;avK`{!NZ6m5TgqZGca{v=o%44&iv)SFfB-DQ>ncY}k;Ay6k&- z@ar57o*ZE49HePutbyLA6Z#L23U3qcrq_6}=tl@>7f%56d+9v(!k3sL*SQmhK9GrY(c3{Vk}%|tb}1iKpdMn1i`)hT zr>5%?aqVgP*W2M$f2s35K**9`uM^<@ltoO?z;>@ZO! zhp<}_{K)7Z6QuY`ZdLuADnNdF5LE zhI;BWG35;@!#Vw7Jc?7B#>&h{fR|El2k{Ko;Wq-yd2>bbf|aCd19D<@EISAOt^QMr zZxtRHYh!v%I~dgzM~};N2m8M+L}N!>XNwziP$x>+PJjBUd>ZpYUnO5!$Vj z3^FAMiIf}foR9GE=HDz5|6djfP|SnN699a|_ zMH`2Y;l*zTIka{joU~&;{2XB(i_z=?UcPy_0=#z0ksw1jF!<)JHT?o6_bwce*I~Xi zclJF$r%&*V(BHHD?hVqQubWSOOzrLIj=qYs=eCDQ&Km@4-LFp23m=twNjV%F6;cWha6deqFcrRi0^ zrCty3RqGD+FZhGMDGv%;V^j_gJA~ZW9yJV70=L`)mLfpq@G>%Qz}zr&?lP+u{nLHe zPBnstenSSFkj1tqy*%?;a?-MvBu{18gl>NEddqF2i8d;hhOS)L&f8tYzwd;kP#(sw zL46lb>0jA@*ckc_u&cR-A-|O+@Pz?FJsxw7lMeh|x=H?h&>UQer_BV456W@L<$1bz zPRNvEdl*`x{TJ#4NV;y=aeEAGhP1|_LzN&1qNW7I{MyPAOtU^)l~HNcEemvm2!FO{ zk*3cJ!ob<|ECz^xkxnm8xe7bXc zx=@z0RP~+lcdevDM*D={b{cU0sRC7Dd&Tn~>1tp25mnLR$zSIPRE2fdz=R#Z5ClPu zu%05piv&LN0DhSpG`Hq$8DCf=E6ohla8v#&n%h#zs2)?P47(3k-1>K0e0REH>+?8` zjs7c}@#*?ur-_2_9Ha#IX9W7!dpk$b*B#Jmz5 zCN#UzkFJlAn*ejBm}SrqyeMo{#*5$!qHMZZuj-|%vh6;8B3Va5UNC*sjkDL%H4^(A z>;k0g>HylX`qe(p&ppE)AXZawWZSkSYZT@2I#J#3a7^sx25W+SB3()E$rgKk+3!eN zs`4^I&BY+sAH{Fp;AZQe%woc($hh8qQRJMs5dGfz!de<49|i9hz3T4fbDuwx$aers zc!t-lo1={R{JgkCXFh99t@@oNe!FpnILAL|HhhBWU-N`=zx}$Y8 zG>^ry0z3Pfr3Zdl^gCtbcbHC8?H1=+hld@xkiE}|!w}Irs2SSq*`HF3B`eF|51fo} zuUpa zYcZL}4EdTsmc_jh@GA+1g2dIX2J^Rs^;wP$iy z{^A3n*dt7aAAlF`gYm@;v6l4O+}BDk+-O$E+3FrlE6a<-VmxaTKNbrP=+#fa=DI#n z{FM4owmdm9;NO}&W8B8AI?%>B`ZdDE>yRf4i(5*klJkuP z<+fvbVZFcSI@<73)-M`^;T!nUcVDXh>?x{8*2_z5<9H7n5Tu(J68#x~5;0&P%coeY z&3=RtN%gbKuDX_a#q(OUFBHBJm;n@n3`-a6y$x2qjFKI|O9~f>+e9B)8Mdz3*#> zWei}xkIVp_NTQ_&ggheY*;RJvX&*NLQFnJnXKfr>u(}4;56iJ-Xi|$u#E~4EV^*cq zd)|A=D~fF9t!onq3s-@ajuX>tJ=e5@Pmy~nbff^l)hEYZcnU;&RZ3PnpfgUXX@y zBig?R{x1I$U&*+hJLWTNqCVolJ9wR0SrAz_PwB^Jx^I1@=D4`-HZ*vo3d^6b1MGYr zj~rtb&Mu*S2q6i#H;m5zReL7gN33>+AYTcvd5h32!!X26#Q7ABTYHdc4pW zU$?S`5!)^{*Le6FD?GujAsnl5Bw~1)idrZz_A+culu79*dz}z9Btw6kliYBewD4%H zx62;(x*V=-umivjm8O#lpBbSYE>ciQhXJoNK(pdoBg)`MP|z!xLU~=FQ4=>>6W7p~ z_Q7_~{KPw)jAsE%JM#UzlXRbvFH;s4j%R=zc3tk>+zSOVP@-R)2Us0J)ySU8$A-y&p^)LI_K$fsbqd8YqEh%zonB`f#51`&@%I#It=&fjpLI+qfq`x^nF}>Qs{VlgPZ*;KkDS zvc7bp@7cu9UfoY-DPw+kvL``b{ z?HVSNS~uZ+$>MW_(7lE*C^E|9VE)@tS1yHza+pS|T01I3kN7|=cNc?k)ky>?re2i) zLoFKB+J)+rqn6Zt`bA~%4ZWXXJHs~}N7_Bbf8_GOk4pv;9SNfen#hC@Pu)nvUVZbb z`=#k`Z&M<=;Ccq|x4yH^&4RIc(}#m*SSPySNGIj{0U1_~aXw4~{N3#4&nea$d@6z*9nvkUipBc*JP!WbZ|!D|dUviv-&QrJ7*duxY zq&&eu)|cw-O=i|?G-&UUMem|XfQ#G>T|EZQ#ld{z)M4{Wt1u;CR=AniBt|D=91ta} zQBz{0#zMp(PX>{`exCPlfL&~0kv%`%TFv%je{7maWf!5xM!^_a>9*@wx>ve5lR6$` zi|4;{JS~aI-|tYDc7_q~z6%XIlm``0l?DEyzolD5$Ol?Wv{^CV=`L7mW0P{dGKs&N zzot2mGb01bG`r+@vA^?mqP&A69Vn$&BP`y*5tlKk(x?CZvk_TPGD$#PfIhGcKSu-B z%ojQC$1!3+=qOuj^d}}DTiY8M)Z%j2Fk_2P0|q)opRLlIM{*#>wZ%Nv2<~{`I;gty9{eIUu5O zM!C*#7LCd{zXlqg%LX~lEqo_+`cJGC;+fteQY|Z5Q{oAMXF3*$LJZD%8DcJk*cYNy z@>ElxrF!EzrLT$%@hOePWq;Io{dsjhD!k&tl1-NPSbwJFSNLwN8n?TI!8m%j`f>@hk8CM)m$=#jT83>)ZiQ1RL} zeQtuBF6y`JeV}vjSx^_C8+C?06ixo&FFrr_dodP@2&++y4bU)o; zRN+GVA69S-8XZ2Z;P{@lVM);Z(_sS-_XP)NCtiJJ*G~>YY-j|7XWS8h&G^WoG3g(T z^*>U%|J&37EPUYmv9wHTzW!Hqd(jS&jRk>o(O>Jr1^=_lw((la!8v|bXQxg^K40(i zQj*;ao1BS(tm_R9>WArKWJ8Vrv9tbTi};`ML)?Z_^@T$+{xKc&REm8diZ1FTwlRbIDIv*(|A{yZ@LQ|MMIE z*Jqixh)leSHep6eWh7-K4_}G+uR)A7cK_N@|6ktKzyFC&6mdM_>7~8}10UE#F2eT* zfg_|A&%*uxUtn$pLasQyl$jh~7{tifCYht8-A#xuc=(?YmH+G2{pXiSRKQwiiey*g z6ahXwrh?0P0Oo0|H<2~1|BtW!e|-Nm;{U&QI64Rg#5T!GG+Ko~K3wFJOZtDYEx{oy zF93yt^*OKO4$ov8@PPtYPWXNyiZeH$cxp< zJFEVYQ`uSq&@?4$j8Zz8!p?XFwnpeU)lK+R zc`;<{U%L|j9Xe{dRcs&&gh^=&8wO^-Qv3)ggzKN2PYk#S)*$lEI$GV&I)QmJx`0_T zu8itX0eOfC93CeL$h{^b)5RQH;{dp*_dInkuVT?H8PMb9MF6o&bE%csf_9THma1A` z2|EuYWZOadRR7HDG{*iP%JbwCu=!rcWMT{>BK$W|HqS&*fr%6oi4Ngp!N4pj1tTsf z&lg-i-`hJgS}!a1YInt0Qul=H-#An7!w!R~(hNuS44!wjp9Sq3wS~w&w!VtfAdBsx zd^ffT%-Cwi^sYvoqjm3Nhe&!ZeM>~zxLJ9CjY|w{U?IdO{+EM-xfU{tqQAOwzX6Km zWR+xk_`iJw%Wp0hJ`37yM$(F|v=BVc12(JOH4KcsPc~y&FCXsGy27oaP7EFb{B?nO zaPEh1>UMq*P%$^%ez!g(j$W=iE$h5+QD3Uznz;FU`LC8mF}nRd4rK!|CO`kkmVbZn zr}(3P`A$yYOKK@UeP^v1>G&sB--7R)e~pHwZ)IODYt4MEHTvJH`}ko_{7B?!jQdeq zLEZ;9Jhcy5g#QA_r2G1UEjawzvae#v)T7r^PY3eF~^YB{1Te{1mztzR{Ng9<(%i@W#;Vy zuE;wXcsOEOB#AF?@YFtz2yEt?@E@KQ{qV5zX!LyN>Jc)(tm89x@Ha0%ZRc|iNLdC* zuPDDm4+?|r0t*au@nmhS4oElj|NVCCu`Kmvie-B2CnSwKPa5W}a9*SU7QLywn_V^v zf5!nPDu2_hzg#EHuk+ZELha$N4dO#oQo&C#d4?2zzN;DCe|MG!6@*bh}d zU3mRv`+c4YW`T#Zx`;q~_~;q2@p|$`(0$Y?uKklfCH6BNnJ~Nw>nKu@LQ(QFg{bBD z=`avT^*%O5{l#Vq00$0xD8#~%97On!Mwx~`o@ul>B($B@pisu71NCfFJS>=AC5)iQ z>%~^V+mz|=el*GYFJR=R?tw0|z3-9r9XwX$>cz&e=EL3f_=L?YIOHTN>|z_XWy`~KDN!!la|+|JzpUkpXsd>+*z-33_6MNh*yAy2%0pH0 zJg;>dT}lKUH?04Ee16ahhNaw$F#sV&N-%7k)}ItQ|4KUJK%5Vqe=QGyC;d=gnl6IM zUz*=M&vq9G?SE8a#O zg@irGO|et^O7VVht9L*OccxKd=t zHL4i&{CtmJza@MnMqGf2K>Ayj67m?)piJfJ&n>)epGv$8Q#%@({%JW|vu}C+wR&f%{Zk8%TZOL}XTzrYW8z~l1_@OM zjBo9dkGts;CY_pMWUA6pSu@9IJ*(D|wlo5y9&ZPSrO2oT0B9S(gi4PBY42&o8c zV>}*jcFUk%aF&-DB9=h)5xYmO%yVrxRfp%poV-?vzRYQZLKluap|q1<%^}di_P*V( z6vybp>{J^R($5F}jJw|PpBmCy?IqkO)EDK60XIN=!l~b0`ThWjMQ1u&C#RS}yGIYGt`OXaRvt)`lmUj+-OEkk2(gp-8)khAY@ifkSW{ zC)on!ZjK^Oa~mA8E`Ox0%cTUI<8ZOl)^( zglxr|2vz&}!p23~W!hG6*p3xJ0isrIH8GYqqUigSSa66r)e(JlEhR5gvtF7%s0tJY z^Yl}-v-USbA9$><H4d0Fi?5~2{NRlEPx-jNDsi!EXYrv*?Y=utgO$5a`6VnRO&N$9F5Nf}~*QGRl zy|?gFrzG|WV|G2|GaXN2efigj)Rpcq#=y|FDOrjvZ!q&QwtC{5f5dXg$AwYQ*Mht3lzC+ z9KBx|xhGgNyoV5YT|*5iLp!|uT=%2Me%JR3m3Aa&A1&;h{gP_fc0XOnx+SKdRBdqM zd^r><&~L}`)+M7^X<>QGmGL?x5!FN=qoJ4fFRTYIZwfwaN?kB#3;f9F;NFP(6L? zG2-b)CeVmDd@$W#p;{uaZ!>|16O8z2go0QBR3gt!%U4uuFGaC732c2M)4w#t3Cy939pnOz0Ygzp_vaC`29OJ})da6twaWY_#1t1#R~6HAanLIf%_m z&Ii}MUq%wISCh&;fUeBXYST$}%qXcPI2z_I5g(M5TohdTqWjy?%foxJLk%o0$`ajr z#d{XSnT=YWb7n;#5&arHMq}KRcriucf+$ni`K#rh{8-=!P;94`_bOA{I23DO@RzZi z^m9W&k>axMQ$+LSlJO$B_;tFawlM3e#&?khG$##Wq;WM-WPDWfr&_hPWaHP%n&up( z#JpxX1X*=ue3^EH71JWrH{u;v4{k^F`hh_Z7m{BSm!d>|o!E@Uo0vg2!|p$1NRNNP zT^0c^Rq46eds85D&6W842X_y3<~#wA=xD(dyYeIGsT*v!aD(X}GG_0fZ-qDC>%~!0Pkc)od*zxgRILY1 z2l?xsCP@6T$%VH_!z_vgg1X9a(Jqap?Q5L2`7D>Sx?AXiIicI0GQg|KlW)=ED=wCf zTV$`>#x%7IYS9AZLi6Y<0RH#y@@1MK)^yg5A3ol{Br-#5JYi}Bp? z`I`qUlc9Kv@$|3#jak)e{_i;$_6Z#}3C(-MCsWX{r*AH{OB53n@z3_>stqEE1ejKu zZabOTTk}yr^J9JhlBgbRm8V?>6u|Q8>)gLW0srv4@IQjq!{kbd@gmSCn8*zk(pUh< z5q>EYn>|OFg2lMI!lXfNBByNkbCQU#JyJ1^TINT$GY)^Ulxo+~-5dI*3!Z<=V(10o z)%yOQYtv(DDsp~l>41cX=jk|b#R^zD5hP;mUJ)OxNNQiDfmx!Z@Gamf0aMh4Z3$1# zya$hoW^~EON=}{Oe#5x`yKPNQfMdfP-x_}x=7a%0tETF zDZ>3V+UEFhWdK12ZA3C+&j)11Ir10QQCLs#;>HiIIihlM6imBKSq*@$nMY8 z56-FERd}?heM0o`-M=}H(xolB=_IiKrh@Dq!){!-1)I6d3b%^Q9JOEd;q={h+~*4_ z3yKVLU>AYUY4n?dgS=A%PP-^IM~0JH_GUyb;w&^&!E#d3Fw(m8dixKZvqD`%CJNCY zRiIxUjL>wCn%f=&b@010`h&=e@1nNf`%C8=$B(evgL!YsFIq%;qREAFn}d^2^H@cr+ZH{p4 zb8-T2@7%#t#>094k4!jeT3A0updm8u8t+JlRC{v8>YrhJ7X!+)`r{swv$P(mob@+@LwQLPhF{}`!BNPzbryOf&AE^ZS}`A6d0VYG^jNH0YIFn<_^IisWt-p zWE|70klGNydjg{1Y*uiNWDett`sk}Y9B`P3a7a(J^HhF(X0mT)eSXCC?yG0l7s6ms zqK_|cG9T}YriaHScx|(vRKYMC6S5kubj6i1et&*|Z%5SAhb(ojsyA$fF0#?n>>-M~ zJpCjfM5a>x*}-8frJ!HT6K3s~U+HWg=E+}>@Be2T8(rA($D^_U2TrZktahU*U(Y=7qTRvb>!YwjZ4|SH z2`PY!XgFdizq>u`j!9#(&!;w0|3!}+Y4Rn>ALG0e)ymv0`1qM5jlH;W=hV3P+=RKz zPrZ^zVWPZ)a=N1x7@8sltsQ>vk9N7*9S&iFlZ7cM4 zDgefDB|k7nmjN<3k}h;M_av7C{p*;T@E^*Y7zLnh9lvQ+AS_nUk;wISV5=OahNH)d zMU-KI=~MieOKejLQ|TfRdA z%-W@RcFgo=Q|;UfH7|>h*@+fe401O{((}qVefMgT&2`-$$A_k)t`JSjmhhqD_RqsF z>ONh!rF)1lJ0>Hq$52j4G2pmqbFJbOtqWDzl}TYyVeW{UjS;ycQU@8=^4aAce3&HA za|$qZxkxMPp6#~1C#HT87*)W&z_u57(t%~4%!Ta&&!T*9Zycyr*}EE>EjQk~@}o}$ zQ5Jb{^|s25*NI!Ujg&yMhPLo5CxX+;djFFAD}36OPo-S`fqKu!UX_&7A&0yH9h0 zr1LFmxP|XhwQ)ii;PVa*7Q#9TF%LTbMa2J~_yv|IsL4Al25`#Qz@n7xk<`f*6zQrK z6@|pKAPF-`;L`5$4Eod-W?VeQ%iQCuTcJLDW%;g)%!boHiF0&>l+|vk2d~zF#95AH z4y6Y|PvJYnIRr5-S0#C+Eb2wILrX;-gBg$rbaj{mACDAk0O2WN3@iUzy8O>~kmEB% zm`+wn#n*WLy$|>H38HK(+PrebzwXaYsZ@|rgtD9e5MMt$Kzyh3vJXP$DS)o!_8)p? z;hP-$WyzfiInR7Evv0DM^NXorVzkS&l)uCSuFSWNmbc+$`E?348PE5)UkPN0B+4hl zy2_{ccxO7I=VQH#xHhv6RE1fmt!y=X-1#3UESK@u^QLZBRf#hO}Q%$#Na}d z4b`PQ>3jb%`q&VM3)VTruRC2Z^!-}R^Ot9)W8Qt`%h-~1wI)lyzIS?9ON^~0V3Fs5 z4#f;N{D>VE43XrZ8(lF=Z>`Y=@tndDOpoBS4}Vr955oW09RmUJ1v!2{ATn#NT9SMBby5h zWLsq*5Qe_suxR6c`h_*XW2c{9a{ylrw@LHq%^El4T6oVjtuz#pNeP3rA%-kYb=H0w z;R>^WasY2ff1ly2vYv1a*S{;&g&Qqa2z9c8tX`9Hy%?5?6?I#O92$9cV~6?Bn}1Bi z|3f!)Gj+Kp`j<#AKLE>+Lui!xMZuy^)H`J{v6BS2b&)C9#OqR3O{OFo)~hv1^k|JfHAfwG_?!`?V6a4Em#)fhKd6l@ zFTu^TPDwkHroK~XX)5#a45?SX7Sx_Rz7LY1(&v#4k<0Fo%FD!zL_Rh>NhxVQnPvS|9+{22f8-zd+uvbzjhnG;H*=IVs`^q}%?Gu1AszE4=A$1x+P1{G5bWKJ;@H7BMf<8ik-& z4sa&JbPDG3LMpW>7ya0sh@OhYXB+uXzc9l2Eklnk8CtxRQ?q)`uqO8{$oV96p4`t)ETL_qU z>L*}rQ^Jv?pXw*4KY9X&Gj%SuL+=gjnzhciE<#0vInROms1P||5x^Sh#Ms;Rakw$h z9FL;{OSIk7cAeNZjH34YcZqJ=SNgINupjJ|dG-QE2Sbv2fg%SZSM_xiioOI?$2 zl(`<|M$$dGL6REIZeIP;*VX#oAWv;n9I==sYWBXx;ISMc9I4lJH7O6&L0(mPNG`s+ zf4}rCbkKNj@fAMn*9LX+?`XLS>7eOB9!o!ilxqHBIl!j;!r;S!Yis!}nBX}&Z84qP ze9L*0s!CJiPlCuoR2egpCXl`0{U~)?>2zBYf)t8w63<&;lRB{zMNqpNZPWo92eGz^ z#b|Plz_P=6KHc+MDg?8r-Hhl|30cbD9#7uLjG}dSZ}oyG`Z-aX^M%N=srgafhjT{~ z-FY+C+mKmOfw zmS6nG0Y4R*mZ$n{KPCCm>}^6sHt)BiTXp!>`?HH)IGo_IUR3UGF7y)d_UP7z_S&Lt z?D#evzY7e(XeEr!&%6p!1rJ*%N1amguOXW@o>~QnP!iH zB}Qb&ScAoscs!xfWM5Uc5=6rSgCzMIo#?8faR56Ldtk>1+lDGVAx8MHPV%H}76u+y z9?*U&R_Vjso_{Rf4$r=;=#_dm*v7i|afR$3`>rF+;;BoMj#JF z5F~n9G0xn2Q4lN{C#^`r{W^CAZ;858qs1=r>6{GcC2K#p5U`GY%A>-KdJC#z)W_NW zixKA~zrYW;>_#aH6=U?=>nG3tNv!#&3!}dY=EXHt=-cmEp!Tc@a$jH9W~j& zB2U3aL2<&`MQ^bkLu21_e2ejxSu&70+mmq5Bm85Z+0OR(x;7t8^Rj{JPpN(OlW~Ju zN4{phs!>@LyLw&uyErkREuXI{(JLQj)eKK+BhB*bBuuw0-uJExg=_%!J0>Oh@#&Pw z$t>x7z_YksYh%y#w-yLZN$s<1r!&FxKwb~23l0k#{)b#QgP~!>sJshu=DT5Ajmw2m z6^d5-+3)M_QxkAHDHHJ&jBurUd~EQo${7F-HBIeB2w@ zgKH#RYO-1W24Ik-CquFQwu23F@ij(|=Br3g_UCx}Ln!hmC+PZToZt1JN%-+lK&uh~ z&S(XRel;OI$=E{zpDo^!A@VtG2c63nD%gk-rR?%7{<-=uFWI-*Oai7ezW1|Y zX`|4tN(RS}zn0ZRvf-kQjXSwAKc?HR-y{ZYpm%|Tm2V;O;-*zj_n2lijaBWXMb~6ME_*Q$%Pei_U2!&C?sbe zugmzHrznJ8vD}(Q7nN`3>mrbvj{M{WzJ5jda^0!1+e(fHRc9pdPCiEdG3;RX)z?n) z(!fUDU7r?!i6;f!G90d60?q4_jnTv700cAvDs%hj>fJwR{PuUtA>>XAt_MfvSj|lj zJAR@!McmB5K>h%kb;WSSA?a|nxOGx zP>52O{uGPU|6)U%0m%0Hm1iWI&kU4JNVwC{Csh9SnBMb%BqpB>3Y!h2?sQWxA9Bb$7@(Pc6k{d8p==`_^M|^df9r)V46P5YR zq-$L96a_u=<-IyMHVBmi9%(=Z1J8BtmUbiREHw^i^RPzPd;sd}Q@|)1KEa zT}0o#6Xqv05K3XBfB!m2M6R1@3E45D`e@70pxk6c2JPo3FH_%5j?0Ug9`IeWn|AJu zmlPocyt6%L8YBfR!W;l4y%IUD8wuXDUHbNlv`nZj1mz_n)x0MKx_wtf?(3gVi$rsU zZU4O+1~x?p71#y*qwW?~znP1k(J~$Gb!0{X&le*QT;hmVfGE zMQhDBRd_y*5j8JvFC$fdA3V(>!kAfpb2F&uRW}b&ZeT$yZ#7tE>fnu%Es7KAA?*KI z-{!&FTq+}<^8gXcd(~ckSysE?pq+0a&0(*EZiaq&Z>s!QCid9RRp`q`u8Se_#)`$; zyY)-}r;8IyVre=+E=aW5W5)vF{Ne2MdqzO4?raP`#5m@+a8~RD(yWw>Fu59NFh)M( z(ZqCWf;3q5xFmVkz8=ot)-fJ?)uVz!EG6&Ql9AVP?~(!nrAC7j<8fdt6A>z?Fj!fF z3kH23H+}hGvmqfEgzR#+LyXPcM0JM?7an5oizU+5$n&s)FI;)=r_5fWOeGr7;;54m z#U?JnlzKhG%EeHSVX zB&jUpzD;5?RHs+Tz_TNDuRenB1!7|SNOrPUKK@mj8KY(E?lJ+jU!a^}Zv(snOkLlO z(rB$0u79~lR)8tHT-pSBx6< z{@Zyw#H<~=HJRilAF_Deu$aS$Y zws=zPB0L`pj8$}sbjsBBXBV@-E>36?y31$q1j-&M65B{6LyUom;n?V_+)|HARP0r) zMPzoCmG(6aY_#O~*cx~ges%Rf+N)9~ye@a}j^W8Q$hb~mOg{axpPpPJK|}rQ!FBQR z8z4`T=?3qMP_w}FZm=4RPdPk%|Ex-NX4Tr_?KIEP^KjggiV6lqo8pWTur#2l7N%z; zkLwl6?alk!3%4WGRh)ef#|1X}2nYNNQhXr&H{?E1-cCUMJ>!)a-FF%4?gWX`K#<`a zl7dMyn;0NDI=7@BF-;>ERVZ?jF+)Qg4^{h$qh1iN><$MRw}GF7rsSumq%U+s+&s_M zlkj>(|A3|zln`*%a!=2^W1a-@{>y~t3W~y0h#95dLs9tq8`{)~P|$Xo;VjlhhPQ@6 zih$Vlqq^3HAl>p}u{YhEgpVk`bKCRV#E1j%!~L|LnwZm-JfI+vJ2IIWl;~@n>qZ}p zRHV@^z5n&C#Y4Wyjjz~7*RG%rp?qi{QhQTfwagz-0&C~ z1{2e+^|X|RS3Khlw*3`x^e9BaGza#ZqFvX%M#3}VI0)LQwe zxCr99{o6nr((1vBnOX$WS?N9Q8M+GxV@HcYALIEHVJrz`b2ZiSm) z+|dXM>68*1>{N=g4;RfY#WZt7qe~|VzoM7+B0)6Im`~sYh1QMo|5ntfTmT71`~}93XXl&cyU3rJJ>&c<)Pu(UiOZU)_ok+U|xVh=ZDxn8~wt+ zY@%HFW7#0o^>ZrZ*H|81fg_(c5*Qkh`S&(=ngWe)%;w;om)c+&Q$~FFu$RX1sL0i0 z{NPUMnplYmGJQm)!=;53uQ+LIdnvq+1~QBDqYXtb|2oqA&6fGj&G`blxY&)e)~5lH z=0k6}J{9z7#9-T=)omGjcbGI&^`S(+rp_=kcxkoF_t8RnVzK!mET9kW!^EDzPUHHb z9QfvEK6@S!Bzhj{!jt;O;olr+jpjD#*3>u%kzDJppzVUdd^br z(l0Tn=ajph)~S)&a&W8eV0`L%+R2dV0prau$UbH#tK@q%cN<&FLQzYhj-yXIKi-79kaaW%C8KnD+ zZipy2JoqfSs*PJ>o)^{)20=EYFJ0j#VLT%q?@rl4W)ri@+Mb~0TO~ti`wEd58~t0k zoJ@?CVW6ITZx`CrKp2UFO>VRIteKYY=c_%VpRsvwBtU6%Nr*`RoASpFf#NEceel z;*?ozID-Hj8nfwI*35M3WR&jxedzK^jG^5i?3n4zX5((DkQVbb1&TMP{V)0`izh;v z(*PWkERC0cT8{%f7S^TFqL0twUgaw$Fzs0G5lUTI;C$?5p7+=-DU_?#jDB%Y5$uX7 zEz6N_!8^TbdLR7MawUhcMFz^8=%VF0b03l-!hAlx-Ua&Y8QIn_y->Jt?1S-FOF8S# zG>ez*!(Zof51RARuk+t|@o023CpbvUVu|~(br2gk?Yv1OtM#KSBE0RntiX(l`5d?pI<3OxpS1$_&}a9VvJ6sSmYiXR{MTP44>( zZ**&y*!~jhA_WjvC>%Naa3U1zN}T^f#teLcUH0%N7)fDkuo37oM7?Fp-aQ=mQjE?& z0ct{!3n%n1U}i&|Mm934@ACDb(M*s4201?8B3$>?z6wV^WtI5BT!C$)!-7&9Oea0M z)^l%Br{tMh_EqM@;d8KO!85`3n1f4MIMVIMN_TjXg^Z@j>yt(V(pm-N(Alpq)Pm3m z)j1x_>COS|XcQlzlP!?*-Foq?hD?^Qec;~;yt%GUtli1pSN+Kf1)2({{@BiKZ4%fW zVGGS3id!jMH(pg**B~-v0b~ooUo`FccL8N4Rm#?4O`in^=Eu%~1k0X?lntjY0&h4f z$=Hsdb2s3vlYO(1YL_l-N+OpY;O(-<8O0(YD=-U(c;X|l%3S?%ooR0;9=rVsTX|>2 z(y0@pU62-eR0_P5`(>$N8kXYEjUlZ?@7lREm0IuZ?uW0Oz?;lRQut4f$71O78x%_^ z>Wo&;Lz}X%JT_Uga1w1XrZmDqSbe{Y&>_R!Pa*?jxHw)=2s+98_GEsm;ZG^ulg725 zqc1v0<^53XD3x~hIna`mig@lV4kH zFN7|o3yY!oX1B&>CFN~e8wqh@~j%yY;KY14lZu>Ti+@NXz@Fdf@*jRnd;C6j;9jN#Gj61cFP*U? zX7r|^Cz6D5Zj?ylOyYVKupUlDd{r>7AU_QuB5if{9ZgTj;Y!4VS3?BtCSM7Z6XGM{^+_W;H~H-@G+ zvKM%n%wn4JL17_`g&sOyK5pp+P zytTupbi1(P17n(^yL)w25}f0^cA-HuFn(*(Y`+}vmEXM(U?4iu2{xaERf_eSpG#^6 zno9uGR!9cdzHKVf9yJ#e7;6`RmM;xt9;1;+tJ#BRyjsb^A}#Hc{Z6(&Hbmm@ywM&0 zN%wIw)4T&*&hUB)t`Np0ad%_^I86GQ-?iE$Uw0&qljG@Mk-zyZDbQpUAS^JwLg4*7 zK`eO_INB$;eeo1FX-Vbv4xRxDY8otoAWqhfu82DRVzvL6gbS%d^^9s*jUp(t0V;z# zSgesT_2cZC%DSg4wiC?;I$Em}J2x#yqA%VM!K;RxZ}vn!W2zd>`F81>e8}%8_O~?C z7OkmFoB^#&)}@2nIob=e{yNMAVKWPb|HvB_mX3yrXMI^{vGP2j-8cAJNWI^nc(9eHCbd{$}2CeCKFE`#z=q$_T`LtFcCjzMsim+Yk6WAKRRXM%S zHglK84NeMZ0xFpTC1IiOvvZL7tygDzUZ##;q2P{=V;RwB#A%iHa73VU_;n)3%0(dc z*xSF>wkO2BoPQ2TM|)QXeQ0nikR)I2Tg*M3xIAUO!T9!XJUV*SSu52(N~*KfI?v1#c+I60ePKOyA=oA zwv>B(OgFFU9XH3ZQ5@mS8GG0yC8i)M0P^M_rMj{^rSZ2=2#ids+Y#T z_cCIGZn=?iuy{Nw8S5`klxG=fKsTDKiOgyLPd{h!Dt}WNY1~6sD&Ab%*c=IDgmAT| zsKlT)g<7~4D@KI9XlvNHFVM#g2+E7}`ZLnzKmF|SpdTjDSRERyJkM#nJPiZk*KRep z?Vfsp;`k(@fXL1T)iOLC^A<$2zEAa!wkW=4le@o`J7?^}VpQC;De=ljCr=6Ue`qsc zrZF=@HZ^dvy4*zDIEOAnx)~xkLm$|Tij!L)GRJ#wPJ-x`$`=i8i18(Uy*hd5IQ@M5 zPMGk!ku4Q`RK3RbpC4Kj#~bE#O8{jf91}W3h~{w>@X0*HJJu}%x*G$%Zf#y6k7aR> z14+#EhcPb>LVk!y(J2#da0vzRKvR+JGV% zH@FR&Zl<=k^JpWYKnU+hzCQa2p^+|pZU@eUM(!l9eB1KHyTD)ULS3mS8%=Z6@4HSl z^*f%`YrKF#I&t;R`RSHfNbImpjZMx5RL`*K*QK;4dgss%*L7rGn|ZzqUw7x3bo{H% z#v~WCvAn+UYNQ4ztESz5MJUPgqe1&t*=H>YXdBkpG)!qj=Ht$)fOPt^7$XxKyQO1f zvEh8a@NGCjl>dNeh7JUwhB#Ir0 zNdkk0$@ivc{N)1#-xX&@~r=Ro8AUGj44s z4V&tyyLumeFr!Q0(6I`K9>3c2N|}(H#(Fisw}b{`+T3fqc2M)a!Fg@EpD?!f!4Ln0 zW5D$!?(>U!X{Knr;gN56$TL?x)Yx1Dz3#k5DQH(N0+`@ z)P35cdEAB_pH)qz23a(uj;}L-$E{&#D_Y=Kp-F=P`NYxms0o7G08mZz9TLDyy{)@! z)vHk+P8WiB>-w@8x6CED&-fO%$6(swCiAsZfrM@4hf=k~j9bV_A>~1{w4~b9fizcRg4g?-8S`z z9ykwE%K45+5n+Fe~uxglr?)YohQlJv{ zKhhFu5Xm}S#gx4E;y3w*QLsXKHU-k!WP|0#u|p$Yt#JO~iZFjj?f=yRP|(Qjx%1(e zwExSkMGf6r-A1ntc)`}1yC*T*1r1;9=ivkI;H_OZj}ACKoZyj>!}k!*Ha;ozmW@yl zE7nc1MX1qIw33lwUegm5qWTNNH%&m`?WJdZBk}Kl7b#*$R(Kj9-g2b|`;GUr|<2c~wNJDBlsQj9pl(oL7`A%y_Q z)W3@p^Ua|m>hU3(19b;?y6H%1>a?7I^X}Nhy-iQ108;QQtu&hUhr`+0MbeA8Eb^DB zFMcDM&60%lM|hMh)~Nx4x@!g^hY%^8q_A=_fB);Rw&9%i(SbY?Gqt|0H(q_^PVQM` zpw`7=gz~&IHYY{~934d>Ty#;ytS;u97UYyOzE&zWrWQ}g3NjkK61_U2ab()5i7&>m zTPQ9-{$Tr(e(g#@yOo09O}=Ax=z>StbG~ik(`lz{M7;Z(XdUfMT^1Ypu~Q^dv>x@a z&>HJ9tb7(>G(hq;yfKeDyo&n4n!5}wAr`!kh=cc9?^mBXGR+g5TLNx!VcHj*WdRgo zP78AuvxQ4s!4%lvn^=LGDh2T>Q^8oQXauZ(zUK~kA*gj5KU`osx(C-pyBuyHKi%x( zUu4?Dnke+BE*!Z?N=-G8kF`r)uL>C(> zOO{hU@k9zfkK(>Oo{`11UwRgZTi@*Nct666{!;H2lT_6hV(@KevZ&};-z95~uCz^` zg)in57*UT>a1CL~$pOCQDm%kqU%^EJUEF zwbaapaj7os$bGj+`iZIJKs;kf*ykeKP+Aj)Zl=44=S0n>Q=fhlEZ{D|3jairh2U{= zZ45X!2U7AMY(EJ9eB0|t)KOQx#gtd5A;HFS$68)XHr3Ue86srgBh=^HVZ zJvZ5KMfr!~9n8QQKUa)EalhZF!K0lxGgRCt=4J7L0ev*Wrb9tk%V3Zp8n2S%% zpGINPn;6~6bkQLH__+0p$}o44$CNBY_gh5h8CDM}WHUp=lZ*C*8)&uNABAxpt)6uV z#qllJRG5MD783IsOM6-5vjx0&Ol-ViFMbPLJl^p-*oajoZ2FDr>Xi`TRINk)Gbafk4gsw{7+#X@ zmtKc*yAmEwhoSdKE8Rbw>r@kx$KS6x0${&h@1YF#Y|PL}>4-=1S!xfwR7$*)*_E={_M^jE(U2B!}h zJdn5~!`&TW!Q!JN``d6qFc-(#;GgjKTaFBETu_o{(E2;U>o5G4to|_1v~txSmp2yw zVRWp;`<5o^6b_|Js*QEY&uw*Z+qfW{kiAEPrR*;$v2{J5LUxG6^rN*F1$TC;KyQ>5 zfRn+@N`L?F$Z>7+U_vO2K*4#q)$&{)7&Mo1gj?v*#yG(HMGzzckX?e_ZgtPRjpl;lBp$9Dh8cF^H7~ zcl8Cq9NPc=p8slF{`;lm8Nltr2}|Wug1TnlJP{DE1(S!+3rhSISLa_aioY`{`=O57 zYg=+P)X?+%fu#<3!oWs}3kxg!uij1@qY6SdEgqj;;uGlN$;Xd#U_to7x1?`ZlOE|^lOt7tHl2R z&`){u_GWSL0{qId!vE^+l>ULx{rbJm{Ug8!t-{@Y#u=Z|8b=&O8` z&l~1H(9ho!;OP%uKyx3ccVz^NWAG z>;L?b_FO)z<)A;|6~CF$pHS70sQw8K$=dq$;|=Eq}0l>H+3Ez<|_ar zdPUiQ*(2b%Q3zELFb2c@FP%?c@)bZSCYe4rjFu@wAZ4=O$6|P07gMN~9X!+O+d5Ng z=bQK<%~3VcqH@}|B({Pz#t(>R6^6HX^$6!Gg2^eiQnJ0>!gc=VRp?<#B*nnkqnW% zT$Nic%XGy=mhUq)AF@kZ9|Y+Si>Do-uCD2jIR%5|P2SSIMd<$+;GZNE{ZpT?S zlLm)d=JSotT0FKTrI)YLgbPHU@%Lf7EG?NwU#Ql9G%Sl(mW#_%(b$A!PP$*yTj0K@ z&LST@Y;-wPQ%drQ`$^I@V}Sxgln9LKP@JVoUpL&Pa4oVKKX~U~hVdc=dHkeh*oy+H z67h=o&Pu#?(}g})rZP3wj;gF&^W)_C?T4Rwn=Pt%M|NM0o17JV*Xl|9cByJMq*qRcHGi;mZ+NzMDcOyw(bSf$^X>nY#Rll^Ai zi&3bMEvs@3#t}7XJCx#D9HK255*+5b-Zf( zW1MRHW$I0C*GfK^(>sJ=nTt7s!B*n{{h?zV>5$F;k!gSY2P{w#MGPgk%Dy)obq5d~ zfMWVdGO=NLftp=$hR2EnaMw@>8OR+r=-Lu)zaC1LhfNn0$=Mk%$i{p)*)986ean%XPnzW}v{L6M}(zQyy@;IwKK3ukr$CzSuzk9h=z_ByP z^#^%sG4)fIgDEOMztNv7$LRlFk5XDd>n@86603ax$6)yK3gbd6X=Kv77)^LHeET{3 z&Zj%ay&n0B_!@>ia2+Ftjbr)0jim!`bSrTm5H!q6xojgz9p`LaDF^_Q|`SHysvHXc??t4eo8D zm%AB?q70e$I+v2zjYWe5xP2e)^j|dk+y`@ny(LDW{Pn|;UQ4b+h`L0-S@z}a$A(SA zPCR@2CO7I0iT@sh{7In-7^Gu*#g@vTZVW}i{2cC2~1!-6Ie?k-$&=QzHaVRbhT2#h)>=sTW zz(y5?W%5DPVJndN1zq;OeSKlMHn!ULI~x#6etYngIdTM(byc;jCmM`hpKhA>k)_eS z%9z`^*>xYH;=HWxH-tn^a(>Yz*+s_Ki$Cv`&PRRW&wP4QH_R6xU#JX9RqFs zp!d10%Ke*-5s-W$Vl%n+ODKmp!?C1+L{v<}YZvV{3#3n^Kx9auYxg%*tT{Z(G64m| zx~bs(?<~QluIZl^=1lCd`w_J^APi@BJQA`SMbzfE?RLHis2BN%9FqTKgbUY@tMo#X z`)Kna!;-4yC@6a+ZOo)~Ojz!imOECw`9jVQ!t!4uiUyt^M7bzMXyoX2mH%EKG%d$G zO%HKdHjF%^tf<1gZpPW*aPtp$Rd!FuRjUewwcAe13uAC^~!D`GS^cb)P-j{{VZ&f=27DibOOwViSlN)K=p;ff;@ zbu8$B{rHZpv(BJUT206K4f1i51F+iJ)cvKhKEPOLn=V)=eY{fm{x>x=EZiXNGiO=?0m&DK)&=APJ#K7+IS^r{N^u68ZgY3+CUm6zr z%s1T3eFY69px~rA-{3rE^D;G`T0M~yIaAPC-)6c&tHkK1EYJ>}`&s-RiK4DG8+2i^ zegf*CxS{L3vj>IPfl*5u!G#GBp55_t{{RFbg*yKo!Lz3=cUeP9Tq7o` zq8@ws`WN#bKio<{%$j!6XKI}_+XE2Jb%DlKfX8mS>-qU_^p6|Q0j{cUqhI~8=a2s< zBUt`rE* zm7Z)4vaip5d+N+!aXUiRaUlQ4mhd<7q)pHQe#TTj;wB_a+26EdEL(&!?vUQU3DYO zvaF{p)}XWIU3=&0ywdeK)anSve6xN5q9{@}xkk_qF{w&{-Xl4)Q$Zl|fI++~@7fe7gaYD?6r=;kx9Q4~HeR6~M4INa`gwA9N)WF&KQ zS^(8O=@Q*)MUc)-w=^I62p=Lhs-C%r&!+B*vTGM7cibTb17(gskRH%5Up;B_zkHc# zL_|_>h#1koFM3)j;u4+7U)q^C?aXs~!42myy7WrS@urQ%a7E2Cd4l%%)(hgcDvtD5D{b94!>wZeezxt;razhso`!qk@aP{Ibmo8q{ ze_a1_m=Dd4yOk2RD)$`reS-Hkm(Nm0EvJrRbKf@)7kco;9%9zHI;8L&k-;BM`~=f_ z-nSP8*JuRWu4XfLm*GRc6mnzC*%T|=LaDLW89|qFqdk%5b0MxoI2-4mLNGoY02kYr zIx>MeEZSAcqEW7o5yaf1>lK(|g<2ZbR--)vFTB>|$#!x_r7*Y6fvVI>QxerfJrI@3 zx4HZYSBY)ZfIkzSx%^nPqUbX89cnsjrB=4nbj^3M5s#?wVY zlzF9s>VFF+2+S{PlD3Xl#5XbiJ2EW(2bzIzH=KR7!vbxaJFHNA5CJQ`n6;$IQOdVD z%-XyhZ@#Q@j8Nt_grc+P9nb~8U{-v@60*5Lmm^wA{k__?8h!Cg zY{@e%j31tAXe7PyjI+_lsOYOXpayb09!X*H2k(TY>QR^=-&$_4?8>j%enLFg^4xYM zV!g+S048E!&<2^;{D&2ZQ#oftl7ScXHQ@?Q*qb^JrbOqU)0wD<{nk&hfu^CX{j( zZ6fY>-H4J|M$`~qS^Z#qiK}$eCb4?e|K8J>Z}CM6N5OE~V}u*idJozaO_0YXFfbR< zXfkusS995M^V{GKK?f!8i>4{`I`L8PPjQhp0Fgx>GDNr4P_B6YY7pRs0A8?dt`dD~ zI)@AWo6&D&YYEgJlZ1FUwH{?>H=LlwNxLg4M69AD8Yt=JNfE3)kEJDN?=nBmW+d}l zv*gd-G~NZP6{A+$ALezIe?;~*)%&9)!&TD-w4#_fBZ?+@9pG(7Di0ItbGFjt<0#s_ z4n_ak$%n)p?SlICw&hgEDec$K?tTR=`F~$!$gDiGxY$j?>4$x6v9EwY6wv>74uoJ^ z?jXuPC2FErSDfWHe{y;RS?*i6QVMT~enE0b4_vT8VJ6p)uS21_0r$GImb;n&(NVGa zs#~`zUl6BWWNea?d)M~MGJTJ5?MnYLMOr8(KVIo(LJ z8tdl9Vp^23lM+;=k27C4+Ck?|hNA9Mh@RZPJepcAlyDxa2&C^5pVXrWd_XBWtE`tOdvf@jTMMf{cE$EROsiH&&3%hb3x9EO%#RBjAcjTnQWjx^-SL>8Bg-*`g$G9nf$fs!zzrw3Yl293yTRT)b$f zqJh)6P$U|!vp6s-9*tJ8G<>QX;DQ=o$nJ{b(Bdd9H)zm^zeb-&@i zj*v|%sF}=ChUcR*m;1}^gDqa?xK8{TH-@S1-3NX$n?@O+O_~vkRF8Q3ZYe1^BHk7~ z7P=FIB#IM1xHM&hk@~kqx2arZw@wx z$slPvJ!}%`rTVA?ET`!#R&>%xml^Kkjggi2rdBOYrH+LG<4ithztT41jFP9JRa>uM z-YYD)>?bXoo)_7kHQlN?@zTT}pm;<;TJ+Ml@h`E1k|V0S>v3ls^({YOO@H^~WnCJ2 z>`Sw?XJSDCfVWk7X^Z5~S^msr#ovHB!xqDnE2e~+E*ZK;T3UkwQUk_PFy(CTE#LI8vph-wDASEcq@>BeKNo zV(01|GHyZj0pJHz!nU8~!c*ydkQPGP0$1Pi1g$O{On~|TA%DP;=2zc8oY+-6TUI{R z53-H>;KpN8`9sP|_nE;G2l=A1EU7a~#Gl^VE-iw(I-L0c4Aq}RrQqNBlFh~2Qzd`i z9bnuLTvYi4hZym(Ka=f#`pPs!5O)I!Aj3x9I56KX*j311o6=4?Zplu^-dHq?g^mjd zwxt*J-UEw#W9A~1h0gS0)!G$=Y@}VCOsnQ}?#RQ1ztOm$hsd6zEvcT>w<|K_C=>r0 z$r4f9OQ;lN?HtELF!p_JRF|h;dq!Z+R^dgLM7Djux3OK*Fv^0u^E`;|CL!pOhEt;j zsEjqyg%ISogV|P0f41;7>@3$2>FoP@oxJbi;xp$0z*JrSTppsGMUzM=5M9CRpU*n( ziJVW2^)~u2=P7|rqE{Y;ZSmTSMDT?@N!lVa<{YWnV=IYxJbBU?#grPz)Wy>&im}Fn z$@81kqE_aPh)zs=!O)7z#F_I`s7p1#e%~9!bgO&Ng==?^2_55OWH^RH(md$3y9BZL zd%iJR+On+;k>ko323K;h%8VR-96|f-`lab;^Qu`p99hx^-c2yg^j)emhu9K6HfsJ9 zV)v(@qL;30Ib0q1$CZCn5c{NH#+6W;{#crH zw$!PVLxUl~H*aA z)d3WF-3->oDrF#=lY8d*fL)EjU_HpCy95`63?jG zDZf*3{&NaNY}r?s;Jy)7Y@?tA(Gzq5)+iv5_VyKpRg+}Rj6=@_bSC^{^r&RQ>tZyZ zJ`F*4t~y&K=S_1-c%tw~$hr)?I>$X62y+_<5p& zg{$IUpqSK+v|NG)P7Ot%MXqtbHD7$huVjyT1_rb36>ZfnNkOJ%+)~86t2hTX!ocKv zCJjFif&w80WJm9}_cr#1$m1D8aJv>g3&K{5F&1m%$8Y0s{L}X(R8mH>UT4kKy{Bye ztI)v?`%7<4<2-UM_QUuw#>8=vmOr&U73Ja`*;>kM(FqN4Tn0o|t1w;DFZKfv@wneD z+C-k}5+F0KZh+jxydm-GFV)5%iqXBk2e+%U0*c(Fk~u2?Up>Lh5=P%o42a8FU#92L zk}|dNKkpjH3uH&QP$@*}`ObFEY+-+EU z-(ptpq^P6e5Il00jtimt*#BV~(|e<>SR^{H)$8;%N+%l*)9!3L9eNRq6RNi=hLOuT zkLM#UeI0+QU+pg~>ku6^fbKD;t4G+)8I6|XEA?d6hl_5PZX#i63<@te>OPe7f7{MP zM=-s{VjnIsTaK)Hcf04Y6gLob^ccqs#K#&&NVh^Mg(*uMp%AYcx#I*j+FLI_%LNLn z0FlbYZ@J;fe5qmy6n6vSH&30aRzMfuQ_ZQdFZ(NWF%&>?I)@7@_c`-INrZD975-mZvc^1=B( zguE6FSYdn+CftT~)p|Cz^{XrCJF*0p5s#jKgq6MLVIfFi&+Xdxx#-S>XFjXQ{@N`( zABCal0F3FI6^{xr#GIb_`}g5rs=LeRrAZcz5XQ*mL@)W`&F>k-r;L`eK~)DPPOTQI zf4)iH0+`ivPuDIYoYWV~NjQT$P4E42o-WK2ICHnfbzDW^=>>Q|WX(HVLzQLLrq}u5 zx`%z2`@&Y(4#?5aaO|~CD`3`@#Mtb4^NWMb6JKBwoq-_RWU*>d3%GQ;Kz6&mwwiWo z^&!bBNf%RD^a9?eX@xW^Sc}q7qFKD$PaILEICu{IkbQFERqCD?+$6J2Fm;qpN5ZU^ z+{m;^UsGBO(gl+JI@XXB5Thy*#81EwEA zuMaQJ`qtu)M>eWcJHuJ{4F^aSx5&_W-rl9jzZJvY_?kIVf8l2;0;Ez1GuB4x%05Q%p~ZH0GzCndhaZ*)Lrkf1wf0> zcBxdkh#QU@;mc#n5_QqQWWe}dOyFQ4gg$#RTolkPJo-$Kxyu5LU{r~Z7KYq}gTlh3 z@BR?Q_T%uXv3^Ce^l16Vv*BijyY!av2v7Gm*Y$(S13!0_71a(qhl9ZY)wyPSYBYQ* zUrqWVL<)0_i=cC#?!xR#kzP%hV@e7?C50R=azs;Y`<*G|8ZNy!ezmD~=@s|M3&CGF z;m~^7`aEe!9~Q{FsHA(7!?74~9FkC82`20~C+O@3APYXO((HY2NYQQ6aG};w@%>)_ z+|1jmkc-`Zs?z+GgwDA9rbVZnO0F+!1!C^ofb*T%9CnnlTAEDmJz z@k{NuB}~hQ`g=|$@YT~zuMJ4-O4{Xm4FhOxk-M_X@EyRSXu+lQcw-#(3Oo9hV|M|! z7jV=rhpxNxuQGKmTzez@!DjQqBHa+6V6mZUZ1H2=McT%=bW6q9e(x2haS%oex3M{y zruN`R7As?fgVb9mrGsm;k49V2B@pki|013f8pTb1pO2YiqT_uguIMV(XBi$UQfI$5 zF@Mm=^Rt=}?y;Gv$B$f1H|_yku#lxLPq^#H>1_}1Yhao&plCA~4TDDQeo<7GeJ53) zpf((i2jn3Nhopbft_yl!C^{jZgnB+2-22`~d*2xGN|Ug@YCYy;!69X*fk@qg2?+@Z zLFlFCM0?<$38i`?TG^)VT|42CioEVBT7JabvWTo!^fZGV?TQwgAr)QIycD%7 zGY&{bJHhVrrJM`_{Cz@(j?+}=U;S@)zIA9U+z_T*7+F~%zw7i+ZCR2D0QlbZgb2+1 z?zK3{?Xr6&Esj~~5UX@oJ8-tCrJxx%v8>H@?H~32OwFg!62gjx0`HC~#9xOmZJa9( z4Qudre|H$Ec`)qpNgX?GMieYzhn;@?Em-$}aAl;; z9 zJmxrwD0O?Y$6lT^<1r>|WQJDzrBsayU}b=6upjaBpeaI5>g7ZkEb-eku~^{u6(Q)c zpS@FEe_W3GjbZ!x%X-#BCtkolK-40Ygg2fSCGAPL12UBU<;p~2bIcW!_=DDcBhVwS z)_eh`<)=4G&?D|8#L0Q=0gBq=;}O_6Pd+!p1xys@eLs`T@q7-$N}Ar27nKWn&(AIq zN1>_sH~#7!E3jkUS+`gHx&{tmMKg+W z_i<`nNwHVuE>I5VJNuZ!55{LS4ZofJqvHu!TLHMI7n!sall4hx)MBZ)%Iya-jv~g; z`#W)Vz4W4b1i`Gc3SqWkM25jrF?_$%rb2?HZy@Gct?VLm!l+Hu9*rwXY(V39sv;03 zbu?>dU~tKXYAb$%&rBZy&^_$JdneiqlxVfkgSXq z0wSU|f)@}tM6^HG7N7}%_beNs9|p>R;pf)fF`%`qY0IR2LBaF-)y8m!S_PO}w=y1F z<>Xx5j72Sqrp9_;YfeJI#<$%~AFg#Q(c`>!>S%u0*Iai+x8d#i(j_I77z@P=3*Sp=cVorPbOQP%49a%cUbWj%T zLh%W39pGNH*JxGj8R{{jz1-0P1*O^$u6Vfb&ll9pnq!6V%gO4pd|B{5Eiqe7!Y%#~ zf(4cB!l1taP%VtA-qKm>Pm+;2X&zI_-wfOuaeZ9@Xk>|phb;w^v@`YDhZ9OC?x19e zXEQfG}zu?N(f;OY4CFQOoLdAe;~bMT1w zlzRItR(P*g@_S>yygQipYxe&NG8CvFQz||C2tdIvKaNNMdPe*~=JnJ{9WOen%+^?i z%vQfdOuawPrCbEV%%$Mp_ZLbFIe@`U-^ZK2&_3wehko>H(K{Q^t9g1eQGrDF!RG@j zzn&{y^Vj7SSt>D;daHv74wh9V>&<w3(gFJwo%A`g zkdancy&1mf{6=>?#5lW~+n~N#&FWH6yJx!8Kqu;Jv?zNKA1@kCS0!XFzlACJoryo7 z{s3L=YAq?BlUYQ&Z$+NY?)&40E&52j+i06U0j3+jqMVS_zM(L%>_>oN)=?{y(bj>ycgU?mUhU0OhI>+wMm6Z*+W1HUpgX?3dPeBK_0?Ms`LZu-m zLcX(3{8OOUv>Z`0;!-0C)H~)xuDBOab|6Aw+UcAc`P-QE;l)a&kmIZU|>lf9VglZM$r;n{FexKf#BN;v?+A%8N^x9C156E}l z(~qU!$b@=(U1go^zK}eldJg`G!$OVs^Us9My57b3AjK2Etd~0O=7T`3!((=T3|e>_ z4t>Ob#4XE3^U1^3T7&CEcmumi#Ip;!W%Wf>20a$gG?)n4zcSI`9%yvhj@{d-XkCrq z01zQi->qr{>1(HueNH_AtzGKxm2m>Y17~Yehmw@+v}lT8Uz;&+<2)>*K~8^jpluio zY7?PS)2$cbfS!3N%g=5~NVdW4Kl$513r287^QP#|s#>RdnU*6?X&8YfXI) zuo!RwA>Ap_7GQx4-uqDSXioqiV+H?~hchRkQh*Y>Y~*X}D!+L|h%f)3u)uCS*(j4$ zfott?Nzb-$)li|Im}CjGw-lxu^VIzY7LDhJJ*Kc4bQj3UbKbg7x0ecUtr?6HT~K)> zq`~(q!RbiH3)r7ojvNW=3j&DMacKLYf4=mTds=V8mZ?UymKu|7Q#*;xm280Lc3)w~Fwnz95=qWyqLRn*- zFg~eDT);jPe5@30mSR=`A6cMP@2c(nw86Hlv@XFY3wD+bK@)I==vUilm_!8R8?{iq z4nGyy9!@)W;5$9mLdDF^!-ReJcC#_4H2Py|=E}4W_x5&oRaKEw_+@ zQmorXRwfck^2D9E?Z#|;}Zdp7SJ7$Mr8!J5U);-!n zVker1L^jATeE;kV#ccW3BlBX-c;dVl6d>m(qX4sE{_&U$Udlsm$*W_{65XoZaV<~Q zWvDzRwb)`h4eKHi7^4ylVlB80(2LkGy3Ma6Z79Wbq>Af0B ztXW^W?*Hbp2<-Grz_1`RjXf{AI+2`RBXh$#U{IHQHuAvi{CVdgR zwc!uIRWQOdLinnTXe|`Hom_I9tm9}uRX#?<(C$&$%(*8JU`RuRLR9$99Z7ZWSZ^+x zY_igPQafW_(VCG)e{%ng2OkJ{MTUs>;eD&_JQg!sqlAbATc$+FnyZ+hcENm~rs2fc za$1s`pS3hZ*k?WcBF;6Kr0W+oAcGRQPtYaQ3pUh~)qMSl^1SZ>Obm@dYw?b+S%frE zdRL&#j6ofp5Z)0fE-M;%Knj@JYXU>x4};-olAE9sbeBn$=d}QS_L@e*qZsbkG;1i! z5Zzz&!=moRI~$v5&iWP7LfQCtPv=4@uNoJDGFuO4x4O~WGe@J^6hixRJkj@knbNq0 z8b;AWNgUeaHBl-v1c^3n;G~rkhGfe#>AsUAsa%&`0XpJ*T{8Yy25#N+q}wIxhO6nZ z0EffH`4v`f)y{f}m%)Q;#&e9(R>SR@Ib)d{qj~md53=)}U?=sbl>x+$4oU*ng=ObL z&m}Q-A=Wfw-vCsQof;=ufeAn>`wiH6&>l;7x!F^wJ!8d^47Ohh$oW0|$s)NPCqI(T zpD|Z$bgUkj4mx0*30f~M)6Pw8wA0t7QNW>W%M!=OWij{73Dqae8y+XccGNhoKv0Lj?M(S!S{plq)K){Ms$5 zYWX81hZ_1Z>(_j@dNm3oZsANfXN!*w^$Sl$>NU^l265Al!^Y^K-sflSkS*Mo3uCHR zG^;GCO0QdSjuC}eu0@-NWSo-(pgQaVJC;w{07vZKYre%cde=RK%z1f<07}QWvT(D# z!}Ji7>MRNjD+zIs)8|=)Tl?&fu_t0td-5AUd4KzFME>ipIvuzLOtVd3BehSDEnQPwr zZgD!#JC*k=DnD!0d;YfSwzkpEpW#Ex>GR|aJMh%)mebw%{P>uFpF##z@kAD+_+m*Se=z}T8iXEe-ut1rFvrJ`N=AK& z-hwOo+rbx84Mr^L`f2c$R99WlzL;jpXRB!*k7CxJ9!lUlQdYmR)7{xM&*unZNv&4E z$hL|Pov?)%7o@KYPETI#?8OyxA9hn{6PX(&cU#!^w+zd5iUVHj0r%h7Qf@ewVJA}+)k{0`3=J3T&C;ZzX5O4x%W}dVXD90Qf=5NbBsgZKf1|A z+xIbs13XlX>0#d}{(u8&r5DNEfcKs6iY1X3_N=k;H6&UTYY=~agUSr;wEgn(05=za zQUHic=4~3ZHF1a(rIValmm!36h3a7UDZZEWZvypO0$=$)wWeL9pj?mi_Y7gTE`b?Z zD*7TS4#CERgUs;G^H{jV_8!LS)QSpne>tfu`cA&Jbaw|Q5_cX^4$#$kW#7L^vG}#; zVk_GUFj#))e`ACNv+=h-IdAiRHvd|S?$m2(^{Fnc-*n{}oYsCbs?k38nobSBS#e`3;_G1VcPomK$HlUOq`2mr^oYbZhW{snoso%o?Qw z%0vmiHtjYyZFg?ZHq>+hvX3-Ft_EtVK`r!*CFHs1YuM4sLa=ZgwUGFMZ}uR1?CUs+ z1-$FON~N#p0cEf{eRI=Op^xaX=u-<6KXvg%c|vH%zAs^R;fjt2$p}a^HIHZr4~{9l zNx@m$iV#^MB&Z4`W;vIk5ImlL^R&IKL|e7-129ovawdH~p2cF`QU-nnNOV`&Su0}* zME_{wRU81si-Q;SxxYgu_cnBlkA@#LO8dj3K@9(sOeQ`6m@Vob5Z@EmcWC(CtnL&vMDcq57b%yMWJ7g*GXG{{1mY>j6uKEywXO8s8v zsAeT=2G2*asyr$!_gMHt-S`Skkqu<_^f)BMgXR+VCNf^3g2-PEA>uzGfra?)uqxAg zTxbw)acDtleVy?xLHeO_H4O z%$eKD%>wk?!Ocpya&%)E(n}X#l^+4)?0A}x1tE%)hVJgX%g9qy_qV_iZ!*`>Mn?!ikWXOhB6IOKB)Hn#XBL_P0w7* zlK!Y-(v-g%hG{6c#0X+7=$9uKg)JjUN$3X(p7`~IB{Ty@s@tDKoON5Rzg?cfpQ*Z>>`ZI6?(eVI z2D12EA+KHpDXbFSW97wNUPcKjvo57)NUNO*8)!Pa9J8U)X0 zbKmF>m5nEJ>;FzmB%U-?sHIWuuO+$(o87WzZHGGt`QXA|<;g48Da!_REK}K`5?pp( zATFSYIkXGS+l-wECmxHMCi1zf@!&HFS=?PU0L5z6{aHwt;I^-bAeNL>Rl}gCgUZfp zodM+hdVt*7hv{r)o_EWW9F2%`p~oOxdA};o1-~298qH}ArssRGqR493^OFb1aq{kB zbvRJAJR;Gmd*7d0!uvEcy5Gafkj~X+YAn4o7+Z|?K4GK{IyA3JldF#+(1^l8`2r) z+8m7Ds%X1aFzLQ2jEW#k|8Rb9&_U_G7q%I}s-C-E0^_A&NMAZ*7k33F8hDY+dcHhq zoIQ~_}_ocUI_S3Gd?B5*Ul1H3!S*s6d60clz!iLAA4|lC# zwXJ_reRN|f&)On9>ftS!CwyynRc3O>f;engcxb_Wk*=MYNr1Ko{;csQhhii8X8yVq zoEKqTF^Q}<(Av@FK4FOHT1>pH>i*J{L+RI5O1sd4LZ*nO8~~xjRF_s&2949LH(#9& zX>a)xHNXZqdVaU=`AOcM1^NNMf4KW(i9OOhXTZal%MG8z`JU{ORO|Z9yO_;Do7>|t zX`RyEwG4?mQW?E)YMC^#&0E5%{^02bhrI2Wtxl9#Q+e~@b0acXzSOPd^PZU=G-%=W zue#C_1GX058H#`64@C@U+g5{%Ry6)BA)MB^OBS#>m0f8KunZ3ouxd#;m3+j!7+Y}L zsNhHJ%xnQ$mcSeXhxE#cLRkMw4T6+`O%^nZNSl8PKv$YPNauu65Rb&r4Ni130O0?` zQ2T6uwwr%`E&OextUJ|(14Snvq)fL-<9ibE<8zrs<{nxF7{bjFk^I9Vu8s~csh8Yl;_6g!&QmokS^52tDj@tZw9CwzA7n2qX7^p z+azUJM$I-pI&^l|MSneRx5*3X{cL1SUiU^x0LZEpX!uV*kD-Pec=0NP^ZTM>JRzrl z7KVY*plBC_e>~?TAq;=sefaz6$a}+aN$lqLAhf4z_GtHF)pC$cgmi&!i9^ZUOks_S z`l+G5fH8Hv_irOt(aE3t{;uelM1VLs689!(_6pGbXaFEdfmw)TR|vnU1lGU@$zjSX z2KRHNuF=ngtTF?WrCSTDX^-Lg*r#fsiU*}V-oYkaH%IHFabmK#i9}%(5F6l1_uDKY zWa=?*mmssW97--iCVTX7)Kk@`T?3Zxb*b(ZEHIoD&4F#9OheF$`&G&+0X@r%peprZEIc356vi5 zIs&&iAS9_C6y+kGL$8shHQ#v@W@U+}o#TvlyFEI_8RCjwyvt5HxJD)ps_oKlM99%_Mgs=ERYSL{Nkd{P9?*eKyvJ$TmPPh& zh^F=zTK4jey|_;L@h)Tli8u2**m7&AH+G(WG+PAXz~1aHPSs;5(1dsl_09gn{WXAT z!dujAFF%0E?6KafHYVbz0F0z?wDn8jZ&~ggT2BlW;CNLPh(Ak9X)fUwxOD+(yL{uH z66jWjlQ?(44$jolqjAtR?vqmJX(g#V4lx1P2`uUk_$;{T%Ht12RT2aEr+0n_{m2Iz zg?Q!SvkKT?wP+b?K|?dsu+&`pGy!}w-DosiD~6HI5j_)Aq4H1oXgC~Ue)j$r65Qz> z!US}D;&M*|ogb6+UZF=@xu=GSC5<4&dBqC^2(<;`OE$;0M z4xi-V2Y2M(hYfWQyEIKXCi2e$Ic@)q#ohjbcWp$saAwBkIroBK$BGP2HpE$)JUOis zd{wWArWtF{p$>2;`N;{K38DvZ)@HI>ua0%u+0FB|rz&Dar4)M%mU#5-KM>T_UeWEz zOE3G+AN|f?QjGhJB=~`T8)!~4Ul-@&R6F}V*^FRDbu*L9TUgY+Gaj@1G?J99j*St# zv1`{&1?my{L4tYjWM?XB06TctCk4vQ7;KLjNlRRgY4+kJdX3y+W(XS6r2XzilmvbG zJvruMX;|Uz%nJ9rBZ0ST7cOG~ewP;x<-y3{txMka%&D;l(M%RT3bgIck}?kbRv87{ z_NnJc%R0veFIOdA(v#SyW~bih@Pp$3%I|YoWk{~zEsjLKw!WwpFT`T2 zwNB6ZacRRKb&PM`qUWh3={MDSGrzIy*6oEeg|uz)7f|+~6h0^i?sH<|HND_7K@H$q z=X|rj+PgESkYGV{weThbIyhKjY=nqa>Q)*t|Kj~U zMMGh>&~^^c#|?q|hjal zWcm-E&pdY+C0ut~XY-c>ytlM(-Ef7_o~irt0hLv~zPeVyp=WSw3*@@8ZS^W?`zWo3 z(K@x|=owr!DFEZdnW`&j`*d%$_w8sVud7ucl171*^p;)++pb><41; z)7xkpWf~;Aq}%(fEbPWi3MhV!?Cwpp*i0guT_!ehd8vzuo$nFArY1=#W?9Yb{Q%PQ zEJ5A-C?A+=kO(<=y;SR49uWYNpNZsy(9AV>$ofN*EKYkh)iQU00GrVuY8LczcAo|F z;XweJ+$V`pe$$q*Kr|3z?A+mXWU??&pIlV3E}1Yth)rG|EUJhWN~9-&`kqsP79~gc zy(v|W@2$BprENGwPo6fNiW96)Naqx{rSMy_T~YqAi3m*zM!UIu7ICyIFd|Oi2$Xaw zO4#%bUW(}g>_&Pu#zoL$oWZ>}%-&AxCb*%5A68oRCRi_@GGbD@+hRV%C(^L!i;06d zkCIy(uIV{w)Y{mmBI9iQ+S^)Pzo+*L87Cs&z=}dB*}fY8kjG?QieZ$p=Vk1%wV*)f zfW`zOmpD6z`y_9!KBPW2$XrRnF)mT4PW%ICoQj_!IuS?nps%E=+<|1RqfJKe< zt$)pt7jSXTUyHoR4!8*xw=QTqy^JVw3lm+ty~2Kz(mFtZ!EF7z*@Bk|*69YM9kZLP zw{=FMFo`l0bW^{UxdcffsA=%Ti3j<^jx&VExe&05DM}(~Hcc2g4iEg9HLnmxnKSlt zQT~x|5dyl_A$Bf9gS}kQZuDum;}4JE$Z5V@_~S-V1$=>O21vgfclwTxU61V%a$W7X z`QivrdN`>{n)v!QI^jM5N0`1YOGqN)C#Bu^>~O$QWoS55kxU8q!=v*_{jpT5Ov?S< z1?U^xZY=p0KB#q$+*8f!i7=BbqQ4Hv0sZvNZZ%SaJft@y(im?HQH;RBCi;27r+HIx zcjeK#f6@<*=|o$>o)>K7H*T4CODW;7nydMu{%dTt*s7pW{tgh_LC$t3=Efu5(Jyxp zI3;%dg0IK@La@?ZS|0eGz17*ZUQ%TA(r=ggNfjuz*&yFu`({Buoa!Cb3$stzR~tT4 z=W_}rk>K3;aqEylXEty=J-EcA!Z9SUZ#@`Vx_7x^GiW(W=YFgD!UnjE z;9X$;m=(plc*q^THQ^;cU7Xr;A=a}u3;vUtoIdo}`r z2!*)ds>Oc~1WEk&XEjJXhWI2cqyEp~!*<}zSS8#~H|Fax*3m8|)C%S4hrxXF)+Wq2 zW}gCE+&+^F$u) z5#YDT96bma$m|fmY>(141q>!~`z$8!hYbW=;dPOLXMRXUB+|c;B-J6ozOl~a&}8zE zF9h$V)DB~3X%canUtU~@sm&mRJZ{zTxN&&j z{%zHPl?Z4quxgeBo2GzP#XAIRtpjisdB}F3DG-8U*A4`i9DD@IW9qm-qbe`6ulrx6 zvNtd9WwWG5oK;k`U5ZzdX_ab|sX3o*L;Q^E(C;f9$UreTfkgLG6(WFDaJuI-xe^qQ zv-m*)hSYv_S}Tby(;0lWv<01@lyrwh(0Y7v@Fp>GiA|9wwetL{6o+b>TcW6#0^bx9 zf6)SRk+5W}#HSxnYzxM57y%yhzmTH?j3`zMeGKkoV!R=$WNyTsZL<4{a-X%MWym)e zd)wJY6dM6py9v){aa+Onm|@coNZgC#p}Xn)uF3jUtKMUF-N6&4#UcrTvi+2pG0AXZ!M{+n|HDzV1n>{RObipKXL}== zUV94sd{WDmim8F(gFFn}v{Ir+1JW_2d-CM}fJxuTn;!TEzZSyNvihltH&_cD)Tr}3 zBBx2m>xvO~D_5UxaOBQy;YhRJmhEyC4ee<{=kLq!4l5dF`Nc;f`|F!ggE6;Jy&*8r zZh+{HvsFodqRm}lm-}4lTp;nQN=|OuonOK1%N>@k>N+yeupXZGCzjkOjz}EBM5zp9 zQ$m}$*k>ENkQ{Xy@BhLJFrXN z9;doAol>mRQ1fCnoz1%c$1Oz(Wxf2dKbd9g0r285%=?is06clJ`$RhplM(nXK!L$j z%<&Z{L}^7!coIsV5%`7pX=(zGha0&XwHC(k-CCUVWc?m|YD=*F6);$jHDZ#n`aOxs{zPeYo6*e3 znXv~2_?OTX@ZXG62=H(uW1mwJg^{!q%~zj1&I@c_NSn ztbw$}ZmbGlU{%1R;%a(zq-YxCE+@UORNs=&s2iC77mmI8v5aZdlNIZ&v_?4S4x-1Pr?`w@jpWqxXv;E4jL zLV9ylDFCE)mRFS9Uo(tc_g(br#PMumPC z8!70WEVD7|5)4vZK>Se$MuN1dgdJZ4RG6p=;_PSt_X6@n9mNpzv0+ve>RDMX;|&(@ ziNBE?I>jVyT>ZfLdd-fiINdy+iygahxc43o#)g{BXb}LkQ+8NrRzaP%6*wt9`Gbu2 zf+t!r5b7yP?eyQT&Fl36*PLpe{&#E4ekxZ;n5A+-^=BUSGNIO%|jWlel~nJiI;czt8)}*VpUp-F_5M zJ}Ln@!8ULu;&XvmbO;#1vtL}N3%I3=`vUDXz4-wZ`X7i8)dr8(C}}K3*OlK|pGp&N zx}rDAyl-v^8r*h?B9u?Q$>}vBjh;4e+W$NzeFk92b{o&80D}H1PaGS$J^IE_k|vE) z%QHDq=t2CYti0XJNT&FloEMRHEHC~lD$)nep!mcRg4qe}s1?wD#YQ(3iyrGUNm=&% zBi>|DLc#v7PdJBOp8Rr>pP08c;NfG;t=ssjGob5}K6;jfhX!}LkSh<$$y@05hA7LS z27Rqs*?O~Oc2nR2)0tPu>DOx$+l9*$z{78Fu>EmGyaqbdnvi;YscAE}JG_p*b2cGP zXXFSlRtWUQhFQ6yiv3?L?45N2pXpQ! zBIfzbiC(;hS@w&sIUSru6$AfybOJ|`KqTt)oXoJl4@9X>7h+UEWsu=FNGEM9zKOxF zQIF9H=ld?^yd2aA{12a52E?-x(KB_=4=Qi2Kn0(LRBGk*1ZA}KbCae9vTC=sY`9dFOdO;zZq zO@m)5!~~{BFbuBl$}UZqQ+l_BuCD}ES|kD^n(i94TN=%AHS0eu{`_@VrlEwBFZUOM zW`QmdzIA*551?zb8qlZtGdjS*R#{b*%6zm>EccK96R4B3Bp?ti!BFR`;v z*21f~5y-Tg`tnOazHXH{{`?0D-tt}rz#Z5nf7u-!w@2WV#JMSi0_1KF7!o)igRm65 zc?zyXqinfE{tj;R*uWL7=U$ZF1j?V`%@37}+>Ze5G2Gac=A+`hzFHt-a5J2f!{sx9 zh;tpfq!C(JO}B`b3Rn$^CUtS`5V5^{U*Gic47jKcTuA{-2yqo4bihjdi~f&q{n=ta zEltUD0CYk$dhsqB)z)65%-<~tV8Z__O4M&YA8q|OrUpN-Fq8)Mtz&9O;E*cNCs(CZ zMzL0Y{^wi9ls$XQG3Uy@(4VH5*cA02_X*%%kwt-o98+h$!5Okxm;vhq}RwQZ(PNf zTw8GOq*J5eX<8@IdLA9;e~sh?_WqCmV(%@3;@Y-$;Q$RZ-Z%tz3ly6Lq zYcIa}=DpLT{b)99$1>9CTU&*oGNyGbwnkE~8J3`E`q_u8(M!n+s@ zMdZhb+i1ti$u*pB^v>O#?4P74z+VrlwCt;R;l~f*H>eJW>vbGAqN5$QN7?*;q+$b;&2N98V^-? z2seiz1{xe_(-*!2B;kJMDgBNSfxUkA?xaAk+9&Yp?mF=Wjv0`mj2E9NT%^O^4`R67 z-d*pHKi{1#vM%}ZwWQ}Q;T3>AVp(Gs9a%+LEoy(htA)^3O~Z@Dk*lT&R3op;zww0bUn9l`mcF)|3-ZO*3_QIiPD58i~_9R;;U` zRUwNLZ21x(d7i?qWVSF5XMu8Iyv{=GS3w)JhyB@fP0WfSIw zYl`i*KchveQMpNsOB&<_h!9)m!w#@KLWIa)ua`if5f6Bj_S^VIZvt_KR{?M2P?xKV zc(y*_`kiZvg;a~7t2Qq6PWmNzkm}s309eU8reA1JZ1mpqd{1MsuhDXg`o#z<#Tk?{ zaUXIHMe5pMPvq#o-lgC04lASqK<{t;YtaXBtFxT*f~MaBy~|r{*&2Mn&mn}MzzKN0?FE0$(E64s z)vZeT(yS}`d?2})cg3X)PzURQ*7yrElrOf{Osb{}ea&3e$@D) z%7%QG99?W~X+GGeyTSt4Ga{qf{-Qv+jkITiS=To`r|`~P5{dMrI)B!->CLjg2{_bk zSg8n`4RbLroIe|CCu=%+zhikHoemw(*T|oifo}IeHy1oL%d$I9no!X33NdJ&kL^aD zx|3_SywKh2u9~cVvA&kB(V*k#PGXcD+>@_*pL@{Q@MeSnP$Im%9{pmIkod~3R`+Sa zOVte__pOA7Pd${E$RW{l0&hOCGX}iKEp57#^wIf3_^!5%in+r85 zMRb42eAaxI^Im%4%=e7dzf{#KwWCVEpDHt}d(>!h%f^j1V`s4c4*UB=7F>&r*;xKm zu}cs>eS6&TX;wm-xNGyxkyHy(&N&d21iOv>^eZgB_O3iK$=!Bw!?UCiyKBCjfDTnP z9{oT$FGAQ?8}Quu3cH6p3EDJ9(v7UiA(h6!MtOPeubXdmiHCS~puzRpO|K3cKZ>q8 zO&a2-!2NM$O&I!m6D3^JAQDJhfMb?jY5UfKKZ>UI>DJYh9)5-L)dk_Omju*yxxJ9) zWl>)nc8CkKwPlCIfziXO;dal{O1xQ=p1FsAXKGSld$!7cawQPhVSO+d`f1l)2lSdd z6ynp(EKs}ZpfV2tOc+K*zf;(?)6cbDt7Q1I8-Qj?1?(^&K^K6wo5jG5WTfr(O*x}E zCg+V65xmLqR5Q21#8}(l{pAPF?CrvqL(_=pxzdn?ivOWDQWzLHTgY zjHaybd3S^Qxd6W2;&}z74~b#+E}n?^(VI^Dxvm0`-4*Mu`6X&s1uz!#n0@tj}_1l`vubw_JP$8kC4pK1r=?93k9I)yw03ex9Rd(~AD`e`qZ!&Dp zo)UCcD8zRa=A1Np7EetEe6pyDtYqb&r_r@Gu&iK&W=O)m7N+r;jVZd(jsg_Bd~NMbX+=;_qpa zHHU80f4iizS8nKTTLX){ zuyR=n|FwARhKex)!fJWWlBuF4*5@WCY{nPqmlNNRbLjIpz+|Is0`m%O-ycw{((%kG zMJUIfZYFX5l-lE6?Lto)`3!i>rou&Ie}vjM-A0qV{!Mk1ulePDVZVW!6&T4ecsB}qEINXeAo1!%v9mz*Ss{2?QRSWx=MdP z-7d49;YjiLHqxcaq+7&-wuH@DfllNXofQv;A?cDqC$-Vl%wwnK6u9x6DFJLhg_|DY z7S6oX;w`9iMNa))WeEO-sxluCs_Y9-}Ptk z3~{ZzZrh)>=u;#b22eYNrC>d5^aQ+pP`sYvp&^<>hEZRU8~j$K zre03qFoTv_u*0lP>!%#^vb!k(h>TFi%ly5!7+o$|125h3WzdIP*v3P9`CpWtAQzfL*$)uS~S3|u)O@en5>$8uHlil zMzi7kPE=a+#u~*si@VAaITAMxbJjdf7_CD+7(9=$0AJ#4RRIdusU&$M+=c_>qk_?% zC-JKJfYrgfh@qY9gM;Pvqnp07f)Hy4FO@@%3X!su?WJ1iT_7G+7RgyR-= z@N)1D0GT9Em78AaK|z<+=Rih+ zW|#?#kWXB2v@lBD{@do8F?I-aP|qz@kn4n3zK4)=s|jRaV#F}H)rP?0Y^B?gupAjpBk1c} zwhG-SapNK|Ygd%-Vp?|c-gM^D#YZr%DX;h^+G{y4C;zC&P9*dIy{}w`!Bvb-wY&?x zPxc}j02_B;p`CV^FX1Vx9}vs&#!ll@kaFeEhd6kqF0=4%zb-2Siv4)g~( zjtrXayiMdoD;xHXF`tK}vgc@$Syc_ER?>YQTFTQ&8jSFI-%{wCmQOn0JFK(Li*Fv<`CP z!3f!Nf5$1T5lg${M+%;8N7A1jPEDR&?GD7lCW(2Hc;ar1WG|F*X?KN>j0Yp~xAuu| zXh3gW_d?ijkcc^cd?vPYR3;a?rH0dU|GRS7S&*#MDQBK^B*QjrWxCYf-sSo3OkH@5 zss-Gk^Q;_{BvR+%7@6hC=+#-zAm{$H*UiYV5um4I@Zh%BSpU4g^}V;%cH*2rC+vt; zO6HuTgMW+GRP8_`v+mlAytY1>a`ZP0cxTWvgYT;BAp%{J0A;HZHi3tq&1p=A%OS@f z;9jcj89}FCyJHBd^UA8xRI_l8p$>b%A|JdI#+sdX2(%ilOJryfhh@6SHdzYDP4BiM zH7G5Kpe-jHD0euOI<9sd<)~0>7(|!F90pjHr0-lquIU~Hp))$gd{U`ico%V2X%VCX z)(VKV+av0qYOf9R$TW*=llu5&krJQHmtzs1c{-cClE$!lZspY)4wV0St&b8okq$1? z<~D7~XCVy_KbQ~k?DMj`#2~rt>-K>SykXA>j{RZD-F)=X(Cyy;0R?qH*aNJ`+f;`<5dwsbcdEb(u>Ex)6WY^t(l0(n%y4D;^P?@|en$L;)?|1;l*gZr? z@|@cIb{;rzAY87(1Nu{5Ee>uU2@BY=DE8tY?938R@YmkYt~qh<(rA)Kd|JW6=S~5+ zE$zE8bw`v;oE$!K^Y$2d@bnD3OHuk!7X12)>+pFf#L z;U(ZX4(s#k)e?$EUZ3u+1~Io3Yr+i6YNvM*KIA@bjxL6cSK~7%Ei*3LOmoV65RRHX zTJpg0*N^C@sfkZ;FHuS{qe^Xa1?UF++=$G2p16OrVeU$*?=TBcIA}BR%#|M#$YZw0Td`rUz4cmRryqY`w42*oul0iNEb5*1o?K6_Xh=lKok`NZ9riq zuMM2E>&ERD`IFoiRN$W^&6 zlj;B$iTC5}&eKVnA4ZZlYe7EGnA(a-nG#adcgh<;lwZgg3Wk5s9pQ4Na@GZy+y`P1 zR=%XFFVWfO3dpvmcrCVF7UjB6dWcC7c?SzwrIwIX&s&cUxEQqzwOW zCzo**19D}?fDm5^+;ZvXEcd(^EVfWDFI=?YC{)X;kgRY1%e!^OnSOY3S$F!R^(>I) zW|8p(j`an3c^5ZA#ZdrHf7pa>MU5`U1PP@zC1RgJrf6LjSSPY88hP|*gQE4LWeA+)hYJF`put^);W4A zr@K>Iy?*86)KlgRw*5#-;$>!0`4*EHBp3$l6?TcwZ$%7+2PiaKp-;3V`M)Ooc+M^bB!m%3{@>Vk`1*%RPlS3=s`x?6JL*akYT?bvLX{Q z&ei)~cZ)Gd=ep_|0?f=v{OecPrzdU(deGO2X^VfUQMTm_yqxyOeo zi2ao7?`>l7x1|HvS#5E#3W)YW?t-$_D`0hDHzdih1hKOpTK-!LfCr`GRdW5w>2lc0 z{ymJYr0!&ITtirCY-FR2R(Lr_0e^j}Yw!6FR68^uC$VOnI9cUL(Qu$!5?a^ z-|s4*SG+Ou7CFB)J3`~s5W%%+XWM#u7l<)4*la|V#pt_oC^x=dF8xt>U4zqu7ce)7 z&YUEQpR{mQ!B%OPqa7G#a}t~+$g;^uu%K>GZMn4C8FgF4NrO%Yk>F z$Wl$5W#2_$-W_d-J)<)c{rE(;q+%hBrvpD`Y$$`0t}+M%`mQU)~BUcxBd zC|0M3BHZAUP&z{fAxV61W}BP$ROKc>d7YjzWhwcogYOjYTQTT?=Pd#)~+dcCCeK_Z@K6$Xt8-sz7B=E)x_nqfR&K4+VMq}y z-xKUFRGuv!2-0M~C9K1lE%QQ`>9=m)U|V`%@HgriVD!x_jz>v}@d05st-L*w`#lsn zIv22wb65NX)WK&4o4#}?5iBtFz~3I#CI_O=7XjphLbp=gdTnoEcSUKE*5ATmD|?lRZ%@DjWN16@}B1ka~@$8dn#oU z9C~-(kfRMnJPWScuzmw>-d4j+jeCVMHpyICGZ+-elcx?S8Jk4Aa$eUSy_7T~k=g~J zj&9MXKp%JefKNv*JdX;3u_W4N^GlscY0H7GGlKXflcN4QJX!7YtCkT(X{qXnA}7Y0 zfl$v2n0jHizpY>}K8f9VU_;JdbL=&{tZS02vbzg)LJLQxTjXgAGfUG)DLj)$Ld6G zp4~p#=y%_mwI{vgpCs|Mo&N|GPT3DDK{4shz12@VK!{-QN z_cwBPW5)xNZo}-w4Oj17`{$lwl6rKNT56b3{=mMLGKr(ch;LHN%k$`EIKtMuQ?(jc z;W>@ygS8S$umhO$CZwBhvO81x1>jZSqS9K;-tdIZ-QCFXu^xw3$FZKw;R7u_$&;m7 z5ZxfHBAa1xN@4_HnnpJFcbtwt5uz6=^U3`~0^;Cx!RW0dP;2wVE5BO+A-a-xM{}8J zoa|ZT`(RX4k=nq;NIJfSF`j>T!2>&kM6LJKkbYUX>{b@B^Zgzdol? z1Dq>L47)RVcKvws-773GSAUn&Pum@)BMWN-X~HYagsNZmg4EdaPk5)`3o=XxwNrIrt+qoV4AV)`Z-rh9d&t|4?q#A_sY|nNy z-sw12#F$4sXpnpo%r-m}&l;F(U{MZqRoB8iR;L%_+HA)Qbw}+&DsWfXEqbNokLTSu z(r(L2){@=ibr^(ZNc(RSl96$_M}q5pZDMfCFcCXLY7e!bx5O2}Z;z!@LVnmC=EC~O z`PcAK?Xl%6HU_jJNVpxjsCFsZds#Wc09O8$x7{RuRxEWa#32}q)YWx=y5eP9SJb0Ns==l4*exeRF()Tv4eR_u_yb29ADM4xm>;0CV5wEz0 zs{k6rlTi93$XKP_xMC=J!&V;$L-eKX50p?E@;w#+z4f0z{%eR4;%=k+ZUKS%u&>Eq z2HNKG?J8!8QUtKG-}#zlrqhnC?KS6aq*#G7zHt9?wJgc2Sk9qu$FBQaCh`yY$Le=t z$*K;o*S=_M!SsM z1_FzfHgSyP{euDjH4=xZwg4VFhlY|sZ^X#)L!pabnr7D98m&(*e6{f~-F6+XsN?p= za)_;rEX~%!jyFW(lXd5wQguGD7*aR!OZ}{+JeCFXAQQpGGjk%>F6SREgs)>OOXF;i zComHJsw=ci>EZAWt;VkQ+ZeUbO#ZLAm9`E!4UegHHY7m}tdQB&p;bqydk6m5kLz!zpu1Pt;k6~orsXqFEj90oXge-|<^It%RJ+>vVU-i2Svwt%|#dr_72f!Rq>X-upG zC(HQlV~X;^n%A;NPM7Hb6=2?T_<{LIh)obHoQZCr8{ws@ValtR5>GqkXN>auQxLiP z>euQ-k=b!T|0kr5O(XxUqf#;yy6qH01^ql*BB1a4@8NOw=s6i1@2s#*K%B{J)vifJ z=#V8v|KwMrQf3t)F{d#Dd2Q{ZuFzyTvh!2>=EfW=CN$&Rf2)u55{yNguDIUxc6VM! z!jO0n?P4M4b^y?O>RsszDERmATh#)hHXP0{EZ9qFmcv}l7?f8n{+4aWzPF17<5eQl zj{r@jwtMF3ke)Zxf;JXa`IH(-lRwYy%4{y(5Injx8$kHT{NmVYxUk=;;k>0F971I> z3V?ybJa4^)JWsdAJ8$nUHdt72lC2K4ku|4 zq1c@?0HGV|efU4Y#{4zY84zv`_o(Lqms^}20}rajw?*2wDb~%64CKQ~33zz=u&S4Z zIudPxTH%o7r3--OGTv6&)MYh*VKNOYbB>&>UA-D&sU_MaHxe!h_R|y3R#rbjOwXXi z8|@ksCG(zZsrIQe+{@0m)WV;ZctHWL!O73-AV_k_TyBM*qbRns8=7u+gxni9%-p*^ zT)k8Hdvo>!0$r=TDO1(T3^Zk*2W}(4?_j@nskbUiSKG4%01|Pj()1`n6);w?PTT!N z9Ar$+%v&`Wb8okgvgbrY!LOOfWuUmP1M;Y=JeG;0;HJ@#=b{2NJ&kdDy@6O9<&<7m`BIBn+jjDhacwpHj6&K zu1qRPdKE-87X63H%8EA%266~FJ@^8^cX-U+=0D%o-l}?KKTN9hIeT zIT{0_z`Ld_6ng#XBr@nRJ69Vi@Q~R*nsjpknZBY0qR-M;E2_+=bq_w(w3u z@1YtfMvA-Xq?}*xqp(TxwAda_e0cxvE=I0qAR=KbqxWIHaF&{HE1;2Dtr_wh4t>l@wG3H?? zImMnmMZM^O{z<{js}{y;zjj>RDyG}Wymt0uh3L)K1md`!-gp*a8RTJMCS(!} z6u(M9&cJ=o%PO6`*5h>iWzp5?)>e8=DgLisl`4s!RMA~Sr2QUSG(?QQaDoyD(SfU1EC<-|7r zI^6A%FhfkA0Nf~m6|y4E7&LWpEoKPsL4?U6$|668H#MC{Qp-7vD13t~qg6)6!qg-v~R$NS}^W8>zxR447Ge1!~qn==Z*h&y*OHbYy{~p#F;U58CbI0C0Y}W zfvO?sczMK`Wr@VpnB-<`So9b>*mMSybb#~K6E}+u+eDs+s~rX834ruRPKfYLtSEZP zeGH9QR+OcnWmAx(&YNHlC%|7I7ne04fyHS zw1;57NVyHG^yBh)KEYk8FFzD=PfACZU z#3+*G3P`VkR+AL~CE2Uf?JtII=RCS7-J|0#Uye%R!<)|(HxmG3PoVodgw+J;!pIVg zHJ}RJSj@rPhxdML*QLpY z3hiSCNu|;UFqDE`V1lrJ87#RBzF8!5tp#AEWs!{lWb}pHbu9*-6++JmXwrm36ph6? z0KY7o%GCAZP^#1+3~G)G>H??dwwx&omF^W+OYJej@7PdFx#sKU@?u({lVa=jyz-jc zy72GEj`%W5sW`s@XY-X8th3j$vDL}{7L;;w*ln)u5V`WZX?4P125}^C+q;HMt#*Q2 z!@Qg6<1>waT10vsf(1>PqL85ZC?7Tu-^->}d#0YUthg}kU5aPv|I72sCGGM=ql0dJt_Fy>j#2?Hdb zqY$}5u#uHO&UaWebWSakL!T9pKO`n{NE95CJGlEyMy$r5s>_ zQUNb5(SE)k(7F{>y%60K-{WXpvR&;=;S#$a6vXiHiF-JS;fG_jebGjbFMWv|9}F_+ z;X2qutUVbfk-Og4=S4AqG~~9QEu%1n=V`+$tz;h_jN3}9@fZsY?m!S_kKgoSR|~2c z*Qj%-}Qs7n#5(>brd-?lzlLmIh)1|u;bWRbJ-qr{QwIW5DRE^-HeUGNHA725b zE8Goy6k(Y>ovCUxoCnYl>8N}a-HrEIeDRBEq}z{)0VxAEd|_J#wm$vS)fm&Aj1VoB zWtwKUh##FWwbqJ5)DJlOOeYR1m^!_9`;gfmduP``tAW;hV+XYTI-(2hWHg9?Ax9K6vCKws=y#iCh5*!x0sF# zv-=Gchx0jneVO0PM%T{uTz}IqVn+@8@+kwFPLgj3pjlkr*C(zZQdn{``BM6j=%jt) zs%>@Q#8B;ZRv09_y z-!#{QkIbF0yX1qSbe6C1XDHpcU-icHRQJd9y3Ullq&~dALqFphb_s&{Z9DKC6Lmcy zGgy94&UL_{IDZ&ep8l&Zrem>261@YMW&Le+R8Y#<`VN<87OnM!D__rd=Q&Kzu`z7R zglQ{ficc})?|WIOog$wUVCxt-k91lc{>-xi6cAKyd9ZFX?ry1KSOp(c&m1T}*r$5U zXy{SsWf4C_G}M{U5DYMuBQO!@(XT6HJLu4Y;oa=R*qWq zNITIE(^5;BkLiiOUoRus`VU{${iA-am*Yi7y8{EV)TI#Au!rcMaV3;y!4aP1=TGlK zjeh|}UIL#@cvEJ7GRMY%vYb2k`tecYNNsp@=aP*Ipc|Mx02j4?g3Jd?dgi#2i>sxs z(vZ1CBhGvoTo=Z?B;G6zHl91p5w1BDdnEa!l5`~+fTlk)q;{B7Js4MMr9jBqTGLhyKP$Gxfir5l zL&9F*n=LSc`OfxcOQnp62}{8MQg592+1^c@WqMHaN7jlHZpQUfD2Tqn<0y?EgTC8n z_?G>Ad=HGI1SEjjAj&^Vw(@``RU$xV@C3g-q~yU^G=zmP=#w|u(lYWb8pDi~@4YNa z?77$KEycxdhxpf*sG{Ei#J^(deSj6<2*T;9cymZ_sL4NR?v`ZI#H(yE`(}@22@E|` zv&df9KnV0{CB-ty*EG{eE~tDl?AWZ-d!X^|U$Q^*^;(Z)G0uCd zB&fTak`CyM!u>kajm}o6U<3H`+O!UEwdLFWRtpc(pjS zeid-*=^WO_Qv&OL_U1Q0a0wQ^PUd_^k))|ntd>FElQb@vEq`6KiVAq7`14NYyQzM_ z@igpM=IXa;FKQRWf9_6hPr!>i;1ZB@rsI!q9SIN;QZI&;;sd3c?WT z-cGqyZaLI!mO`*A8wH6tw4Y>q*xBQj&8Y%4;7^h!UAfoOhl)fw+eR6l6Se!YF;w}% z?>--n^eL{B`R51Rd0)%I1(ij1nHDf2k{%u2G)agi@O?6B21|VQZqf~XgOx%2%fqXk zRyD;QTSJ=+Np7mlvajI>fY_ItqL07Huy#A&{0u0^eX8*9h2ej*JqO>DSlE2DzU0JT zDV4fDqYn|`UODLBULWu0wy(Se3ZKb-qXx2X@%L6z>1PD!guhN33IcG9gK|gJ`zCtY zKHT+{CXy=rCOrC%UxOdF+6@H~gG)UX7}av}#ThDFU2azF2P8iP)#~g}jvm=sGgy zqrBO^1D8C7J{51`Nc5Z&V>#u~Uz0p)@P;vg2}qt8el@b?+sj&#fETZ9$X95$UO9~A13&iGIve7wy!K`QY1FVl-rgQNL9&P z16oi`3=V`_AGtIW0fPKa_>ws?$G=uibEdf3#-M>>@AGJe4 za}a>%a)}w8La@4<$=$RQgc7mtv-KmZ(Q2~ltKAV$eX2w!Z9WT~rmr38jeL{0J;82pW*YyfTd7=TDbJ(WZ^`tH}k7gYXRp2iP{v8tiYH zOrAICB*D7oUAH-h*bv%a;Nf*jhp$VoJ^_gNx5Fwj;Y87b;XnK#0km@~PV_om*C>li z5C>Qo9xwCZ&TwBL^)k*5%Oy}{U3lpv?Lf6V4>_wPvPfI@UQ`989q0vqi#w29B~@q6 zUjFYEpcjJR;x)xq{1X+Z-LD255a-6TEwRcs4pwEbI0WCM;8c(B?6mU4#6E<_`KakT zZiEKW7~n5bR^*#_1!V0mwW9E1JU)_ws%xR2uyi0Fg!@mgLCipT01lgeg*8v+v87D$ zgJoP{yQP~q7-*PeVa7K@MMV1-0NTx1Gt|EUAcF}x_vI0onhs|)0$h-H1RoBva1J%e z9kcMdz@=p}ky(e#a}+hcj{DiA=lf!xq*^-k47IgyR%ti&0Fe7yXk3AMUZ0Tp@c!cv zSGfEy0OKbi!?Z(|VzNWRrY`MSDlVV;<{X2ITD!%BJzVq)+X@#MF8BL%9%Cv|FUDj! zig>X%7_HI(IT*rxpM$a5f@wwNg6F%;|o0-p!<0E(Hc{BI9xD#i?mcks9 z6BF~I=D`do+}yCqJgc3&_sO*+8P;!{vLs2jV?_al0KU+p&d1GBy_zxjaMA$&|a(mFAGGs49Qm@6r% z3Cdkw*uHf`zS-_7maWp%@u7EsS}>`Sj=&gkMg#2%{At!cFLMM@t#Y!Hc$*g!M=-hN zMHQ5tZg$S3>%A6BlG5L6=Di3-h{pxx0}YJ8f#FZpB5PouuRY({hfsuQL4_O;IOvNz z3Xg~SW%{+Vcs4EE0-TBP<>r(UhPizs_yMz#K*ApyC;!FUlBm1eS8tih8+EFy{|AUMtDcu)AoYGL%XaZ@V`=(KXpj{rAERC zBrA*G`OgupXNzQ-Mg8x0DWE=#f zNngm(ese9=R2)+kx>tP0|7J1t@oN&-E0VGpioZVSKhOJLXa5Ls_7RWz2RRUDPkdQf z&jpm4Ctnxy{BiaOv0}m2PM97uhTz?4sqYs+k}j8o>n40bz?2ew;J=7ze|;B33^>&l z%Wl477U1B>vU#mt54gv8&|ob@Ndu6g*_`+u!LGS!dKxNxg{b`_SjcRXY@5N_fUP+o z0|3$4t-WRx@Twj`U)HvY5 z!`E6ljetK~WH0~6nD}3<x;I>?#Au0kOTZ^X~APc$dXG1EvZp7rlmH%0nmaJmx>%0RHoR|Bn~(9R^xZ^i*@GdZ0Kcs0AM$YU55%3&K@>vxfMV z|Hps-$6Nk6pa1*a`se=qe|CSw!JvmvKZo>^kFoMbJ$$0F-YjkPgEg!`A`rsTXCfgE zfq+0rf4o?c`-kHFUJf+%fRzT_AN`i-HDKrHqw{bmO~Rs2Cj9r?K>l^&;$TV_|2q8& zW_$>;-1BUTm+n~p3?+8?nzDa=+#jFnAn{2;oljAIUok4BfzBL<)XzNmubI+;hOE2= zYhjGOb|>95IZU5otw>P+>k|rtfVo--N=+l`1cUtl^&-D+-W6I_?Z*?O;0lu?Lm! z^ZvS5(xk}Y9gZ%Utd7!?wsAlI=L*JUfrl|3qV^d67!i_Q3ov>_Fo2>r8wJ$MXT)hm zMAFqYG+62A{2%h$=ujjlSc8#W0>pK7$>*DWGmnn3*toe%Rr=Ikk|v%vl*A@)Vnas|$gvgZ2QW?#U8^ z{GUI6vT~Z3hcoTFn>!@vgZ!`j#2dax;Jp9%!t^>W(ar#cxrmuLkLRt{l~`g+9MxaL z3_=J_NEG?>enrxwC!Q!x`~CN1YLZ3Az3ATEklw*B?&oo1H43XKkD7_}{(PpiLXeu5 z9x?Cu&ee5a*f|ri4yXeUU%7?ReGIXR6g10>sJCEF$fDA<>+;h%2;#@>PRTw{na8D? zIgA`dbCsb!!g}Bm0Mvho2|+73{<6YK=J)YF|s%2wx5cnhH!v!vW zJ7~(oyh_XmIBQY4Yh#Lw0p-`q*}s|rTa<&`wD3hVB$ob=zeW=n`FqrZnCT~PT5?B& z7_EAIhZZY3WiAutDe*Ave>Pf&TG!{|H&gdekT-UNSUZyvMRGm!@L)TNU0>;OYs^^@ zBVU}7E;jZ@Y_D(B1%>+ZwIA}^q2C6)3M;^Tq7+F}Ck_UlhmT z6b(TBAEjuyW;Ghq1E!|BBInyue*S zed=~URB$M0MA6HF9CBKsW!v z+(*W|Z&xYAVz)l6I99N0ofgOc{d}|`tk%4KiGq4XD+`uKvTX5!%AO7zkLUYM63fWG z|MF(mFjWj^_r9AaV1EQ=Ea-UiJ$LF~v(Y6;TyeB3;bT%kiJ`iVd~H7eO4%*UKWzGF&JC94Mjjj;)xlhw z!Yle~HjP<>1k)4)U028Vr-7hYP(bTolV2>DtXOf@{Yd&N`fzqg)I)XMX{o;27qliC zre`XOt*D6S0mlVFRG4yHG|s}Fd_u&<)q|}~)|6o&Qna(Ey?|kPH}Q^WzpXe5`wlkkb4!v<{Ycnj#m;} z==R;cz3cK80>sB5@MKrsFWZdSArmc;vTe)`MC>(dW3?QSc+`-c5eqJq%gVuT%{o*;M~tovl2dQ>$uZ7=$z*v zE`y}4rm5W7~^WEGzl0*j#_`@CQ5`5X@MGz;)q zS%040tfuMnL%%I60ySoJoeka-cy!+1g-haOC#jNV;QB`A9lhfqZs{SgM zT3?TG{I$o|ryFc>HS>b;^eIYBfqFM^(v$4c_<@lBS@OjGUpvE%_bk?gQ zTYjVd{GVSp%?8_BrPp!VFg;5Y=;5f|JfpJe%%}b-129F1#H*2WPp23)W`NJNoEh2> zts%fF_`kvi4eX#UYGz1&nZ|b}ERctT<8G*Sv67e$V4OE$w*N&GQg49s53A$^mFpKl z;5YStomUq38uk?$#S$4jStAGLyt-9YNC3#q?7&`D#|Hb~Z!w`hYa(K-RtEkj2?|WM zlWa_o#EKRFe?R^!nyrcgzNzUGy4>A=jcAPntO_^?Zw6nJCgc^o5R>s+19u}j5wcDZ z9%8szgW)o`C>H~E^Vi1%O%2bn6_xM4z|q7+4)n0X1Ag_P(vai-MX=$nuWKB+s|>RJ z#||nffd5G%64%|e4%7v`Ub^dle}5zbcAZ!GE!QR{M%;3}$tzFeOODAFCD%|q=zSXc zr(gvuohutHS`9<;b35`8(p*57#dp zwFvCm6Dpy32nW46UxwG9!*~Gm?;B}o{BL``r+|bsPdVxTj~`&dn!X0#-g}atfBq+N zr3J1Szzgg|9GuG5U(q8-#A*5!^6|c{R%OG)?6+@g98r}eL+t&UGg(&rC%q+$@&T|q zIlz#fWJM07nof(~`hTIAqt`WN&oX}+{b$!qUckx1fRy;sGu8%98d$jN;(HD+uxn*j zOq^FBVCJJ=cJXreV&sOzarPQ<|N9(#L?q5SttH0$f6#WI_Ujr|eh7Jw|J}7N06n!QTurOj&G;!VQu4tc2r%_&gn$5mOeG&osVzbbtq_}m@zVNpQ@PQFOi?!B% z1^(W(V5jka!VNiM?`tj}el1A9^WR+y6#=_bz(y_ltZwUeVPV#NLQj12M(mBOl(;&y zG{k4SFcSC$w)CI!49Ur(R=gZj@_vY5hT}=+>1pf;yG!QX+_+kN%24k8P+@8*S#m2!oxJu$uS5?Knn4C-CZPU>uRLtOwBRp?hjVsZq zwYg0XH)9}}WrjW&N&YyNE=(^dm{JZlpF93#&3^kWp2g~XU3^+d*r07GFE8(>^AWDy zLUZKc;Gp&S;1AbUN_Mk*ZtM7Xxz|>AT*=%xFpF_ByBPRyzb+)|+sIk}5R)ET z$^0?-fK}mNP(M!XD2ll1%@vVac4&!-lNLSez-M{?^~kB# z{<}mDyh~>M$PWevjzBqivhnLBxLWWY|7ni4NPezw^HP0qvt!`rPr^>&-UtkMP*>N^4QG{(BF($R zvvyx+(5-Y_t!ZA20sJxyDNx~FIwc~@F-lsBd1F7wmZA|VQ(}^vCZpBWr5kCJ&1G6sL(#%$&ZEdP4>jp zWkxQ*OfM?2zxQM!5ppTSmB&ny-$)}G>fpl0#)jr(W@OA3_5bVGDY6jm086>Jt z;+vIzkpRZnY$p_)SRlFX>5ElYQ74SisXABs`JAOAg*;hyR&4_tsA6(MfbZQ zo_%xdOSa3G7F|jmL7oYqnJ^(lk=L`YoJp;dN{$&FwZfVvI8|E|i&QfjL;$umPZw1t z;(b#1tZ_KYYVV$w2JB6-+`G!V_Yr4FQ8Vpr%)$Q8c0VlSivZAT|FaN}q)b59o<+S#y3eiFTW#RT%7xTXT{54^}KX2&RuFbTat!@HgV(z-;c(#sd z#EkMgZC2u5s(^F6X1T82>5prWbf*KEqUcmtx`mKh1&c?L259nM0=|%7gyYxW*9>IP z&1dVm8wQq#zwO&mbkv{%4&UUuihB6<2Jcpq!om! zg=ty|crVtTLEG(ic+`}WOdeb}Nov%we%hHV7=l4cf$^@-vdF(y>vulGopR`iz?jrE zSnW4V)0;%$LVy?QXcm&@a;TL1<@o*>@%r0Kg*)wn8D+hcJImIxbOVMkvey=>@ zA2g%-gHZz~xYnz4RtRwbGE7{KMBsZVV0h#V9?M=W<%EJM+J5*2p@knz982a8zP~nU zQmY4^#F)5`Oqupld@31o$NdyfFjatknrFmhwoD%Dw{E;H_H*sBM^-CgK16MFQI6;z z(t*yvuN5ECddyY-3LV}1JWEu;Qi;?GSBiXt6f1XD(* zcyNZxQhv2kJXAeU_e8;NyN{VHF=|hK=kL1~SUX3K1EjxUTRuOGB)P6qsYF79@`FiQu-lT{USIBq|l6$;B z@MI)Zzq`L>{+%XVsyKCKWN)QFTIJ!JdR!zC(g&d8^uF=W-DMIcS4ad0J0_u$ zQFETUaxyh{c4?|lO8DDDg6Mb92#5To4uVVy7!_pv-f6fFpuiOly9=rH{chxK**@)} z*l-hj@3Ny5E7G}#RC!UW)V<&J?X8dO~aS9-_qh- zO&Ay$V3T&$-F}E&>}?lK#eKP$FHIsw&rewNzxs;=OkkJHjzAQ)dBLkl5QYKsLTu!Xt+yHF9 z7hCUafi*{)5(C@yS$_hO(jxDM^1*|m=!}u!PuTr(Z3KKnZgxwPVpni?RF0e5vJ#@~ zKVDsG`}&ZQ&G>J(^H`z$-o-bU7~@`3oWPBqQ0!AWnL|XDJ=Cn9eH8nvURyjZP91kE z4>w-irLDbElq-1Ws+bX>PI3g+2vDNFwUGO1#Fg9AC5Pe&GK|KIyAA1#UZE-g-PK(( zYhE+b2EtI_YTAnu#b`i>eYE@@ufQ@DTf{eZ>M%Kv9Qav^j&(WLA*mP3VK&Q^FmiuQ zN#kg}$i{_1(CLuqcdeBZ4xwlxRbC$LL=g(V%g?mY3vlOo+Fg%EoqY{uw|`5a4FF32 zxUW!;q_+8_wCf*;C}9B}L<22t8vEfY8l_JSy2Me4-dpD7Vkat*E*>c`*cJ}|lI22j z^gOn2SZx*#Kgo7CtY{sKB~s}Vp>Q(yX5vUAKhxg+(ESL4-XRKA7X`T$eDulBF3+^(0wKMsS* zErdB7qDa6yA`J*+K95#uMUZ_bPnI46+nYhE)F;>LQ3?1q3k)WE#=(@hG)0>^JMu#E zriEUl=>oe1u9N&hYqZjs;PcD5qD=qEnfSWm)99wJG}J*y+kUOwpRfl(~8FQgRxgFx;+28%U*&|CN^| zWfCX$VSYn&a9-zrV{1@ja z|CFN*1#Smu2_CWJ(wMas8GPz*98Bou6;XNAbZQi1-DTw_p+#%WcLWM0_MX;TkFS}u z$vcd*yDO{GO0G`T`KIK!_iHToU1h~L?bFlwSA8WYwyJZwnsuPQL);0=4Rqd?87Mn! zFgEVpzF>%#1r83*JA1#SHw}M4x2`9XI&}@ndH#o21iDRLkz6`?b|D!_!j~UYdL4I{ z`E@U*iscT1PAt0{fiiU>P=61iQR|m5!Ql)B$^R#9E=9n}`kEOO8xxc1X8hr~Z$$LL z%X(v9@e@sMcD8ArCGH`0NV7x-?T-z434q~Wm!`uf_$BEARq-l#hP;2K5F=kExj$*oO@S_Yek#N22n|aIx~QZaohn^(#gDs#7~RQ_?66 z%8`Dw5@ucYEA3@{(ALeQJV_luHi^A*xJQB>2%}|_GVycO%M=7a$wJoly}$47t!y%_ z!?cPflWA3*OrgaOs4^sDL#nrP`_#S8px$%($QIgY?ty;tAb*jVV@fXno;)dKf|^QY z9;uVJLuN!^QBwD~@gVH7<+j5ym`n@d29k}8hCZw&I+b*Bzh5-BRM0r-ahpJXL?_q;KLpi6_rK-&_vEC+Er`MaU4(WI+OB3OW0@Cb5O6CHf2S+OVd8xH z%U6ILkaSKB0fSIR*dtVlD7_6Cfo>FP16rvog`eMLeVt;keRtK0Oc2a{eKx1HgOV4W z=9Bi3N*WVBds1hWc=Rpmya}y9I9=k&zJ&@tg$d@CK#sXQI?F`rkEBkhKXWI`LOh6{ zldSy^8SL*eh3wyt2BGF5Em7(-j=2&cDc~EiRtby$1|1XB>{uBX~UZzPPjS z-0gTrg?s(5S>oUm+sj@V1?4sm267=|H*t-cC)Jn_8vowrNFhSI#2zgZ7 z_7F|r3K&p*v13lfaGP8fd_!j+>2@9GnV5uTKYtZ+hHQb;%8shtD-5|ezB}Gj{aP-k zoJJTfl?3m4E?!;Y3Z^wpmK*31^80}51}$aDB{IoEZyEw3nh<9y{a>IYJcJ88>kdX; z=0B`igYGST&}t_vk0u(rd4QBMGkDqIN|n6(4s1ZV;4Zal70IWfZxTPkob-R}z_r#NiA_n6`6OQSB| zOVSwCeLv#ZFGtsD_jMpKaS4ieW4NZdZRR%xJl*c^&n>z}BIr_yr1Ezppwf_Sb4le{ zkaC`HY50;?@Qc5`?wxRV-H638MeIV&7{~)w4@js=KYW z9{GE*Rfu<6lI(PM;f-|$t=@m$YKqk39kdo?(8xCTGs7kZbe>AFjOM3C`DoOrspk9c z$^oI0Q5jFOTtQJ@!&g(mhkpA!mz*OCs@L0UrldNVq2V1rarfE-tKL%q`Pev;+hYX+;7F-0hw3GKw}Nkqhh zG36#>IJ+pP`QsXE*;`MTh*ACa_eN!D>=w-KwnihNgmtIBaR?u;WUz?JsE`LHxq@6T zXv-FNpY>YIQn{Xcpvo&fcUOQD?j9**yM_;yMaP4;!IwzXnBMbA? zkxp#4m-bz&qjnIhbTkTYI3E+pMI&=kFNwkd&25 zSYfH?rA4}WX?+g&-RC2&rmJ}Lt#|Jm|MFV3#FC7*S;hmJ^^X2usWiI2phLaTUb7#G ztzRJXp!tY1MG+je);ubFTt64nbej`_zF416d+FU0vRu{))arzh&8RUO$G8*Ec@z>*Qk;(U|s+C5mXEEy5E+Y&#thrJ+)Xlu~$s6z7A2TYZlYF$D>G;hA|N z6fh`7m$bL#AWjO+Lpe$4duH>7dE!M~gvFe7f1REH=tF_RrjN(@_>hX>^oU)|vJLwP zQ-$RU8Bo#Y^V!-E_V;>@Ev=sVsDYu>`Ad}5v81@dSCXF@A>C8E@|;nPPT5qohg?vZ z-a~Al#|r<^cW-3Qt^j>CE?6iA?b1uM+5ITLyA^clWM8rAwhrV#qbq|t9M-*%9qJO7 zN*{z&HpGuXc~+yGcFV1wcSU%ISUZw%k1UtmIIHY11Zj#*fr?+wUb^R^$U9GyH)|Dj zpZZzD-IBMR{_ao6p|8FiRrr^|tE}hV|2g`6H65#5sW#iD7!2deCTOwJ4GpU55^Jwd zF!EWYJ;oN6XWO1d%d=jmQa1W^oZ zXXw6vJb!sx&u$FsXdVM^;8r1=h{`lU61@n*>6kRj^M|y`VX2X3KO`c3x1a1q5`VC? z)SJKs1~m=|%^30>VHdUMq&4*qf@npe2!!QGgx#NoSoEG_rR8kgIdQfjFT>~B_H@h5 zt|{>^wbj@rc}8m@+$E{Q;g@1gE~fUoe5QZdlYdhy;1mB=K@?xj?^<}=|7PXpZAZ#A zhrwBd==+J!JEDUhcxO;E?1r6ya12GgTwyBrY|dzho$OH$%5DGmN9d|okipNfR}EN4o_^5>msQ*mVCm|Y<|P1mGXUB}Fc zTFTk^AW+GfJ{Wwmukeyy@&j7XOC(6l%D{Px7M_g#fKllOW3jhAm4T=ANSY#8g%^1| zCvLDko@6svg|ZoFWe*#5Q>Lon5ns=uHj*1Rt86%&bv8#*k*jYUdK|EIFPg}`zN0Ul zKPD;=aPMl9_a_YJsRK{*5uN+!O~H%14O4X92pIK)qj1*Xy7_wrp*y#ekIgafWCSkc z^~IWXw!BoWtKIe7U&IM;-m^~Rx8do3HJLR(izr!MW67z4VUfsQLL$dvUWKGqJ0mZn zKP>WnmK7pL>{PmVx(8o>oV$EnkC_lkGE5RqOfH})5)zZqu8xpxObRqZ0)^Ws8A~Wi z@Mq1QWP2(U-mQ{zHs;BiI{j*mXtR1#A^^)3^QB$S{TmX+EhCNdX4FN%$EPy)d{xuc zKrfbuySF6frZSE2e^e`!a0994n~WZNrG}X_6SZr6?8h3(pb@4$(o>fc7R_P(EyP=O z55$8cJj1T-q`CTU)N|1){^a<#x$QM&W_?t+{{EgwNgYYP;%M#$umI4ItIyzXO6LPg zap!#>qF~95KQdQ-?a(28&DQKOc>pR?V9IgS2iVQ7DwHA%MUhVOBi^X*#0)*C?FsW% z2ngA{tq*rXLv0~>SsW>oF4(4Gvn=_o{FRAAhxi9V<6j57_KES%jSZ{M_5C=nT%y8AQ1!Fl>O3~)t&_2w|u5+V?h<4ZX)3kuwTd#rOJ=$bd&1=FN>62D18LsxL#ZPB3}MrL91&Rpco?XUZ;Wz9H7$dj64X_QI6k;fV0 z+=eI=L3~T z2A*HTZxKt<9>Hvjb}Fd!yRc<+eN&0xgK307Tt=LCK`=sp<>;>c&> zubcmM6DL9O)twq274^{u`)gfQ3DHt9oBYC4r0n$bv3M;^eMYH|t~?yNM@y+Zv>p=? zUZYhv8X4R@o7@a=K`aP&$X&qb6As;KmBqP=hRIOFq!$0_l6)ZL){f$#c`iKB+ea{! zKPje37a;x}gIc2C`e-R3f^nA5h-|4-D!NCSQBc>=^8hXd6>jsY8I9doD;_(+{COSgOHqGj;dqw)6{1Wl+H{D_Wpo6E9u2!4$17WCTo=7)*rO0HL z7~N`*L?Z6WL1$Wu^#=4`J?+*J%+c^=2oc}!9s3912Xk6$a0)3mzvMpzfykclFn zi1{jOpy_rhsPzsy?BKD@XA9l;%IsHSlxL~al2(3S@~F`W8szZSI4*b(jG8H>DbJ2} zh^C?fIYoei&3zM_IZ<93$535|cqK4AmZ_m^F!!eeQ)#Xn~_~^Ahca!^FyuJOukYKsfL1 z+)?C^Oq4U>O$dMT@Lpt04L2{FW+VTkhUAZ;AR{XU$$@nDA|n(=w{}~z(Q~275z6ys%ms-rtp+@C-@eqiXcM$+1-+igZ}2po*_%&M?P0N@1{=Qu#BKiW6ft!vv%(-bt@&-nkrkjS&}{uM&Q z0+=59h{UAE1E=)TOyg=(2l!fkIO^IWGLIu|W%@5uTJz2|nzj^+p5Kk4lKDag6p}aF^t;jC4~B9sKrv zjhWZud=+z=J(ffaJWOYPcLbS5sLr#cK{U`bCu*;HPR~(FTzCS?&ag2!RhdYEFSo$fD}Y)s(FLmcqfcA!zZ9a=CquZ;fUV=Eag z{|5b>dD_>(uu%-vOQ0sVAFjrJg{A<977}eal(l1Q zNrgcwA|)L~tQNut+H)+E?u$lKkqkHtLFdX-+V;<%y0-=ATUeF^!oZKCYWf6ZvBAM% zfkuGX388|tj#$e=3cDeszo8eAzWE~3@OW-gxh+MHvk@i96tHL$1#&}HgZ`Gx5U!`b zCqwgK9q}>MryH zU@L?D)w0T9?Nvk$C)$X|?uf1y{Y=M*f>U@kY8ojA__Gg~U^u16p*iL-ei;~yd7ZM8 z|MXSkCCL4u<1Ss#E|>=gST-H>U|h=%6cX7j?x}LS7Y*L6QEp!kS$u#LV#%Ye_kK>f zI!PWJB#_}twRjX2DZZIb}ls>f9&2kCN-UDB32{ z#rlxIgy9TIS{kLc7Q{zqNv0`cXJ&!?L)*D3GlKKpdVrq|Pab|+m2H`OD15+F)vA1_C-3AKizQ%j+M!ytETx{jXx`g+% zHYVwaG*}3|4cscD3;S5WJB!Z|PA3?xzuQrA!}VE(x2FHLI)dG!BJ0f$o$@!?L$H57 zWU$8TQik)8S0=)you-aGGCUpMo*?6>h&)dCmcnF(LL7{hOq1FTOPN2v(u=e`HIF3u zKX-m4J9Uye3|CGw-?wc_aV0-*!~`u@{b-wr5-SyS+H%CNCC4g_#dID15NN-xg;_E z5-3c0zQ5#rt^MNn-+PxIO~-CUNY+UR_Fegbd@vD6&Y*}%?(R;D5e0%&$va#b{oShdt2Ba`hU3%QAE(d@1++O;HaLu=_@8H=D*n!9rnDtI|*OHiJ*E%YA zv_qYIM_|j3_?Yk1Y7{`B^(Qv=f{F#qH$A(e9LnE^Dh*XsWXLMLJ~ZA8t6lOd#L=9g zyhE2k-ZSddo~LYoccEDY8YhUy>nbbPTUz9 znIe>-48v}L|umMhTa`fkrpC=77zA- zlezJKc%r-naQBexy5$UGu%YJ#Tpa0+E7kT!rlyaCKRVZ-Vzq1`Z#VUocm(tYVrJnk zcMMTL1Z`}9*m=*7Jg9-x+Z+Mk{(x`7yzCDz6@GW+zHhUv0s>Q@hVatM$Tb88#CG|? z^Q7xhn3mwgpHU0wE@vnyo{q*Co5f@XYcno|sri>(0UbwoZW!b<7mxJuFW4gv4|u~f#abeH>C!=@!SHThnnHU~ z<%I1IVu5m5#X_5&7P<{c%Wjj8qnqeg@johxej_Y8NZrWOwsOT`sWl^x?Atj>z8OA` z^nBDiJ?1~nMA$j^?wR~JUPY3RR%ZPVI&8g!*Sx0HCr1FbRbj=7JkYyK@M z_Eq&3SqQViE{_z0&#|k{J-7oGg_Qd z(ze4!sG&Z+B?dtRQsV84OX1paP=ae{2 zGo7<-g~(Vy4>1wr=4yEElVHNe+uoqF{VB1K2BztfgMI1_E#HXC7%ktG@*}~KTeLn* zBkG|+=Tgy!Hzs*TpEGar2P}s1UJ`VRgxKGi?ogy4W>1+Q5vM0lYJHEZBvGh??GM(m z!vuxiSoLSAm2#vZm}BrOj91C^QvAd^*T>%YOcd6tTx9 zt{hX31P!6LrVD!&<|dVf*ZhogtnESyBlyC8h0?Cu7-Qbg1m61VDf)=cn{q-@j>pW5 z06$HZ!58K~%qOnOGjutuW5fGHhn6@v7|Sj)#iX8dv$lu*;UU`pZhWTb(5-ycA~d^* zF^K|b#O;ued-^@^%zg35ZcwDtV8653^Oe?19jbm_RIpt}B;Gyt7qWqb^G7%&+C{}+ zv*%%UZWCN4jn94gZ2phTN`UE@f}`gUFi9#b z`jl)KC(AuWj7)$pJ4i33iZEF(gAD+U?LeEO&5B8?k>9C|N&6*BiQ{`*ps^(r<|zCc zn;qDoc*=eH?z67xZhKK*z|E?BEfI6+!t&RO5CrL ze7-v=A=m%8M2%QDQw!!4?MCxVlC^gYt?TCBbtZ}gs)Z+uPM|^y_3LH7e(xDU!dpP; zM|dXpKB=1mVx0eYd?Ad4jKxkb`M4A23k3^5F4!xNqSkHg#~r9!A51osD~1&Pf;}V$lf5r*}VdCe*LCarRM(FIH%^td8# zW=kh5Q9vH@#p=K2`sOTR;1|RCN#oIe>Q2l&7jDait3lE3Pjyj;ZPm~qkfr+z` zA#$GJV(>485IRR6A&%JE28<@%?`9n=@K`CzDc&>*aw23M%vO2X;?P9cT{TQ}_0+s& zKM-^(zg0=@Kc?hq_wq1h2h|JTMkdck48agz9ar*?;ft^Cj?e-Tb76#3ia)I&^4vF* z7rE?01lj45e)TdQ?Hc&iuf&l;Sf{!Cu7~O0?ZPaPClU+AMQWIX4d<)U->KJ=wtgaU zxqIQ9(<$-V-hRhP@hMuqc)Z0rxbuRQX}6Y5Yk0x^4~qH$ zbVk@q!hi_5SLgCig=whgxqNfE0J*4D2Ufv)*_?(9;&L|%g^>=w)?Y%zYDfRb?+U_| zj)vL*G5!uMO8=VK3I0zH!q`eFOkn@#&W)(J<48AH)0HaVm3Yx)ezW8tg5Y6%+j115 zK@-hwXS{#5r$Im1#5|} z<-?y8vFvXLxybKjdQhE7ELLTMII0|}Ae~iTF;QiYWkUHgh>l@ACBsk)M|~v>UG4ru zFhaN&gQOsmZ_oDHd(nLf=4d4Fl=f7x*$bRPi-@x&+pXY)_Pi|_ zJsZC#cK?Gx=z&x}I*9b4uIB%5Xi+%n_UQ}$9LqI;*VN>x5OqHZLvpsVUlQ;wPlLm{ zy5dLjqI!`@kR^)XC~PSYI&}Qq@&~Fwm=H!x#JZdjYTo76u5fqm=_^IQ2;`W zOg^}}58@9llJ3W~o{IcbY0OG?W=taLokVw6DwM6PF~k8Q$cT;1iOe!4NN#Z(5$w$iMhecEx)exScB_HiAO&{F{O6Oor$Up zqPy!Tb`BBJap!jUIx^?c@@dLy3cghsZvM2hJ!OP@b~*Tklw>Km592g9%BVSBnv6|& zKWX+zOiz>k&b&ziNuu#-2tm}Frc7%6ug3yZ-sxoi=e+qhf8b?6h9+V$7grN2mfhp5 zhlE|-yZaIf1PgJr7-z-2X(sB1DSRQ#bq&8um7HQk*7TFdvSWRWH=}M3`Haq7G=eAY zu0Op`)Y2w^hu1)f!5H))D4&53+{!TY50TuaF3f%cs6P}LPcIKM7jA`QbQ=d18D#GX zcI224If5TfP+_+5g5wVczI5hIy$pqkY9UGb`WdkPOW&G(p0^hRUs-~f7z=--GBf;5#9!d5CQOv}G8oFfog+b*wSFnkA;Kr8InJedb|2*Q z)UVGQi9F70Vj}GhLw0dSmR=PPJ)qe&=LUfT-|&u=V2d{3P4e&T!RR3{Q< z4FWo_UuOcmifu@Q{UrEFXFepp40ATgHVW2orPU(VZR1uCct$nZ$$6T=(-F6A=r)ua z@Vj*DKX8NW&)Dx2QVw#Reh9h{0xZll!Xlw5;$0W$9e!Kz6vRXC zXv_3lJvc0dgX*xrWGrttB#hRm>p&LeDXtgA6{B?8Yaa`c>R>6bH;9ats#P?kYYqw?K6_j(zMpUa<@ia0s(_KqsCG(13!fV@oysXzJondysz(}5mt>bn`;9L?fks2p0%|B@mBNM57~9Z zf?RT5W?lc*AHmZu`ho)(qmPPVOsO+3_XD#rQh7yGEm(u7EiNbMv8d2J8K`GVQKT8u z(j@)Nk1wYEO|s)2cC3O2g4#;=v3O3F8_8nkJgNA_==nKh2OE&F!6x~HnlnswDS*gA zQcK!}NNk4Snya%_x;C4Ih}XzFOkH*?k5 zGWSF6)hs}%Kkhxmb*LJILq%qOdA@TS+onZFlvGtwm#`a}(S}T7)AkVyz4Q6eSHpEb z6>wTM!vmD>jU`4%#*Q}LbXV`Q3#mwZzO*3ceFY-YH%dl|l>_+(`ivo*;pTc9C6*ML z66&x~{TwpB^xD{zMhh;--7)cSE>Mw45B$}bgHmmrR;H~PW6?Z8n0ApBv@{B>^V126 z*|F-e=(|FQ85z4Kyqkkri7iOOQ2{ziWQUe1CP2)E;|M)4WG4?lXX=P%+Vz<(_I>RW zncvN@yU|(n=e&Qldz3LNUxplj7&w$x?j{UWf9w!xf)gcPEB4`LPbKba^%is>3pMFY zu(65wXuE z-Qhy(XIDqVe;oC%*SE`MO7;)yLT582c(I)Dh2r3}{E!)%cJm#?0^;@q<}O>S;lIBo z)tkpnzF@|Zj`AnERcAc~s)o6}d1`dw#}Q;b94gMsxb0yJ-1@bbVTK*SV_EIH$GX^N z;m}$4X#bZXHYtI>q=^w0UCnx}i1Cvl-F))_{x8&=7q?+ljGbK0%W~Gh9v^%13>3<= z^aHzM2~P&tMDvK7>wkJn6JaZI;gj`x7z)Pa;R-73y_=QN!lHoTZ_dz8uC~(^{)X%XcbV~dX zHTbi$LJ;~ik67gtn312vw0ipkf=)G`|HIyJy$sfUDkNUXwGa}4-54*sKm7!2eD$?q zO?8mmqh2f^wC&+0B}4?a9S4F-r9Saf7RPEI(}uQEOC}pK^zLvee2oV@pSt#V>Z>8L z*8!Yu$S#R(3oVe6*ta~k7t|@Pi~a5@*b~iKM_be~sUD|W#`XSuBh>-_#hpa3wqfng z_!bb&b&VI@nE}#J&u=BppCL%Zo_930e#*3AwT-7y3uf6Og zbj9Vxj8j`~JL&ih!JgkbR+T)^#MCs}6Zex&0WJzCt4tI=&q|@jAhsL6D+|klj9j9>?$BoED%C^zktZs zajE5eQ1kAcH5cuvMPkT#oFq~o%L`Cy09p5q(c5ywinn(6HKvu^&Tt2siAB#oj^4~A zpm;a!biQA#+-vnEUDXQFp4c!uhS^~vmo_kuOX*RFS@0GD=|~|GD_tFeiHa+6bn@_S z*k1myO@AI(*JZ$cP`FmytM?q%mljA|5;>CjkMm;SXH|WXfw}Cyx`|e*UPMI_2 zZU8dtc`36^eTY`F@k4@>?b-0t|P1{s&apI zVj-UpUc2X}Nrp4XL!eFwI|=mqokd1H5;<_ByFcYIS=nF>z_oKtX3-CcwqKckR^i&!$fkb*IIX6>oN5Vp)_kWpvxR#rZ=AKFNpgw@=xtlbx)Ts&{g=IN?o;gD>O@8D}VKf??3_^SO>^8Xo3oqpc730hG8h?Cc@1-F)wQ;R}u0 z&X;Q*g&QPNSs%+eO(x1sXQ{fcW-m5@k#+WwvZugzX2oaR>R@?n)jQJ$nYPA)fJK9n^_+P(O6$t-Az^ zE@m5we05j?S|YieO$-4$cNiit9PoyD%F4>t)A9FG2KK1`lvtq-`O@AqQD-YSY%P!S zM;^()$`a^B^#+?I#YGUneZJlQV2{NFy_`jC2+uX;ctVW>8bf*C2g&ch$uzXvKPc*w zCA%!R&Y=)yiId~{_ElD;vByOA`H0EAJ3K?6Q%C6gJEly^@A0PmD``9L5ivt%1EvdP!AXUh?J&=)HT&^7vS3Hfz0H<=Vd^w-ZPvF?KxQ#5!bCO>Q!}egT zU>_h+J|wQi0YrJ!v$=HAjQJ%8N5;G*$G@Cq3p!btIXK@)#{)m0TPm0RHFTO=oQdLG zKt?8|$-6a(PWd7tLr$TtZ&G}b25^Be9964%b{_&FhHg)Pfyui4?acUqJJR%ab;fA` zxEO~5hIS6|13e(e$ysPYzesO_yqE=XDUeVk6*kM3>X&3ky3~(DsHmBk^w7FjTazRa z&4Rj0a&x8bcLW`l_GW;y`7>r#>C;2DNonF;Ci;>)>}Wn>+zcSBE?$+v8&1Mv%&-*r zNAytuh(7KmT7|x^(hu&cK^xwq)P)*E&O>&Ya)*Wb$W^~fv~;Y=LTm=fSmE{g;M$iE zplj}VIAt+fjRSVgqx=t8RtOuTpv|>)p~zKd3>ZQeHRn_=0q(!llP^9!8gkuj+MU~R zS6AR-ZTqMR6(z!1!9W{+?Uizdwh-kIQ6)>zt@hN2^X)TAp%nT`~D%WlE4@i)q##cJ*pd0@}@q+p@p zPYLg*AML49J&;3eBPz;jnAJ(QJo}{yibx_+Krski2L~xZY@xE;qL!ZWF{Rq7$ zzGB`r#KyQue|UpcLimk!x8xd;8}zf< zBS*bY^EypLmH2>o)Ypk7t)1uF;;3uJsGvZqKNuAOLSMF#ZI zVG7V;u?i}2KT@KORA8g*0sU%SQ+)C#hA_dqa_QXU|Ct5Q5Zw^cnk&oM*v)k*`snDN z=j`RO>9A%N@AVdZF1fL3FOJkU@PDZK>!>Q<=X)Hcq*D;2OG(K?cSuM}iFB7V2aq~6 zNP~1Yhwko1>5@)~LnGbryY=<{eAn~*=|WwsYp#3Go_+0^$q^QC{)RKZivP)~KQXMz z(wq*60?frWtD>g{Z20zDRC(IMKBd5Tq~|y@O~@*?^m;N+38^BPxnhsgcZ7@EvF-3$ zWYsKIV(V+b_HDV{Q0w~LVc*zTM*~4dd;0y|ebV6K-1AzN-8r}^jc*+(aBTj^KBltL z@t2TgU%3GbX6&i@bOb?V=YGskM3iSE9JANwWbKe>KvEc+xDqw+?CWr&BHr41LC?H* zUNyq@4h_}a_&4NoNAGSuWN5tDU1WC6^Gj|y+!j_@5>4b;tVFUqf5Aaj%Nb2*ubWJ)1L z`ck4_&sI`75W}_fAs@{ZVoNS>xf~cAol?a5DcL6}iegz$Cn@nD@b9FD9;C7-IwAy_ z$M);X+8w|T&5cs-NYsv;yL{|mY)pfb%&HQ_OD5{Di);I|U>q#@3o%NB19Qi<) z4Ec3k|2ukHZmD&4i^x}J5k7%lMPVtRS1YC?y+5bN4Ym1he`zfKOU*KGCY>irM#KSvsY+gF#PQYMxTPEJA`Yb$KFWBO{? zNjjU8u5JziV8FD_=7R(0E@e4Jr_OuZd3ekecP)`p46of>R>MSE!r9Hg#*vUt=qQkX zoB1_fFB7~<|CGURBJj8C=je*2ePjI#hCbcMx8Ux*$hyiJ6W!}ULMPd>!^1E8P3H!D zE)JRDF@(Lz4D1oTT9!02ga#h?^fT{$8;ViQUgBRgk8O9sKca2a8UDa`Z(TV29Id*x zI%^|Yj=Andv3v(N20J~k?5H5%89nmQzwdb~x9bIX=cKF?j8>Fm-c7FAB^dF0;M!qe zJG{QyAVolQ-}*C|#}tU(5Q}WJ@^0BF=#J;^t;72>w*Z~?wyR%Ae!aJ4>WxQd_nAP4 zCgKsp;_KEU@P!7tP<)JoDB#14oRA3?H?zA{W)@yrX?8YOtE3QC*XBhC;a@-)l1AO$ zHbWhs30$Jlvc==WrMnDucmBnlQ+7)oSNHef3U9@&f)$NS=~qDKLls*W+Hvr5oZ8*Q z#OmyOXkH4h<>X%>eD|L~w8ywV??Ly13|8lJdB|YrNmArZ$-1EYsbbAVcn;hVLX*P> zvYcwdizJ_keHK_S^&Or&nUImkp8ufu+dq!Wq~!Yz7V|mqnx~pZn26qAa%5pOH_3ce z-%~9A29+$b`RREiN4DF3P? z6unTYLgSyI=e)gi1iMbgRweGfUGVkaN>W2xv+n0}T$tKHK7lP{-|(KVernPYp?nW} zj4voeBO7zOdOW$B7+sbSl`2(DrEXuELegxp`nBwll#_Z!X>gdWs)^yGZBNJ1-e*du z#U4ZsbDwNtLBh62kdcruk+YA@8E8YtI94?hhjg*bm>79jOmorwddsU}($MHsF)fMm z*#KI4^mc#3!dJ?$&0RQ+Q#nK?xa8<;VDa)#x@n!A%uhzeVzm2t_LN+zXYB2@?M}g0 zeZr!bK<6A~{2Q#llY87T_lf?>X#FcazM%8~0ZasBiOvUeqkncfAH;}8!W?kI5juT{-&b3Qb#&UMAZg2#F(y*gZpBOQB@c*_-GCa4boY^HcR zKEd`wz#ULx8@Qm6{lG9I9?ns(vz@O?!KUOd;T2Aznqo1e|K#LUBe%)@MKnHcs7(yG zJ3q$Nk@&vuve#Qqre~oYxMLLJ8YaG7ockYu&;tOX3JY$iwQ@)OzDch%E`I;-@vQ(x~Rgb2XHU0eVG^2d;?BaN4VZzaES@*V*5Dui#Fwq^SpU%eTE zX4VmW1}uAV;~jt6?9FBSGs;qH;(n?56RV3?tX17TjDa^^zP-3rqf^Y!QGoN`Xg(Tw zL<)@jxy_EB1vxEHDojjG_l4YSGo$`8zhuPzb8UJ)ehkX%W%*QOp9efWm=UZW*Zr?9 z*D(p?`Z8;qy-BasM27>@6Gx7_r}vF9t*RM z)B231sezC#z>od}cpK2|Bw}c0-ufZ--`#&2SfG~0&TN*J=Q9mX>UMtsMqM<7;-M&EnZAC}zDQ$*mb^3{A^SRyfC{h?*;?+2x?TsrC{^6XyJ(gPc?Ilr;DtTGpGD`a~c8$K#5#^mD}p#dKeDxlX={J|vVmrK^wJD#=FV1B!Ape!JuHn_Q~#H81i^ z5{@%ppuR_(y9s%*$;iDx!)>?d=Erkx!2Ts^GQLKbK;1z<-2B4~Uk8IY4TRB_nYA`rRn-I`FhPjB|T5og9h@~;Rphd`FkC$ zFE7IW1gUg3kl$R}NgfDpjfh;&Dgs+cU&jfQ+1H#mIqLvI!+X;uMEGO9cv1n9|C-NL zTolSKC=R?NZd10cn5SNymIZ^L%2S>v?a-R}nn=Ry4QZo?$X{xPx@wpeTnl z6@Nog?Q;u+STjv@uQb9B4LkwN~8392|FeXKQjb!H|DfVQZq!3?a4tawV83|sW7$0;JItx@u z5?L0(u!;LBu=5+yp)fAQp;uNTbySVMTffaW5}9Wj@g7NHi0ix?EjdC4tA1p9Wa$I` z)_}3`@tQ(FSOg%e1L`8R{#w8kwX{_WRF~nMV0gXs#^G?IW3k~#P}e@C*fazQ=y$SG zqn}>)L;Q7nqbWxHgP##DaG$8@@%?>eITJb=R1@hkvq#6?l+r2Wd7d zpfvrjK>ET4ZB01=d=6G4T~XFV=D&Pu4t=4uZaec7Ty|Ab?~oXOn_z=tT|jFiWBP*; z-VAEH5YqkGniQ!Hw_vVnWhMi(zj2u^NTA6`?*NB)=x-lyg*bIRFedhp61;kPJU`;8 zrh4rNFZK1{r_oHYB1j|kOC$Pge7DO`U>cSaUwAhe;lO_MDlo3f=LTZ$>m*s#68~vV z|9x0jHP9=79biGBU6fQR25 zr4Lpet~2#78H}x+14dDxe?Rl3DBoMyd(;rQfk_c`RkI#V?QJhCcW$n|SRkV@8{A+9 zpGmxcpX(#vtK}OFDeF-k826vX<=HBy4_-1dk-$qyhovpmi?1hxa_A%X?b~bn3$FJN zu}#3kw9A{i?3WnF50d+EXVSJ4U=Q~Uhk zJn7kaUMJ~S7j+P>A`8-b&$-FrI_rCWi|@$`ga{$Leu!hYuH?z-gD0!~&d^;C*Yy+p zBl$^Q&zs~A@m=^fr}yq_QHmaJ8El@{qza>;ve5EDoFg-{Z*AJl)7cv)37q{I%R1mQ z%OC;u@>kgdtTVnX*T>4`i*>b?z8n{xZ~JHXI2a8_$lg<|L!KeqJcA<5UQ+*qK))~? zFDmg~1y&?%e5}vWw*QcAa&TnqR!9;jhp^5M>BUsf?Ku$wvf0Lb;3gL0OiM330+R7p zgPo?1MeTZH_e~;$iL|;b3rZuZpIm`R$vubz9)!6fnsG?jaa#TdD}cPiJXeNBRca`9 z#gUjZ3#wSZdHDS$3Tlh1tq_E_g-Cxvr%Bdj*le}qOM4|qK9ZA8Y_31q_D|l40OXxP zOxoYR;kM)3K)aC`=z3e0cCq^66;HP@XVLvb)k!0*{THdO%@s??f* zQs4d!6|#zRe)oF*33$KLVUaYDWP>WFNdzFXV{|Nw{f7HIn$EEll@a1>F`hd!qn^MN zospGXUpA_o0@i*O#erNgi92}UySD`yvbKv`V5?H_b=?J{(B!M*M z7F^dYVhhxQ0_2IYG!E;ZJ>$@X@A#+ZPz!Lgr~7+2^%sHJjyIl*4+1nUcDslyhPg!$ zWsC*l4xpKr?z0Bw1WozgyZ)<>nU}V}qOozSLgN6D;O=Z?(dX~SHGS<#FG>T5U&6s! zW3ktTyo`^@(!_j42-R2Wf~-X|yqUe^HLJpqdVX0+E^)xB7iNm{xS1*L=c>-895oF& zS+w*!9=dPA<bK|mqETlcl^0sS$$m3nA(-gF#lQx!MtgT(Fn9X zEEjgxPhO;7rY`$B8&W|Yu6tH0&@na9X#kG)CPzr5nHTG1TA#jPTSsZS1OM|CkER%f zd#ohCWsmF?i{^utI>#_S0DLf<<6nuHsn)TgRamg+p8CP~IMO9{zWS`J$*h(Jd%L6c zXHh$W4Fe(1Hz*RM~L@H4f#Mu@QHVws2Edw4Ufy(E=z z{P6oP1*4G!zU90=-Cxd`Nzc-IJ;0sgVmR7F3i5HeCf-%C8OzxfVJ!Zc2 z_Ga^f?z6neJ{wXLNvHRz6Yy@-H=GggaU=Qn#i*rFCDr$^q&P4q`c=&k)>dU2k7D(9 z>m`R*+owvs!2oWH3sz675A61fXlSKg2Am_k zT>DhZ1mU{}T>G~&|L58VCbp>dJPg8Kv~}+K!u|-lCz)_0p2Fd+o)C`eIHY!oTf^Y0 z@bkc~cWna!jmg~kw$I&?L#KI-ms9CdQQpbqPsD-*;@=%Wq9-!a!-+$C`QwA0!EU&H zaI*7hMyl)C5`rO3JBt$?)T1k{w-1QSig=A&9}r$s>;8<*8w8&Td|aO(3aJxf=c*py z(6A`d0v5;HIA6FspjMjXh9zyzM#ss)%mhmRnzAg$G#_HloIAlHS|Vdn`h19r7zMb7 zg^~YzE#5f!0I)wUXHsq_q)19Z!W9~`UA2{#40(6=W+bW9hzLwM7MxmtBo(2Xvow3* zUPG;BTr5(7Kssj~Ba#_6F$WCu?tNc&TxHSA{G%or5eEc)K(rPU>28EWM~@sLzvt^P zTXJ1&>@;qO$l~c)VC@S1_C%Y0exqboH-dVj7z4Mx z)#CL107hKo-b?@D_CwrNc)gfX7B!OR^1-)ju=ZI=AA~j63WB#};e(@?m5@+l{YCvK$^Z~HY`Z_s<9}1;` za^OlRQ`rL$sW5acek+Kq#bMIT)Y-F)3)TdHH_6NeIIJ9u_K?36m~kKw$pOE`Yk>oHYDPK=|{Dj2S$rp^v)in6GHy8j>OiEB}RtC~5MZ>kR(TfRjFLs~O zdc&Y_{W36otLx=Uy%8buSJ6lW;@Ox8%v+9|c_^;Gw41f-tt!=-9J)dD*waVpr1h>x zsckCP`ekjC4e37yP3BOs)U;GMK1&%0^1n?$#_m0F($rw_6hpbTfk8Sv8MwToaQV1i z)?oSpQojV;n~+V+e=mNGNJIFq`M52(QwK8+lJT0u%^sr3MK^@<8#dnbW4=)v`^92J zSL&6_M8>VS?(0mr%~`pp$$Hp(NXI9%jNBfJgM3M4Pd>!%&sha{f&hK}7n3dj z_@LM3w(!Yf0cdzpxe>*6W#9T3k#QZjRL~IFj=^&;2!?3>WK@{rOvr!c>xA=Wr(Unq zF%Eb|E?+IHySRspB=cyDnu5*O>9}!1UZ*qAh4#8rG%S3QzQ-oZEZ zpSbnw<4DY~v3rk|AFvWjDYC73_17R5ZZ{8G~wRBTwrAGVu%<6PVmR3?Xz>+!-)>zkfs!k zV*73N6A5bC)7I{avgm}=HxBD(v6lbJQy|@)2tC-%1a}Ns74GDKpa-Q-aGEqsQmHFVgN=ZN06afzF@-dHOZC z{Wfeg=q~_uNFn+mqVkjX=ok6*Re{eLTRie=Oa|Ps(_4J5r{>`-9;ULCC;j4tZW+G? zIoZ*^>#O^?f`AIIzi=d&A~o!TO;&m)sVmlm8pG$|X1AA6-~PN6L{@;L+LaFn1g`t5>Cbij z9nOyy^D=WhJUDi!9FviVD@QmhS9jW?mNp#00{GYXWZG}Jn?uP7WuP2Mf9?)N-4h^- z9*Kjp%wA&VB%%lPQtM)$2~NY-5T}ifH3p7CSUF-DYo|6Uj&j5}eNU_JZW3O;7OXh) zezv)#*6Hr$dvAV|M%w)1BIM*MHN?bD50&f~DXK5ul!Vhby(KFs=oWmqo+)=d>qQ~x zvL}8d=%!``*e0d`yFTcs3y>L2E`}g$+RVQ@(h`6G7$*%;j za?r6 zd_Jn~8@Cdl=CYiKQPr{{?jxBw*GD?T^bj8uUl;n?r?t8yR&1Lt0!coz`xIE`^oAbf zH5yb)&m6SjSO}PE@$nX}#Ik>HVhx70e*U|{M?LB#bNiq`*8eT>5wdjq-x42nLY;;q z!+Kxi>)~O2aVmdcoW02$8k)HZ_6B^mosi%}`iD^P&5bc3Z+^*jwaG*T@9_xA5>=3x zD=Eh)Ue+8}kq4QeqyN?6iagL2$MP#@0JB%r`_D(Q5$7K=wE2jKGby!oPQR={X}&Jx zNu+{!Ata}vE0`GseRsk6)oMw`jdsdg@S#1NW>-<3 z*tbhE@$wBJsV))3wLpn4k{G`B8@1e2pDq2`1ekbY9c=0W8qsQS;vd@L#IzK|Lg)YX zCCU%Rb=)P0Rr5#%Z5{<^-n-Z>T_FdtW? zPy}*}JV{xS{HP15>AbyUJ8GcVFm5CG7=zQ3DJDW5Jn@-Mdc!zCXav?xY>Yl=vH&!; zM|?s+A^R2W&kcg>sL5q91jOiOtJrcdl~V>&dH_Sp%-81g$&r*_yeu^Trt^VQ!T zf>SxZ*^L{QxLSCaf#2da47q-Mwm4DB+mt=IUwy8|6%<2QhI3fs*9``3y?bNPSXOPr z*zj&R%--uAbC|)_*4;puafT-#BlPjl@$!$oX&``6Q2rdOe%t@4+Vp2fqD+)DD3zMO z*Lw}E0*=@BjjONL*+bT`wYtV9w@V%5zrE2#RC<)%?@ddy{WauLW%SgSYPfOtADu<@v zW9M;k3z(?~5k*vCNvsoPrP?F}3-LaHjFy(x^{&Qk&2tTuVYG-ns&sv6sfQ)TH_c*d zU3!1H=~$X#XA^{`C!kiqcXVNKQz5A5G=wA+a9llmziiV5OxB9q;;sng50ietkJ4%XNKS{JyqG=`lCFgHZx{g`o{5c=HI+=`?Th z5%IBlWxlbi#=u2 zZ+C~R;*;@ID^R93c^dkMwUy`DMiH(J*OMZQCy^dq$#7!fjcp6fABo)Z2mxdyoO}-Z z4e8BJ<7@3g_2$)HvkELZ@&e9Sj)#lObmKO537R1EP|wL{9LhARo(*R`r$6fZS@ONQ z;nS6Z436i=Cb!d+Z=!cMW=1deGiSFRH+i=*dgs2U2D(fHXYu%j9UU02A?{s;SlFdR z2bH#@YLSe5!y31@YQ#BT$#dj;b@7zd?4f*>)wZ1yHL!33vDKNOYHg_dS(p%>Kc?_$ z(*PWpyh(JTeV^SGB-oWhtNXBmlC)()7g%F^^u5oLY!UjDsfqaZBrTSa^F{ar47aYF zR(qE~q{Zu+9=3h4y%jCGlC-z^`}yuSbLja)@AK2gAx6Cm=c~`a00r02&c^WsM+QXR zv*2MbGID{XS2ym4%qc$L)OcaK{PVI~EgW>G3+#v?BM|3LaZrL{$@q!^w-I#o6z|Kb z$k(@4i=;5EkM3`Qr7G)|pX>UR{Bo{OyW0hwj)rdYb@rb+95xflIXn5UKqtj{ltX`F zcx7kpddTIMKK&?ipLO5qqsEV6rwyspW@8B_OgmvSQG4En9~ixu6YvO88jQ7?{4
uG+Qak!8-$fY%&jFuyw~PFT1+RulsMe;ww&%z-w5&KuGM$QT_f})V{l|Q zxR@|Qe|zkRwAgh?WHGTa@x?%6MThv1!rE9a?cARG@=?nk!RT(O=SAcgDY?_Vxy<6` zLUf*DwS|$}DEDg94*4x)A&k2w-@Ex}vj!~ycU^Y)=NKdWqdi;0E}NTw(meLbQ!#t+ z(;R>Oc)cC^CJUMaQm?823PS&dA@4WkkWGW8s{XO?@=gX1O9)<}q?yGzT=z;PvLs>3 zM$)C|=3unvC|jKUiznmh6?2~1r6uiA25J`9*XG_o9G{;8=5c2~Q|S8S6wX0A+kRz- zi{MUmfOq$e4Z^m?%_h8=X2;LXj*eWvU1{U*JBZMGdbCK%^nW|crL}p-lNBzY;F6S5 z!>IP#b9Dcg>pp>^_!ozR)pk!tC<)_xBJyJoKlkjC9D>Kivbk&n=w-J-4%=P*Sr+7p z_vJKoATf3JMCDI!NdK8jwH6}Tv8M*Zfq-#}lG!Jd)%x>3%-991LX^opW^~6%Mm4Yp zD}BuM?xYEbd?ldG=W*s_GIewY{?4$o(?`rM4UZW7K1@O{F?GF1lm{MJPc@SBCHR=+ z>ui;?q+~!uWMtY2<;l_?X_b`)rs@UZFN$%ot| zj+?-yKHdLDzP0GAo96lD>mLkf?mUS*A%R~rxhE7XGk6p)6|}ozAi;ba9WA>$>{<<3O?RgE!G{ z*SHi`v;L`D9^V{4LM**yZngQ{cAQrAlxSNlf38%jF`?S0BKtn2fIPMNe4Ua~rmf-5 zj=|M085KLCkaI(EAG`ckLJLaxsCBr(uwMrSzmQnL%~ljGYe#a}B29>70xMz2gy5Ml zT9h?Vxv{dCEy@lh{d!KG&2pP-zXD6mLR%~if7SNPoJw!`K{1qXDwc{z=(3L7z1M2f z=hq~o^vL)!)()0a%Ps>A;$!=hwW?j_t8!4D**nY77r1du$q0}9s*;4Z!=Dybcq{ZT zlKJL{W_Z*-4;DBxA~JKkT<$D4B(F;!Vp%RGWrnlbJrClvZvS3b4)AU0#e5zqElDJA zy_$y$Rf0M%M=*V7P0n5U#mn_Gk$ik1lk!q1MjqB?A2u=h5gbpT$8{s=+pGFV+1#&~ zjs0{y#FwsGVD+&g8yP+d4itv2k*!5}NLa!P(`Z8IKg;HnO&&j|dNqWb+~M27e7W83 zX(MAKgPQ#G(V{ZM__jsX*W^mXoKMvewF$a^GiKmD9?_L}tEbZMxUKsF0X06%H)`s< znpeLzsWk8O;W7G&`YcJwm-ehN21AUB-f!)0+oHhM=n#I57viuSF%a_*!wHR|onX-L zrA2^JG^=^Js+D@}Cq!PgDW~AscU&u-6=8mCWk4WXrSf%t=1IZhPDeR_DN)Axb!_pP z^1=Hvc~?C@$Tt*6c{v`>e1xX;mE!-AB}jgSrM4^ixH=epm?pb4YKd7-SCt(e<&}eT z5m2aZWutA_ODcd6!b&o;>+Z02F>TU|rFcF{DyO|9oabb@yYZvjO)3E8SZZ9am#E&{ z6hrS~)ES3z8RRWoH#qxeF!!zP2k}^E`e@nC;rB9>#oReBEv$HGeYld(XSP3HdxP$F zXe~ZXeYp;3-68o7-JTMevtPAaD=UiRddTf26t2)8_rmwm>afn>!`}0!`hYT;BNn}l zq~#GYnScQWU8!p5(F$fReL2_zb6G@r%20S-^?$O7k&#j4xAqrB;^X|TN=+x`8xS2h z^>V%2&T7TJf~Q`rTBr!KMM7jeK%GI-&;#L)b!OcA3KY7hHHAeg(oQ?7cko{Tk41v{ zoVZ>jvgy!e-CnRQ#&xbNFg|a#`jC%eY?R@N!TG_#XK3&7LLReZO-ura=^(mKO9o!@ z9`|<$*3zZl;AHBKi7dj!I*>H$N0Y{VoI;L{TZpB$$Dao7Sz%t}P~3a!E)GFADH)6V zHJ?lO6nkknVvriG_tCT9o9O#$^mcx}X9dq>fa0=6FNw1Y0@72SJkHT%!kX>0{oh)~1yu=ASHt+?`b*@Xp z$#O>W&U;C!q_NC8TA!AKy&pINEdls;sOUJB-dM7h^AbcEQCqph)IF4%ld%#!b}L3Q zaT6XMPcn+H->k^~ESp-NhRirs-{l#NN|Kca{`&>vih!d%X zNUPmCp-i`Vd2G7b{IcZzSCf&EU$;k{4^g{bm;H4F!*fyyWc?Q(f7XvAc8&Y|R$QpE z%Mf11dAd5u-(0L;5a1dVhl58lS2u*!j;G?yxGtG^fp5N%Jvg{R$&!o1X}dVoYA(XX zHZUHB1{(n4Sj`C6@pwht45n>G2@diJ>b8*erJaAfR^XYb&nkM~qrG(GIao2=DpLWw z$90|pz;g>uralE-L-M8BP822<)|<%G^<@TnfX-RG(`gbYxSTg?!2!M)2 z(&;G&$f=ijl2Oz@F(_QetUQJDeNr|f?_yGAqFJdiWr)DbAY?S~Vq&(G%$Qc~24m~N z(+X>|>uowTH5iX8K093Lh0|3Z9=<>0x8~M|ChkTrUdb zWeMVSyN|<36j)av&2p~BGPG=|Zq#n=sLcw2DGBIR&FPgxhDRVH_MO)=MG%^2sUaQO z-g>9<4u>fK?nVelH3!r0@CBq>XhN6LCwtG%du!hD*6?|xU}a@6b@cMT?~X^*noqMD zm5ZBaXncO}#n2%E6D7HT?z^M`7b;(|Pz2s))&!n`bkv8;%V(dNs2?z zBKA2CU~AY8Tx>Ha2RgRCn|^3GHvu?fdJYI=qzZf{;$8&dYJ(#28{P2GFmJsMr}DaT zE!(+DgJ!DhX#x5_VGc>qE#m)SA1a=i0eXf0MS$f?Vqnk`EA^?p`?$AUJ#8^Ex(vgVP{5bh*X!FE|xh|E04Z z`s?8aH*H)qgFD+ZvjT@#d$q8wm1{_hf%-g+XiCsN_@x9aS z1Ea;K^-@n$^k>Hv@z}{pJbtr6iVwVR!|vw+dLWnj)bEXOX>J^o1PXQ8O;^ zH^|ntS{+MwF9-#KhHgQF`JLB}>*bShG6`t9LbeEGEftr5a`#=Gm*a9^D-i3eLh78l zykuO;tf{AsHolLn$z{uEO;h~}PdoTHKg0N~hNyOhI4)M;?kShnZxOrU+yd8lymGg> zh&IRGh^|<>&#gw`?{ufEOVb72--GWG)x!l39j7lm=|_K1hjnQ<#S)kO@&ZobWU(%$nRDaRdqc=$hH35;XAAU}9oEO8Xs^T5JHO?lL02 zp|6F5j~bDlVW5MjI3%22+0rhg{dX@h{{szpBu=#dpgiT6>mEqGe~*i5AGmS35EOo& ze^+sV;NQW_w;g@9veY(2BpbwTw}+mhw$R`CLMFever|`v0oSqFMbA#Z^LMefh8~Mx zW_V-~$HW@5z|n^mvs2Y0``Yp;v%;k&v%(dJ#L*=;xjcS0J*8AUkaTcJy55J%<5(g8 zsMe7P&MAtl+3m&cbEjv!yY;!N3^qAM+jcZRg|J|(smR$4G|zMaYk!vL?_+FwexUkr z3VA3qT5Ck4Fb8~Uo>8fVhBEZm=T7%DTjw<-K2^oi&110mu<^p58CRYBZY(SZK6w|# z@(gU5#`}oz+Z%WMb%(&Oqf}zPw1~K^;R?Petx~R%&FSec{#JLma9J>Mrg&;!F*?70 z07^TZVJ?b*O63cOfcjp~7fxp8gbEf7{DF!+4LGsyh-6D{$AsQp`sRfa{9aT%oDvhl zlW>D~qf2Ju!!)Gpe4b}jm=~8GB`uV{*E**e=PJ^#53lXZ*6I{fM|UY(ZlKFw0iKm# zYt(5=&YKQYutn`p`JB+GWZ zgF!$l9Lv+|CQ~8VDyjB!ru&l_$VoPVIK_A*Pzo%U+H9~-nl@6$KE8nO{!pdEnkgK6 zn!l_zeSmC2? z$l9OP9irskImpX;dAanHDGL#!Ig~pYspp!1hfBq-2f*o1_^5Kh%%sITWI=?%(BAv) zpK%P)xynDcJc6 znM?OASYf;B3?bLgeLf81u7!9!;P}{q+tG%|DD7Cgc+7I>rORH2n@okiz?Zpzr%9FJ z3gK#CBT}A4x_}3Jk@jK@aGm4sg6VW%H!>&C3+1H`nQqqE?*rbpL_}eRLv-_XNJwDe zQOUU!Cd!$~ca&eHSsd50qU2#)-R-PPXPw ziH84!Gpd*m*;*Z}vt=Gq`E66q%}Tx|Uc0gJE*20JKPU(3e|HW@(tcZ>>U z;}FR3O*p+Cp)XtqHk1d^95AsXQk!7`lypHSk*WXvi*H<9njiGM6AGF5uFqy9A4OXF;H|B3MUjTHjqNmd*$P zQ@N{-Omh?DQo=Z^w#IQ7J7ukH!mbcQZui!5rSJty!*RwE@77NJuoCap<({7B;SZ`7 z$Fm>epdh(F7c>HQ1B;P50HvCcf*wyW3N44{lxZjr6TQ^ zDf3be-`f2uNNYko0Ap~JfJ@+|3{QCGJ>ZPD#dm9>P7^WDt%w@=&9{q62K;D3RZowW zMB*ccp1`O&U!$JdhULQkrSi|;1-32L(&v0ucOHgwXbxJ)Pe0$WUw%lqI}gX>;}le- zWkMJWSecxp=VVXzc&aMW#es#2RUllt)T?T6kMo@DHf4U{eX$TVe(vAKl1_l|f=va; z(50Ua6KjUFqul`7Bk;!TTIB4|1o40A1@~KO*VcEU{9LB1$0*S*4t}0Dw7j)G^p-iV z`QW2AHaPaYxpWzz{qFyPhgPV0Y4aN{l{a!An{V?tO zPIuW_;-!vFfFnri#{zzcLN?89%J*R6bd+1kfJdi|5j1zs{fH7g!AxWpF0S85!UKZ? z3SyhS02ZElnc6PiDbTE+uG=Mnbwywcm89jvnnM6-8PR^%#%Uyd#^>>8B_CI+wYkHr zbawGdKy_fxgp%%;6(_Czt~&Fp1T}jk)$k|%HB^g+qvol_kB zG)gt+H!8UBZg#7AV^Los6uhZ2gycQa$#-_QLWEs%dBW(tGG$IH9LG>c+QopZUuNy8 zIrPQ&MAk+9%faOrNb{qS)7cn&tm6qAYPv@o&0pyL8v-Dr9Lhu5*YjBxE?-Zn*IE_n zgDHPn&Q<$&MyS&_eZ5ap)&FQ0yq+0A;bDDxkV)%s$qR697fyVI$fBcS6u zJ$A9Bj(p|mst6fBlR0^bMA4?ZbS3iJH<_-W4YeiANFNh4Q&JiyawyT{7s zllw|VQBl0@&SFVPiEStC&Z1q~BzO9xB1u}RVzOz%NchQ|l@-@k&bZdYaLubP`7204 zU}8$jxYmrutytp80Jso?up}jDwihd;L{3<%6q}FzQUf5vQfgOW1^Trp`>A=U@{3;z4Ar_CJurmCd&@LvdaGT0Gk)3AcQBzRR1@C)tBlO<9uXe>n-x4*@^fa+N0GGW_WCUaO{zw`6$q_3C%}l_d1T zvfivQ|Hw)?7d;!)^QR8H^Df_Y+pogb|E|!@i~W6A3d;Iv$Yemk+3s)8C#Dze!fNo? z?Ylq`Z+6+DSbg9Y^m^G3d8K$Gkd&Or?B>33fPGLL{g zIQgS!YQbd000$SJq0GLlx@`4toYedxx;a*Zn0q&Slw&(~ae@rhffQuAD%CcJ?9NYi zuDXdLCPLHU!E%|49UzVpxsuI2RC3rj89uq-QNTx0u?!&~p*kRC ztEl%OL`GuOSbnq-;cOr2c!EEt)OSLt`i9n2_mZfntSn|ASW7^YkwR!Uv7_TDS9RmO zt;mj^D4;6>gJ5Z{|14OuSX=1Q4(ml3`v#UbOv)xDzB+SQRb-xcZRf@RL(kx@_j;}B z4Q0Iaq@A(Ir>Si&M6&*=&m^qrDCB%c^m>mAy;Dkr`W2;L3kg-GsX)I6**F2gF&^?CoNqo*%P<7qMP_t#F}S?Sq9btfSiQZf;* zC-C?czDfqrlW-M*Xf42Lc%l{h=+}jmSiyB^AL)iRX}$Qo4@#r7a=KrV*T1m@qeCmA zz|~Hp$bguac|$~AZ)}-uaGVMOYOc$Z%t>1;$KAgMYjoBak`(q|K`ifo<{1 zS;(kIG#(H+#{Zwl-BIqQ)_)moRe?U$8o!C6>DbUsmFarKc3#QZD2OWk1Soblc; zVEi0)`Hd2u9|9i3P-VEe)@kZ1^BW}?FPGy13FyJpz4`nx8qE4niSmHy&!{Yrr1PM( z@-$u?!1YE1jkD&(LSP;X|09TEVm{<5I#9LG>huuqKc*L<%YvuZJJ($a@GR(!mW7C9 zV+mhkvI{F#l`=Rug-oHU?#7vK`Hirlna>(BG0FCYG?UwX8;_w5(m^SyZ+R|5Qd$PQ z#&gAy$oXm0q-0dfe>s;_aQJ~W0&F+-u zFGd-zvRt(=OIb5h(;pF0Jn5J9)=l{XZKp*og#%Z4`U2&nx@_koV1XsKBc8m#J`sE1 z_80~5Eu<%{%8;iNl$6Lx%g6w-0BPG)g}iTG-HFehUc1PAc|~#J_VB0Vkk_ZX#YPj) zx&tFBGMnZUe1u40irGCQS*7Q<&7B)Fw_y8~7Dn1$S}Ef7_4BrQk+5b8q%&rfLjxih z%L?l&mpfk*>q$4vnHiq1Pizw>K6C>kIDI=OTlwx5P!Vdwzp~pughLna67ptZ25n3a zkudW5tcZ^9U$gg0K7PQV7(k)?5!VVu-#By@L3wP~Fw?7j2ibo+-pYMx@s&K(#3o`* zXFEb(68Mj@k&u{}hevxa(8p_W$l*7hnsH2R{upgRaD;T=qtq!$j!#7W_l3@ThI4Ul zUFyE&q2zH3D^6UhB)-e0t$Sx@+bD>9=dlg2nk#o>r%{Ku5nXs=q1v36@{2Q_m65+r zkF&s!5Z?Y4D_ncxN%QBF=o8G7jwtRL)uz`?OGeRwaf-tXU~ zfU%3y)3V#Ua^(uTD0D9x^%pBfv(zfCt(^|$y;x)(5q+|U*2?}PDY z#WMQjBmk9g`C)}#{XBfp#NXYj%J5?x;S$5CI!A@i2wIshS z7pep~Udj}@9{!I5$co9V?lyxbf_cF{GY}lzAMn`Z^(KSo=1A(b7b(SxV_72RbGOhY zB=FXE08`~3SQVkOlY0(e0aC1+U0G&foZ|&6lOA0_xM7!@;Q(sK8+Ha3ctM2(=E#}l zDVoU0NV>P5_uMTct%@D$3#hU`9MGFH%yx8$xX(|6Tz>APVUiD&CKHN`hws@rv7nBm zi&gvRr8j;JSLb?%g;d6FaD8L|dT;jmv%4WV`v0nWI6n!za6yGj?F@@elDqlMg%hZS4&y(%l!8Pz!mz14a)BSPlEL{E z)>ceG+0N+SiNd-CH|U?(U$aqsbmh8y)W*mwFv3;;PKvQA+bp(ht!)KFlYTEDXb=8z zltzA>8-D;)=ei>dgqg!7{r8e+zx zGNKS~rJhoJfev+7tWtA1wCjJxNUo=y)!q?%zK|G4J@`uo#s@*uMH z`6jcF5&PMYTN-GzH-kVuvivEAC2mP!;cS#mDJ?WIb<&P*MAp%W2!YI~K&}Hw%io`I z8N}hp?bOs9-=W60_>N5$O70SE_Q2dZRy8Wn{8EVi;rE;WkG;2!s%rcGKmlo_L_`{- zTe>8qK{}+tpt~CmC=$}$Akr<}jdXW+NFKW5ZSK_@-+Qn4{&{1(F@FE(=A3o*UTgOJ z%(>PMbVOoLkC)b6RpNyDor3^I!3{wlw#Xy;rq~8W$b271T^S$D!D(skx zbW64w8_38gohvV}bmr<@%fe1h2*Vc^US|kup-0O>BAb5;4$ntj#O5JLAdwA*!kK-V zV*ZUC{IvqmPk;LeV}Vy*Fdr{}*Uw&UGR)G|8Din{Qjiy)z&C;))O^kl%3vO*dmB{H zwQONd94|N9=aHm;AT8kykd)sLN$B)9ic)T)Qs?t31AMFOjL@hSLL})aHi5z;J9#Y! zg_X;9fAfn-SQrMGMXAOUC@qhoA}gwbOv=e!FY8j5MD7PbdH_H@@@G{B_|&W7l?axK zitMpjN;utfMpQYO#KR%$?>(=6V% z#_8iWse~cJN=66Rw%E7z63gV(PMl!qxZoF(qsUl6>)!s7a#;L8ru&8@FUv#-QoW55@`IdP+;IKx`}zc z6VZVmFgL0BC-j=R3)%$VYvy3#pp0VB40gRZRvrGGu?ugYS7Uz|&i}e45fX4G)mL(? z4{pJ0%Q(aZQUr^KO`7v~cmDGCy5buQq6Rq=dz;pLZ-_SkfkDO}V*De6`;!j8U+K^R zYc^)Wc~}7;o>2AAKUh0Kb0Ds1S&;wl$loERKksu}$MpE~c@%_@C?Or<8A;;BJMp>2 zJ2kOZ#R~SQf06M0iwo4w-Q6!k!OA~O(VxWp`xT`eB*;A_FHZ_cj8b!RX^9uL#+oFj zp4&c1ID2{NdS+Mb%RdT`sEC|q1lG6Y1C7H%xz9)pqTV+*WlF*F2i!9&y;TQBG!N!w zc^}>NFn9o{MQ;~)?Fq0`=ybJbC#z4|T~Gn{Sj@kHvAR4eh4{}!|JxNBBfvED>eumR zBESWSZS_ZTH>njeTb?`o5z_pJ^mq_?nB2qD)zuZB z@uYiUWPtc?5_TlzzQ-5fwoMWdE<6;kUl(HMo8)3j&O~H39lU?tq$n zdGs~|n&abMzLnH}JoG#K0WYLPMA-nO`rDOwCzRMbwt35Qb~x$O_Q%`>uQxf59EFL( z0G>(;Yy}~8o47Z3=Y9rt^XYM6I=76MP)jC1SX=uecl%#!@K!zvAE6o@qm0U zODY>7*ZKzRLZbBOR7`OtBOh7kU5LzD95X_RX1T^&*4{w@PmK15^Sj=(Os1Q1SGMvA z;^R(PlL~?S!N7EZ{u6n4Nf^dwG52Lml_^Q&cIaOe0wz28E8?AI=(f%@;e#{q0MNy} z3)VJ$?l?#k>n!qNMvgyB>4Y%|1iYA%fFSWL9LjG6g#S{lTU%ufShlzMV2HN9a$s&Q zevfQH%#Y{ABs1OwqF)HQr)2U)R@}OpAB6w6t8@zB9!_p0^v?i+`Cu=Dcn$6%`fRJy206TfOvXJ_e^l$3l)q9P*5Ms3N4>}2s};PR0{+WDHfPl9jjQSs{%g|C5@gxEfd^O)gW z5f{rmZb0uJwtZ{QjOVi#aa*%@6Fo%Q74;4hGdE8f^7#cb>ye%=;AqR3$vxe7kU-&J^k3qx|^~gw2>OVq5!> zQZO7fuTf5hsR~KHP9|_!YSit1mu0zu;m!q*y>~v%e z>Hp#b$Lmb}Xf=z3W?fG-Ih_tOX&Em}-r@7hB2z15 z3(9EY)Byj@!VvbY0ZI3z+1hG(ZDpFIi|I8T3j?uZ!qYE0UqgH?&A9xp24cqIyG)DtK6N@z_CLj=81 zI;z2S#-ei;-qh2ZPK_~T_zyumfDYi4CD!9l!Rr8KIz-&(ir3>QB#P-GjuOeh`)b;w zDwM_r|0Z-20W4Ngtb z%$@bx>)6;R;^5+9U~Qu+pO%-tkyWkQ7h2nkP~5&c8W$l_2Tb$C#|W1zm6E2{J}|WL zbSF97pBBQMyU2M0G%qA8G3LcQITv-y&1Qp3jbC=fnb#S53spI+f3&f)<%FBZ{I4+U zhvp? zn%@l^TtKvl9u|2rwSZ*(?NUioGoi@7&x|{w(|w}OVj=YKnTyJx{Rh9_5t1KI{D&+0 z6b+*m8>c%2ycJDbSwBPC5nOwcIuWj z;WbjHB!Lz)i&->pijswiwd^%SYYv6%XG|n!_YY4xf!=OYpSLC^l4i42bjsXZF3BOG zb*dWA?hxA$OiCII)oQ(fT$Q?R>A24e0jRvE{GDB0X#nO!<-dTce_D?DB$HONl8`~k z8N>sqjxZ3C(i@S@wBpXoer$oq7N^fg6M2(V-(9<{Y2So{b8b<<-8O{Q)|_ZIfsrIF zj5;tgL}I@)9YKE(b!Y7MnW}%ZN>Qz6?gJg&F%$|u7wN?bxSYfsZPsU%XC;>z39w`) zLII?Z=042`iljSl44;lHJ@E~fxQgyDT)fj*C4!1`hXnd%xt!1mQT;fF#Ryg&@$Vk~*V>0pj<|Z=;PRGDO%&Yr&e7piMM&nnP z@9%O$#SEW@J}CkRlbM?44UY~>ynS1HHbrTxrydm&f(|t2ODAyY3gVrM>||$CSN9R7 zwwL4uT-ut58g?dJ?Tjh9R&0I=Y_I{`qoino&XgD69)lTzU-w+ejw?10FnC_NeS&Co~h$n>K%Jrw)4s;a6|Q&I%! zHLFNZz;jsmmV)o`dlx~yYjdMcue;<&Yy($7$QxtY-lhP)1%!=UPGmE7)GZ4@KSsyUIk|PTz9=Q zq3)_6>wItQEf81S<^$Bq``Zsf5NSg2`YrCAL17m@itVzXyXM+4RbV`B8@4alOsAP% zm|&;6a=l{WFi4li#<@kQ9GSQ_xDvNJiW7#s)^MygJVd@bxZsqAWgQa<7G#Uwmd||t zJ$|Si(NZ@ltKPkzgx5B6+tHrjv-6b%1_i;fmLIt485JN?NIG9FM5R?g$7`c>54Sm6 z8d|dc!&{=ULb@K5)*-0hyi``6GE}-8o4<5x(cED;Fz2={rn8@{)K)lJ$ZzI?8Up+@ zqh@2MDcDsu^tw_!*USpvUQ7@;J91WAI*#`Z$fS1uNQF@0BD9>fg^`ib?3!y0Tw^rz zFd4->LPeNItewoICm3HI`MZkcHuxYIZtO32U|ht4K>!}1yRyNmYRk>YNUY4DEjf{1 zm;C$@y#}q)s<-UP=j5QBoeQS^IWWDnT0wq(h_GMOt#u5K;S`wZ$~@!ESZj1$2ZOD# zinU0goce4BUNL>givPOgrwUNT=<*hU$pz6HP|BfID<2vIGW(J|FZ1nuF`(Fu8cI?4Bh z6SU{ep94`ogT?yMxet@Cbs}ALteG@ZGpDQ#@sPjAZex9fKDBVKu*|ChaaW$75W_^znkI&3`0f z9^VjkaP7pD_~Q0WHnL5!&$EV$^OCX&JO2o(nBg7nA$LF`XE^3+?O%V|zx3ufq)qQ- zsatjRP5P!tr+lxvuarV73hTQCj(*{h>@luL479)R(_3s!0Fe=+`XJsvE%YG5GEIAM zR+8ey`_A{BhdA;P`jcz**EUzDn?(T)Hh?lpDhD5nyrP~AzSYIA8)Rd9aBL+3_1)#B zT$cE(j--JqSmL&eU(X+CFV;F}$0yW{Ws#8EK|drrE`0Zc8q^K&c+8zI)?ffUFr~o^ zKI5N8SQW_=K_E2;ql6(-QifK^G2Qh!Rz!Zmr}J>)r}#kNFf@>2s_}~YYcO8#F3H)u z$G$VyRySoY52Iq1TCZE25oQBR{go}B;O{8})^l+80s%UFd-5c~(G!`}-v;YBbwB2&tsRGDa#fANl*E6^*!i~Xn=yF$MFdufz@tmQ{(2qUd<}Y*ZXuv;(YSYNHmi))_qY*rwCi~df>b=p<-juY@RWzj({Xqit zD0lWnfC&~#0dQpRHcy+eC+S^Ovx!7=qn543!X_+~jgR^AwQmoK$Eb$wK zRgJbyaBQJdac)Z)3*2$z9`{bx>6N$VmDXB*cqO3nZ6PxM7#o`^n0b)}Et(S&pgX6! zlgQtKkz2%L8fG8e3Vuj5oooF#V=8K+)7mgBn*mTe5g;IAQyWGmej4E>hN65L*R}yW zhJ<2ifZB1SFeXJ1(tM1@9deo_onV>kurcS(11YRP?E{=rzbwl|3ImM%??Ef zQl443>C*i%tCRGZ44r80N&tWJtxCFx=8;*Uy=gBsTPC5$dTf&jNHoN z&Wt#|{^&Amz=H=!I+?=sp%ZW3x?L3uc&(5s#hUwH9prER<*6W4B{W%68G<^)x54xw zwoE?Yal)TW)_N13h4l&tvZ*3T!cs5~9%H0SMRPd?e>oaVlxl-TC(+7#3g`eM(rk469C zS%2g5!+L&I)hc7jYUTlp?4V&lyxKUwDT1VZ>(Y7asvm8_cY6YA9V442lhkl1fH$dn zzAF?>1oo%+^q+lD)YRdyRqs}L{ zk-Ph`Aszq*qcM)okLKBF+<5e!im49VV}N055iHUTjzIpeC%%9FYC~lPX{S~ zOAZF?#>2rGO28L-J&k|SM=!asO;TOfi+(gR733T=6X#Z5&oy<&J;PJ^ts0v8(0DBt z%wnd^Pu)VNxOlw5&4!IMP|Ua3dHRQSz5sp;rYGe@UgZVT=VI2`*a8IWYDK7jU-mz2 z{UTPoInfhNhY;7B4p$C);)}ZY$eJfl!vI68Cm&TO2=DM7OZxrvy~iC_E3;A7Y(F&r zKhs5^aRnNV-U>+_`Xo&`SRKUdx_ubr4&Z|GOP~0mL*HXLy%|E%hCboEazpa;ANso{ z|07WbcCuvMd*t-I(wkQPB+fYroouy)gc1lw-?8Ua$Z&GnFxH}ZYtvMMGDO^hmJT~< z?m!{Z=z_Tvlj+ZpUP2R6fUcsBHJ3N^1p#3)p|tj=g?T6Vh#BJxJnzd$3Q5~L4)it$x6Ju=NfP<)H_P=(`^=-+%EOQIx3qi`PXBg4yj2 z>TYi^*ZiRV-E3nm|9#&7P5_V0TaumQFJJzA z^52O<^nX+TcP9M*mFk~Tp|xC(SkF$|=1RscQwVG60Px#PToY8NMGLf8)fQ`D-C+m; zN60^bkgHlOU3ZuUTB*!0pFIy69I*VV&?<5WD6|kb(X#apZ+RPTkx}oq;u40*fX<_M zly7RmuFA8HYapl2ai2u~Stz_XHq&rA8Pj9@Jej z(ixGH$aErNeE{WnRjNEAYz3T-%4xAHC^Mm9db!AV9?rtbN{%u&VJqr(xn%!fD)%p; z{NDq=zkCs#1LS-Fj(57f|7d=^NY~Hhm^tZKqY#$k`uu|Lb`n}H3ovJu?4VIiQvKe< zh?_m-xdY&1yX{G;q{}mz3cuxfZ^Di#zH_+WCek%`jdfcOY*{GKtfvPZ&s{$$OxTtH z&M?jBXle1(kNabH+JCFB$KV5pqmi-YB|^}U)?tv_VOh<|Y;_upY^rixIfN*H_g?Nq z)sq6PCkWZ1K?EJ3>y!ervMZYU;~+i?T=v|Php6}Ow-eSJCFxiVD#VYUb~Av~%1mcM z$44XhZdS_&0}mIno;Bm1-M6UOYc<~Qe8J$@dx~0r&gyu&80x>s(r<9qf}L0&7*7b7gqfz58e4oVD@q5 z0g@>%+a8S7x||f$Gh|37*kMf^t#n0oVAb!&iLa>bp6GFfqxO_gUZSsXlLpC;7Zqip81XLV4Z&A1abXomW)&9%?@}j5~vY6cgmY8Dl5lOX?5y4 zIB+OMyuLlfqYFAL<%>*AB#?+%S_`zEVRj2H_RCCvI=sdVkdF3vhwZhrp9LL zcw-(1RmSr=zr0m%6G!^BjrDPpn$tz3BCD^6swD==EbQzrbcH|o`}?;qwbfefTsm@E z9Y!CXoLt1gn60H0R8?_JnD!dZRFw`Ir1xYM6qKLHWONPmcOT)hFQ58iSS_{oG+2K^ z`xODmga-s58S5%W`eoG z@@LhcwVP8@j3xt#a2}UavFqI&?H4B1%7@7}z)2Qssb8Vd2kCIc94-j2Gvaw&$+ISR zW*9o-$PE^>u7N`W1l+c*>y1R;Sw!7TflfeE4dT%dZ7&$MBZ6x8s;VmuC>N?hSNAyW zcjA#74IGGvtS(!`&#qCgTVHmqXe3;#tHrJ;)bAn{IoVo#YR$?ZiMpIOzv)d(QCJ1_ zC~NoeUie>b_0=pO+&c?On|iynWVJi*y}xAd_Mjw=>28I9gnmN2TMq#NadX4)HmmE$ z8}znj;9L}yCQtXgAE=25Ah4prYsM;KPDhY`{FFJ4l=;wh+jPd>R(q~*PIKmDMhVLq zl9uQra;{<)b*7@>ptCnWJR}tHGAPmS#7UniMzX0}M{l1y3ANkQ8eBQY98XYVrVP`ecK=W+$biJid_h@6Jw}f13aCtP&+^SV3{u$N9@lKc*GA_rEUS9&m zoa;#~b7FWnDe5e!fI0sj5*A$~l|qK2m!?JI*{IVrcmg!rm2=f)mO_S35uyWuuyMv0l$|m2rM#MFmID(FJY{o2%F5;ei0i z@97_W;e$zuu>}!$dg|bKxB$mv=Psm&@ruTanuptUflrn51Mdx9I&S>IfQV$-bVRh!?iq3? zu;Bq3F?ju0SdGigN-Uv!G$k6A zXAtWKMatt}o$Gk6vS|u}kGCf$Qbsp^&FE%zQ16Y5 z&2fYwB7SUtW+mhnUC+;EHcg465*)Ab!ft2Uu(sgSeZ1I&#dO^Kuud%xGQY^^`N)oX zxN;6$_ItA)^jtFcCI4te)-3S|9k6H^=tqU(|-(1&6!!KsOtgRv)%T;>PV@u>( zuY<>hp&Q}ID}|{7SBo$mfgvC#+ue+s@Te8KwtLPLgefp z3ZhQyGZ|baZoj_t#*hqdAvdnP0AGbEvIpo!^~9C8SL_jvr;O(4-8h^!Uhq^=40PW& zuVa-6#Ov5sdCAN%?Yd@>rBe8r`KrYiW53hbT;(}7*MPFD=T399{cMDwH&gscC7cV4 zg~2iUHdAY@`FW!DSZ;(2?^XAeUC=@?@;qg3zJ1_ha@);e%d#xfr$*h}z1Q5#-3>kL zGn75CY?kD@I(U&$T8<;wsH0UI!&!9pG54u=ih6ZaV_p^IRlNB65i@lzElTYgDp!}l*=0i#`U*ym!<^|Jb6o-zlG|kmk9aJrF**C3Lv35h*B6nU?Bvbf1&cK5 z-kLI+v&s-{sOi$oQVEFEvygb)l6rw3F{vf;)H(>v2k76lf}S>-M-l4dHpr=H-t4cn zNkVjiW9Vy&jVfrS%S>^xZAKz`Q-?BSx^AvFIWw*SbElyZf4R~Z z*j1`||G)+t`SAdk;T~qg-g=98k<(GZsEhWEBb>#l+N%3H)XL0Bt17m*OHX-5f+>V| zebs2TQddeB&dh2^E-j~CmFJ#PXI#U_SXLv4Zk&T&=a*xQjT%vAufE}zi@y(l7-YIG zrlpmrgU4$!dF8$}Q?1Ox>^);ERrew0?v=NEg3!$LOqY9GhJ?9NwKMtLN;>Cd;531C z^HrYQ+vUV+aOm=KZ0}(qI4Fuvy-aNhXLm0e;gV7xiofI=vh%i6TKRftnsr9tD9aTC z^MQ}Q%et=qG(pAUKIC9wbLDa7(RrtF0oHh)$kq9g?rHxNPo#E*L7`*}S5NFJjr-CS zDsQ+{9Gi1Z8_xqVh&JLO^IpIsL@cE{V}Vvo?r~g>lxkL&Ssg{(yE3iocT57EMF5?% z0`Ds6XAHu~4XtKa-#>$LN6eOVI)8pTSUtDzk&M=pSI$f2OPJbTL8!Ho}E}T_}I0*jv{_<9?}|s zGm)~tmw}O{+~=42av!W2OsiTYS$C~hRP4|5v|VT2z_`e^q6>*cJCrs|wMls|g0rc1 zM;0&;nlEE$36f$=2(Be4?{Tmoebf`=jJtKNswED-5prZIn?;*pk)tZ-hZ|%^n`6Zk zn5WrEggv{TXEtPiC64zI@d$>oASEYn2W%erb)?6(T(MOyqbdgP^)hpiZk2sC zKmE?o6i}jzEhP^VeW9-b$CCNBCzh39$VO-`18g5v2nM}kaxt-#vGP(cGa+Wzu+Ia+ zpV*$)*OxG(C9i;W>`KHi={_q$d^I^}WQI zK?mA-R^T`f>KyK{SNUDW7=sm-Zw&`NR}L2_D~B47hEpqLJ1{KzR~J`uU||2RPx!aw zmk1GB;88>Jh1-cHNvd6*8Vr+*Zjk{k>jcYmn%%aV)ky-$bl5X5)~TlYq?@344$G#u zRe?5)s$}QRx)IUL_KDpWI~Oip8_?R24+z~33v;#W8`XFpL*l8;Q4OWP0Mv@J+?5+R zNLF9J%Xw)wsup+|rewLO%Lo|ovf+n^HqPj4rJ1%zn6H@aXYD`Vd{lc^ncq~V33#G) z3o~cKq4Ri`M!eV$Zzm72KjghT`vuSgUMQYl>>dYw-MCImMFTU5m-?&a7Yr}-R?$yL zja8WK4P@O5icF{M9PPI_w2Mi=JhW*$TaAfc9S1+N6OXsh54|yb1+jVe*1CPFmv}!c~ zrdd5@E|SOjc-)Vp;A_@q3MwDv-rim>7fbMRk6cQEmF@cB;{;Y-THD1R9{p2#jREoLO94%*~gxva*^ULDyF0D+7=l&Jyfot!OZ}}XXiaajc zVRU_y%@O1J0Yki@ZdYW4F|4L8)B`tgudu$^M3GVnE=u)73d$yK_qSayi+e5gM<+7n zve(O&MST5e%*~set_E}hqjyNGg$fdRmV-bQ{ck6*=X%3o_^y{3r9?j}jCDzth31nR z`vNV0B`WEX39E^utVW7a#O1Ra=!#)+aWn-r$6k26F5_x!Dw|b{A0Y?#rmcC(ZibqK znR;{>Eu)5qy{s;R8+XJPXL<>kvG3YBbZf|n-oXgqG|1&^a5`QVib^&JXz;!}Lvjl@ z*YE9Q*Q6+6iXq6aKgLNqK<7$V@}8M|&eg2$rDC|faqt1uom z(EtMRjw0#r>4^abydi}5n`wt;+riI{2QKvBnB0fzQg(>iX>Y!M+#gRteZd(<)vQ@! z9EK%QN12|1QU6u>5nR3bQ)VnWttVj_l1D1J9M91|z*u~Pg`IQqOJy}%E~ML*(Cg*~ zw}+6Z_8zvh2vx4h*o(suLO!kPM?Ju5?^Z3&TLBjD`nKE*ODsu?N4Pi?TCJ$Kc%?ar zw+xDU8jLVn<#oSK2`A2kOqi-#s+?(Q_J+U2?BQMR=+@?amLgh)#M6|-!oQMI#^1R; z-~Bg-qZdY*Ijv{5XdFedv){Fk31lYPl@yp9&vrj{r8QS>BgLEd^>^b=l67oxT5pT@ z8e6`50);T&PcObxVd~S;*2X zt%k$4w0x-47^`x5k@qvU0wPUY5Mh)Ig>(^<&8ZsOsnsZ>m%}P3TD*Xx@M7 z-u7f^L=LQ0NP$*T97Mzlx$IX>AYe6uVKJEZcIG?YnW6x~|4FqH4qIDu!D{gVdUdtv zPrecn)Rp?G&25tQ)HKR@k%XXgRVJoOI0IXR{~O7b zrG}=j%Je?mcJ?hTVAhk20`Seqk5_?538d~Mqw3LDKQ9q}4=p@=u!!7A7UO&n|Isk_ zZ&TpUMM7&p-=yolzN>`$+pqasZinwl3BlY69sFk9M_~Y(9X-;!`IXR)pJ?%R_$m44 zcfS%-5TR{CL0hyqAr*suZTKEAT*w7O?)k4+EHocmAaQzA`(c9b*M>U)!=KdoW)l5M z007?tR6FqsXPdSNe`&Z-4KN&iPDt@rP+3UmH^2#rfhy*&kADRY13*hHV7QO@6YAT; z8~$5Bf23T6^!$KOmp{Gu(ZBZd-=H%9!&}(VG5^u)^_#o;orG|)_m)?EtZOme{DGat=|^qmV5DQj`;Ph->$HnxT`z&_sRR90K-40 zBxI8P)8hVccz>B@%NM{AGfr1*;r!BYA#Y&#{|DmEFNr`pmZ`Vh=kXq!u~d6YQU`I@ z(XZPs`Q=~c{x6q&m~Z@|=_p8;7r!0`FjWFz>N zYzX$@x23uy*7c_~!g|%RG(YE(3_$K@JdXt-Jf6Z^2=d%-H)%qjKUkH$1Ik8gycGhA zC0SPJH%jif8-fueqRJqXXnsi54shF}QSFJ$Muv1V!`nesB09 zej!*34I4Ss%GKD-r7 z2J*O_QnEs~8qol0ot5@k-E}J)dmDYZ1+Oa6Vv!Ls7B{{FN5AZ##}PlJnnd1hoIH34 zR8zyyo57K=K&6{iLWYls=4CFhaT^o?9H!J2LK0)O*n*&6$FsZWSD^A#6=MXxGY zv7>G0ESrsCRiLJ|WI?A^BHJ3^+=q?Y+A!Z(E*?d%nV_@{lr17@RcV2gE^K;QL*I!7 z1+c}JFKHCg{aFkLg(gc(8GzHF<$Iu)4Wm>iHG3ACv8^}`d`u>4IxZjO7G5Xtx^kUH zybAa^Kge#Qa$j~2v87f+CYNz|aWTmfqhj+p-m}%X0-cGXG{_P9{sw>kC?tav+K2KU zPUH?Lu?UO(^8vzPzzeTyhUDv&c*M8+WfvDa)u(#9y29S$GOaXh+~;&S4b!A%Awz&6j30;^~spU%zfYiyda}#3BXwGKV}RUV3S# z!*67@PRI05H$zWGnV3<#P!HC+wp8cN?|(bcO0@i`vGDkYP6QmoC1Z-wvUNJTsPWxxw1G#bC+z`vrqk=+(7 znyHtS6_9EhSJum=UT#JbAgKpKM@!GI3h0y<-bCHmV&4y<_T#z+F@#>w^Z z_LT08#})a3NcwVOCwfnBYCQ%O71c`-6z2t!m>8x}oj}!cvpgQ(pSK%(Pyz{13N-Fh z-nx&^_S=)lI4dI&#@oY!c+dIWE-jyj7(X<4CGx2Yvl6O?_!|@qjKw%^cD$l+Y{HyM z_DG(dOejVSg-&0n4QCl2h@HDv0G$a9SZE@7bK{6IYSHnvF9ktVifGUWC4rmKB-dl@ zQ8o&xSW4^FZp6*w_mzxL9KF&C)i2iPZmcSoLti9dZckRe(Cdq#%6XYbxm%2lrQE0! zy#9*G?l9a7xexkYo<^mBNPyFjD(bzT3+MnHAfO0kbMwUIFQR%ib8%dbMxyq&%ZL1w z;(q6&O2oX>b4*HF_vTrZp5fTg(XB{l-EnL@ND)J%EK7Lbo!fm@@LT7bo5b1J#Mygt|t3N;=0!4?}p91?+*-1gwGU9!+`kD`K zem|D~6C_?$BhxB#_+V9CYMBhsFw>=O+aK2mH>x9c9@`vhPrZxV{Cvto;N`;|+zk&{ ze8sF0;cQlH%7|>%Fs2nZ)Q@}R+>VtKp$UbJHm?uTI^2pf8-(AkfEi@Fpok@2iB#~J z!)!0QLn?E`u-~F$DI)b$>u@L!OE`vp9?VI^0LGAPc;0@_H7MR%+L+UJBOFlHxt00G z!#883{kY8@^vYWwcUwOpJR}2XZt$TKGu1LfL_Wi?C`Pe0Hs1P~@^RiX${6)xp(m7q zB-8A}_Yk-;2NoY?&2Hhu6{{3@v$>GNjRl;X&8nPTAhirq@FPgyKUwP;%eVX1FK1To znrZL|1>g(kzGFvk?{O*R(9ft22`!OzJLCRc1O$Zr!=XO43HH_A1R4wqH>;PW4%cTP z^0PWxD)p~xO~$_@H`r>9f|-nGm9;@+bG^|o=dHRa9X%GIEyhdpV>Ogw?WDq5)DVtI zIo)4CstV>NK}|-Vm-3?6vMt@>)Q0Wqlc3ou8cl7e#_rIbCFspY*}O`RYOD9 z_-qC#=_tncJw%{8hhUK`z=2tAIl!1*Uvy-osvT{7o=Japb7>u#D7}hoPSN@^Hh2u) z+GPY6s4Ck{dZ0(4xO4?e%R#!#YzA0U#oFvenyz0YWWn3z_M7(A9^89ci7?F!7Hp7s zcBaPcS!9L@@@WDQAM2$$!|sRqzPJR#Jb6P2;oVa4s=%G^B7gD+3|Q8~Q~G9^j30+xg)-%{@3RLn#72w|D@- zm6`Q>ddF!$?r6ZFa)j@a0S90$#dX!}joNi{**8bZ0Ny$;%QQ>es5@I#PZJN*BgYV} zggT*QKKRld&8WHB6Q@4U)a`TM;}x{Ux5RKd<(D{RFG65V6(n1uibi_@&!&cw+xt}0 zRYyW=XTtUPV-Du;3uCF&sR^gjRy{V2r;B?OKs9Q{y3QnN#J)}Dm=V~OzFxkp4hM;M zCf3A8Je|aT5@;AjC3AD7D$WuUWw$*|c5{Bh*LTi6SL+5|C@D!VdfR5#(ZRXS<9CO{ zfxiWxT98>Bc1^8%IZczn+rd2t#gg#3?N3MuR+$wOTJy92q%y^AaU#$YR$yKoS z3q)Z06@^-L-P;wRiNcL;3pqNR5^NnPU07A~DUP%59A|uJiwf2zZoHkoIA?Gyiy<{| zEXfyEs^JOt$r3{vpmR1VAdamwvwW7+r+EhLKFPl%D}Mz1azua(`rxil=-((WF6DII z_S1Do%e_Qv`yP=ofv!=REEn~eTv`$avvp~5$dla8eO*rQv6%#>Y0>M?U%^?7bH}St z$i*f^k?)NxZHrrNHCRs6l4?p5^jfBWa3TV%1HwGKmfs^i>>4^4b1A?mR~qx~c0M1> zus0v*W(QqNfjS0Kk`bc0Flrc^I+{xCX4KwsOd~h`w8aMv28tNzk1$-3u@Om_4yfdZ zt@gyKo5wNPO32Nb(bZ4tl#iF?Ei^`3U!3kEtT;IZZOXkq-+|L@uH-}xw1 z$KMXcaedlGV6|O377@+=aunThzoR~K{CEb}Zcbw;&h}V}?Zkr8ti*7PmSm^qg<6@x zI&{$ZOhv_}@`Gj<-pd?sCcwB`RaWeWP9}S#mSpS0*v7YahV&@di)kg+3VqXRwiTS2 zy$8i%;>%o+whd;`NMh$+8U#)+*E!5A&!v}4blIoM0(wa?vj8u!Pi9q$JcEICcI1XY5R4==8wv?IlV4Hfa*Q<~Nv7TL*n)rv@enqKmM zx|L7(5~#XJnk~JYf)k-;mXjW~>bm5L!c`iF=2(Lm3tg&F%W?VZb6d`7!4uy!iY(OpL&Q4`|W~D=UY4UwPwIhdta4v<+dLgZG@Xo7k@V5z^PAa#%x%) zn#p;NXNUGdZ=U018b#V(O&?csEK}~S9KhS^#)N@Jx zs>EwDNxXF2b#pYLCwks6B=PLSw%&+cI0Gd&LdNy>x$%_TFB7oosi7h9gfWrk_HQH0 zhb`#2^T-i2-;sb3n6A-0b-He#kf}I0IH+^wb+q5v#h^BHXrJs{z5_&xk0#2tyRAm= zU;iBV^WnIy80Cw9)F!_0HPL(zYwk~Q6nP)VCcrifyt~l6Kdc}lu{mn0GyVmAw1+RT zoxaXCa}GNjBCv(aH@!NX#kYU}ywy~AuT8fv^u5*MuDHHhCLG{g5+DNSOATH}HcrX! zjy?qZ$LKV-&(d&~hU3MyT#?a8=lpiGvDDt4(lX)AsWz2*S#Ebc>x;U}&ki19`ASSb zF9MDyiror&Gt&W0|lOoR_Z_>02$b*2x)gG~( z9gQh5nN2$<=RVk^Pv`$~kAwP8=cquk&7|8`^gguoAexy@`B@X~ zJ#B^4$lHK&mYLfp#V{N5OTj3?VHosZizFGKS`pCC_3UjqYV&$#^5F79NoaUz8ut{F%A&z9njcvV;@N@$sB`EHs1 z9%1$`hwjOzyXa1oTb7xqDc017k!yO1Z>QIif`=_9Sgs;n@_~7OWHs#^red<~WItOK z?ec7*C9&jdx^lF}H9!-64~wjOu-Xr!_|5)m33Qz^ER)3$BO2JQaC7B3Vj5XZ@N-m_ml<$Pe@~M7rh>Y2nLv#l>x?iNQQChRCLBLaxJsWH4+hEu)?!#PkIGOJM|>r_9u8M$r#sZO3spk;rCH04 zA)o?qOW+L?Mud3|^~S||zUicLexumPQK8$k>Kwn;W_^)=_6?mvx+M1cCY@1!*LdX48#GcXxMpqaY2N?ygPOrnz6#=Q-z&_nh;4-}kRO?l@!Y!PtoVyVsg) zuG#Z5$6J=XEWdQ3uuG{&MlZK{-X3>6y^jIHH_rk0pG+wY@FRL9l(VKUgR?c}%?1gR zdU!}cFSyJ zu{BC7Z$sYL!jY&EKWxZeuPdN23#iDP$N-QXBkOH80ggdp#KebWbI;HB}5&yaW(VvxX=W(n7NbSs(wk%YRLS3{7HEpa6DnQuj)CLRU6* zFw&o>g}=eqz0fs{ofl4fFnBawSnMU^(vt^6o9$BEDCZj?i)78G>0O;~%T$=++d7(< z?^o8l%CRa;Runmt9W-`o3n=pm@FcagoXq8_8h!o_*B;wq?`dDaabRzTaQg+`6+6cM z3D=&bxTvfS@tnxzYp{_la-AAob02*B$h>E^M}DfBNH_iwfyAwds^!4Jk`i)h?$5p&aS)&N4{3|!<- z^9|EHU;5JJvfp50V#<`gqcfd8#y6cTNggYT-~Hs;eNMn^cKT`pJUb}zI72o~BN9G; z@>06dh=>>bS~C0M>!6##9xpv8HoNa<|5lTyCMNq>Uo;J+>$T3CWG=fHE<0B5pYatY zBOPh(L(ksEKX0(zHnZw5P@1BVilL1G>hdHkElXH<@enhPuItJ9tVB&U2Z}~23~3;` zA0z1~sF~Ug7V;<(!pVT>#Y4+MMc1cDWt!N&9GS!|`fRE?+MxgFsf9n}Yed3z+m!bv z_Jrz>ek8)`+?CbqK><6Ct53B+Y1)9yNoi-I7$R+hw)K7*P+u_!Pu&8pUUlu?zA5fB zls=$J@Fh!kYeH-fEW)R;UftSXB?M<$kdIpBi6v5=oiPBZxmGlr{}gTlig_M#;EdJY zk3aC-ZMKRJtSwd=Ud)~vr@E4!Yz)}p9a`^>_lu`mA zI{O%7;wuE!05X#5g09JYs`cL3=uYkssfm!x2;&Bvujd^oXC_wdTLt{7wOeKkX8(~Q zQ7R9n;?yx-*9gEoKtAG)bzuUt`$-N81>?t$ErwjSTU2ajV?2?H{HtZVbM@LGM4|iA z&o1pFkh6t=9A9=uJed*YWs|7%d-?zDS;FuGn(3ll4+j>>=N<6L^v{OHEeID)m-c#p zp_={8p8p&33K%@71@%2a3v{aIus{~<`2ao>_s>l_{RFSC|N0|;@xTR;o9IMCtpsYA zS%FWY1=EfoV)0w#`8?E8p~F==|G!&K z8lxxX(yv%-e?+J6|s$f#F(*#JywhL(~kbk#Q`!+-A}WN+NZT2p6!|TEocFGl1*uPh0=BA^U}di#3+5{{k`SWEEbYrE6TzvF8oq-{q+RaMw$zpASO zL}1*+$jX)TuwcVqw?Rb=su8ONiSgipvJ*d*jF7*CxBwG8hal}^JgeWmCTUT~)=l!g zcuDiWssxY<9<^o)t{WI4b!9zT#A2E*+Qo(?c@7w%SxrUwfAKUGCyXq*)L;tT@9m+} z!QwNBNJ%Nk{o0E-_L2iDrlv0!3A#G0i=2oGNd6Ukh9E#n6O-3Ff5%Dyf?#$1JA&xo z!>{~>1@K?u!2g$9{v)^i-^?LCwY(a&d}41@3&;yq?%zyLPYW(OcKV?_U=SB(6Nc~?Nfq-b^rtEEkY+rI;)o*wW*c3x^@tHtl@>w)HNvv^MP8o=Ru zEg|){_N}=@!>U7PlHWGQW6G?b+a5=SFW*Hj&ODldK%khXS$V?!i)z2~76mD>Ww;zRM<|Ia$f{h)wOxBcxcgC~d1@BCF$^tEs zNtbv|=SP&OT&wMbTV|tOT1E};Mk*W*_?&FUwqV%>0dz;!{y&wKo{tbZK6`CJ9Hk0l z^My0&BS_Pg`UQ2w8_m4P`BpsB2GDE{u0KgG^Zn3EBx z%E~IuIk!0#F{jrZl90J1FPXhF%4_kl=l^J+N@t6xR;6X>YTlc;seGvR%U3=nQ=MtT3W|O zG>`~C@^5_p;uoZ}^;4f6hp}c9&_&17Mk%1vvseM;+t@cHjZ=q{byeNiBH9L8<D1*G-_s1v;LeIw1c*d3mc%uUVrN?j}V>?IFyPXo9!(0UuIl@$whJs8R&-+{9eR zji!cYll{qn!R6Eg39O%)bq14o#oR9RT1smXSY_OGPitSRX`^h_UjnFQ-W4qwDT#=y zjwHmaI6}k%o(}@-wTEz_#@u!KmCkKrfmVG4J_|1qK9hDNwu;v!f&5gB7n5#j?eldv ztIstrcP+MrZLWA<^_>0i+R;n`%_9`k@bT#YLUM-*2#2GEkOoIxJwhFCIP!yJi=MFD z{z!~JIA3mgdD!>$IkUq)&t~(2cHWj~5FzfZf<~2zW%kC%Q#|J*xBEM|3PXrW zydmnam!0*xlGk4bbby(sIH#aeml%#Xk+a4y80#IBqKf`9-$<0bGu`{P$v}X7oAZ z%%-c=2Qu12B#{L4yu8{Ejle;J3Hj(akr$aG@2tscAId$9PvHS|X&UeOhd^J6hJWf`-*OwCzT9e1H z>MGvqL+srm#hTU3*C|VLdECyYaalytvA}qc9zdYY7aG2JI1%D+u<2I>XA20f&Ub^$ zok@sfKin_EIjtH>YdugW5}iT6dzerfQ99vx$1!jc~FG06Q$6*Er>& z#s9LncYxP$aD0*0+Vu)_=FhKXV#r+@tp&bpo;ca2+kipc>utLQZ8wu%vyZe;$S2vH zRC4MZmY}U7!M`U~TEr4XSsHddcHpn_=fvT!)#f&zJWE+uv>_-m4kes$m1usKBYGdg zA9o1M-n)})g}cjJggJw#ch5No%l4J8b{AVppDteluqY zU$gC?6@&amt<}x@0bC5im45QL6b9Nd1r1}vdf!-Mk=1ZHSX1gr$T*2tUHGC|Za2Ao z*gSgT0ldYCz#`k8u2>ZXqLKOdFq??SWUzfA!Cn_fo-wGV-tF-?^E8kgjU$eWG~$il zsQnmS?^Yn_tKDE5BSpXe(Zb=6jnY!k#YzN(HeW=!-O`7jG}x9qp03=mKFPMzI^rgOz?71?ryvRJ-Z#bQbt~g z!DiGX*Sq>vAGgQq%}IkK03uO@g#vo1Tgojx+zlWhBrrVDinX(X2G_ppE3Tb0NCqH) zLLd9}v$H#%`sAf$v^U`Sj9ewDAkgW{=vv4cw8ZngUIe!S2uUF5UTg|QKy6w^_@1sS zi~55dKTIn9d-OtEsX$%`?jIBb0}IeeOfmERtrvskRdHIp6Od9SuhxTW4Z3U~o~km% zdTFiB56OvYWrw`?NtI19u~gf$#sMRmST@~p88@YQ2Bx|%k3g$kKc)9Ug6UBfX!EdZ zKJkLr59}tjv`ubR<63Gd)34rpth#b%0a#9x&}r>znx0KCia{1Kyp^ zRBsIVYu&Nz_`iZIb>>zP-0h4AVcXFk?6%La95?f5cDj)oE&AC%FKj4YbXqI{A}dtA z8X|(idZId!V6K`flN5D8LgTQ(hjQhtwDHqqo;$hk2wD&xWKzGNzu0qVn>I!0pD*eymecD?xc4yl~T+h(M73=oI>=76&TxvrffW%bBCQfg|6Q;tD;1Om$X+CoaMbEZ_w z_l<>8jB*VPa%QI-)<l0XS#iydYVXcd+rcrhOydGhvXGU^r54KG(+wo-#2&c5<)T zJf(m9(*841tl1A#SA9V_sv&%Lqxh%BqzPX}2<9>qj2LBDTl<{h`sf= z-VM=k)2R~Ak>x#6x`czyo5S8l6m4IzLaVk}ik7|Zz$bmn!Uv4n$x$Sj9qpO=>qnVfi&7vg$?+-4glz_Eo$FnUZrxcI4 z7|nM3jK^VQ)d&R@49c=MHG!=@V1${qk3an$ThRd@cJao!`EVKWz@bI6Db#cR`r$5m z;{v!<+eI4C_i!a=-10+cRfR7ax5{fD|Ja7cYXit67v9r;eAXN1;9K5^=-0{vPQFIO z_OVWju2zOqc5+}nXp4#cvw{9XNY#YlJ{E-2GkI^y?O=mWQ+nwJ@jR~^!scBP^Ogb> z*tq4E$~Y;|T&life4Z)CHvoN<(xU3k4nVNHUgR|!&`~VWQqQG)qOEdZTv&($%R%es zaSymu@+d85MiS<8Q$m`iqq4HB@wB@uA)m%guYAw0$dNf& zsaoy0aNER0e$|qD%dijb-pTpScQgZ}`lB)DHOe9UC{!c2)j>Id$wzzAlx3>m*E(~# zwN?iD_}_O)2czyZScapZ%~Q*Wp6a|5D&*k2i_c9 zcTi$b?K!x=p`R7&(NQF_&mnz6M6*TW#Kc;5PS7lX%lQL@VLa#h=`&E)@4T!E$rTMn zJaryMqj>a#k8pJLiR+F`W6Vf~_D##(hUNzB35I#nZ@`QWvWMH`u$Rbw&kOdGCAvLt ztmw0i&-hNjXllGsW`up13LTJYPS4paK7YRfC?x-<3{j|?HBdG0JDkfWOE1jaAbX3X zt_fOJId`96B`W%ZCFv3hS{Gl(s%@XyX-%HoAz~Sv7!CQ8&b6jC%uOdH;;uUi){6eq zg=)P{Uzp4qGfdvz5e^lsHh)dG53%6_Ohf{eNeQkZ_eEzSo1=)=8|m4$5TLraigg>@ zm@DR&a>;*3wU_|W+W#^cuq^vH)T@MG=a@i-y%)fqvuakZg>K|ARRHa2#>)GyhtJ#6 z+llas(nJryZoEzvc8ilbo(tXM*8t`*>OsFC(CJ{3Hjf0JkDj+!4aA}}oJitSiS55? z&Wi^D_-%TI*=0l*lNRm(HmgeQ8++c<@n#0DlIucf*%|ljU5JI5v*nhRb9|nq7x(UQ zyHBW$$K_+tm@{v;f!1ihoBhZq8SsGf+e8n1It|>t#}FPHZSVqA#%SSs^#swojwNji zq*|TW?x(zbFZm) zEM;e;`a8YB0oY~lzw3k9Ga5}|0=j%e3PZ*AY^f3h!m4^hEkX8=lb^a*nr!-p9=aPCx9P-QX2)aPP}!dE84 zf@$mqrw+#jG>BhySVsC^&e%=oQXo{6X2^xIS=#0l#wr)OSC4Xm7*sJ~@Y%K8i;=HB z=_zNYIh&$)56}7og=AX=@gIx_6IR*B%b{*ezuH!;&tcPV2&&CD?+A|F7?rG|Oxv=w zUFPAdHn)R2`QuN-2X^vAiDZZO-?=7dnxm&b#+nhe!gofrw%xKW8MpAF&qRxL`&8jw zs#&|>7Iad8_U>8ptv#eko$|_Ah(~R6qv1{<6xUY$3GDJOLk$S9J!vG`ag~v5+#B9^ zfBHtdf8g$})v$Z~00{-@@cFaic9@62F8FoE!bjKb=*cAial2by>5T!fhR{R zQViT^rs`M#?sv{Bt1zIKLF1z{-_beuLA@~up4Sl(n48LgN0lK8DV+Y~YUf~r_+z_S zXJD%k&3QT-DETUsfxg+3hEi)(&GWNq|CkcQJm)&CZmG|4>a?5WXeS36k>9pk5H;W) zP2nb&@5Zhl1C}d@Hj^yDNuDKc3T(F^%SIb7#_pHpqV zLtC;=B!Nrcz#y-w!#gUd^Xb)Aa83|Iqm)cCPzbJ>cewlz`?-xjXL0S`exIbliV4J1 z57w@|$Di&c>d(9ITF_zlbMT7 z2hce)SnabwB0BzrrQmIyZG-vQ+n-~fXR26CO`0b&U9Ze)9qpagt0$8w9qketTV?Is zZwC})S(|PNuazM-ri(2TCMj#`(>%L1Us7q!(|Twqq8ka6ty&k|>=4n-*P8h31pa2~ zbE~k0F=SB*Df3VBb0hl$F3$(DvbD2{S>-IZu^R6r6|8!;zUw!iR{|)Iskl^jn!Ga3 zt3mx>BaNCwx0FSu`4wkeXmDjmNn>3o-5>>=zkOfKOj){ev{#B7INq7Z_QE=+8^QYE zisctOqjP!6--qnTls!Z>B_!_W(KcM1YBnz`u74ys_6qP7U@5t>C#wXUeB3 zJwPb)=T3^!Cgd3KruJ!9eCM~u(`1{~O-rR3RqPN$>fHH-<#2ZH7rbj`_aO`@RM&%$N#aA z2?dHS)cY1dqq`gf@D{h=*)MuoR~BTUmbhy1&UOiI-DdW$QF?2FK)S56DLQ1Q(H{h2 zTbHjON#9x*nnmg$MT?CerH6w`+n$3O9pWAx`Jv#cWiAMk?LUep%SlX%)pady8xn`B z4B_tQ3{D=21FA(i$|mCgMYrD&b{*uQ*Iy60Qrhrh4^)z)hAfzYLl|wF<4l^B7~cpO+L=q zO5@UP^U9^PZ#FWLvoxi3a~g@~ESDWUxVJ^|>c4N#1rE~qkr&9^Ilblq1HR-kR}lGJ zln2Vpm6YNpZ`7Z(ee?>VLoo#i;FVp06HPOR(2Dyu7^iujz)fvQQjd?rt&EX=+T} zUby7}^LpUyF^gL1T+!^GT|HTH1RxznDs@ap{9l|OEFM_d9Z!*=acZYKFLIzv)Yq#J zPjx?abNCvM1W)`BWrm*Bf0%o4fJZGoyAueJZ=$>JZ}Py(JzYbshu0p_C7UXf!YT`b zk0)+4`-RnVCgJIvQI6njT!*!*Ae~YJ!j~GS>68O2pZnnx>ViN-O#uF^_!@u@`gu?k z2qrPKUgZoQ&_l20+tFvn%O~3D0q6uKUcNb#;1pK0tJd?YS8gmQW*#XMVk}7JNBe^# z%?!Q>{3g)xd3ugTjZg>qhHDr`_j$0I)!X}exVdJypY!bmz6g0+SC+576tYV4+B8>{ zxt7%qL0)m9 zaWs*Q>{?8f?(?_EBp&BZm&dODHBnwN_CYAJeF255iZs1ZEs_^Bo5<74KC(d!+d^If z;x0)HSb&U_(UU85uFW6}-Dc7N!2e{zEF+YHOU>w~$m2d%nE~C33+hdP;s7lBnyXgZ zg(bGbVK}HJWc+7p9P4gtg6*z-{bG`}sYh81gEUvCuCzwy3N~Oh=R{4~)*w6|SWBmD z)j0JztH*tZQ$_Ws@0d#d!4XBUCSP{I!? zO0zTjO4#zu;h#DW;`KLueV zB5Xn=qf8zt?Ozx{X*UW=3aVA08n#QXcuor#X_}SU3e7^SLc&QASvPVgNW6cHFdIrj z2)z(q^Rq4x4Ch4ZF1-4-iVPVNhec(51Pwm z9QFt~i^$WFUyKM;6)Gt$K8)yi(W8)TIp$g=p6@$Gm1BJUNuv9%S&Zvkhqtj10P^1Y ze%rAy3#lQ9wuC`U*&O0y>7s7Y15+4JS*2-2zmd&FayW2qlp&^D+?cXATg1INPXtR4 z7sIb8%T2+Cn4FIc5t-FW)PG0YKg(Qr$z@QM@~riZA_^#btt{4@I{Y93L8PTnxiH7< z-kQ4^NyOx!iJev??Pw~~HJeXb(sgUn2xtGr)laT4qmdWq1S5|~oCUBkY^k1dp*HH+ zKIOn;&11lD(A-uQd!ynwhFM%khB({3;?T6gks#h%h)90+e0PfJ=MFJ-Mz9@ZMqN4j z3fKO4joNOvo2Z|5Xi*5^Dd)DOY)~6Phu}vBp8KNSSoTuF@9aR{e0G3#NS9mxbCxuZ zsx6jm(>4PB=3we1Nki?Y=^fD35%Xx7Ae`?Z*G9)FT@nvj3@qwth1z=f+-ZoF(w8L+*mO zwl@=$e&!J_?By0po~ce^Y^L&Wn#_C`DuGeMq2d1w&loaRQcUdE6Hn$pN$(@w2k^=6 z3%vS7$&W>JD`gHZt{Xu^I@O=1**#GP&YAHkU@(bcFazj39r?sYG;h=;s6Vu@g-Y%8 zlaa0*RwNv|K5Hl4ZEN>7o4W|4{)$4#D&O@@^z_s^`JF$c^jXDtG(`YituQXw`fOT8kFftrzVTfD5kb*rTMK5(G)qoM>a0mXT2mqNVYjZn{ z_RxRrxhSq(L8^liD-{$CGZz7Ha2CG>E`J@71Ts&gQ zM9VwsB_xq(eWMs0>$J%qHGABO_^e;xm&tJJePmPlBK2E%VHj9>8ysw$m1a&DpV>|L z2I1Oe3i%$dCrx8pb5L;zz>@!dg~`HOb|LC>!1#08YA)|Trz1`Hfmd|miMX#SL#~>Y zO2WSO5}77ZvjaWj=rIlh4B{7SQtYB~3^INk(Q647ym2d5B4cc85#Jc!-|Xgs^uP_q zHZ75_*e$a_!7s}@X6zOVOE(9fQWFIWKMw8{=S<|Cj(=W3`ciO+tEv0usGCT3fBzfB zQX8LYp1ckTzkPnhYXZcSYqjAQSL^+yinn*mIz-*SA5Th&KD4>zN|!*x(VD@=GdnB@ zGsHRjnEy2_YM>UfCizO>bJ7f?iiqP~r{lnGJB(V7%$fiOB2%dR@QUY&O0iDn27}sG zRA7#b2P`V$D@0~wPseer8XF#*yJ3@=td2mYCRv%Y9cbPl0r!?VF2mQY)#Rv*P@u3d z4|Gs37F!Okn}S8SFbIFs+egazxN239Jb2K^>3Ho(Blu3`!v6A!?|lsdGKW{!i*v4B zJ&-#|f6C3Y-H1OggBB%2VkkFkcUW$!qVGXN-@>P^PH}{jQ1_OSKC+Hu^U|g@+Us_I zviC^)$?-qomcH3)F+Sm7^$WN+FohAzLiHuq@P3RM7DfF%i4@(PSZUo84sq-Vi*kMS z!Uq**zr~t*$vW)wUw2-dI2ma!0If@Wv9?z-?zEc-0ts_;zd($rg%+_8%6tsIZTEjJ zAP*8+=tJK*99|~la>)LPJeU6wM34=)zVdpht>FmfqvChz$SRjvUXd;;+`)`Xj!X|S ze3?PY-?!68plJb@bh@VTnWA5m+&;ed=2cvIx>GILbL+Jj*4iG*U>*G*hYOykWLwcL zJ7cD~wQ+kM4dJjM_?#7mtxZ>7v9!sw$QiR-sew*US#EFpHylgfrVzRVQbG@!uHB}_ z+WZjz2r+$O;*=o;ZvA&vpzDF!zH!CCVFOK|3suP}P!d72)~z{##kho*`}<)tR7Ev8 z%Ymbx?jaX0A9}Xut*-Hm^ZO2438mkqciD~ckPdE~2$(_uM(jmhQt~oVS7AyS0)BYS z5JEe^6>kI;xz8f8~q;P>+*MTBscCSb`&!QWiJZ72!b@IvM(rd}f zogQq8?4EqxHJ`v5VEXVxpdg<{1wJl+Y~ zjqBM$|E!vVxTu+4x(}fnYC=;Dig{D{#x)j;^n00iFRACapNw1shnxJfRPs7k&&Sz4 zR|Ex_nfnbg0={6D;vjGcud$4u@LlLex$sfoOUq|_lyML7Bu5<})1-NHw)gxx9zcE) zd)ktQjqsu0L-_A@PVti&w^@=K=hFgn%5#LT#jra%JHnpcRpx_Vx2`f(7KAPC&o>mj ziKf{$J;Jtr_LA1@yhFn5JbTfluqFIHzubgxkZMUBMUP(im+)AkKo5obu8cm?NEcER zUSq@1XaJSeL)*XAnu0ahige{dN%>C#!inP5uh?+>^3qrSM~X$ zFBIluIBP~i#m(#AT!`Z7$Rk#6478am!gcAHK3lBB&FXdzi*CKPR0eu$wBeKS`EIsW z!&X9h=48i9l;6v7zgZkk4Z*`=UgSMa0ZtgE7^Uvl=;TwUww|o z-AL_Xb-6#V?wRdd2!EJQ6NFQ4)zKEqS}LLOhR3&8iU`7dy+)TxMRIPRO2{MroLWmw zE`in{dLj1+ixC14w*4aW3b%@>p_&=-xw%WjEu$U-?=X{mkojUsoaa=EO zkj;6Q!vTogT8f8=w3UAb8?o6;s16?Qd&cUz=%fpy_s=7l$6CDJUJAqe9vABYcks-l ziSNr=9yQJ}s4>gmX}LcAY95xwi;K{gw&(XIJ>xI}x^1wzjon}Azp{qUptKE}R>+pO z9E&R_847c@gXHv%nk(eZOGeCrYL@9&(rWA1?6#OEK^;P>V+_fY>b-NU3j)E?0!LnNQ<$ z#|b!Xc9{EK`-1f)*4GSy$uyuZomo%=B&sE7S9wQBDo6S|x6{B$edh#ihpTJ?YK07+ z2FD|>8z|0qz$g-8GanTxJ#fJAZaq71>`m$q9w`ETiwTGKAA?hzSO|7wwIl01z`DHb`!4RixG?Ma{M~d>OiX*?pX{_ji z&*LaAlXRn=wu)@Q7?2Q7r}@?uX=}h&N4qB7^99QHA9EqpEb}_vRNfJ7sXjo@kkOLGfsDtfOVgA%@zOv zwBx)fN5B{7g?@mj$l8`7SMV~PGIKZaA>ixya0EZlWCd8*kiza!5Cc!{@y5?g2fn=A z23K43^JenCb!q#shSaBrA9*OLe;lt56ygdymv8I#i;tKSHV<>#Z>0g%yIVyIQf)9y zNFpSR7jk@ewpCe@w?EZ?5Smb7l+K8E(NrO(ZQgkK24$agzRVQ|Y}{p1Ymuv#rB6w4 zW$yIY{H8+twl4jKcM}zmR-elDKA}(xEhg)#)n;>^x^FC&J2q z@g;bPM7+(@7m??v?u~Lqmd;RMB&D6Ccx6#a+znzrA<^EfW;C_7hTP@$uKN7;BF+v1 zK~OXOGel&L7Bs;LxUk4waWHT+ir3ydH(0A5KaMFQArIJp?<7mTH&>c!F zohZ@X1W@|1t*2Nyr2n>k@nE-aJp*(A1c{iDbVRAInH_PetM6HZ*&a-1yofY>u~VkJ z77O8_#fj0%ah%ew_idgQvmKZ&OC(}B+)>2mPrYhJy z=_*d<>DbJ0x(C+GFeT>#IS9ZbZ5eK&I(&O4#9@bCz|qK&@*$uAFpv1fEkD&x%f4cz za-$EStR<;HAk}I+lujXOhN+cMpFIp2EwRuPBlmQ21C)axb}$%_;woaVORHn2Nxm|< zFW%jnohQ|g9=gclmcGxe*%yox!wa#fM8J3ScedX`x6`bML)bQW7yNQnWS6Ov0g>$TIJLz)=(a=wxQf4&X5QnFE%$;u9ke>OxqGWPjZte z!Ehc!m%t6VQ})44BySi)TM_@xf|`DFkgp>1oH_z~bP@X$(|T0rEqVZ z*cJbRTo+2V5QXsE;oTudFAcK~^@}mUS-R~kPzlD%$!p4VFD9Gf#4>oa5}v}_U=9P9 zlNuI4M5KYi-+|1r+mEV7nBhx}8GHx~7kx4AcaUnS-Ixb|NyxNuMo45!Ys7hC?p}(| zeeQi_FlNFcbw8v3BtU<{YAz3lP6J;NZIy@gx!vU#2@H(#C1%rEt(x7$FgxCp(+_r& z+P_&tF&~z&d>%EvitFczA|O}L2v)-akBC2^)Iix9GDC^@(`$oj3WN|Mzj$Xpa#qMppGX?Y%D#yGrLl+?;H8@(>ty14 zHG8s80n1*=s`Xl@tFS;ZyJCkvMDSxk*)k53{mzu&yyh+8`z|OHh&{YvA`u*d0%!9W zzlSb?-6q3wIUbRc;D)ev-oDW#5m(vzc&0`iKADEx)yra}3VCYf89$5|`N7lkb53d4 zBXaamXV~k+1t1V245kQvF7ln?m~h7foIA92&X_eQpsAL!W+Fs&A0)DTd6*gM3XV;C zR^%xH5fI^;J(qYBjXWAi)8jQW4$KhA&Rm*4*hW_;x?UD*)PQh6HaK~xddHbQO2b-D(!#DG`bI;ZXJF4>pW+YZNDBu9+ zB?IXukNaJgGm%B=1}F>`b@=NX6~Nd9VOmvyI2{l1NXux1?xF;CECb&n7gS@Rk!wP3 z4Q(-i4vBG8ufv^aE+2c(I*+kXeoUYs)19kdw_bEq|9f)Tev4?Q6 zL?RAPAz_$Y9{+Sol9M7dHqAP%IFDG!ju9FClBhbvnk8jEQ&$cS+tYcxyxJ`nmz7zS z@WZm!1Frw-e1APF*rv3TP)zs&9wXwyH@&9sOWf~8)xhiD;`^enA#B5&afuWymMOTh zfn=4qgIVAF&}*=nwA{YsrTc!og`q2zWNpN~4=c`4W<7!_*0x2t)b_;^u>_=je$lI3 zIM(4Cv?t-rk?_`293DTsg&}JSLzdW+0~o2nt7p$dhQv|EFd*7BF4U#9(LAx6dW}o^ zSY$!KC&1dT;qcPf-><54Aj*(k@-&3_>12stk2{@a6*}P68O){GIgPi5BXNXXA&{a* zN^+UJ0$MR9My}=v=F5PHRr%yA**4D#^v|a|(_}@huMA04Qn~hQ7Na=aS}bYJpLL(k z7mOEFXAOBNwCVMToF}fs}pqFq=uc}Wf>*n>EsA6BabQ`{q7ci zoKa%3nCn4{YXj7uH?{LmbV~r1ozm0Mm3Zbr)6F2g&fn{mndNDPubLfz^5ph?-2Jv& zm%x)IiNU(Vr?P?co5fgJx{6&RKW3<;>^xBATYz`Gp!^;$`-7u#A}6FUZ2VoNwhIRy z!sQzE`|c9R?M568bX(uL?#e~Wi^o6E=&y@paw=^;S{EJ?q1T(y42;Vr`;yc*F1S68{}Uu$ ztih)2eRXCl4ubNfvoCHGYmOhU)4&N!G{&xS{PyT}k>WOGi%6}B_i~)*^%%Opy(-NV z7p?5B*+RcY)b)*Bj}ce1?jZ3pq(|O%U*CByacQU1q7E$px_cy|iiFIz!rGlj0MF$~ z8jG-nlhJ%~+>22V!ATiyMndVeTeDT80Qrze(Nmo5TBodorxNDtx|jh(o@$d=Msj=S z^=cQw03H?d^#?Zv#IeYzmw}@y2r%9j0dcirT*%<`TP4!wz|=`cQsI(u=-6%L=>r|O2RGQre9pH#i zy-N<*yHF?Z){n6u@4zOD5^&`GT1HCnokE7=Jf1+hi9#W-$Y$)ycz%)nSBU)rgYkf>+)Vs< zR@ParyIVkC-|Mt%px7-M4aEmHRAhkY;Pyvj$~1$IMt9>X8fMIh+}f*y+vTf28GyE+ zwqO@sDjKJhkq=K45b@YBbO7&37ngl9S`a z>Pug2MoPJqfV{|eQ_;B<0ssT#Yw_Zed6vOfeBoW@yXf#e81+J^3Kab0a&f4)=wJ)2 zV0XsP_>|=;qS0+$@wsZ2M0?ryDXxA4O&-<}N)(wGYO`gQy^lO;GaVC?pa?!f^%iP= z>QE0mW=mfN#`MyQ!(c^0QoIhUkkt+@AiNu_Ud4)@=0S#XSV#2_!W$UCvV#%z-|?Z% zvHyvg^z^`Aw1kBZp+bLocH+dDSUul{Mienkn@mPXcR<^>1O~uZu}M=5`3)t3gz_{y zYeFC%qDby;-Lpeb91%xK}i^@Sxo^Q=vkdSy;Qe%6Ih-8HApX8sw3-LXQ@>gev4a6Ft<{zLa*ve{16 z5O3cXQs7$|J^NMU;E7N+BOW2oA;Dr>&jIyyzauoiQ*m%GZp1loww4ipzLWVSObdfS z1=f%G-{G|$d3a{94YsLF%1dUod6N9^AH+XbxOU4{8|`t~oo*@cqL_A@O%$aA1E>p+ zrMR#^RvHZ{yn6eIHS-}p7vLpeR6v=*L5uV73mME4juoWX!r#fAU*V@y@;0=t)2tzj z$Y09|6WAbbgZlP=PN|FiEy7<$3K)SxfJj8>&S6g{7p%X`DRj|YRkc({8QmJdqoX^g ztp+Lp*nqkn5-^7lEIB&o!q1NjbPONddSV{WelneUIHi|b|(=F`KR64KJ3ocXGrF+-BFMLyOaKsQJ455&y3~SuOVkZ@ncREv&Rgc zYN^M}II8=1RQ6X^=O_GyXOEQUpCc6~#s|*Shpg-Jk5*ig^B7Waf13n+B22{N489Z| z@Ec5!<=Zgmm|}j%_J19Yv=Bh4_%)Hw?0@74F#+mp!c7l2g{d4zzkz8`h$@lV_x_=q3f>GeZo@Yf3%dI9yd_TfnVv&I|bL(ZKc zKO5i^5lCezOVPkS(CGk*X1xA!{Aby~KZQvk9l|k1CQoSO$f44>HTv^6SlqcCA6u$nM|b!NNF{GkKva5i+KP3|NryD)x#e6_Au)> z>>qslcQ)&LF|o}9``RQk z?9=^&egD2z|Mral4mrTBI6V10qx89y>_M@d+&T|z2;La(K`7+bUnFhPWO&-}l zV((wl+*1w18Xg}AwJHxJyq$pD+FwfAZ4IZZS&$h)fCO;T4!AH^XAx-382iRx%n8|_ z%g^kMhRE^)U8!8vAcwW!F(8GgY}gky(MQPTBI0_z_r@g9r3+GzrA2%hp`+RayuS7(#D zVkKx;&%7tDPJUOTslfSU5zUbjc{H63Qm_ zPjy_N^Q4CV#nxMfMcsC7-_jjJ$4~=^ASpF;4W)EQmq;n#&@gll4T6HCz(|RNbP3W3 z(%mH;A|?I)InV2P-{*d>`y1cbHaPyrvDUtR`@VL|n#Of8aEj)fo74#ox-~OPiNR2~ zKMOG+kG;f0H*0c#J^@=^wsG6!(|Qf-O#1Le`P{k0^;7gE>EQKA@}*^`%IRiKVE0Ml z?*r{ZPo5zU-m49h@H?S@FK0`1#b^p`Cu5n91X+?kPk&le@OuY8+Q_@fwd%m;%5|iD zCu1S~jziP@bhAcg;?uKK!1rD_-gqcl6pBNlRH^i7C;9>1(x;2#O@oU+8_9P4Okcm{ zOcak5+W7D08(T=v6e1wl+@|#vji0sTW?Cf?OM}<$&lzM~$x5eRu zlMnIe6(!OE#MZ+gd-l7l*1G`mFcIXrq%4`*F>s5kQDS4FuU8|(AUu^zRqTSM1RU&cFTkQI@lY|Bf zOiV?5f2+wd2R9vR=p~$Of9YqYo9D<^!g-(I{&2Q5{n;ix42|gP1jI0tqrD6MTuJ<> z+W9ny%P0?KIr8O?|HTEhAbP*0?LD9}Ng}yi{mh3a$m^th^QQq|BRWq1>F~c?(;xAm z=<$F#z39M9VmC^+FfiA1Z#p+JhVpU2SG!g-hWEV`+fEADB%@Na6-;lw-IN{lS)N6q z6DzECUrUdl$t_f2nq=_C(TY}$NrGMQq1Sj93u7br&;csuM5Ax6pNzl0{P4kD_eQ5J z=|b~dP?O|6k~aJ#xIR33CBE$GyB~kRhj}snfT=fKYV);JZ1*~V)<9zdMLZe-dwQzi zAR1Y{5#D-VvPsL|Yd7PQ6iUqZwL#Vc^?0;T(#?&_YBnRJ^GS0FJdak{LB&J~zL(Od zPl&&{wTKJ~xIN+ZKXtDaUZ+A^@VP1OgcA#sC#E*-nkG9LtI}k`}TUH^N?FQ0lbtV}dSkPsW&zMofcBBq`DgH49W7_o|uG0VY z5XnwL;Qe>UcejC4@1K@8HywU68BIPJRu=d4zE%XL>$-Le08VeWD zkWDcef9O`ZF03F_2Q zsg;xY2jwXM!Pi5?I1 z<2pB2QU#<=#GZ;eb`ZMV#=}+v4voz5q_4M&c;7Y`a4@A`CQ8JdvA3@JI42 z4VP?ZWMBtS?rrHKrx*%Ll@S}vM=Ez>p4TuBfUG>0q;^hsqJ%8cIP!Gx%Y zQwm9p|65-z^L$fvadhJI6pH?dL{~r842c(`K0efWzPK-yuN<+`z!uqawAM4xn;_HT zLO&E{k{8r*g~D1Gb4Kf_Ad%Ly8_Ya1Pf}Av;0jfIP!gnoVa&HH#`vf9MGFF@#I>C; z4|@D&j=$38fl~g_?wjVW>)8)?uFIg#4?t<@f}M%=Tob6mpiz?d9T&X@@P{OZ)wKoE zKwkPC{o>|$QR)u|{vAm|nVL{-VG+l-dgWC*1&Eg8L5YceKIuc)%L z$zy+`lK0eiR(crs^U27`PWZ)1Wy72buY?Iw_$)L$_UDZVVEj&n$dW>}EL+>-e#iy=+frnSjU4hw8GXm9IysCKEgKS!M|C?tgm-a-q(#yrVZ z`NAT^K}X7K)|fJ@5AC-CcAwWHPNOfk3#$g*%h8~XM>4{j)wVzBN;+6cb|=b6IWF>W zv(|ysARU>z>#C^()K&F-gImuYcp1^=Va3gZPMn#4|9IY9?v~qnI9w{SQsfz!*24wV zccZ!*2pbFYDdEeO6>5d?`&nC9t+-VykZa@yT~CWQum&UXIQ5~HgDG_Ok_Gs~8!ljM zB!aB4SPrbMlFA6;YFlqFP11K>&a>7nrt>^J2M;pUuhLcThA}@Zri4=@nl29!G2|Ke z@8R?b-)?Vx{0Wof(>Vi`^Alm^w=j)XaPp?BJvgKcyuFBQ^t*V-y6`%%Z>f~RqsIl_D`o|P=Q_KDqz z?c4}akHs|?>R#O{0Jx>eKLAs3FJ7l{o0W3^Gz(~>;3-N>MuEZz^(5Fw(Yfp7zUp!| zdpnAAciY3X6DPX#$A7B$Kcq5uK`ROK+ghTbpRM1{qREY{ZzAG$KF_%AL0CF|Dk5ET z*p+CdU!cUj4op>Oy}TFND$K zWY#gREbamX6){>rBzO#2)ITcm%aihTH+aXk{*W+NP$uJvK1tKv@>;W8=w->PUq2uz zz4_tPkB*OrT8ji_vXV%%cPB*fl4%ErKWg7(`?`mbqk?*~rJAI(1!Znfo_q73M0%B5 zb3S6UhyV=zk1qe~PP_%{{BR+iaZ(q8KlPAJF1oQLzpE^YCT(Cc9aXAhqNA}sMNBM| zsXzml(^+>+x-qY^b%JShmzjwdztaD_bDEIBugU<0Gk4YUqpgP8j(Y2Sh2g1sX{)>; zpPC3i-kk6h8~oL8P!tySl{3D_+$iB-K~Dfr72o9Yr|4yR_2yj1NJ^JjeI3Fh9A2to zU^)05NaJQ7-xjIyTsQ}vn=7vcV2SamY$wSC2JH9N!;kWa_gh|WKly2OuW%cT2;K(X zoiFV#Xb8k^iK;kY+QNT82DrlXI5e}U1SYyGI4KhsvHO?JY0tjnf<#iZ;^>8S04@Ky zSF|AU6fXEjPeP8?(wk}KmejIBBb7?QKJphXybHS`=cQ9pS#$vTW1%WPc9!zGd-{hy zS-5p>AO-RKm^!^pSm1H{O$185nN`X! z)gd}JH9tQ;f3~2lIMf0^3Zav-&9v*Q9w!;Wg)Xx>J~}l@f`qeEs>0jMIyV9s5 z+CjR~yFQ3r{@CCxW(bsbekj0Jg0}TtBc(@z6UHwD7Sp&Kz@kscv-^IdEz`k&V0#%|j<3P>n% zguD;FSWtlShGp`am!^F?gb(udHw5GEuNNodgBs!)uD-t}&!l2OI~OM8kdN8;`H929 z=q5Y#HTI#H@7caSkc#cLsQLTa*|7B+P>`XB9pZPX^1kGg;rsQCe%d*4xGPef77n8? z=g|O`WnjM3+%cE5gXgR;U>~wed|EWD@-&-1CzIq$@0AK`{|0cQ-uxlnqmV*8B3$(GD4u;~A3=gDAx_ zCGn{f&wC2g(^dRGY&9#d23%|BRj!!NxA-QA!IqY)=f}#bT1xeLty!vt@U7PCCnYdo(nqOVQQj_1K7GueABR z`TX`bh0Q?Ni$BDd<85fRdMz@3VeaoQ`e-%(MSJsmjs{Q8nhwLAWPDHGrJi_+QKhpM z%TKA!Vf?Gj|IgRX5xuVY`p4&~Wk>l0#N6=lLB^ZZ*UGh0Qc@8kkM2%%Lr)4w7{yWD z@fnFPL}LYM1Xo#>AMp||0(J9i4y))m6Y^T5^-&D1g|V(Y4(O6xV{opsc++mP%SGVk zm{udS!EDeuQ8rX86a0?rd36km6oB(tU|e`M{Shf{3Cw|ez_G=k2Y1TkoBM1l?vDrS zlDwcR&;F$tL#53yS7*=_%^t-!Ki3XLL@At%R^G58MB+DD4aDEejT)l&FmTWN#8o@? zzvt0giy*48)+wJ}BMIZFLTwIcfYSfc)=NWLTw>F@K7+%c7Eh-qfieEh3_z2fEc@3l zD3nA)^F#LlDH$^V;b7PFt`%T{t1)Qi=mh>)hciC&=``B0fxl;&=miGuEw;N3mEx&q z*zF5rz)NXK?@96{rwi@2JLAnxvWjFfS6~_0&g~#z&-p3;>P^THkO6wqeT;Mai1y4 zzTkU9!uo4=Pt0O2nF2hV#jf+dvT}{XUOR!->rUj=xK<6-2oB-ta3qs|DVBHvI80)n zeN=bmiOm4pkcDX>e6=jqKkrj~kUtUYx>r6nQ8i8bYbVlIEA#TrBMN0=$eCtB55BTv z3>{H~5io$&>%TI#KSE9U2@<_X94b&dZa&w-o^mt;UC2(a+0{?F+`U+eI}L2kL8e?E z>ZTkcIKH1$OG{J`Ux~H6oDnVggzK_!ZU!*B-cvz_6cJantF+|5G&HMkWx0htuf2I- zyK{wJDhS2TX;eczZ@r@QKR=&K&RQ|N|7?9v+iLGWSR4s}Zgs;K`$yaZkt1t@SXYUj z$+uWVU|PbGg$O03oT0y~zr}O}O(^T&`Gh7!f%JKFJJ4|G;|= zkm_w-TftA0@zg&&imd;OE|eiVO~D1H>WGSD?A^9OBGBwu9V?;apoZ^G0EEsQ=GWT8 z=%FKMIk!a{w@3#^P;zT&o&OcO3NuF@Q*#UD50+zDr5L+C?ulw2s^gI z7HRlE(0S}lC7C{q*X7hU!}KC1wI&J$tP=!ur&i{JWOJ1i1OTy2moHl9WKe!vmh}NT zB%+BLl?^b1Mz=T)CqsBSv)OnBj$Ne7pCRorD5 z+!!vAPBK|ipC77cuX3*P6BDK==TDfC(PBm{R;%zODd+V}qeDsq+fjY5?ru|(^j`Sz zo@%h&;dyBmDz+shCVniJscJcoATqFHcMWoiio%2R?vSneVd7*y4vY7aU~dU~aPYfs zI^N<(A~47LEZu^DHt`$fUDx|^^)v?jCWi}AqS1m2&uZcum-CQuuKgZJB&b%dw@wcn z>s)oH9>&3|Q(vKrIknFec+DwQtq>lJRKu{_OOUzUO6dwRE0>c*BB2GM(u1lR#rdv3 zd;}u8#{kVqyq$oY1$}eSzg#WrHr%cgH)VI}P=KSb#z=IB^HbUO;xAje0GC)~ddO9; zNCpPvJ##u9w?htZ-|c*EIj8T3U!!viRO-3ZA6*LM*R2PMGM9@YElJCm*%MQ9uV0TQ zlT7jKc;W79E!M)(;|H_EcY5j1o=i)_?8Vmo(r`6xZ_%UNPqTg%g=5@wZRU8?hZ8Wx zZP6=bx~y+$Gf;Mx(hupMxC?nu7p&dFC`e6ByncARiW!BM<~-E{oqO0!_AFVVp9#k{ zl4(;}TE=-rHng#y-^rzUME}qJMJ3RSrSv=DF^m)U3 zmoF=H{1N-=Kpa}Ecq?g#^za{PJMa|lft+My<xMq$+hr z9v3XQh-EcR4YO*$vB|+`&M~_>PUq6jtLKd@qDTG!;P|M~)%UD3Azk53bSC&-JvHQ}m7VOiz(f#d9B(gtEsVRLB?u9TW`nnEPW zX94S;NQ$Z@<=?!=8kp$u!5G=_>|*;vS}9M{UU4yC;`(1R0OK|^yJua!Q1s@gFn@xC?B8#_ZN(?>Yw`nPbf}O)>Gfcg zBw8c`;twlWl#pnS$q%YSSHUOVWTc4jW(|p~B!7F08G<<`K{*HuTPx#>CmKs02g;iL zEumd;&TaJfebW1>S}VP}2@$U3lo;q3xJ0o|sqVC-scGFFLax=$H$f%(`4OCumbq4~ zxw=Baqy+T1PgCxn*UiTWsCgz=8STEWqe-w6J#>0^!VNdfPN6{Ohi)=Cj{R8q#PK7? zn*ch$$INKN+*1Jrs{AC-_)OQHTMX6xDHeqBIt{A6P|QRp?D)~R4$-#PC|`7XK|tkD z_~ao~$_d5Kb-&5P6Ikua<6{k?ASxvJ(3vaW;1%l?5gq^hjBP}H%@o(jaj8+>NK_W%E5Luz<0&nUC&v7H^=PA zzI5`mSNclj;>f@ZR9If>Q0I7U$Ol;`ftP!&Ql|b5KmJ&`+ypkF!S5C#s1Xp0Yic|axetcKj z{)zC|az&QJn_%*@RwN3;eo}9=gssq2UW{)g^=c6&7S@wS{Nca>MUPU zT+W)7_!u3>j0FXEm7-Gs&txQwGI$xwqr!#pO=>mo7_3wwCOhbguKP5~4ttdG`_pGA zeCVK45>-JCCCkbb+0DztMU;?k{nDJ&$wyf2qqv#bhtb3Ea#)r?gj>SviE%#Cr8cXt z#yYII(jju=GY7em;|o!EG!y3|$~m!bx1=~u>MxE*35Fbw9(RLgHb0BG$&YT2CtuO8 zS{zHBM;C-4!0oKoce{7j3A88Iaa~myk3D4ySwf6hUTgzcu>;G;8^+BBR%vow*5$Sm zIAdZNHZgazOb9_9^M{iVTQ~1kGcHcp0FZ1&Tl6LgzlSEwjMRIE>a(vn&{x*xygz5E zx}$*8ko_NN0u>2(Y&x9Q7f2zxG~bR$YeoY|Dia>O81yaCA4_Joyna6WcrRYwn`+f; z%r9`Q3FC0%>`H*D?D?0H9j@XT%SXqP5{F_-DTusX5?BUb>)&S9-%1kgy$RAT?^gdW z%;rDxMB7h|<@a7ozISxs%Pk-dPg0F^lLR5K8cI2WKU@*Z=wUus;EfR$y2)4mt<;wz z&yGpHb**FizEYS5abN2&$#x3JLLR*&Kf*+y@fkM@v0>rsv@ttj61K*G9;W|=(6f7Z?VT;PxO0n!zZ*tsWElaO4BECK?VH3dMo{ zXkXrwqX;|R9Fmw5#Snfq|CteF_s)ekfOU{vA`TOiR3V$O)c5ZTS{Tpw3-|MZo@N}_ z-Y802f$&v_mis?%KEE`_))?G-gK<%xNOmDw7bQzQcw9WQlek?F-PwX3pCqgeE*1 zUwu|js(BbsLsNq~;=ev{A?q|&g4rID(8>CG9_~Pu&ks}WCCE8u6k)ioQY|1`aN-GO zJ<0Hco!VL_MQ&|TRqo(js+NKHUVrNWv+@hP=)H*N+yrv7LaK4gA`eqx|#XDWds+N1|G| z_Wa@rTb9b%%&q(H&@GPatC=6jJriCl`{2368A!(`$L-+xU*g4CDa z+wvH{5{3bS>@SSkm~2s-+09p*gINd~Dg5EbUI1!)<|^tqJ*c@u$vd;B0`w*~Oo0R2 z+Km>S)N5T12R3m0Y?(loaEviejrOC+<_+_B>?#e^=p*2+61`GVH51fDAZV(l!X4y@ zPSQv~#1PRoQsTRX^N3H=G6SJwShmVUBjlwr_5+b=*sY|;%}0}S5J&}nuO;=iziWc2 zAb%-AyqJs&US=n0OImllH$L>>oeE7B8-9p@SGlF&!u4$N^LeSh-N|B}me9GKJk`Oi zBMlT`Rr3YbXS*EQ)!&LfbwM2mHN=INR~^b>P{E&#zX_fX5i{f^+M?CoW7VTQC z*1Go@x8;xTETV>)g58Tux54syWY(3^skSXi^J5N_@aaJ|ukzP(Mzm$YUvxe_HSP`a z?`a4P8t`0fVnB3A(ZT$HG94WzzLwkx|i=#&EW@i?>={0)$xyi za2Nyy?AuFBzk$ho)zzB@3k0U1sKvO7`I`yPzq={qxabf#y))m)2|W6wJ8l~k1)pXT zcc1?727CVeX!?j;2RBVigeA8WHL?v@&2BJU-tweXWd)g^#?LSYXp6RMJIZr6VbO_1 z=J&)GYkmqiucFQg94scd2Im%o;Icw!JS5*ae(-^CP7q-@Ll;Vy3=KPPT)a0XBm zt>wU4+qKNd{J_+4x!UjB&JbbduT#K8f41uMcQS+~41;NddX~dIG)?Jn1WY7eDl6dp%(C_`f71gZM^2xwxdRK(}TwLtG$5(JSasZX6 zwcaCesdohRJX5N8KdM`0l-!Ll_?CWLV+_nvH=Vn5+pNwnBl5|LHP!yw4`sQiSNr1m zip`sKP56%{H#VF+3U0_^tIa1LT)a1Mp0Bd39he?xh`br$)Eg#c`Z7Z#pc3;37%1zM zwiEMx19JJe-&d2#ouxsI)I*~T@>}=$(a!bB(&vUke#|EElh&*Qp_r#8aoNr>g#6#B zM5~j`PaZBLTA&{I{!$Ucu!9m#C7860Xl9n<^$kt|@%%vbvN0P|NK;!*QZ9rIY>Jp~ zxvpz%-Z|lxFZXSl@+ckpB$lWGl&|>Ey2&0nA<<4pB~0_^@vgrP-f!WxoD`HJt9LHjWTR$UI z9&X{0xDgQVUGcoVYNJYSIFxHLKaR%E_maJxK4%Z3z4N;v(v+h+yOFuQOgB<%;Tcp- z7QrXqH=W$_h+o{4vz2(e;GpM7ypq{iOGE15OM3Ie37B}pOGzhlS!f*Ki+)#T=Uk`W zZ0N*8SYM*ppS>S$r*S0TI8}4z9O64chUKE&#MLd1bZn47W|>!GRi~clLyGniRk(@; zy!6PSsrz>HqmvYxRA9_6Oq>dixi7V5N})sFB5n|=tM&W1rU!_LjnUj2){+9)*N}^r z$J4)BB3LqaSWhQB1cs?V7(ZR3H^@P(I~;BdEdFByMpaT>&j#fgF@Prkp3}8+f9yHb z(z%Q0&wTgHPLlUF)f3yMsN})Yt}sYY?D2e>zYOat9i-gn1COob@tEeTG+b?6pqFs) zZSBCje?_bhfnHIBi1;2Q1eix85X-xtr=fD9QI6~9*%Ul=PBMv}%PVfUm!$W)ST%GL zFr+l{nK!d>5*c9C&QS208rB4&%%ZEy3>TgL9>}usdqwl>mH{SY?Z~q(rnxDr6qv6OaLzIOHb4#3*oe>Z^=rW#u z9?YeM-~hgV765z?Xgzkob_E6L+LcEN`R7UbJ*XA`9O7f=!vv{$z{+@GSf0m`m7)}H zEpux*q#Bth&MBQk#IH<}LWnBjB4J~orpG6!2FG`~-205LYg2}jSpC`&qAvg9EBr@ZfzY~c~wDTEXw{PyKOAf+6~tcErNB^J=A}hkY*+6%ycik!u!yfE`y8Uw?w@F?owO(B zt;k2GhoVbo=^najRpl$pxhS5Of0_c&O8u`Q^}o;w3pD}Kk))4njBb8Pm@FS;=(``6 zSJ}MyQ}u6t7g^OW43nm6;vPvLYpn?fDWP)ly-h?g8&XAY#drL_j_8YnSSN=7u{Ew* zO-_4e?}kV;lMXF1m9*Wlhbs0`Y+0)|0@qVK8jeF16~tO|?k#9-de-l;nMD_AK-UGh^I(DdW3{ijR)* ztd?VNm_amuDGvH6!1?O}TVdRv`x@(1?k63FL8OV1mkS~xq{#A7f)31BIpymJZZ z_7(`y^c5}8&(wyRBjMsRMCfN{no+~(@ohGLv=2RLrdL+?Z6LHt&L>k}9?>U>i|>2v zV!tI$fNI$LGDfVZNx^>yT`JMB?orSkxO)C|uT!9~aa-@@mHKYsLK8=G;WC|Av{g)0 z%g5tam6zdgD=L49=oGPB!wG8u6S8ZhVomp@6*ygGA5^>@n>3?o<9r^-yaW| z-dH$~+%3Xvg#XE**Oy`5CmFqK8y<5SUFYocUB0u9(VZ{m-Mg~f7F{ZNvmThpV%81- zfm;y!`s%TCm~nZRd>a~C`x2XYj}uuXL^wie$K`EfSK3dO-ix?DN2Ks-khX>bXtON@ z(-O$sDvF^4Qj<#bN{SNAImzygzhV;TgPu`k0_u5=Gz#MB_l9@CL1QF5hB zwS%hshWUVG829T)=+r;>1vq*k@^c-H zJ$pd8HeYq>jRLp{8Hqd}4c~+Mq0U(DWn`BWLtGH+yl?2zRRTQ^7T6gAzV`$K4_6m2 zuTZ&Q;z8$EiQIWF{JK*Gwq;Y&(ux=Xz=n0br;b)fkWm^uYt^Lziny_0LHDf&Ez@_^ zUXS+Wh2rXxv;AZy+*N3_I(BkM?c&EgK7TeB5%qjCJHay#FtRwTsK&gk#DSm!#Euau z0_RWu`K|wRVo)!J_tG8>r~MT~h=bd79Mhx?R7PEWrFZ^*i;faSE$BFPAhjc?%D!R) zXJesM;@tkQw+RozUFrW|5sph8A;}~EV7o7qPN5_;@cP)pZbXG4YY@E;V`~Aw66O>2 z@CA78;7KYki^ZO4jho%us=4epG1$Yp6hv#zZW1@j0oTRiypkb=EqcQySoZ-SIW(;H zOOAz;FC2!Aczt%PG)WlO!#pgQx2eAoBnRYrAH4wP?yB*xg)XN#^&HC#>r7&U9h^fk zE@ke{s0%r+mB|1)Eb7{lo31A@2ZNPylkAFmTPlkRO<8W+|8!4KlOD{nEeI^s_{8XN zo+!vSi`H6A?b(??=_wOMclE_Y^uh();snc6Bsbm1@+ zU=0@&X&?{fk#Qo6+7W4kmt{Q(=}{Z130Z9(6CTvzKZLbkHPzgq9RIX2xN?v?Lji#c zBqlgospN!gX_*7bMQt~WEhYvYbwY?M+R@xhaet+E+}iC?vCACG6%5&=EZ^9#!m)Q( zA$j!BkF8V}D|CNfj?-v7$2OzfEXU`Qs$1&jKi0*0 zZp=pa1JptB^9eCilg@kp@p!BT*@q{-uHw>2p4PulL{^EaSV`l~fk;AfG3)+GryT+i zk|xxCCO&bCiax`2L$nU&r|?P}b`ca#-mXw#Sf|0DU!dl|z;o~%6nDqH&y5^MihQx| ziRZlXp;DbSs^a@cLi_(6UmslH_(tAX?P?q@ODlTPMcXUDXNfOV5VFkcqJJ_WsSYj< z%#XNMaV4JgD7t)XIX<}3AR%rN{ZqR?*9EGaXu0~f2A2dU3UPEpJlxa&99Re$>J@3p zog>04K6+QK#tRbLtkBTQxV#mA)H2}o`C*)(*{3AzT7X;GdA%*RYE#~P5<;vR?X#s? zrBA08GH`J7_BmS5av{pDE|VZ_F6lB&XqD|D+m=s&hPTw%<#8VBTW`i5X}b6gL5+0} zvDB2t<4VN7xbPeEA40g3uTp*v0`)+{6q|mGYNdctr^3o8^-L0Bq7phfQ&XK*DVPQ* zU!apJhxZco-fMhxG^j@$`$O}D_eYTx8Bd%VKTwYF>CmyzB?5K*x~gij(2I5D#*yFQ zo7t4#^*qhy4+HA8PeZdB(K)e|;=b|9!#!ajTrGNDVk_$K@5+Zdk$~{pn6bUr zzIY9AQ#!Oz0RuXb%h}I-p8;Ndd`{V&AxtfPoY}n!OIHNPt7XmHu9Ox2Q4ZjFbEY!n z##2e8uX|fm?Icy&td>#@AGCjGioaoQ;Up=sCIjJ4Rf8g?Ls(^inIFTqzt>e7Ew(z& zHUG)CI!re3f8#l68|_eUo_My~7L2h25P4r^I4J0@wcq633T;rq)3&dP9$Fp~H%3^q zELZ48Tb2Lb6KC0^;M62mJv21!!8IdRu=jv%2VLMnPX^XyR0`iR4_%P`an_X&Q+kM% z&RL}gvn9=#p#tg-0fE|~c>CIn9&yr0(%|QR(nF?CtwxjuBJH24^3y(KW zP}_jy?B;+o*j+%0d$A7U44AN@B2R#@K9(^Sd++^ssSF?mHV~!!b7odJosiSKVQyiR z05n3KjbILMMMJ^zhw>$TalYW_DT^^$Ui5ehd;_`n;FyfPIuPbMJEiu+LB{8Y5Qui~ zY(6Z@)+SB}HbBJce9!grgnj?f3`St!sO%(@b)D7@`%>kq-|W2!^qFuVb(P57+rl_d zK{wy+&k#*O+|J|@Cj;|SFYq}7+mM9!_x95^xevBvHYgj{6c8UUdSd8|KMA&JDSiGW zy#xq5I4P~N8!9=E@F&#)u4^qVl5xYFXL~PE=kBktz+1nr*2!-u4_>ETUDxOH`Tbr| zS%|Tq*67HQ569o$BHLiRj|^dO`e-(Xb84Jau=T}j@pQgDu7qiCcOwtR=e?VCE_vRQ zZ67nY@zdg?E-3AyR)N?jlS7?&Z~>gc%Q=eRd5%tO5|)@QzQo^F{`0%p;q)(l@N2Zl zeL@lrH3BlY>Qm~Z*db+k@+P{Xn(^w#$SfRP&|;9iz*vyzcv!IzJId;&Xt z0t=Tr@G%5hUS9^%LE%IS$^JZN76)_v`bCSse)Sb=6YJs{vMVbw&&6o<;M z93sxK=rLl1vy^YnfH`n^@!xhVxEon-1$sO>7Cy4d3n~3r=K6R3<=-*wV7p#V{Ex~U z5ZXP;!WYmgnFDRllW7-DSJVq}xcofBvk9eMr!v4O_vzQ4bjv+^rl zRi;qCYJQ=VSelEEj~ROs(U(sHTem4*1EzyU&D#!$QR+VUlnf4} zGAQVmYMGZGrl2roSv3+*1Fr?5KI#}#%N5N6N`DwdLbm_~8}N^7h~3E6hzr8VBSM+U zrZdyfO-VCIDV-nGqRni-hu}rVBRN)d(5!AeHMx+f%SF7Y+&lQu`0n{TI43H~m-zpm z>z6a&aj)=(&OS#a zKa{vy6%L|cr{>L^(q@`Jb|OAN^q7elqeVuUu9yS&eU}WmqAdBzz%e;|Jw=7>USMQR zOJj3zJyU4}^M$ffu@tIz7Bd4!O7BC2FX~Zn5eWSO&5k;9mD$mC?mvN$4+*6NEdtkm zCgLi1`k17IZ381CcL-Db30*u;2vK3TH-zFs18fO;uYn?bir2h(*@wrf1z3ls1#BK1 z$&l^jLLCbfXYh~kEaSSacK*4qK8z3j!UCWCpOX$U$L0Q~sS-^KGU;?Vn-}AtWc>E2 zO4SzzoN{O^-Ka}mzWYahk*DH2;3Q%RxydSJQGHG$y~@BlGzP2j6y<7Ol%c@JZMVSK zzJ)||rU~f_<(|$bW2lxFP1PoUKiqIDtgBa>jQrnDod5jXrq0{}sAzs`OJ#<|@41b3 z7S9u1!nXGS_M)fKT}d9^QR%+CXvHV-@gX>S2kEsGcja!8Tx%@Uy|051a_M z4wm~AsL02Y`-{84;3$!Y0rUlHacycK9SAirTF%E8*G(5mlT~Xp^)Qa z{A(adfiGG}MDSatLwPr?(xNdK<9)DG>Nw{ZTX?hpEIjxTMjnHq)@;_!#9`8+62m~A zaoTa~-PPeB6nk%+j3x(gpZMQcPCLEhVj)tb-W1QNv&=PaL8z-#+JhD3fIcXv-~M3R z9t%wRsOI(DWEA89vw6}>{u`F4Xl+e|_IGB~rC}K-`Bw{orV^q6Y0`s^uenEXhoAX< zZ|~ZP)ohHrBzs=>C2T63rTQStYr_?C6-y^LX>jeqx{I^^h8IXgTkIj{&@2In3g75H zEe?o&A_)J6Ey-L7`I|7_%er=qdMpIS<*eK^cfBN!7Z9oxxLV&#*xngTcH3w;k~g>q zAexE1KyrfiF+bR2&a^H96X0Q63{UN}G0MU_M zZwF`aed>&=y`x(2?56iiT?nAd*F}1jA>VEnlHO;&>kVpQoaazkcu8RI5^sT6@v%$# z*eL$~pR#6fP?0|ogaxi_E_5oS)-8g;zoI@(ysh5_IXs?e6IFYLG%mgBvp*ZF|~<3Oul-HVsB@$ zn;|8$+kPMy%tLa|Xx%DaztE3s+Gp#Ym`G9P)sO!sTKx}FJ}5&rf)egd&EfIx$Gg+p zcv?|8266Y^mP5|&tFvu(sl{Z(lJ&#PGL@!E+n=+EU%lpCB42Ti&?J(v0b6nqj1bng zs7+#@sKpO(`GxnU2kU*hf8?YeDr!2Gl5nEO{!9~l=Cs({6Et6aaM2$%sr{+pOK#r(x9AW3RGD8SglHQkKvY8=7e+9-2<%SQMTqd| zMVxi2zNI@oXBj+bTs?8l@97Yu(C&>-4?Y@O{jGEo3nUZSbFgF@)r3pYX*vSp!n8(U zO8Bwg*_0atNFB%&d3D-$Q&JC8t@zoG2arW6;YjO^(sFq^17_nO47OI7TyXFVFauX# zSw0*u4sARKfPUI*7r~C|bk|rCsp)PI!ji5`5YT4E_Rag?Pk1N_-PYI|T49c5Wk1cd`pauc8zR;@$NPSQ!(e?;#W zb+{YeUSD`Ll6M>pe8?GZ#v;&x@fAH9ltadXgHywfJ%s`C2i~E2X~;{q_EaD#QixG$9cq_&1T@X zaOw?e3eJrQM!wZ+(9ubAfw6^JZl%DozY+}%PX00NX-XFW(%<}jRd=7~M|{}l^b7k< zb)F0YT}TP;Z!*KGasmZ9jLM}*`|Xs4h8%pE+KKF7Xv*A&4)=`*+P{s*_A_Rc&Lp~{ z2r)VdGu{F@Sf$>q$~=PvU9Yd*Sa)1zJdJk_!{fuz19zvhE8NMtXE2IqsXWg>4V8dJ z@GX8#IIFypQ8}*9K~b3s6H@Q%CL0+ZVY_v;RE@*JEGT~YhQoASy@mhoe|(?+Si$F0 zFrT0RJuq*|+(-30OLT(sNn5zCL}o4rdmfxwKh(*vN6bYF;^>(z>7yqNvRP)Gt^N22 zzjlqb-_pOQ%ZFH*O+NTNm%a`qm*KDxL;I_MC$HR9(wt#VTXRiyAqvAZkbsZW*F!RQ zr3Y`b830GFXXf#vQ2$$5okUCDe?LliJqrU;02-;8CH=+s?cGIBj`2L&NQldi@7lr+ ze^N-l$L?zp?qt2H4L%-jvePU~{qo`9G=_pX3eV=08%R|LhA=0(qxP;+jo1abqC#gg zu)d&S^vr?!R8~q28Khs^EY0*9`1)+lros8o?jy$Tk=Q2zEQ>3N-g_h8b~O6GW7>_o zxJ<4Ycljv6NcYzGfDjQ^AT1IVv_1JL8TM2)nl!|Tm716CQ&}JX8SSpnCf@FanvT}q zV%FCl4WW3JITvAlaEgTIUS|PA%tS+HfU=ur6BgLgTfNN8x`;mZ?+-P>)+-XQmPHJ* zjZX*r2aA&`foiC=_v*K{Qyh*xr@95+kdJxs3Uj4iNiZ~HmXV;4Qj@8{l2^cUtN0CM z^?;5hbWdrL#8bs;kNkiE7i%!5mHE`#SsxaLw)}eUHxNKxlR_)n3Os^S&0KQCp^o6XAS+)|cfpBQgWW8{V`W17i4K%Octh zyN+7%+rjl@NG9xr!&Dh*sH-z-J3Z*H%Ma{gkT52iY&F|aLPBG$u1LS7!I#qx;*k`p zv{l~#bnD-`lfeal>Qq|QI&2rx1RHC28{_H4qz67ly<#2l>xP@=h~)J&)cloVXH&g7 z`T9b8cB>Yw>T#>dJ3!s5(&sX8Em*s3Mf@reN9+~BcG7>T^~eHVv=PZd1yAMy6(tI| zWvaD@rfwT(1AW=?-csKC0J9keb^yIuPWFE8HSMnnuhNq`kp zIW1mey{h>vNV2h(QK(sNu5qHL)^SPNHGwkj&)_I`W@?kP4scTPUTTxz%k!X7;A+z? z)a;Wu6kZs#j=2U+z<&rKLtDnE(mj6vx9f0ugFTT2$dq=F1_F%zpNGhQ@y!xBc^a_3 z5~=6lFrUy_W`1Kfs$XiL!iQd%l`2OuT<2Ae#KePx)e?t)%RDXeulue#_e}n1u2_2B zW&rx9TjN~PnZ9OpDFjr?Y~f+xpj@v1is`U~ z$Xdtopzw^0{@~KVD*^*n7PJW+d>fykLgvdNAin;FL!~orLIhAI;A%JQ?$32B?5aqY|1rvIsayBgM`>S_sv$M2ASM#xFfl&k&kS@^*9ywwQVM`FSyr z#&Gr1vw*credxd}%f)oFBO(De3cfPnPBdS1iFVEk15^G;_1c&FF#>GTel~R0V9ft3 z`?Z9>0aCYG>C_kf5DQn4ImD>RGs{<>3lI)~qqqn2u^Lv~Q8uY~`(pDg$DRyaOXBx} zwwWVwz-hdZ9njLxziB~uPC>A$GB5ls%Gi?+3N;V2$-We4W`_&O0&Q|A=f(v++p=uF zAl;n$OUxn4UKs3i!|9|6IO!!6u%xYd&O4{{(j)ySUfN$Du$*hx+p!}=XSJa(IG1To z)*po1Yj^Ke+owgm-uMNuEO~BL=@OKhY&I392$O%`Cg}4z-1gnSD>!l{f2}TN6Ldc@hou3y5gU(B2fK`nPtBV5R9Ty0?Ii0lO&R zpnnU;mk-cukr+<6V}Qc@^95G*@kxq``O3F7m%sK?MpXyC3`gT6IFK)SaXqLXr;j7I z|3)nFZ2u_;lmlcjm*7*~2ce z&K>Q*EmDbZFmm2+>E)SX}vNe{tzM6V%|020k&BMCK673e`FaXxSL^WRt zztx$MRCezjV?{rEW1IH&FGqS9zNX}&rskg=f9KRwIwe%}Y|X2_N-By-17e=(Al-W> zH^fz}jDa7RGa2$B09^DYD0Aoq3$Y-|E_nFG%_Y%}D(L2;orq)EN$ru_x&j=nvZrMg zt^}i+8>YtwJ!RgS0haSh^}EqrVb~1<;3{Wk8{CukW`n*)UHkAXV=VG%Q}cgG&S!Ym!#a?!_s0W%9HO!ChY z)7Kumvg>Dd`3y&yVh^mU5Hhn8YZg{=m!NUNNow<|^5CIzv{Z|INJ zc0e$Ri~i4iyGZpSYG%_m7i^=%pbqXxcP4#BejHGgQ&AoB`!vzOZ&Yn8#uNjO!>U`r z5ewQMyW?|6--dLXEyk{y+w-+cA%T^4Qpg%+%y!Y6Z8PC2YkjNI`j2z1-bA9CmafiB+jW=>DfTjL?CvD0>#HcX)uWxtFD7*}$-0F3 zjIuv+UMR+hP0=5mi!LI1wxeG2rA+=?6*jFz04~C1^Ky>uFF;6~=*=;oq``tIM1O(f z&dQrH9@u?RW$NLR^AG>vb$LQ7?AIB|`J-z!o49Ui1YVOfA$KU_{apH5%fw&u|Do(J zfa2=2wQo4MOM(S=2=49>EI>$r;1V2yd*jl$OM*KD2`<6i-Q6Jsr*Q~0@@{74%z4h4 z`#$x3RTR|)-Br8y{@YsXTE8nU|D~eCM*2U>(|imtq5Osy3c!<(7=Ub0}E(14;BKME>oIbU}+_tXz39flg3HLbHP5vIza5&*f{ zcICW`&5azXS1D)9m0ectS5AWeA!zIvm7q742n1UD6f!V>g`ruwO(mO%jnreq+*@ql#Yd zt%_7*4DEV#%wt%GCM!D_|D9%4nXFYz>|Il)y~xpM_5KyHGpn(?a{sSCvHD0xv?~Ps z<^b#X>*3+s>_4<*P{Rr?j3K;7(< zkWauJ7i3xSkr5;xGsx$Z43E|^$;k?WIS7PrGCXgj$>=N%XlI%xT4jWSk!czV?lZNj7BaPv4z2UR$&R@cZ9Vy)w4VQ2>BY1k;Sgw7P(kyu3E@PdF&w1ee|R!Md%vji@gG9>)nGqzS2{TUR3+4Q}Ii5M`KmuYVbxbtnSLPK_b(* z1%A{I??1r}K4AEhc1c(0-e#Oce`Rv3|M96NxT`gtMe9vH;CsSzTT|RNY!4tf#dl~w z18qdI9Bz-WKg~w9Z%m?_k%dW03+Qa}n`&Jsi~Qnl{X`6o_?MVRfUDkO-*<6v$H&UZe_IC4ikTo;{Ttm z?7$mTk^rVmv8RnD=YQSlf8X-|Z(1e8^A3!T(?G#J}HDjtzWFmy#g05MaOl zpV#$2{u|o_fB@xIt-9m?A_e?+v+o6pE+z1>21!%&q_Z%>MC)e^0n-XY7V%DNhFN{LuSH=n1Fm zJ-h&b*%7=pQvo(!3;3ZVHu~Q~iQle>c?SS0K`yP5h0a=wd%n-Zo#Vj_0}z_)NxD0j z|KrB`Z{vLSWx7>l`$iIh%fS5e%WOQrOCO3R6=M5lWZsYtXz0HJs1P7~-fw!c=1n*j zBJ{WOR@~!xWa%3%CbxYmNmOZ6kyZ@=V5kG}v|=X4mMXgw09)#tSnFl3uFLEv$D;)Y z_g`i83HlBCBA#+%=^A18tmpf?3BLgpo4gnz7mdFf%|!L@sHy8Z-U18rvCMBTsbzCG5ykVDyAF#`F2v^!(7(~pqNVgGW{9QOn@(j6K_6sc)?Qy`hnJ=Ab*SARnt-)6320uNb zn04xer=xo#|L!TNWIeBdzELf&Tn|06X691tD#?9x8C473U)1RhqVas0iOpPXCW2SE z3fkRfE1)6me}b;J%LBE1#kLc44-LR&?(3y7yVNT=Y1jv{LneTqcpjdp1%3^$Bb_x$DL&Hw%71f4DDm5BMS!>inf@dOI6TMr0 zlBd02)*-BxbG2cM{AUTura#AY73w~A#98N#oTyMvTs?Yze}BAG(185$a`NqS#UwglnP<&8 z?1Kr~h~N=7sKf`(Th?CE=0{JZTsH|exb`$@Z!VhEke|JQY^^7CQE}f;0QIV`0Di(; zd(vvYoc8CJxv#2)?+4?_ePX*qVoa8*k~yAl`CF=vM|Jj;(mgu_@)Rpo{}*?#07Eox z)$@AjW}PjrhDiO`vCi@LkUZdEu)Q$(tJ+lQNgya1;^GS5Lt;n`^DpRmt1)VF8C@ z-)~KC+wKlZeqGPSeT3=%Ij?WUPwN=XPq__Fi5^To4t0|C_A88kJB&wg#E+As2(Zws z>Jp1TcLq3)@>pJN^@J_lIC7pac`ErpkL!ziCU_~7sB9=8>!2~VFA`fT$&qzbnNpi% z{>_jWjTu5WvKce6NfIIFZNwbaYL zeDH^rk8SaF#zUF*PW=7pFO?Kd7wwfK`ji_I%+yHUOO*pVX}AoARp9F9Y-~iIeCtvnO8~GpgorD7Gny{F)aE0$5(oeZ&^`VhIba^6ORSE8F z=9jt+L0|Ye%$&QadzkITsscZu*IVAz2AQdn!xZ$LLDn@KY`>hOc?5A99cKN0< zuV=0a6mIl*ZI+H4DuCW)XN&nUFEJpej!w=ow7g>=r@RDIWZ$7hq3BhcpkQo%R6O7J zTL$9W-}y~X>kSSGVM$(l6O>Psuln!f@e{~1IiO-8QU=hgx5H~h{(;(tw9o+gqrC03qU8yu#CiKDf z(O#0ES&0t(`V)pz4ahKWzd*bMT#u4Fr1{FvfUDTfkT^j+uVy-FuZWl_Q|tt}s=8nftT~FeA7ii`5qA9~jsT#;FwpmFx^`bd9gegGzMOOkwl=t z!?2?>+O5{DHhgwHG}EMHD{Xjx==kf9SRQEZ%#YnMbT<~i{B!oJ+V;Uim5mC8{r;wV ztrfbRm}VD$Fhs_t)Jb3z3kW@|^oh3tG-`wz?uo}eYx{8JCiUnUHErEI>l>R789xl# zGgl@`i*Z?0dWcu}0slt8`bHv=#efwFlO)o)2)*Ja$#{S2yQ9_NoJyXIOQSlH=H~_( zpdArSDA>*5+Z)Q>t2-lXn45c)vTkCREb=7Q1aIhd(6PCOrs%?Mh6MOZzsL*%o%;T6 zzXL?(Bt|0Qp{QP}O?&ZERNHEy(x1EW(F3qpY#$sLHP4Z;ZgZ)p?d_(>>1@s|H(u-xhvPA|RyX`!D2eP> z{-b#?S4ItJg*cAs;I-IKms`za+(`gXrUGm{E10xY!uNT8+OQpL0*EC=!hkUj z6(2&q7*7(L(q-2TqD{fP481z9AnP@+vqt=Vetpu>4W~ihax{D|An=%z`7&Q>*tPe$ z0}C!z$Q7c}hh_q-4h3(3YGh)X_RshqpBU445%&T2XBN*HuBk5R2-EK1D1T|!3W3a3 zoqAg?l7lb&83944@|kGksUio}zGZcu081)HAwQv1Fl(dP3Ikel~C0a3Wha<5lB zfMTZhTW&FpxJJ_WlI2q3+2lWrjknasU+;oT)t{mBBuda)nju*8m?am^30 zh5WNCpa~3bnt(OeTliAw>}a9lq3Qc@;-I5CI+O>&H0EwYQ`snU>7u4rHl4bjB(hr~=^kNQy1wbTEBP*Ud^2Kk` z^3=_j<9W(ha1Ob;g2F?E0F2kc zylF0lYJZ-Or2EUeFE8SW!81=cH@-586(^m)zY`d+gZDrjH(45w#D9!12{(dtN)q{G zN@eB1{!QbePgM3VM9E-~-V$V-?C+t3#hS-%YHvx~(^!?KFRv^up{u=p2fh+=JC1kw zC5D-fv3_#aW^@vwEDF`FFf}gDhC_+7{#BsFxu2ly(!sNm_fB}%_97~A&Y;l#gx6}R z2SRj>;y(zOCrLqmSH30G+)OkMLd_dWWUztPv{eTrSWDV&Lbc6Y2O=??%4r4Yj~sKN{t^a$d=lMgJBD6Kg~K^DdraxD zu1af?Jg^e)Az55_{8n0_pnYP5zDZt{JAzW0WX`e_z#&E9tKf@f4QuD%u^B z@>0g1Hzs+|jWVX#X%SFyKM-_jq$rt>XL8jvZOq{%F2Z6Wzhm!!V1^p8-vuTzwWA8M z50Cu3HSnc&ngvV8Qo_s0dQV!8BUrL)B3Iy^BKmc5StYZ?Tm(xKSu3e8t`LAuKfqw2 z@EbT~YGb0u-}W+{BC;E8Tj=PX;Oqty=zo9r-CeGV*k^We(R@6e2hw>gIKmr}gr~_2 zgLK{<$*_Qb)xx17Vh^u14?R)cluJGBwH3>QJ1(3_LumQ9J(e+C&UbBZTrwNarl7#{9+bH1xIuIqbCXuS{G-N*lCB!@ zguni0BhR*|^ddGVipxMNU4!ff9vqm@$LY4gh4jYreXH_{C#orUJ}4ODAnjM`UH-n~ zo>;AmZ6kG90OZpqQOiz4uvYUMH~oyk$h++wu%mHy504uKt=x;ENhqc-m$gMaoul;GPxW>hNNeD4|WbgEK(>$Zf@*$_fxT7g4$${%rtDy~i z&&xMy1`G8bquQkGe`t=dQzoy;KUTpDvv%6YbTzBN7w{C~!7k!GVhSs*JCCj~;F{g>C;Yw_H)uUpo-@+yQK2_NV0O%;1jj_j)1@s!xp zzCEz4ke?ELEHR6U)j{y~u0?s=CBKu)^5WH+D+wWMH~q~u@2%d&1uI#wz&mlBagp8k7Ct|f>O+eYVGG@yAbT<$Y-^~Ls1mW2vi z`*1awcDP89BN`#iZ52q1}FKh%UrvY%m}c zcUoRv3T!DN;j(Q( zDcREJ{@VPcstkfN0dYh~I5*(;ZIIRh4o!OI6YGYEqbuHE#Fq(Lyf+AB(Gdg{CC+oN z5J4P&(7+`wAes(l4y(K?=_9(UoeiQc*p+hd`W*X4h}Q-qIbtWG?g`%%`^)mp0l!{BKRc~X3{bPs3AZ6%EXSBle$%RpRNLp^-@m+}pew{H z7)n%p7X2glz858!$IFM29q$JET8ey6z&VEcQY!@4av=om>jxe8e5vGP#E8n|r;0|0 zobb4CpV9-gyvut9Z$Q&`hq%|^J>;FJPb@eZ_P|c}gRuTq|5sm;*5KJAu)s@l5lv_x2_U|&r?NBu z7_T-n$6-<$wIvIV3faQpS#{LJj3QZn)~UBiU{Ede1e45`XiR%dj0Mhjy4Fguwci<> zEE$ouD14U5DVxXk%UOX8?*QG!DJj+17M<9<+huN&BDjrk48U+4ppL8SDyyG_C*j9L z*S9E8%G$@+mMVh(DD!+cd$qToO*n?#CsJm^2G7jyRAIT?%$$E*R&Y~V*E&XlI)(VN zRWs|Or{c+NwdqjuYA5Dx-pFSRa0i?VY)(_@t$Cc8*)kR{@~^9J9`T&uU=J~IlbtNW zNSK1g15%T?I&b!w&E+eBJX;Z1UYiG<=BNpm|($%UdRg`>dz;=4aZ+fW}^608}1^R5W ze0@wOob?brPLa4ciP-`@@W&t!pP@`XO$me4*$ z7ON^m+!euqhQ83lUQ-@DONsWL+7egSTL{T2B<*l&dW7uN2#lP<-+ z?4-y?g6piqNUomY>t&3)EpqV8TtJ}ASU~47QWzmFYp^ z^v(^A9_B%IUx5G!?G68pMKUH6Ye40fh0vEWdm9Cvjtm%^mwVJ$?)pvv!l?~j6z8B5 zFjn_L-Z~nZSy9(Ft{$rLoa4jA*0g$5_v1w;j=-0aKHTk_h|@me8!$0c%WBA5e8^Q~ zd$Zj-7%)hIBy%hNUqSFBgf>j}!u4x?uk7Cv)poSP8R~MlWMC|p=cdO5N3joIWA*Uz zt&B_y~0jNvGI+4ftdT9-dWx13@y|9!&GD$X=hNTF=?uPDQAgZ{dT@rouk9u^Zs1%jj$wyH{LBB2M%aJHBH9(iRi9(g(yG42T- zT4$YEn624;%b3_eMs+&1_kbmKZWeA_SjfX#LHc63*0Ls&Oo-J;ADdTvbC(iIM1FA+gTe_Zbd5!{Sp z?wyQ=%ihV6@w5B=*%f>2M3!YQIh&o=pC@q25tyqmP=?GBY|ODEW7Cv&xQ0@BIG_A5 zq@9xF9LW${2Ysr7Hi2LGz=S{@SA6x39{e&pIo&Ixl*4SoH>Dmaa!#`(+{nboD^i?OY?D(>E~wDn zri#_1MZnd2Zt*AhA!K;Mt)2v#{v5zad#>@`6K=J6oCWO)C+)u8GiY@DCYExunz3IG zG(%w!o@Lf!ps{9Uchco|c8rogody62xz^AAURSfi)~GelHH_UdYL&&)b3ya55|iP? ztgh>NU^X2td8M~i>Q}Ipm;G~73#-NB&(yIqM&Z8)Gt{;GaYo_`XbDFL-7zR{yrf~Y zzxYyI)3W(-P`lJ~r66J#hcesRqWDsAu&Qy~v;2r4exysy#B@=eW%(msZZ85PP^~J{oY zXIbvc$C8|f!sm+l<%^u(59pyfn;O9nWgqCY7S{s^7(@BW5ZqmAmQVMupD0m#FIIj6 z@m|yTyRVdUHNWxk9M%s)8<1L|nY*ULnQ)oI*jBk>7Oeq`HrE&GcrF}uwfS$Ogr%@p~>EfSl)v-Q}iWPB2>#vgP26ABW~mZfx3P?;v|`(5)d9R zBQ7Nz5JCj6-XnlzyMcaPgaU<=cl9ypa#^g4-U*kPA!%fW%BB50pj@4e7S)EcK77Z1 z1iR=zQJR2Z7WIx_NXNXs@ARmQzSpCcoskU9cnqb`DcWBrW@*GJXc0)B%Ulq_VT9mH zOb{h8H}QO?8<1e!&O!26u1MMO&-*gVD5%<6C@@Btfg$z*=_?1dSbS$IN8lT5q>g*= zFJI_zR?FfYww-tn(DKo`*=U{>XXR{9utWY<(e*jllK|V=3T&bhvBnubCiNeywK#yo z`9Ghve0FZNe~V);-g259{`xcWalq>dpJqLSfuBApF{%MEGxtZOzn9z`DaH(C#V>rXQ6aVjSD@~_HbX(ZJl(5^KD4arvrNumcl zxj=^Aj7PG4M2%pjTw2JDk2EpwyUQW&mTUX%-15inT(uJQ*_H{Jw&cOP7{GzrYs-^r zC(-z@?wHf7XCQR3@d};GZyjg}4z;X;(N`mL-;+q7v-q(58)y5t$LvuS&rG*u>!l`* zA!1!4nHiP4op_UldYv_)`=3svmO5lgJ)e7r;`#`o*jEZ2adJBTa7|adsX&{^<~mzD z(OIJ(!D;d;dUwsQtS6sE=4G*`i+M3meeg!pS`&T!@~Serep@!%@$prti&!JHPy6vA zOaEN*b<`6mzEr24b&l0=nXBd)>s;RmZNr1Z|Au;G2#a%GYBHyrv1dv`WPZhpZ2R51 zQiSP^J6sIcsc?e6f^vp(ZTa_lYpsmThcf%EBfB6@hb(Z)>bfJ;=4@pha^&%qXmidw z^rhGMka;<@w5Lk8_y#`EJIrK{Pqd`{rFHunn`lgJRx{Z+JqV=)*n70RH#Q^)xj~_6 zG~10thqvG4d`}IJp=wj2*w%wFVnb>eQ2bHd%`gLt{c6Qp44I;isd+tuvNvYLK6ZB^ z7&18aG#6eUUubl^WS9x@e1)9hG>?Gg(f_ICvWZ$Bw%x;J>L9Q=#+R_k4FnY`9XGSG zB^QVj)vJcmoyr+Yi7r(mdPA0KWZUK{rOOT15g89cC`-rRU>I7K6<*_6S!BJuUfgEO zm%bntHXLDp@UB$yrSDjI6gk+|IYCFgL^pYLf%P}D!(aRzD*wu2Otb58B37^u?v+}_ zREqBF*lzlfr&lxHFJE)N*f%3%v2pS)b^@w)O6QtvKdmz%!(#KQlJXfmS6|060{{=a zHFWl^pby+m=2lRHF1i})H()5N-9s=2-I08Lbz{xy!+eQ(jP0FKY15+1Sv)PLp0R)| zSCTI`jI0ixc}F)2bG*=AIgxhZ`(tgoQ56hwPOa`xlbIC>;FiJ6Hv_fb^Xh*|&!falR&K;pTa5xYH}v<@12qgGV8n@a@kePutAdGXB) zdh8|IJ|z@ENi#2iC=n$m#dGs3mYu^?%wyNZ-o z$CxD)n3j*r%aN7JKv9^uVN_vTN8IBG>7u^1E#JXcni*<7gM*B2v+Aqw&hsOiL8Z@@ zROzxY(74Cbw*hST=(^6r)J5Vv2O|R{kNRU2k_HJ>3!0ikSmtN_hoC3NMsr#GIb0^| z$QhJ}kl0AUEIB==;rJiJz`4qR@VcJWad7A{aoH}SzRU$7#s^CUD3ZBAEB3|#!-k5t zm#K#PqH8xF8B_~uZv|zgdcAQd)kxqs0T2*@FyNZSVAO(*H((%+YPCLr8X70h4ET0k zBT>pQ87vGb|8zqyK4g4pr56U3TivSA*e z@*BC^N!$VU!&U}6ADM$6qFeM@gMX@=7@j3Fsfc^jKd04!-2q2~nf$n=uBcwXcR;0n z-48pnVeFc({@5-{#B=PDo;@xbC}8biqqXV0G4K)SXKp^;u*C}l>izP6quYy3fmc!K z*lhZK)jS&o4`+ypx;<{kYo6$rCk1s~Qy}$iIgtB(P+FdS><3wLIdrK6<;oMCJD&HT zxC6vJD$~s}Hv`D6fi-=NHGeh~mTiv04FFaqJk0b~^z{b!dN@RK1716E{m|>qT*8@1 zT*==kr0Fa!hBBmP8Jh?d>tyo;X{MJ5^hZqI9FH!61iv010@`GW;EPlX>zrFp*0S}s zv_mGuk!iPx5QT|nDk}6jhTOLh+?ss_68*Zxup#SWLhvMM&gzUtp8cF4(KnhDEG-Jc zaIjV9-Pe~hHhk|rh25dkk*|5QTZOEz)MbCj>Ia~qT?cWt(NS%59O(wWDAub9Cj4_o zPQT|#1snZ#3H2+jKG1DPsm~1)!t@5&v5~wdUt=7i(yG0LuH?H+{8cp51>z?eTLJ-{ zi>DrafpST80HVTqPJ<^|uD~RZtutND>3UXur@X{?`;7S~bRIFIb~w&TA& zlto@HKgXG0-+WxbeZ27%jX4!cvWxa}U(!YS1e zcXS~wu*yqc#7Yrm^`_h4>0VaZ56w>PR2xnq(U1C{D{L4*AUeRh%j^ixD4jsN`F2%m z*}bso)8mce)cWq)`|>42A(h2KJ>!u$GTuK8W&T6qV?yUYgKO{Hq*HFUFp*OOY5%sn$0G;cySPg9&jmybZw;yH(sl+^Vfu0_BGIYd>4PlO!N%ziizt z$}!+9j-)vB!=4ckADKbF(QNQ%!k_8Bp5fPM821AO(MV{SYb<9G!baa7Tzf%UZY2u$ z%a}gq244DBXWd4d!<$V}>AqwxIcO1B2og(b@dE=@KSvK>A^M&(aPYhaT|{uSMEugR z5AyY4!+&0fq>&`nNW6M^4Vv8!8GovMM$xJBl9ofp3caVO9Lihc3pfJI-C!NyTK?pa z2ABev=rZ~@AxwQ8HxvM~d)_nm&~(%^rN|GC(hn7n5mzlR)&(%9ikl^hpZp0#F!A1P z`~^%Kq4QQ7PmjlJnHFYyl`o$Q-l|?RNQaBPB3ap{AP`ss)oOpAS{CP&zS%B75Js81 z>S%A$*x&HXy9-WV$7MZ7eF~+EjavLW z%%lUwAV*CnN%s^$U6P$aZ@v?R0tF6s^pcna8qMX253td|5Rz4#Lu*-U^?1jVu?b(p zHZqcg>`P?vCye#rCp^At5U<^`TU4!>0+{0n_4#O`M(zn+%pw}l@ z7m2S*NS3dbGDN-m6!445+)-GWX{0(fGwG{EoUTic=3>>vY*rbYoz0#ha@&BIWhe9@ zYl?7$NE^Ym5siPS#5_%w+9lXY(94O8Wj(pDdO&VhyH7;r|c}?3CwD2qb5qthXR(;&2@AWZ(At zAE=mPR3(8U-qA9La2pgrI&~0|M&qo~#1%+{ZQU{PL@K;3;vn+akc+NybOz|{qU8GC zHvuAu(3Yz)iUh2`XmEa3bBde2l9PuC5emhMfqRWy2Nxy5{sqWeiZp%4*!$dU(6QO( z_o!ul&~|=cC9#<70^~FG0G-teGcZ(}xC)f_MvmvVkUh`8j^VNva|Ig$p1twX5$$}# z$%|kKZ6?96{#Evs_O0)%BS0o%q?+={q_yL)NMDAir2gf?`;4;7oH6ZiGl+GO94zMi zZ|K4qc}WNu!(XeZrhN-z@nv#cc_cUTMph8t-YVrzuSdRDZdrSws;p)Hrt0US696(5 z$={8ZiTCADoQ}B$f#hlZ>3-H}QrNoZ&0?KRnn3x}v+^QeBL=c|0G!Cu*L@9qrxl26 zH-e`-hxb8SgGj%B86;&k9b-oJ95VF#3g39PciYKjGI;KAAwEzeA7k=%|TeHA1Wy#$op&k8sT1;QI#^Vt?Es} zL_c&+#MELDh(yZepT_({-ZcY7L^p~pW_;9da!$F0Jb~Al{~W^(zlRAq6zM%d=`}F( za?8?tlyLaxAglleVIB&9yn7ozJrKu?ZzI$ni~8tFIkXW9;TC4;P25~w#sP0wIZWHL z+sSq;a3fs`H%Ah->3RLfTi`7e6L~SLQta{3dze^9Zh6^`D@3!+=sPOlRJyl4mGz z2U=by;~y9?xlrdvv(c{TAmjK_+vKtuuGFXRappc)n8W@V zBypKCb{=K|8P?4|!=j>VwjT8GpD(q^FGC$+hwL*0Hm(+4OovMS|ZqX{xS`vS2# zvmb{U`}_y!2W+SO+m>0;FAZJSq2%bZS{hJre5y-wm~~96BX zIK#P|41xUu2lHF%V4;-|Sx$}DUx7-q8^tglk_8BzEk)DrgP1jvHseM} z2jppb0>z##XO5pWhwGx2YeXN&Lt+B`Oq<0NPJ~)wahWv-KniE-f@qTmaS6Wa^6Wyfdnr^0;As$9g7C{MB;#KZfmrS)r>CAQ2f zHqoba+&8gwLpnb_ek$H=*Mv5ATB!mWj}ce&;Eq*3=H{ix5p4Soz1s~+DM`f7qw<2j zD-v98iH_=X?uGpS+Rwif03*{SPG0?{Uh>Z7Aj9@F_C{pfm_WhBVGvL6Dc6uf-{*Hb z0pEd&x`>m*2Vl)SsY3Kvg|W@=Z(I`n!Q``6PPQOQBnm4JJ9^AyTIiQohzhbQiX}kA zqQvHwtr3k8rUCkq`Q&|;nv`TLo`zC~r=E013aYXeX*c9{Znt!GVUX_9x@979b)j^H;KjCS#` zTDN(WZKETCAW*XkoD$db%BM5#rP{&EK^_gRqej|TC_ zi#7e3;kP{Br9op~TU5>8WvVD6hh_;|U6Tkn@5RdB z_56yFFIKU17$E>WQoRIrOR|}JFfPFGDJ&a6mBfHSPp}MfJkQQr)bGJpnw8$?81C}{>00v<^3b|pOyc&qzbHPpwk@U7HM0I35P_y{?o=99=b z#YZaVMEzP16HNO}=;tu1HZV34_x7>6h`3E>$x_a&fo9Wj2}!_&IvY>m^FG_qGH! zVJj0K6)2B8a$}JU3LAVK1X^UFh&H>M0xSbR0IBJIv*-PeZ;klQSO&pY?$yJoY1wGH z4PXQ>^2y(X!5?h|yEyRqmU~|TqUYw49=xGtjMJ^#_4zgA`&Faqo{RKnuN~3u@xzmm zGS~XbanMxYaSz^KPL3oz|F3xV054P@DUkdWk%D6&f^Y~ResGt+KQn67{>B(UK;m*m{0YS&=)Nt<5Ra!;N-~*P zUJGoF6jOyAw3alxHui*!&<9fpE)j zow;td!S}xhbv`Zb8U*%)vFg;7I!_-W+%&PHw{>IGTTLRu0Bs4@!hP9foq)cd8F(J? zN<^3)KZ$sLC;%!YjK(!MBn)1*+qiPi`tnRa1g;~Fy=bk-aznFI(t~|iN>C|681bD* zd2R$+Kvv4ZpmC1JWvGcrlbdRlnoEuQbGFR;ZX`BePtYwM@#@g?kk@W8mu;sI4T7ba zKo|8JzqJp6HSnei1*kd1+5+WZ88s)M>7zLfDCzFBc{YP?bD~?IYtWD8zdeQet70>o zqK@uw@DRrf$CD|MunJJ@jwn}QY)at@Kj!r>jqY%1i#X!YF>Z+KkBbS3qvE|oi+~-= z>yJb$zOitdfOI}IUA6f?tVlR`#qh+&lVw)vU(1%%R}J}BYe(>8gFUYZNoklrP#kCW z`y|6eeo%n&@Ol6|rTi8T1IvZ%HirtEi(c~&%ZIkgh)aDUD%5JkMfkHQJT97tl1b#zh=u2|Xf!T*#}`b2|t zmgcPwXpEKhvVA|hab#(I+Fb?r{jdU!<*~sj^MMLB8+o1%?N*p0-(X302y3&Ar;C)> zY~@$(BGvKD*5ThUc42&W5KbM!fN99d0qw!HC6J}Wq|8?0I4IbR;&RCyg1uBLwAuI(hnPZ5af zBUP%-22-9>F_WVw#PE^oxE`Yp*wx_r-9!d+R%$+l9-(r@B8~@QE@dw6uC;kXYhwtO z5}TR9J`t4UR#i)%(^#pqfJ6NNo7Qeb%RMY+sZJ|myB1vt^7R3rEzRH-HeEx@&G^+H zllCFc{0gAqM$}{Z8TTGc)7IF>HUU~`@4*{fhFKl*-j2(}^>tW7A0B5p1*0sOl; zZM`as5K(@)zn8KPdZcAmNiG6SC8PiO%P@7Lnl82)7V`r@+cY!lK!nqrMAs)}FEU2j z00WVqNn5jbbFB47`NK?cg4yR@OjJs?kpr+aFl)!ur3G8~gUtf~TrsiRA8Z=U3akZ8 zf`+&i@@BT7Sp1qw4k-ev0ITxGr4i=l>#oltfmrjpr*tnZu{pF%orVD$Y{8V%Ga6jB z38U7I?)Q3v>v>du9{$i~Cf}(=xo=FLI7f4SZc>u%FB0qSxzzWx&mCZc;T~*4#?c&qSJ#7t716&JT`D7%FmI#AhFF zyme2(IaqQvId(5xl=(Kiq;g0b(52}gQA4=Vxt300$zkczsiTt)yLhb)?pwp(nFWTw zs}#*k%xtoWnN%zTYh98d;i7t>YoMV9z91HLteqSGa5Gl zyj_HsAw0D#*oB%gZ#7Q{&K7D3A546Kz3~a^S>#9Ee5bRQBmy9*A!%oX?f$MTXCXi5^|vQXw%q5^H|Nrk};OL*&JLFR+|iBK^W}>GIwjA+{j$7pVY(Y zquf5Q{&!pC|C2M+oqLv`!kjA+8D#M`uct5^>&F33rm2fIBF%0*Y}{2WfPLl_h*I}c ztLxipfVne#b7xdAERvxZFUN*)wuR1?zXG!==U%>E*$H9_-F`(1y)`FB{2fHl^7^f{ zmp65dz!Zg1^-$sw0q6}kVs-g^iW`LDt7kdTniaoj;A7gN^;ty!s0j|0zy7(f?atw) zZD7)Ne_%z)#ktzDV%owCnaz_84eegFO>$^C!DoqItl3@ov}P_0e!=(i3xkcV&2ek; zwJWCPAJ@&Uj$eO*C}5gX{dLey)ZS$Af|-DL`;Al@Z`tpHL>B5yV#%&Z5*`_mq06hO zKL+obT)qHCU4BVm$Abr&_)QOr^e%31!|={nlN6n3%`F;F#TgOMZ1HzCXJf)ZK4h4e zxaSO+9)b5jHnM{POE{&6kxRHw*X0u?DXs!exgG;7rah}cTT2Rx_3DCghSg~(hxZP1 zaoHx@m-!#2yujdJeVA${8TXD6rW7R3xkKgT_~tfpeDT==y-$h@U(Bztf6_duFn)dX&>b-zg1mgsb-jAe*)#$qgaoeZ zrzCGoHGI)DU>@q$L_9qL4&+>jaw~#}Jg8L$8lOef(n!FKzwSDeW)y>8J1zHLC-&)n z;kf&rRH}#Uk$aC`V$TZjt^**3zrpp5fOtQ8VZMIXZUvQBxgvUFo3=f@Vl)qH}#k(H9}IYCJuk z;6dl@wmE-=F&&`gf2KK{$oI~hB}t5}C0pmaZNf)h^m86nzH@eDCH6{ElfgH{D;-P5 zok1r(@>%etL#0y4i1tC!se>lG2ju7@{=~xa+tVMofG$!FeRPU7O(2F#!3$da$gXRy z(eY6q0WH)GGvasO9F5~+htM#_XsW!PDPYedVXm-5g*dyhv?hi&L3h92{?MDYA9~7| z{PDF3C=M-7>D$mZDPeUAmN6u+4{QO5H#R=@3BJY=_-1ph8Q(4H4L|P>3pgoRgvc-k z`?E~jbKA2)mW;ija$04>?C9dl&>kh@q-K}ab@UIvgb*do4$!VBz{`i~+RA5=i-~Bv zwR)|JBWX`jVZTYD5};GKU17!3W^y_hNMPus$FB~D(T6fHA*i@X4S}TmQ?ZKHP_b-4 z(b@iuill($kXv}n}mTIvIN<+fZI08vO1Lx|p@4Xc;N-g;z76A6pom0CmXKE_DZANXq4 zy0*8WazZ{~DYCPZ^8eARv3_cBdA*s*p1pW|&3iytoU3Fcx|#8%9d+JpB%O3;k&Y(E zXYdG^w`}Tq-CU%wsvtbS`W!8fXk^MAqLmTq{1-h@4gGXp!XM3bPuvpZgz&_duo;uX z#JskPVvh&!0t9;=@u514i-1WK;Y07vK^WHB@`}C2Li8?6530dv1_8e903_zQC87zg zLj6UwkI-1>)kKivW@A9x75Z_OG*=+s7q{IEI>qp1JzauswS?@r8sa_;(@dBy?uQ{i$yLnl+$$QV$FAK zvaKKRRDBEsa=v@To9Ji6u)(|VcjkN4^F{ijhfc=CKE_=*!z6f|G6%#|dNM32-hw*8 zCYMUq`e8om8kwyg&NtP!r+J&zs=jFmS||D<`Kcb?{5R{zWV>bHX)oq_J>FAWW1p95 z<^))FCl=NBr?l}>WOA>;{O!bVpn)ej)AetEyp35?=sF{U7-wK&%W@&}Yy=Z>rQSeR z&CNRBD*S)!on=&(-M06oI|bF#c%5k#cBk?w8;ltvl_5fG5>?hXa%?(Xh**Zu6h z-*=y}ea0E*)BELo!+SX3y4N+=TyxDi|G!CQL!{^2LBgEJ&hwE8^OC619CJoi5n9& zT}Hh|fS9AR5`QXI>Fm5zH-xV~UKCCn*dZc}n#USQQF8*fh>PLmfqS2`F zdK8^hbx0Gm_PP-@e%C=1CvB7?w&mKM`z(FaFq~%zEfB zo~gE(UBknL(fi?=U-fBwop{KJ@DO#_*RWjrML3OOwohI9pGsIuC*(0B(CCebR%3aw z-uLG`uPr;!976^>+pp4ozz^iCF~Lxlp%7UAZ#sy&HT+X%OZ|vhQ8{< zN6i6veyo1(=oN8(DyjYgOC)w%*pEnc9#=J8H|c=nQ)ujPn+0q@Etp1QQ9)FC_z=tLxkj%xHTGFH2yho^mWsCE)W6E7uyr@=ksFb5gA@1~N-#!anb469$0H6?O82MhFK98!n%MD3!L5QIr6 zG*lwi!YmwADGq6p!=ks3nsizxr$_7!_%O3T9}Ny4A6!u5{9^usCg?gB9JcW0h&T|% z+X@jXkYK4WJW<9d1L)ejSqQu;S`tt{=Eov{@90KCyVNUuk>X$yoZz+d!B=m>4ukmf z{z7zPyM4f)UKKzlES1xMn_QIej!kAeoE=Ina+pv!!C4x&4Y#c{Ta12%8JgwvxA*m% zOsl$iri9uLr+xbFi*+$gu;ktD^^cz%BkTP_C{SBdt9BLep^2REi!+J1-qG@KTG7Af zvJR{2d-J`2BxhhyJ-cqXotz$CX|hn$_|x$zHbo4mxKTO&U^M&qwDvTrdgR+RnjT%W z1>)zIKv@r@`CiKiM+>Z~|D?euzFf+AV2GO?rpvP+M(&sCR8Kh4_?gMt5j^k4m}4SEvp+^lq3? zZS2lRA+L+Vy2LQ611-)H23vk+eSQd>#kqKVU2g!f)$_AgeX@_S76{_wJ zP51puf2xQ4RUzqhhBV%{rch=}GP)v0blMV1r65E_kAdOVO| zr?3(M=2K1Ib;>tbR+*2}nTWE=r3O%aZZ^VbisUD|+oOS29-uu>GuzM+kTz1knYS8I ziGK~K&ni}{`q*a#q92HW7`rd(HcnIpvGum~e3B=QJx5Mf*ryrPlGS~0?|DY4zwkP! zOcd#}FsKzO^p_>e@&-&4*~J3(U^1F~Qc+BX?+u+G)}#pm;Bl0rx_l=MyDw7cB1-lE z?tH!bnPq0((Oxpw?Le!uPRb1IyRkgx+C+qNo<0y1pYim?jxn>!OaGbL@WNgb8*;KH zQF)}}{JBf-@7$CQxhNDu!C#<|zxyx$`MC)PKxdR+LmwUe-Ol)%&!G@@|Lqq0pTGD& zKl$H3^>6;>1$bBgyMPb_|NmkZeH8XU)2>n}gLQp>xoEm~h2#i)a%qHoJT1k~RBpEI z;^vKad|*%ofEA0H{Y+(QxxYx0)z5upQ@62GV0xZfbpnotko7i5twU4#YyLt>0=-%c zU`BqtV|H9wlhp*}o%-@=6JMy?!2AX!aj-hm``S^Z*jiJw#W09i z*!#tsfuDna(Th@G!GT7txbYG?U(El$Xsi~U5$?}kKm#~k4eX>=^8z)d9LJoG-?Mio z>|&_oeQROmzFfZ?OyeoJ+s?sT|4~pJ2QsY#M0QOiuEU8;cJUKDi;SR#f;@e3IwUf{ z@aQvNxVunPF!n>UgOQBiTLT`AbOlL3+_hez85Dr^Z5xlJ=v;JV=~VL|4Yf)@ENl$l z=e9RR7v7@(9LOu`6@De=#=eFID3B!POK0pp3de^#J9e|Z+6Xh;OJ6vYXEEtQ^;YwZ z%+?v#i;OYgZ>QYer{vzsFSox}NFBGD=mod-kQ}A%k0#Y<7U44s6WutwgVPo4Kp{O{ zWu?yku}=_SaN;DviWtOzMpdT|RiM{FBe<3XEEl$+U-;$Uc)e_JuC8MLK$@>#psBJN zXQz5t?#W>{-Kmt%J4;a?BjkD~12ES=p%R(3nLo88V>_0<8~-62Z3{-leCCswSVa-E z0dT1g$O4W78IMikuf9!uEG0Ge1#UH->W!nMmpVzJ~#^>8L@lxt7_(xVp#k} z5*0N0>FHFq$Z|ep)b+6ssC-gK{X^3pZ*N67?&C@K=E3lgd}3E(V!z=A2>?=%PyuSy zx)OPFmY;OE#!a4L5$iTN)gPSovGg!Go(KP#n@i~1W`oAz=;sw zSIme;pN0kn3moe&SADtVp697C!MgQxlxRES9J{~1h_Ddx*v6p|@yLN81||RmmM*gj z^d<7K&)+N7Kc#uq4?4?HQM<6?uBonaY-VS(O~aoVQibyP3(}i7?C(uGLC9WQf?Zl} zTuDB#>ApL%bl|F_dD~~6p|AEWsgP9J|1xqj!$-<-O*x6|-4S$!>%8Z5gIxv=t+MDd zCN1lUB>J~uTeKj+yPvNi1IWWkUec!jugU zA!U`o0NRw!^CD9}2JDoGlITm#v|~z>#q>S958z(la~>^C{hzHRP}nj|U~gqloQG)< zqSL~z0!|nmomyeubeKIYV)%+yOOH!;1NucfsJuPTl8ScW@fv_2p}Hsa?V4Hi%fzeg zTJ9R{I4zX*`Q3K5(4sPU9mEsCsIMjOXfhEBupQz7`dB7>gQs^~UEfB7yb!W_aR5H$ zQCN#9XV5yTg%JLIylsfuk76LH1X0l*$!}l>>iy06ZeP=v!{acqCCqI-;7Srhgnn4= zZ2Xg0<$g=$M3G)ivB3WXZ=0vu!nTtZGp7@Xfk38t;W+G%r+q#IRyEeh9FPNuV7!T7s$A;y%~ZMDp&r4>L#W}0 zxum9wO>e42-&IHBP_b48AZkrF1mgGVR@FbSVd-yzxAazU?>I$7|}F z`6A;aq5|;hEhY-{D_eG7l|(f^Y|MVpOJ|&ZbS#F^gpNU=2*82xx30Q?nSYk8UZ*WxVY54i@&n9vj z#*%^im-?Q?`(*l_uZ!Ovl%`a)!~<;p`{Qy3(k-yH)$*;IncqZXXB3!!C}jUUTGH~6 zsyr{M`CAR=?GQeolc$BO@G^n}MSJW`uf=U#1i$Nk)nuv{ee5m8PG46U+g7@Qi6|5_ z`Db;{nV0IVWp<0c9+yp*tF#_BpT6p_w)CW*ZN^tiv+sx&8s~26MJM1@(`1oEZ}KWQ zSz`BEcqQYSrww`qfE6mnztw)eo$vM+d0_vSkskx$m#x{U#gD&2wf{)OV8(z>3ibyA zs2_wAnSX(+5RAhCosZ8&^e7&37J^qqw5rzviq|ST^}B;_Q}})At!FBy8^r&4r${-J zjwVT1rj+uSy+hfS4G%C3PNmf>4-Pq^rM`9+JpuzB9aSV0t>NMpRT|c>GSEt|MH4QICB(f$+DUnP?heM` zgyW;<0AvN)VM(YsZAwVzITwlJc?j0~kg)J<|tY)jeT%A=6?W;S?-v5hsWn*D( z-O%E;C`=%-Y@82Rz?@biopS=*lEc5VdQ89|;tfscHkp{Z^Kly@b`^v<$1YFOoOc4G zongDcu>aZw`P3BvGWBc+{#HBoAZBWJdJ$}jrj|70N&4@9=re@EuGK-xZuDPG)V57b zQ{|13M|c~A3gZo`#(0P@a!K^2aSAtQ9GX?{s*)PhKcXM(j!b*^_@AfQVVqT87hKF_WKD%qcwS5u!_fqAvBaiQAk!>%Rcp~-E9145#7!W z_3j(GCWXukJ2L!6U(t^EDQ9`#yVu`eeK}bFv?VTp#uQL*E0G4fj&R8Nl={_3+oO$G z+N$29Jb{q`S+zKv+oWbr*{5Z<)e{$l;w3WsTq7i- z!$MDKeCP$-4k{Zg2T#i(Bue*1N+#?1Y9>klNdSz}YdE~5U9DdC;%ppo+p+oXlp7`W z^=Z7wL7FA~jAI4~_ZMyb`DzMEmsTeA%FWmG)vJjJA}mV4dUABtFKXI%d4P6Sp)};4 z2^^(XQd?RVHR2wnEx5OCPPA;ke`M;`Vk2c&0znvR@gyceu^5hn90D#14($?}5k#gm zm>eqLu3?74OHOO)WVIUuWrQl4G3>amE{0%s)W8jTRZNP}=@AHYRNThIOx#3CgR+n0 z0+jKNQVsTli#t3zKvpyEEw@`MHpk{NE)>klhF}%(5$O_M$zApWv{VGKP57?Q${gw8 zZC*Tm!Ujc`?bZ32C11e;>z+%;4Pag3voL5xacM-{rMus;y+~l%@i5NzTq+GYkx+nQ z(8-H&D|$tML5$bYtiVpn>zd|#z}FB|X_oa6fQK$H6@A2ZSYRF*gdibQc;=;m5HFvL z^kGTJsQ31e=I1Rue7BvQQvp-dG7JXGl7{l`%Qh5J;+QAtQX!3xnZ%QAClMOgC+n~j zswX;meifW^$ndS7Oqs3RM*?c#t&XgrS#UsqaWGUYdZ*R^*XQuXLVuXuvKFt!)-?D= zQE%2WN{}osnZ|Xw{4I=;=DEM|KB+&?jk_^q9$typxH}31%c1o)@LU-?KdKF;M>GR3 z1rQ}dtepnf)@N(l_ifQzGNEG=+5Qt%QGJG!q~{FUZd=YsmYJRm4E`2CjSZow?KNj7>X<&sR7>s-*U_XBZ&iV8xT z)8fvDiNlTSp@&4oTkUN>*ir}&&(oJDw&dFQf4x^&Ni7ZD4@L zQOIM#`+gMkb3qA*k=c$DEV|RJXqh4Ng2sx%ppfVkOd4M5es=Q%fCO{>f`sBfi~(t0 zsII9xhNpvfU;|xaB^kd~7eI3j$jK^B4DG8HGUvZg?*(gS`bq7QUw&6i+fzT?#v?W0 z7Cb2cs#=`mNL&q>Q1j@Z|@E*hnW0 zuM`KGNZ_hA8>ShSQkpR3d}v8l&=FwoDN4-Yu-_UzdF+S|G1oxFR(+CGHIEK82J)+C z!qXOluRsU2D4b4Y1Oh3;M{$9pi&iniZ}@JaP}dYBjoF2IO*4p;_AFvgjEaCFu=~@8 z`(}XNO?f)i0Dm!@z*A=1?2cSFY{g2-+=a%Zc|!cgsN``d<6I`ZP83?;8; ze|!`b_55!Ba-k-zU3eAOeu~8HU2V4IsQHf=^JvZY!|HBUx_PRQ66y?AB~5YNIai*I z=#2X=L|XTwdX*W2Sziy_vB4RsGHYwa4>8l}`qqa;Dy&7o5CA^$(#^@6D?I;?7ffb1 zNIu&AC_qo;B?7zQkyOC+AwJHTb|m4r%hFLZW(i&j-BPW3hsD_B5gqH`^GA&iqA4-C z8f*L~2M{bws}BSVlex^V({R0!rjUnF*r9N$B$7eA)!gEKY}8F8PSMjL9^()2z)lE^ zubWMG&X+)O!ho%F$!6uZH0gyT=h4U$hY&7Ats9YPrH+)8pbb>`9@Pug+LvG{K*OYL(52vq`qIp zlQcmm1RUSj(HLZCL=ZUXS#<-7!E}uh*=j7*u6l{4vAAcg<(}iMtD8rrZmUl)GJ4@r z@Ao$IMwh~TWPE;$bk;S5*bcys{pogz0gC~l%_8q0!NH3mi;J{1$b5rWUjUnY#GO}6 zZ`N%uzKotVC>OhBz8E@RjXu6SgdO4Mh%YE2sg#?f;^884#AGlem)+hD`GJu=Grr&7 zvzA-3%Y*Al%SW-0;BYZ=+JKl}6Sw<2>#JFYl-3S|x!U@aRcOZ(0UFRzs@6Rc21f&? z(ycy;e<71V9ds9XIm_Uw#gRoZkkpxHUf{o|5yUYAw^24d!mN8+bgFK9(!Dk;59=W@|FOby(Giou*NQ~XrqV=BX!I~Fh^E7ioNuDg-L3mMyzt^9CI#_` z+4IaorAt4RkGrV$`0XzM1?3kb!hJv~6f}aBFO+mS#F?c7U?xA-FLJ9Kijc6W;h2VJs2nx%QGgLUIMug2#-Vua?;{9Bwx1gk{ zc)N^hZiPg{WeD_OF**843U#O!Dz_0^JU+m%yZ3sH93P;MT-`{S%_E_`KVCo}^Guf; z?1z0+Ezwg1bILKF;#ptyz77zFwr@jykXeCiXt}+fY`N=0NmwZkb&Jm5Y3nGeIJl5Z z)Sc+1nGjlNFvrLT{n&hKnG&JvfI};Reu&IVe&F9r8CRDHP2Mg(-)$a~`ARs;1smvT zeM=ZzGu(HlKHZak+AWqxp~Cr@g>b@Dq{k0Qw3IZZ2yHT4U(6ovQE=Q%SIQcnFdtS*acvi%GbL!4e5N!a4|TKUx6LR!Rb}`O3JK8dkZPn6|?) zh^+3+{zv4gb$Yyk@ZtN-Z-1yJB6wZ0>toHBj?iSxC>mxQqZih15?jVROG%=KD*=F~ zN%Ty)V)bLG;?I0P%S7HNQ4Ne6%d(?=dlr`JxYlb}K40Zk<0+Z)O*|3?iHthVABKRk z6221?q~_$Vcx~aCtp=C8Pm5|g+P~FqQ!Mz0!NA*bJzh=v4dyDYqSJz5&}!Uv3r0Ne zeT46ZgH-a3QlFTA2w_g)ck$F9bn7arsZP}9yhC8LuqWGHEi(3d7?sz%qVf}X?|m1- zA61Wj8lH7$xq9!L>ltI2nQmfGCiy}pi1j6AR$Jgf->jV#CFF8jEsb599EtZ(E4W)SQ^f<`m@Iblobs8alDLro44)Mp5wmSiqg0Jxwl=kxoe{_8Imn z$WNHH4*Q6`yL1!D00A*BM)uDIpeO^3bmCExQXP=7NAg-p?9z2Le{)@;4!lFQ)H&xN zad;AA?DnX6eM1OS!(9+~<>&`rPd4=xVnJ?j_TS$2gxLYI;vT;@nDFzuLt4RJoyz8V z8G$SY+{ZnyGn@COD<;FtSTQkviz`nd+l3;cfW}akI#n6x{VGE7m+znFj7doLVOuV< zmZF^8tyT1ut2Gt87|@wO^npp*`;B_Z3#-J(5@xb+YhV#(j_D@44Cvfa=TD9c0U{$W ziEN4{!-ttcz4)W6MLy7d2?Av=wc_xSFn8yho{#x8U!Zne5JIdjOCB4sPd%$%ANT&m z>!kE`i0`9?@zaTZDetQcHBh+)o%qjeB%3sHJx#-cU&&a$?yU7<38|&){xnIIDyZF_ zigyqTx(^4#IJ!ZkwGL>%PwSDPC`$x~p@my+(kyfrx$fAmt>F-TeSI zIE3Sk!}5hn%?^af55&T~8`tfpLvn4|J~@@1u>n5$%lXe3uef&}fGu-n>cr4)rF$zJPL_KvnsqMutgi!{bGeln zBJZ8}hZvZhBvp`A9e14@3SVR)1NUY)eR&91#L#c|IZ8w4gIX$2two?FaOG2r26=Mglg(zek?FbXooo)7U-W@U^#z+0 zXX}CJIXs#Ko2yRh7Nv>7MKbzeGp?7x#JSlrp8X4e;~=Y}Cdu zfnYFs=HjkgBQ$!=0eJ>F8~jAW#^urKwKD9kM=B0{a>+%&D!|!gWBr#&UD*ARCg*Q)B5`{pC()Kct!8#jFB8?yv2K2uGr)+ zPr6-kjWSukdpgeRem0vYs2uJj@)`a+7?Pfe~p>fhYU$Ouod*_3(-^4>ARGNE{GLe;-&lg#)#Q?z5@Wi!)eF^SJmTWuHlIL7>UrA&`ZHD z+L|xORV~o!iudS?CM-65cV>@{k51O-Gp zB5`CkHQ8p4n%E8kDeHI2WpztuAVwzcKc+YJteNu%PM|B+Y(3s61oV zS<7*0lDqK}@cO{anEerg(RfT)cKc(xK$Q|g4s^qOJlh`a?r4`q#(B=Y+ zgTosZu}ThC*Yo))zpG!HwZHFZ9KK;z^+**j5cCinBILmoRZ4SUeM!J!o^tf8ml&oO zaUh0LI+bX}U7YVutfO2P$+`?4s)hs-ihNB{k~ zDtTdI{y|3R8>h)HZZfAtsVP5!|D0N?_a+l~FedR|#H16b5T1rX2K6#d+t|~59@ME4 zEoqxPEY!VdKrURyq^4eVzUxcliRQFh(6P0c7<=k?<&i}Dy8Wp!WTqj#{}k3KVzl7h zS4ha6-LZ5BipMJi z0!q!V?WleaHr0SGqUUx^9JGucf-ZYnMJ%?oiR4P84-xYOfd!L=Zyz$QK4stD3ehWD zWs7rZ2xfk@g&<5vZ=QV;-b&vasr$s4r@v_J)*+dKOK{~x1s1tJaf<_fYyOro)>RND zDYikU-plLpUo3#c&>l6y}J075td&6VA}uKmCX`0ipR z#2mUpOoeZwQGTnOZv0$}4ejx45V=_!4V&KZyXEntG954r^#P3$S>7Br^+}U0!})Kd z3HVm@>?-tIv0Ufd39n9vK1Zw{KY^KJvE+sKeh!EB%=lLS@xU}73>xP8G1G`R%-bkO z5`NOb``e`8MjJx5;_k#o{89E1xM2~LP;k9d<(UOc9%j3yd)wgF);+{!jG61Xx{4bS zjbToU;V0XRH6?sc8(-elAXARKtqfkP?{BeR4!nD4d9H>AAHj$f!}8u zdD5KnPy|{%DBk@pj#&n63nx7yP1vzKJkU`NHwx9m=1;e$%2rQii0q`|*YzDF*7s(b zq=j#;p86$*VbVVwG23pmTo)`_ZlyVzy=gQj9fCTTAF;Hge*D%YZ*rh?I=6CC=^g6H zL+C!=fhB0r2x_D^!h!@S8<~&%x0b4Sj6$(~fXL3Hdgz@1 ze2yxneP=y_(N9&T!o%aQt}E4J!8Kwnkqjp5ak7;d2>4ZTZvU-_T38%GtLDO$Lz{?Df1gsq~LPi_I>%R0aQ*~4pH)X;=sQYJ1O48hXQlhmb< zC-vVzR)KTxe!9Vpg@E6rRGn&!=sY@6X9%VD^f));{f1^|Q;YINW=E)^J0>dj^&3Fe zSj(rOmg$S5`dJLRC;G?io_D>f!?K5zF1w@ZmdiF94*f6%mBf%1e7d6u_Ny)xaO0TN z#Zp9;(>t3!aQd~0pDtz~>~@6AQwfdBBV(suU!LDPXp{)i&U2l;+8(b6orYuk<#pqG zB*G)s(={6s!;5K3eukq`c}a0mWw-c>wASy%Geu-iY6ex>lE8*dxFJ@RLS6+8z5(Kx;G@}g+ zMi9e8^IKEGCoNiN^ciiwHpf4*HbwifXe*(k;L&ylf_unL;XBz%8!S)F&~8ihUuA#} zBKb5f%jbY9`|~#~;9ase=B1PTP#-`~^DkE`)}wTCenJ$8jA~*8?@?2%>rz*%t_LPp zyYNIs;JZIKj!ZgA?V3D=r|S{q)gt5?L>CI!e>2|D4S!y~{2kP;`<;dWX@qpj_mzaK zf|^4|x{z0s_0cd?GzNFX`+2MvONjrl9dI`-(MOCa4#acU=FNMKj%qYHJWRSjQ+;lB zJ!^$Z$at_&l!n@J%lwIILbQ$vCc_YkmsRwBj$_E(P=RP%cu?n+ zOyhiX%88)=2y=irMJ|?n6kFZoMKW1|)Tq=t5P_>iKD3rUI>s@?oYv)CFOFfY(O}!8 z@OaS>CU~t$z$JG$?mB(>uJVKp#ithem9=gx%T`LKD~~GnK>^q9M9L*YryF|5}53InCycTx9 z7e2bPI@XDMF<@)pLyRg`q3)0KB4bivk&FCcYQ4_3nBC_2I-fd7T0y(J`1ha;!b&4! z+dGf9ZFz+*lDzI*AJF9Q^;~f0YCV>rEs#s-H{%03N|FQXw}r;a1VPmR{Kd75MJ^-L zKzi}y7N}KnIrc@`jiT1bdb<^FjkTV^=gB9%dg|<)K&LxHjIbO;;*EiAPyR;3#c4M| z9|~b-{UcE+DbxN;lQtpGI}=v~v}X-pTn~MYGO^Ai83t@Ij4J?|8zMZVkw1)_+nZ$M)*lJ~kJ>|TtN zo0S&hbzsxtYji>9&%6#J!CGST47>LkYCmvXQ-W`AKMe^hg;fbrFXVdpiHRD%e~!mB zhZ2m9$suO{RyQMOW%A_7UPp{0qN(64U7m-|B{}k{1V9P-#sO*X&6q0i->}>RWR2MJ8VKWhHbnK6(ob9f7N? zD2u0Q5W^`;HocjmjCMc#wRiFhr4dIg8#*1aW96e5d8gO@IS-s+*j)U->;}D*w(!uQ z=1c8oEm(-9+_XFr(}Cj&mu~BOQ^NhPtfqtpH$_rxdUtgInY1s88Q_swlPE{cro<7MPCp z-YO&lDpc)M_Y>_?v!CB^*KHm>Ze7n4StWk$8rW8@{nCq0G=B_$A=Fz>cHW-rGxK+{Zb-~TGp%>5YdRRx@~B=C*s)hl||@%T)lk! z5+fbP3QLSe#2w#r{!k_!#H3x5UlZ2%Adl&XpPn(+&?+AV)+KNO;#VEeG=bXZ~|VyBMvR+EME_3&{m zp1q>TU^2nfMjeTAE1yQ?G)ML0g?7sOIAO$f2n{IN`jE$ef0mUyAX;-`r(I$!A#o=+6a zh>1upW)ax7_Z3HKKBOQC-mCbhc6I;cr(D=C{nooh?a=lwXL2bd7Q7xMDu~Yzg1VN} zK)(lFL8ciyi}??l4;;|P-PJZCX~VoNzgfv-9@>nY{e}4)0;fYSZdL|l&G;5Fl0dct zvl!m8pJ((NdS_4l2GT0oXbdl!BEj}f?2_X7kEM{GYy!oP9!Bl%0rH+N+>NeQGaeY{ zInTmz{d9&BhY=c|GV0b34umJ49W4!icPT)$gXe}3zgbF$py20mu8^Wh=j&bAp1;@~ z@A`?j3_s%&@KMRW$@bkycG=QZV|EiEwdv)7&Vto6{blda9TCsmkhR1!H4rb$i7EZn<(FF&uwoG7Lxe zP`Xe-MQijL{)sv=|Yv9k4W2PUY7^T5}}d>EWrpayAz*2diu_}g~n5*H#Jg^ zaudR@52lPdZVfLdcYWm`YA<5Pk?mZLlhG_n&=^h?tm@7@^hLO`GO10 zte4=zxxJs`Q*buS`Sx9W9$ENkEe(lSpC2M(&1m~RfA93=LT%X^Z{p)0b6$S1$W;K8 z_2Ub_%SWQXZ(v;VPKse8MF7FApK>(9=C79+nsFi~g8VG7OJz_FU)2lL<1{Q={RI>( zkk44X6s)5l^YP45c|C`yHZ0qV)xzPpe%%%h%tKkJ?Z%N0h*bB8{dp3?g6?P!#$uLOF zw~BOLJ-k$31E&MJ&uAB!vj)tx{;#_y#GYJrvdadao`2 z#3&+3&s$F}Hz(zzPLt=`YU_Dw2F|;_hx^-~)tvK_Ag{3fyD3fehsN)MqqD>2q$;aP z4-e0?E!#x4CrzdU&sfOt;-bMMLNE*%gb-hw5(xL@ycoD<*!Bv5opHZT{5u_& zC^VeX*I{JdAL=E{t7KlP{RsmX-3G5kpTxw*QsAG3hIJ@H`XDbwu4I@CCZWH#OMLaY zb!B(#Y~mACHZe6#sTS9MMyK-PTYFh!v82hGoC@}mzqkrF5rZQ1isGICpeNDZeFep3Yw2YQ8-{2+#I}bjejR>TQGT`RYzGS?Qe5NQ0>21hF`VM9QlmhO3vU z5PiQ$8!;pIkDdRJ4$uHWY#BgMzx_~LK&%^%fyqV!L}GbU&!P5$wI^!V+C!xQvc4U z|Ix&c0jv0c*yn<>_UY9&X`+mWDQgs6g*azM^wJa;>KF3ulFB2gTdsQ*F$96 z?^mlEDcpbmVv&(HF4p1Ed3Yyt~hfaG}wXP&wQa=^U5Y)x1sv(X7?4J%Z+npAteLE z2Z}(POAkv#mZjZ$u~^gp^4>(3Uko?MzP{EikWguiL+1YYEVjVBd@W7LbbS=f6vh0# zCd2>z{?HI(4=`X#7WcZ?qDI@9JqblBBhOc8Fr0k*UK07qKK6_~XLV8_u zK>O8Ky?OqT26d%3(N3E~0}&}A3n%<#&~=oKKCS zWWUB>LgFdRN}wB>8!Q4hG3xy>0lfX9F{?LvSpRxkqB~d?Iu(f#d9FG?^aP50<|o(Y z);Uw0nxhIuoK=c-n&Le1ygwD^0Z$%?gmk6&K0YOdZc*WpdAY)EZ&**O+JZU31=><; zd&Kc2O|)LE{p$-7Uh-WY>v$ktq|te{Gm>vCoeu=uAhgf~-L6*s?Iw9Z4c>QxTd)SE zMAQ^Ey3;UgS4W_dxhCPzsYn*<)II-{`)a_Ux2YpkHig}E@B%LU*#JnvIjt?j|F!M` zQxIGJnX9}{!nZhQYhMUez)E)U@8lLAdI?Azj zOTb||c_8%qbc*CiPw1j=5?rOZ<_kj`txAoo*w0~^bG9=|ztTd&}nO#Cc9Q!$oREi+Lg^eN^ z1Pp8cON8uVUH*ZuQ1?IC9~iaD6~M^$`Hx}+qR zrJe$PYHS^?s49!_q~75?3t8=-xamd3R-9a{2!mjdd7{0<<_|CD1?C1NTN9RgW;gYH zyGq~klxTWI~%xSyW;FMNlg`N=Mp>2XEL)_|jgEi#L- zQn601Z;L1kG;^@gq#S;qDMjoayXJ|@=5pPXw&yFPql_upb@c-f@zWX9iu8yoXRT2h z8uMsQ_P_dfAOsgcmRea97(Xp}DcoyD1f0Np;rmY5e4VFb?cyS}b7XBK0ScF%S# zN~IU(rMH^2VcpTPqrqFj-kQtLLM7mAT?=BU#Snhr++jOHhcKo%h@6{3E(2z3Ok`M> ziavU-QlBrK7*``CndEu>7>fmqT|$Mju^3_AIVH)`@&g5!YC`-?1$m0Iv2OjX5>##TabKI6k{MHGR-v95zut76w|m9ioXbWF3(nQ7WN~M`4CRg=w*4jc&sh6sffSl zm%$9Mp1!GgC3&xTLB^Z6idR*s-}J%RdT}9*oPa^nVQSq2FQpY4ju=VQ=o!QhAqIf( zh(fec^CgI_X||Eq2L?d(!qT<#1T*$j##T01)D|8Go34a)!w_M&$PO2scCL|o>E8EL z<5Zzf-v2`FS2ak1ev(br2A2TYPo^&+W=fcHR5QAOoIxl5&rmc#m3-KIi=2S9wq?v} zSZ8EJj);X2zgId?8leL9u)7Oug;Jt;r&)!H%esI3o1>ycKDjgUgQe6bvSlIQOX(3V z4r7YaxCh3RnpH}OQ)r{M`2#3|?A#ArGdrK!$203{rF%Gji>H4(LLA}{Dyy#)8xet8 zgTI}v*M@-6C}DloIBqrBr*!&Ea;>dj!k9EB1l0QWkBZqOBw9g7Ig@lVbtDmi9R+4O z`TaMeSiA2&R*CZ>hDn==Bz7s#QSQOD8IbU$og&5=8a)pOi{{s(DFJ%aT|yv1FCMeb zpgzT9H1nJ95uWT}j0_?$$c7VWZyD9A3r*e?^41~@SjoxDKc#(hEK-v1(@@QBy@ly? z^%3ND+uFN4nCN>P40Vj7#81m8BDgzG-!!^A$htMgcm5dpiLZ=UKWRKpHzAB)!Wi2aguBV8%g!`KJkdeQX-(w zu}d1>C5o?il;7@-7cpQ6@%Ep_Ksmge{PhJj15|Fn+z^??g(d~P2*1Y1-!zUurciP4 z=(M>cM!#lZ$DPq)FSg))D-pVEoO{{OI1V5oJD5 z9f|$S=QJ@u>gu7_28AMak8}6GUvnPiPLsBWKQeWKXRx;t+2a2u7H zPbvQR3kYW{wxZ9aVpE8qqg}j_UO)zQ8b;;r81QU|7 zO1?@LG@bKi(rJ{0jOSjhza#o8_tI;H5TIken(BNy1%cK>5Q6D(TuwJ;VEDneAKX%m zK*RXht1sDk6Xq3Sbp<+Xh*V3xE)K${>;?ke>V{~1m^OdSvsSj(N})1F`#FEcp+ml| z=mo*fSi;QrF#aob__^Oon6|Lnb9JXD*Gc-<70E86Az#iPetf@F$8BmED{Z@n(-Uwz zOpM$q-`^@T4(O>T!Xkw&8)u+9ET?Y_h=mR4&=lfnCti9u3Adn*@*8vZ{&reXi9Yc> zvuS9%&ZhkGy{=nE8btX@>G|4_f*+Ib`P(dn3v4RxlSo1UW$C=p z{30*J7D77*-;;!@Dq?`%n>T0Z?n2);Au^47UE6*soyCG+NCiT= zNEKGfgGraiZvi#WM2XPo-VxIQr^=!h;@UZ@#pWVTYvsk@mM18cTnlCHdcME$Q^hXB zT8IlbtAxd%qc#Ym5TKuru`ns>%Dro^T29w*aBTwZ7(LeEm$^XBGt%4WM8C_S+*eD{ z=u9^n{C4Kz)O8x9`Rf+k*VoMbe0MOrz35pk8*X(iGQ{2_O%!DZ*564=L&I{FG+kGV zt!_kL3K2~-6#XB{-a4wQu3f`Lx*O?|ZUG7DPD$wwDW#F_?ovXeyBq078U&=fLmH&> zOnkrh-QPa@+vkiuh68@^hhwqUoO3<%nfHC&Ah>SJz1r>Ddr6W^9UX!4fOt8%4)$mc*p~ai|~u*2c_mmaF!#{rC7xa3;Tl##MbdG@yUC}djD(@ zHmDV=t*u-)l6rFuv!5$x8sRkLdD?8sJde+sd-Rm+y$O#S4VBfLuCX}@qgkp@N6v)3 zagg&`v(>I8n~zhH7(@IK8~L;qZbR*$&wg*>wT-dXi_0b?J(wK}cb?xp zpm}o+9;V}_yZ5}Q>8u5t0o>!G>@4gv-uP*0!)`}E&K|D{;`L{El0wNZ|5_Rr$P{;A zh!lw~lvg2|t)Z!qU#-cO7zaBkS4&*ro;nK-y@DF&^d4QwSloVm^NQsZgwBLm@`JJ|}_r=+?s*3KC8y}0YLo673|L&}QKUB~Q zVRAoET>gO{1L;`eX1?vA)QnFUc@6P>XJZdSG=J8>uidR2^cW#2Oa}hTv(Kgw*sfkn z58-bf4}*zjJ1mYfp5+ZY>^HYQGxBW^99GY4-)d(>*XOf+ga`aiy%n#flP`#rt!bxn z0jpQ)ENwg88`al$=@0JcK}DXh+l9i-@6WX!$(41*)w+%dc25Rkg}H=WD!jTR8g9#Y zhYtaTVL&q7BaMOqS%8lV9kN>n?=1b{;IfCT<{CitODqqoidv^h>h}H38Jkh6h9jTLPQ@ElXHTzk zp+nZ|!sy2q-tBx>$PNPRO<{ziDXKxWf*CWDQR`|H#FI=>*lv$)GK#e~^5eG@Q{Tk- zrBjFvC&(i%RrC&>>*S7+MJ%U)k2r+E35LkpYX}hxs!NK~x%ih$R&5z3$av+rTZC@s*H~4H0^obq7&y@{mNp?kv5?#^4!5stZsg=Bqp7HU_HNc zKL}%7%>Ar6@~$7}0LelPd7*9r7@zRSZiVq(~~V{IXg@E_PkObPCrjKb9E6Jb%?MaThjR_wF;+a%RD z3GV9$m-}S}=>;^@%;drg$6Z6DEzO3BW=K66kUcgp@n30m=FkOryB_=A$|abua{=`4 zEq-;5^9YHlkyDL5Yu>B#2SFeGoC ztUkurRX8(o!|Hn+e}piEG@Y1-9zg;TBJ9Nv9S+GfC5;XKLAJ5_g+}5IEf+kQp8_tS zxDICX*8yA>^2MhKj)u zT{rZ=*O|JpwB8@WVHm+{>wAc_@VHG#*@2-OKjOGzUI3qFqcDetwEuvU?=* zjvu;BEsl%}&PT^YSr7|@iEP!&!WS(8&Bq_v9Os;SWYMR(WXiZmw5jK&TL~B4p^thj z##uvJMoc-Rr7hpoAL+WB`sU-iyb@{r zl2&%Yd(NME)M^w0<;G8r);vwb<1u4>DsXb>@opb)y`g8m0BM$7qYS^C*ain6`ws^m^S%*FPANPioJf!39WbBwiWFoK&OQ#N~n*7Y`V%t{9i=%f|U72}( z@1bOoN%C56S3|TcfwAl}j%q_D!aeI|nBM1dPND=&8n)awn_0?lrrCdn%T{B4ZS!>O zSG4P$Dox(V|Hc09et-i8mA2%8Sfz&SMsxKwBqaP^A^uX*_J_yU&=?^1aRncU;l_d} z_*y`W8KGXf{i}K?$1EGJ^9i5|%J)(K{oq-BTlsPoja+Rlx0lpY z_|U*0qZ-*Zx>i&FY}41r`r5H%VvBRW_e0ld(j#k0LEdHIH<{0~{!Iq;ucLTZMqOS* z8~Cq|qu^tqP*E{$12nnnu#mR95>M1nxFFPxXZOTdkR3hkCJv_5{G$6!| zr|ta_7GwYwB-vDJWy!k5{`Hvwdxr+mXj{2vYMX6kiHZ%)LmUWG?A<&kFs$Ia-+*4f zLE026M!G-&O*q`I)aEf*CgdK{UyDj(ZI@YenpMzupFy3~)3hT%zF-ItN^_oQ3gNv% z_JxXIPF@My&%Z`$5q{$$=4&{+WQeLjRv*0>fc{+cV;(OipQFd|N&ewsh>5xd5#`D; zl8mj~afa<1QF8K*5D*50s-SC$1ms!mva%qc%If)S*1%RSJ+<#K6oOxb7rmcdOV7%U zdU^jZ97-B=j%u(cS}9Nd&hRqv++Bat%k04 zo{66=Dw7K>AIxkQf^3Fpk%1w3w#4wiywo4f0O z>v?o?Mlkqb$XA1j4!vFMDiNlm`3!R_cG0(*@TL=X?nx%7UDOf_$uL%>j~9b4F`(Z{ zSwHxB)UE2U@ZHAIn0|M+nA&f^5Fu%t~^^?CS(gt)&@9B?61- zEm;l*84Kc}hvO@J&0JJ5Nfj1ye}cFK2V1t%wMaJlsToH`QN6_^a)Ll{y)s?l0bC~P z^fYdYL9{UB(y!D|A9Uh#wY_6y^}VT@o--!os#e22AVJz|cup52FK?k1r{gSvO>mW03+MU2259i7>sq+a9KPm#)HQ zcW(W?4sa)-;BQ=FM{&bHRM738Dq4dk*1L=#&pL$d6fW;rE^_o$d$(UXTVO*s&j_Mz zy!`<17%qd=*cs%if263r$(V1}`APEx=NR_pV-eF$f1OBgr%0h%!~a_0hU z@(tP}7=vmhKRdRm^jd-6c^CQI(rysg88;3t1&%KokfMK)2@LR*K~>{zoCO;rfQ(A+ zpqFU}j1T{6>(r~V4%SDb&wUrTo!KE(^tsvqf&poNxAUQM(G)*)JmFuf!4mEg1~s-E zp{VBBccJT~pLZvVzP?>;2!1(;YS6TOlmi*X3y1b9*_M<-3}xw=VGhliYmpj96DnSF zkU8RIm>^1%qoSagk?zf&G~Lodv5d!f1t2+$huk~!D9M~$u>fZ=^l!4S?w%= z6)#ty5~=essfnohvvLGs%SBSdW4E)LWFwTf(jGv09ezt$^)T>3ZWgOZE?L+uv{q+?{T9RNx z-(@R*I)G0VS_}N$;Mjq3l?iW>=MmJ;><=`W_F46eypDPgg_2OeluH}Up>bY&ouu3y zge@u`yvJ}YlcRc_l$7iFayrMYL78oRd%WscCFss`#O-Ib8;Wo3`kNcq>*V;RoVEBh zwn||o%~sFgSJ#G(HTu;mX=N_Poys#gUAxP|Er?m^I9Oi6C@teUoHvmx_jB?^aw_es zQL_fk&)WRgsoromN!}X8qML4T{Wk&?pJ!iEpwa0ch z-9p+EXMvTK?hAs=_*;dDr%>YL$yAVzvOzb#Uq+M;AL&;?z z>w~5i+E9Yf4c75 zMf|F#VaaX#z5uEN9f$AqSaiu@3)gn$gO6m8>H7uYGwfHR(H@?vWPy0@Qb0eW?6F+(+{ogj8WbHK!!cKoEGI}PTmQ9uOk zO`{KkJzk(S;?uQ+boY1fdOp7W@oi>(+##1#>rms1)Nj$=+m#=$4QHvZrx7S}&5HMZ z_wIY;2g1ue$%l7U<1s|tq;Br6mCw%T=vD)Uv3Q7z@d&TV1zRy=H_1^LmK1-Jbs*hF zXm((^k_&a@Q1S}++<^!Z4$og2wRA(LcUTy0U(UV-K?#~G;GBdCK)SrBGe#Wiv3ZKD zWM@_L3w04j(A%Wh4d}+y;{16U&x5bP@A#(~(taSHp$I%IuWa)4I1X8@KBuJ_E~t=u zh}8tl-h3f8e=Cf@z2b&c-4?4z>Tx1QcqKtH1QAS@Swes&^?;7*eSR9g#CVc1O0ku( z=baJYarldd!%~8-x|?MMzRCJ9gWUAMm{YT6BMj!wf1}I@oBZv|ndc0EWsrpr!M#NM zU7R+`rY#*q!!ipp>Q0A39(jGU!7S~pbBj^4Rwuz+3XQ9Q^&W&ZSwce`dR>0S3=M{3 zK|~Wg%b`SM zr#FMo6i|(ZhekZ-uF`LRcs~?uZQyq%g(_nw7N0^+BIs=%jPIA z&#fqMCS-eoQ$HtL-rlm(Ch@KfE(#a^#pR`;*KHY6R=VR=MW$W|noEbzHZmb1i4Yjs z$m0+12%U%w16DO(>xsC`NjkETV)ndxI7^(Spnl6~u^`p*E&8~*j{*&vnztqz-*Xur zXQ^59t*~I^!Ed&}U!*SG_dDYw2SwM&)qKvMXq)rB%OImPenmapPasL+ex{ncievy4 zyoKL@Q?DFZT5pax^`9QaA_BGy2$;TQHP4t|Ha}iCo^JE(Pp;HggpB(8)c4mq%GEqi zqjmn`>>hj<1ec+dqVB^isl&geBh}e zM~Q$FGQ&q}!N>i#OK%>ZGz4Ms#Rr*N<5r?B!$Las8ifTt!XMXvwA}Q}rzK+3X05K~ zMz|GLAV96*?FhK+#OyCNu@rrp+Uhj}6dc^Mxxqp_2!vJnyEL*P)a?;PRUYT&vX4RTs+gAxb9lNZ#jaP2l3KpB|?oJgNbCOfS zFSKoP3D3ZalF%j8#Vn-3H6M#YA+u7DW>ueV4oW{|AsjyKH zRNl8gk?1%2E*K$IuLSPx46(!<6s;*%@jI)r879lM)#+6Sevl5Q>X)qhUj%9fS1fhc zrnzF+UcunkGG2l|-236>vK4ne!*M5%n4pUS=o)KW6eq?k5|q(?RVUdnYZL~aF_hEA_AsCNbFMAUE<}Hyp9|SznYS2M=X~3v?bWLL9K@uZ z6sjB?otCgUk2=6XJl#Gckr&3&SXab=qW$XAn(TAw$JF_V3S+yZF#FxKSo@n}R?T{^ zx*!EQ`fge5MA@%x#^Yaq)_6HmD9<#k<4VsrD-BrA(tg-|MUQS^cySc4ID#k)#fRZf z^|NHw!KAB44rsllLCGWk@;uscrs^*J5IQL$LYC(>)3z9`b5;7Ii!<@P4zKe8XIg$G z|JM-8lG8(VYtDfF#&bfGi-q)s-3<*Vel|(MHaif<-n_RC-Qwny=;i5_?2tKfzpl{M zXQyWksys5^`ADHmY+EvU`~`9H`>e1g8%)K8mg9wS)6wU>*m1z%mLABW z(OdG``9}ey_e2-=7V}a7|zLM!8gSYv`pvU# z=+{~Xrto<3(8>ZEV9H}1!C_BPFvcznWQayT$6OY8|71eFhz6u%H~;I(fuV9As7tED zk>(upAb0CQx4SVrJknDaAtBhlr#5}+HakJaIEP(vl*Wk@0j%heK3ZB`SI3wzGMoXBF* zN0R#eqxF&{U6pI9&d%!$p^Tu}HY`&?25R}JX@PM7Df$Aq>{#xtU->xT{6SCjFhJrg zsVzHmb3Ioq+7~`AJ?Jh-NPyN*1S_$*QG`ak1KjdULXUxo{SOwP8b&7M`A*~pj)bwOsnuT3 zY`aw-gXdO4_+xX5S8y#TbX~PQv-&2slMP};buAY+jl(33&v782ik0D+w{@NmRKEB7 zHJ3u=+^WKU#7O>ngH@yI{aW1`ceks>-HvR>S~xTZMdrtq*TS>be))-d{O<4~`(jx3 z?eRo$-{6tf6v|Fq_h;;`SJoBoHJcr$XOim-GE1~truM<`L63qXu{6qEdY^G|us8?| zKDK0xh-v8sJ%{lO_FF^v>x+3kj?a|5;IPwDj_WT)$z`3@bWd2 z@CLzso`7@a+W1x+$S6OTI$wt;P>^c)R+=-Zd*lVX>@w6~LU~`^Xn#UtSbCZEsQI$o z!(LvCV;9q2tli%%fcXT5p>qfO>Ba4tZ!odFi<0iLlnJymCP0_eb}FlBC+Q>p3Lzgbe%CMPwrZ;$ywvKVvF?-7y#)G`RemKL zAfnxw)L#Qdol&!^!MynS$5S9QYG{H=)V0BTK>z&!MXz2&Nz7ZT9{tnNQN$kK>e0x4 zbxj;4-u}A_iZ7hiv_K?o6kppWKkF)Mp_=NEg4}Oh=1=j(Q&+`Am3nV!a7AAF$A$Pn ztzm^I8d05{cLhoKac!zEhm7#4xi0+C=obn-mCQG1hlPk;X+buM5iEi(c=Ti2Hv(01;z86|gJ z>G~Xll@ELmJ6$}t0`pKlf7WD0xM``r5uZ+fv;~{*P_zypR`+y0CA|WV1H7--CdonH zW`X5nqJ?56mo|8x~pf@YMwZh*}*cTG{}La zqK(BEfHh>xCw848^yycG#vfcQ^DjNYC zxVCYsT^F}RGYZ^Pb?j=YpRMDB$OlzFcuW^E36!z_f6 z`aNQ*F~GngA^7d{sswN=qMVOzqaCMpRoQE^@5x7JomYHT;YV2WRhoS_9=QK3(Fz(+ zhy_Q3ECMVR7PCvQx0p0?rtJ`uv1HKMyOAP4O9}r~NtZfjK_NC+eb%@}I$aFf^W#dp zxglMSzS3{}__amncZ16>HqD~6pfC1ME{i`Vs(~%w2)=sA0H=5iS3mk1;xE+Xsb|3N z{@o4(oPSl}+XIC^H#c`HVt)$35XDba)7~Jeh8y;yH<>{tGw8bYvJCKEG0&sCfIcDcRdgC$*yS<}14*u^>$ z&as!b&Yinv;({maa|$|HE>41%;wBiwzu9|Y$kr-qO7+k4k%*42p^IS@52p=P(Zk^dyc>$=ObQ@I*$L zRn-*?5qvpx-J_KHS@2*=`qpNMg}Kviy0u#*)X|Qa+HZyx+dyPPytUe>hU<^~2V&vg zU!+{&OZ{sl8#68`y;N4Nk!2{(7w1HlE2Y4eK;t}=*qKxAnVI`y(k(78CHpe*x`v`Qkzdd8|sw+IRcNn~{ySJPBznt7u zvHti?L~TD_*s@igMPrchH16s6jAzN28YuP43Xh9Q;Bfbk5rz% z7-6WEy-=!pAKANnYJoC;>)byzZnGvCDp50R^XXi&Qj2^>Ofu6-`)gfs24ud4kw^m0 zcIMd%i|U~pyC}_zM_=a%Nm8#q%bsuE*OTu$7)Q(zdbD6RA#4zY5hV0l8XfIjCT#(# zPCI}@*aeHL0WxZRS82oZoEDQw94XW9?y?A}W@;A^g-&$wrlmND2764nPJ;-O%?xbEX5}9@FtHl`A z3lx_MR!35KaVz-+8Nkj#9jc*@ZY_-9nAH2mRYmnRo1s&SXWH@l3J8kX(ok1ie^fGL z+iIj_b@yQD%xbTWzri>#Q&t zyl0L$b&y(r|M(Sw9kS-9+}7GMX{_d4jWjawfj*_HDVXlbr6)zA@l;SC9F$rtqSVA-YW>gQ(K~8IrQ3A1)L|fOPL?X2bDV(cHF0Z3GFALl3DHH=0Ny?a$OMkx8kow7$KX8AY{e zyA)Ol(EZ4^Ymgcj7SJDb+!tAK#Q7u!?-9HR(T)GLd{6eIl-9mvsjszAte>wK+$&8x zzI{*1h6znk&~`qwUrsnh6XpE|y_T5nPoF|)d2#;8%b@wk7k>}$m6p3C8X6bR8R5@w zJcDiHpXw`XB*;Kzklqb^0>^Qocp9QRkz|fpygugILjZqM01xmtX|U)HNR@SW{VbfI zGLCs1voq#b*M@gE0lGan)U~w~0Ayf;+TljkKf}8Va(|V)A`SrsBbAiMGH6zGhHQTl z=qBppH;T&%sA)4Kri(hFor2@ zwCIzx4sg;%kg4kC<|gKf_onS=X;t~#dmNjlHa+-I$ZoEmt@pQ^;nJU5nhzld+{9p5 zb7kURC1)dWAFBEj`Hx6d!rlQ*IE_M4JN%niqrzbFukGh*z?BJaS!+FCb^@iIyngx{ z&d@wfP4zXhHYMKeOCSN#SzaZ6*K#L>9&ih7;rC$Pe(zi3vH4!J-n3KM znx_%U!|h(cxdsV0aG6*ilDjWlvYYlNl+N$J|4tL&jaHyh5Mtf^yVmW1$JaUjF7M?Y z`c%`5-rKB8ck6jdy$7IpSQ&fM9eXT+^Fd9hskW4ZXim{#7TtDb5l7b6Vh~>acDxxTyTav0n8ep~VP(rn;)EptN4r*n5+2 z7t16(9x4wbZTF z682fl}C-GnVfz%(R1W+d%_wucvTy@TOPNml+<8PRa zq{R;$zaK5|^(a1Iez^#=s=ATCh@`wN)#4ETLRh>YMv_AY)qW0N`EJ%8_<3BN zwrIU=0)eM|Z-oCohlZ#^RWx`Wj@0|TI+WnO6}&Obs)T$BAAW3Gc_ZCk>EVFoT&W*C z9v7z^(PP1BiK$u#UTWEK#VR#k-Law}N+;P7nU}at$rtxZ0QzBRexKp+lbSZR?*1;^ zNsNSj5UI=uX462? zu%~1eLy!%9%-QJN_=W5biVSOyIz z$%mX-%P7YFytP=oHI8ZxZzyW-&yNM`NkVwZ^X##p4bUQ*73fuq%sELXosWX;mRb%^ z>gL%FO7}sp?YTQGO0H zy`L4dH;Z0!q8U2!ZM1U5ak1f&uOh-O)`9y&NbQ%wrRKp1;m2+C*$V3xqG_*1(A^xU zHQvsV`cFxO$7=LHSLt#?=J&x#vo#q#9Ek+28lH$LKi z8tc*J&87+2_jaNl6eFkc#Y2?R3}-?!A0y_4APlj3Lj9{*@ zM-k5rj*|Po2=jGW?+grAuGP_05c92iSrLp)xzg_BTuVR2x)Xkk2_Nqhg#(YVkL2;K*{Q@>G zL*+eaPJv~+vC4(}Vdn3$+g@U55crHr`c>=9j@FQ8sPcB~^oBcNBz zTvA^d#LS~yLZ85j5pibTI~T+6_~}9MMW{^f+44Wkcz_4;au>_ycsXs^s|U-L%mar{ zPQ!+d8};(B|8stXdAY>-Zcw^Oz`bFwIx-MEw1oq?gTp*^=my&QBCK<&F{wxUEo*pf z*X8Cah2%z-+gZo`j8L0s$xF;z@+!nGDAWO&Nip!In>u<#f1)w{C1MAi{f_0EGQ8*2 zG=kUrgGBgP-|Fr-Un9C*-bcUCu|}JEmck6YQ`_*n*;16z>JYepNo=0#CP`Lg~oI_!1laXDI zY`Scx&-KCtdpa~AAB{L}u~U%?F?3)2ry�Q^^(98+3X>_&c!{2W1F+rVvV2m zNV0BTU&neq;YQWL%8kbNIU_nc^iN+-CI24#gq4Y33JB4qX_?Jr#}GI-sQdB-Y@2L9&(6E63hk92NS%=YSR%EWESX6 z%Ma(4M&bky#s@s#iL1ANgyXH$-5!UUq3>$7k#!!)^MHwBV)%09(9l^X8y02AA;8KS zG9PRbi^#u}LB>6pf=!ZdBnVj@vk#J)J6C*RjqikiIa0>&&d0((22IpQdVqgt@z6<> zMtHow=6ib-etc*+^6T+C@4LCW<;`QSwUNYmN4(*y%8f8?zX1rS>PD-IY0?h5|4bLn@-q}!Erz?5~mdXJ0|%#c^zdn>s53sx|!6SWdU zf*5g4H>K}BZJtw_^65=ut;;LhCeT75M)bh>ZBIBYa9_Yn;;Ab&?0=zFn=0$haC!}X zVj_#q`P2fAeBGz;4@!3~Lt1Qr4E?jMw|@Nv)`i|w)Q$7R2NLKtPD4z@)@&=kipOjP zf$HKd_En4{XR)+tN$ z)!%>d+^y^yEH^@9g6fP)Wl--6G?vU&&;I(!>n6^@ql59u%rUn(}O$_Q})?x-j?(<720`yiX^03)awxEj~~vkl7H-U&&hA zAzrrPy&S)*Kx8g~gWx(;fAtJ*z|Wly`F)dqqvET>8PJx53Xf2S^q`~Y7;m3H@-#@e z_g19&n3JK$p^~{njT~Y;b>E0WXfnCWaadL`2#@58O60A1r7Y$Wa;?RMT<>%^(-Bvx z7D(;zag?7nP^k`hgWa~na!Iw^3k~&^(zMgsKeA?YrFXMfL%_d zMp4z()cw2fGy)f81R<2&VKdB9HNFb7N(=E^9Xr$`o35Y%UsQ#y;IAWEt`3KV zf?G*EchYL&F&2pA1-Z}-yWb8;|4ghY^n&TETn`SjLX{5-Qt#^~q@#F#D2esrm88a) zmqrAM-ttwyvOGTFZ!$TW`^L8R!^_@n8nxV;VXW%(GfTt+af)F1eMv(p@6Aq+AkPmX z*x2vs52tP-;zG2ninkeilZAWd;qy?dvBYbUCWGp8a;$=^oPH060iRil500p3{BJJ~ zxD4xA$GBs$om}Dbm#QV5*%MaMv7Kf3qZy~rS9gBb8Z&>1xL)?Yk&P-602+g|IuDmT ziDiz?+zbyc!zyS#Ye1%Dk%V{UB29ccoLh0CeDX(%5rEO_+VOkeCusuy7K6c1ktp-| zF%^=!;#F1O2N6kU998+wYUlw`Za8GAZ7N{|ttKZc3T0tU^um<4&Z@3*hEq)Cj(x*+ zUn-;I87c9Yyls(vmUfvkY>v`Qmx&@V>OlT(b;_={!3^2HR467@B-t~lg)gpwF~p&H zfG+ypJepntBGCQ*d+1xYWzx^cy}J4@ObpSRfrJ6@eL5>Q&IO{KnYp4!1EABr(#AO| znV@a{xWZ*WP50;Fbw{NigkdI;YLyY&^y=wey4`tktb*4>B^?v?N=RCkV zlegpV6k}j9=N$;cNyJ#?sp3b+VLc+NL0KXVr4Hz`D9}?fILkJ^Dp=CDMM#~FTfW&i zZyt*h-F5-H7yea}!}-s3pzZ|=$-VR~-Z8l$6=}sVgu+rj)pQ9?1 zZd~u)qt=OZ4twb(JKJe*d^vAyJwpwq%%)*VvH0)2-C6G82q>a)J7~hM{4CQSqJ2YP*Nk8C6@4ymXVF`3;!j9_ zk|o(TVLXvC3s~mAeJDok!C{?;ff`uviDGp%_}+Xn)r4U!X-onT4}n6&I1OZF_@+4B zGM;XiKPyoA-#qv33zsAzSr4>#xrB;D@u*tS?d&dMxZF$=0!RB1I}~Z{ohZ)OOqP;| zBVK#aw6xE9OWUd@3Wb4QO*t67oDv6c7i3b^a$Dkgtdi0hGF#5)bw-S!k6ip_?`jaD zBPoHo{S!E1`x`f8%>s!VjvfCNUwtzoD6&t5WpI{+qpBNUtqUVQghX3~J)Txig+5OD zvy25*jt2(+MWfklL9g9Em>D7EKa}F$LZ-1+-$d3ev(I1C zyoMK2<$X5_u}*@zDxN3z$ozf>w?3w6`>^Sa_WCY3Br_Kwr|gaIDcejHg>{|A;WgV> zDjC#db6&dZi_~F2hy21Zdg4}4w2%goRVCJCx>;+vMdR!DLdd;1qi^ZX-oj?lV=M3= z0+?&G11{7Io7f$uf}ak^7DWD5m;I$N`}=Fc)9$T)>AB+UJsj%5F2@HsbLj8cJwjg9 zd2%`Kb58uyr2bX@+x8eEmm)_c8PM{3)@f_!yW<@Z+peAw`And z62JYnbFx1kG$1%NgW3nW-^+J9d4uW%Hw6 zzekRR)qzQ%S7kFI?Ia)m%0G>DBDTMQ_(!@VL<_Dn(63SvTg1Is@l_X7IW$0vEkT$3 zNAlL6Uv7jyJ%p7abmets;;VA?I{BgS??O*RJCmrOZSKM){4a>4z5s>LAJYMqYZQpc z$^a(}*ZkqG0O}Hb?`7_DUu)easdPPlJ~YCyvO}=Zu9D$DEYbPKqxJk>D-L`p=-hApOUG0C@H^;oarHNeTK8eF@-z#L?+WgyFDIYbI82 z+yx>%A%|w`?LP0%d$*fzn~_sNL_cwoCPumBay_n3mGj&qIr-O*D^IAlU~R+yYSH|^hQ@#XQK$$5 zP|zl|;->$C#Qn$r`+xp2=HnAwtdZI{@c$ow_4fh=Rx0I4K=>$}%4#P0Umn1JekwT> zfC=@J1E1l|f1c0(?MD1_u`%zV!_G)w?r0V1VwVU1T?+l%)%>$`{?8AeLcxXhPiU1e z{4LG<-$(L)-B=+ROmH))lH+mzk+uGFHUA$UV%|L6%)BWh-T&Lo`~Y@Ip(bmochJP=UX!NZNKhM3U~H0C{Ot-j{XW z6yo8}*4%OwH=$GmRC10&;zO8E2PT**u8 zyocWC^cav~vw5Sn0JRpc-e6DFK}Mu(x@B^0|5N>NRk}{6&C3(tR#NcoQMK7ff6cFD z(0jb>90bWgAZF;*Jg3d<3w?#O-GgnBQ;eOd(qgd5>QwOnIIJp{lTRB>@Kg1QShS4G zD1j(I1=~6G0!AB@jVITCs1E?V#c%(_%$cjPU;>Shc!J4dgsGF%SU|T-nOxom9svL< ziy8n*H|k*J4`gae0p>t#a#E{673S)4PQk)-C7{-`*PE)GgyI^lO|l z(HyruP7BHa8vGmn2as7}zxF+pZ@RF8?`@0Qd0f*Wkx|+D=xW#L)I7*3{%UK`e7ZYT z%Ag?ldMt(F_o>Nso%dt3N9vIp=|Y*VHa`EZ*W1YPe1*YuJ=e+NPfciqzy?a&_E;|H za*K2e2~8`dUarS9S~T~2A;MwvQiM!k7vS0HO!sQ7*nSoFUo(B`{VW9S+j+W(u2d*e z0baA{uR7&QAP-8fDlC9)B+NT8#|d>N+Vl2|Q{=45WU!}Y<}3pF=~cz3*{DL+h)NOZ zYFkrdk-8}9cZzsghf?M|aZlmMCR`a(RIXPo;dHowQ^_RAa8=4_cW0~&$Uo&@z)r5O zgw)J^Pv^hg&p(T_loeogQYzir3z64X1}*cyY21sj%qh=*6FJ)hO0 zfXK~<{Xd?c@BANT>ckL{V&AL=m{l=tHwS~Jy@n|KC#C|T3LAV-b=aJc*7^9N1<-Y< zaM&*Z8;uU7u#LFx8LlY{7TdvT z$8PAv_8-l*{d+zlGb1XHiMU^=3P?_1As~?&TUl8)$;(d1cfaD&6@13Jl&Cc7wkhNy z{QgB=R#vtE6^z|@5H`Ry?QnBuaXrqkicFhA{RsC8xw)u(hbdZix^9>OD>AS!@rY#BTr z>jdz0yzH&}e`E4y z#xf@Yt_MFw^Z26YwBohvEenWhNP$1{1heQ-6p%sjl{Nscu)ilVd5DwmEfm}WIsb$A z#S-t2Fe7b3y^R!ndjdB2Pr_74h%d+x;a|MK>?TsEzieG5-Cn}tCvgy?q)_J@zOudx@Tt)g zY~-JhFH$MX`2_G;K_EUy7H6Qhcv)oSSVaB@0;Hc085unMzkRcPB#)tnC&Hmu$(RES zBArzrR)+Q{06{aMQR&>mAgVO;{uT|L8Z!)ssYuJbr$m!T!Le>q3rvX&C#(6QWwVjg zTnq|H?d^Tu)g1AK>0knmoEPG2+yRp01KmNb+Rb4bFR)m^>M73P`eJpW@a=^*_k)C< zZDMFMA};fr+5d;VuZ*f|U9t`CuECuE!JXhv(BQ${g1bAx-Q6L$1qtpD+=2wzxVt-V zaqd0$-0nX8UXT9${=nG4hQ(S{^;ON9RkO}m>Fd?><667L6&^b&>e!}ff$^8Ic;tGE zS!HAG4m)yst)>t>PVYdA+T8Dg&ySEj$dX9ZKfF7ARvw{9Wt(TJ)RKMe2_E zbenQi8%K1jB`XyC!mYobRcLa;5n;C$unx3(<+vM!_{HY?-|rweZpoDLvLK1@5Fqd z{0~+Bv<2sj^&Xs~Dcr-%C(T+#@YgKbE#eYU1ckEcoMkLv@n#<_>?V!j6Fb*#SPM^x zkFjgw(%G$qNo4gXRf(vO^$pH=tNMSpI^ol6H%IKxRY^oQS{K&xcV`(vd$`7(kqc0R zO8`gmjFBN+lp)f_J1XKyU5wjQV1l333e--%R2rPw4y;zSi0D#}u9n0#WzcGpOqu8N zyk(-_G@S`O#fs?EZzslnuOW)hVd+S(>6S%N@i`wFD^&VD1eMuYYd)f8qm`O<)^dou zcSL$h%<)W3KCN!07B?33Em-%`*6`0>u?LT_dSN1JXyjdqg<2CO!(5%c;{MGGobs~E zTN^?qXWw4|k6E2up(-hObP*-E@~au^>ePx zI{nac#P58Vvhy@GEj8IF3bH>q$+Mv(BfnQ2A|ks_YI&x*PukU_$xiw11=$R`7l>F(oggv+ucQN@FC3piWYI1O1AaFQ@u0L)aLP1coO~u6 zOZhko?e@~<@(!k$*|@ua*qqL1PQ~P0>_9@Z^o}57fS|hx4#OMr%a`y6#41;K_iXdu!x_q`YPYD$Pe+Rxl4!TvNXvzo$`sx?VMtRKM}RR zSBeR3>iH-2H=cNmB}gKP{&V<5{x)6Zc;S?$TTsXIGCg-Osr$`QV}cN}__EK_g8)rL z%kTN<)^=hJEm^MI_j0uWQzeblhF{-_2o}>CLy74+(nK*R9SsLscvUTO@t3q5lgS3P z%txWIPTHqA9k(E)#?)V1z&TNPcd`7SFqtRD^=zYC)|&FzO_4&wG{(PBSwa0QiNb9b&|YsGg}S-{lM#-<0m%RM7UX^FaOI{D;%?VWb-03YB{ZP(%> zkhLl6E-<1GJ`1pq2+JljK%69{KiX_;Cx2Y*m`_D3?UKl2FZM;-VPJGDyIpBB)pYFm z7rOAg6brP=e@>;?ptL?oOQKJoCP(2js_=9ig3t*Ev`d8+Q*PIbQ@CuF8zQM>9w(VS zV%C1wdo5k?A=!_OEI24OwK~_$CjIy|nxL>fpsf|*Yl59(-oV1e z*XdT6_Q~p)d{!S;{k15Xcq)1Q@wbU?`cmu%=~~$XOI@4+!MoW}nx17St0##-o>X!H zaAvnoB7e>R*GR8&aFfBV$*f)-*9E&+oa`6*b9Fa=z(ml9biE?7-e zFNh10_*nfdSPy5f^*p@Ek`fN(6C5QyEM*EC6Vf9|oYt!S*0wGF@5JIUm&tX?@4kHM zTyR>7ji!bjl?YS5mT#&sx$i`X=qBp*`k3~S>{v_hR5+#uzb5WLEd)UBm+k-D^2K@6 zM8yHMLL(l9XGo;@@J+LWpYc0+E;<*m{bkLPn%IEmOC4><6C?#VJGk%%VBx9M=mzBFIH|70vFrlEJgXQ~TX z?mochapNvj!7qVFe`?V4j?M<*9=GAtlGXBd(3f}QqGSPP%zM?}Bi(9WboKNh^`oqp z9nv6n8z9WXH()~4$p7 zV-%5v`QZ?vbf*4*f;{6M=<7}+SL$v%>bV716bPKw;L#fNt*pmsw(He(;?SPng$nuZ z{9pO>nvkuULYEL{$X@U#O!VJasWy*#IlcJ#EX6?Z1Sg^(_`@q8POp>RtCIMMT+%Co zzUbVi17;6Lu=ff0iBHqVjPLzjA4cbtJc(~r@@bTc-Q0incmZjFsF%)uMQh^vjXjmI zNHA-5y`=EB&N40Ctj39Z&SQOeM{$Rhe%_t>6ETtjr=x632|#OE^|K%-n6v+)JQ~5T z2SJ6K`ak43e`GzKPmzKn=v)9E&?ml=H4DSS3%=Up0#l{j;e6E0!lA!j4ONxGfy8W9 zX@<@Pk2xs+l*;CdN{yOeyy6cvWK4x9!sM=6qWHO@XbzgW;6Z=M6-yQBK||qKh!I7B zRLU07dU-7kV~xEU9%_e9rBu{vk+-OuVfUcgiOlx)^Q)OZbbSM)4YApU^7!OCfAOet zlJkZnG0TY0*MCWVE@@B)3RNU?jm!PSHUbQ6|7tEkHf5WM8)cuuBHY8>ZPnMW(n;}K z?2nEyr4>1FmQ~`S8sZ(f3@#~wx}7)(7<8m66qe5rS;`aEMTg6c(R>*rwINtQEm+_z z|Gm)D?V{Ak>;{GZF`Ddq${Zyo+^b#?=}C2R4WT=+uJ9ZAPs?HN9`6z&O?t4TOu|Q>< zVIJDJv(zz0Q4<>194YiGnjF%tmwx(47C8#Bl&4>XQY?>3wTq_ zOeu`h=iDt|i7VSf@(+vFUJ4pvLUyYuQ9jNzT-)1IwEN?S-21qktayJkuGNye&@cs!U)h`^DdLvI%Wt@BOy!{8@25Srl+!9w^ zEI@nFQb}EIs2kpEXD*z<*5E?mC%GE+7N0P#i}Gr~Yaqt=jRSnw6I{Lx$Yf-~61cf# zc&GOCnAu2A5PN5K_;Or)KqTU1l3VqHAS)tzh=n$e5FbodIB=?(3T>x+&0O+q7OoT` zBmiHD8j9TojsqxkuBn!Gr>d0;@8!8i{wq3pzy%Iwr@v_+6PXc;sdNGAc)R6%-)j0mj17)J-o-u4!u}Q$ zHbN0&p}vsgJk>ZLw_S5?ENLJj5b_$8nQB5rbh=3P8)Yz(Z>K`23UxXMWw7_;U03Hb zZwuY8O)vtNzx;r;Pnl!sH&l(Ou6s?gSl(oro+A(L0JI(A=W^GtzU55Tz_L{Gx2n=T z0U=3x&`@>Jogz@YD)W6Ao?rc}Pf^QFexSybgW(%zn=SV9Kv&^{JRdSyE&1|FZdeB28 z@I4}%i-trC>|RbF3awlX&5wjF8YxxI;F?$*m4aODCmEOq1qO~i5O8P<2fQ zF@X;ZLo^|Zp%1??=vSv&(r1}xxVlTVN@i1C{XxiX@;>^-quj_lGM%SKo!nMW3tSaf zjl_&Gu#Ly%ID?_KzK=qnReI0m0+^!?_=-`-fLv?1eki)SJGjXy3)|}|;JGQuKbjb5giTPYZzpMTkMT9#N2U3-f@B9 zMHkZivPAwVV)X!$pZ zilh&0FNGfj@u*Gfb=WUFCa#|9bt-3<<4H1G--y%WMH>zA1!FqZ31Ip0QkYm#Fsuv< zfNL^w+!EJJb+DM_7-I&}Ju({(dv79ZDBz!H*>$8nIy=2#DW<4O(YbB%87$|wX3mD2O+ z$_EQYsIq2($6dp$L1qW(CCs&CMG) z7;&Q`?e3DyKde;rEwF$^M#RCtsDr#HQ$sJU2zg7e{RGvd}!Eid8_}K|L)S^!m;#5HUqfORs@ju0n$(gP5cmu zUvAOHqMuv9`w`OX=}6Bb?%}=>$Q;tuE-M6OL_r^WMJ_9Bz*i#kY=Vk9fb3aj71fK* z)>1aSW5lM}ECD2q(&N(U>+MzE$BfTml}Dpgq|wI&q!Dlgz}{A`^t#5mp+s5@komn< zHN9{ggULBdYloW@0#zTIG+AHHl89Snuhw@8NMy##&5Zwqn7n2ZH=N+iQmfxI>S_5F zx84Rl8guye>_Rl|Sfdl`%2hbe?c|HCegihY*xQ6YtU^2tSG7B9(Nqrm{kkH_Gh>8@ zMX0vlqV-C9ejK=|NbDf7@$Frsf+augKdAbIOocCa2#MS=Y=SqKfe2{P5`S6#7{u9G z0|A3l8VcjJf(Qab!5|lpoGm73EW$x%1edrwKbwhfixWisy*2Q2EQ>+LutNzKUpwR` zB_)=XCL%1kYy1*hubESfzfEUXYHR)i+-UTqV;T{;Yn^gC5J@$-XVB&&_w~?3+0Mmm zZf=i+My~p$5UP$NEIu_O3Xr#E4J)fsx*)LTUvy)qfNO@}#EMbEo<)P2QvL{ucQQfh zd;>;Bu>!c02ya{G%cv9SZ4j#)8M^@ z^EQ_6r`MC_Wktd$9gvJtZL?C|tAasA?P_iwaLNEA?UQ8CeJtqiPeW>LCk!t+S~-1W zAn!gGQge(eP7%}!5A8(_bO@s6C!EC^7%?4X1cF#l??AHB8j%n>vY7DQwmAQ3Gnjl4 zAd;uDz~knR0WR?_L>lkE6j!K_$MTQ{ah>{=-n@<>YF|+Zw?p|`-#*S|KkK&7TL)KyYF)t2_Mln!(@Q(Rw+ruMSZWp$QKe&!^L?S{ed9(lw1AP$ogl8QJ$7<#eHqp`Bpf^zah z@N{4!ALBIYf!M0q=vQ4=EZoDuO}vWL9y=Ij!eI()jt4^@otm z&NH5PC-~|c+z=c%QOZ?;B0CI7%1f{oAVKt9UxzR23Ca8r(4%D*x`QghX#0&6w)!<4 z10&V8eY{fpAwvKTQ5&E=qSJhMC?*EHodgZ@eT-SoeCLW-QFU^xWS@=J((joQQ`$K9 zj=TOZs)PAbG7Rty_oYwqN8hY7fA1&B8HR)IQsXn4O85w3l$L>IB6JfZ*X_Fr{0WBI zT#qVO7Ah)J3hO#8Tjo*tYv^3fj^1g*3bMwLi9W$m1kNpyJUZX>NNJULCLw~CS)+6} ziWmfe7&UTT+Y#WHdZZaoz!}wsXk}_65*zjp!$M%JaVaAF;Nrf*VE8p6N$3q65ZE!) zY?QLz7EZ?J*>H1a>}vjO3TcPh$lTSW(pSB#*xVsc0kA1m7NGj6*z}H9y^%qeW82ru zmYBHtrd|W?WE!2Fa^WYF&L1#asa-5zR&iH;2ZR;HLKcrfGhtYK{8B9dhXsULWPuG3 zrx_r(TvHU}{Lga62+@QfWTH5-HdHQQG3}|p$8}NTA(5x~OyAMvO}@8H&7yX({v`~Y zi{Cx+=zmqIsVy7sRBB)L%%53+4x)c(7#%bmvFWcVn zdM6=qUa-V0Anc2+3Q8gqPO~SdJWzg205+Mqmub49aY0|2vPe&gvJ4L#=5Fz}?6{zF z8g}qm6+j|4;l6A>Pb9>%~klCi-7{(pQA8v^Sqce>M)pi8)6-6MfGgGSbPj>cI$Ub=&`TrU{;@y8N z7HBnZXSAHcv<)kqxv^3q+%-Sx}zRS+sm!9B8un_C9!k__FDlyF9Xdpy@PV^ ztI;WsT!Gxm3NRDJX&oxKqvRhQQ|DMe`w6tiZ5wsC()E$MM{)KfqKJCzhHi?sr`X&g z?}*U6=zhS7fj;&NU(%CZa04%yEK`X=Oh4P`TLHc~kUWW40q2?bPIx<(&P!C^%+1=X@HkDA{ENh`nw&XsyR5rnDZnX z+U_FCv4|7lgI~{h?V66s4{~PFG$Oj!!Y7GA`|F`Lkg(g4+otpe0}_mAs!+|L={2A% zens%$&j4H?+FuQtKIS$#MRlFvLXRoe>jJ+V8it@}>D(pmpE_QKqaD?>X_a%k*#7iz zfvef1&EBU&b)og{;Q@u9K)**KFm8rDvJKIDb$zzTTR^EY5#ApB*a@@~!>iGLzpcq* zw_fnP*Tk;YwFpPxy%P+cGXB>4W$WenYh-Z>Ma?oC9Gm-y`uJsT23zDqE^6!2c|>3* z^RDr=eLNe@VN%DU-2-F|5F6Tc|A-Cni4lHMH|qo<1T4`~Jknf5t_ABs4h6?;xi*3) zl8jSAm-GWg*j{vK*cXHhuZ7(%1S*g&XQMn`O@VB6E-Q?{gMlxinQM*&VNZVh`4|*# zcD7Cl_Bnm+0g#)M1EJuLfmEo$r&fdS$``IKFhlu}F=>@0WA_1|*TBoig92p_v;7E2 zlP&j>-Oe|uT<@p!o#!fb<$HC~jjqx_ zX4~Zpv^K1s{eT$Wp3Tc`vKrQ}AKqTB_3okT7NCEgH_3i7Se>^U^>M8O4$CD5LFcr< zJGmhTBQ{Y<#Ia|O0|*uyFb0}}fNGz}HHXK^yE*=&p+PC~{BzUW2dke6)QGsWBqd0> z;Q4J)k@#U~)wuZVFx7S#_VGQtk3EzTa#c9)ia%u#I&WRaw0gR;|&IiB62t{ra z$J=jdNsy~~x_nlAP6&r$`C9Jb0uFu_{B-eJ`GRm$H4a0E?bCyygia+@!|I0Fc&MeO8;>y|*J_8kF_W7gq5CO3VtVS7AW z^L2+#Rg$m-jF+BSK+UJJV6##igv#?yxoh6a{mbc>rJ0*(cJSZ@o!m-r&jVSiv@Iz% zPq`Gf)E*_PU%#ZUW)Vy;v|SHx?ZAJ~6D)N22y_X?=!jm@?3ToWo9E%4kBzRV==wOJ>Q}#<6eOS=0H$7bDtxRBx7?zsj3^w1Ms{{1==*xQyDOl(nr47lWWSRDev^=3Y5cUCKJ zwE$LVxYFHd)&)X^R=vhi&jd;pin{?@LJa^FH2%hQ=zd1>rK3Ina(cPVAd-giz!^gHUUE}D=1|2S~S zx_){di@+hBYu#8Yhi-fj$$$l7iYe#2`2m^YW|Ylpu@uq3z;)RH8k7g-X@`kQV4>g$ z%EqCT@V%8>!|wqrBRx5eaecCBc=Cwgy_zmW2dt?`t56`db!w;)Uu9YxzI<0W`~}o4 zGJ#*!W>KN{co@zok|o%@M$1vgtRCU!^8- z)i*9j>nBAuB@fo{4<`10e^Vm)ck&65P{9%0suTD2!UU%HLTc_k7*u5`H^}4#gBLX7 zlS?PP7xL2Gk5u}||8h=u0HHxVt;x#RLOOBG;fPly=n`gLdeJ@b?A z9^~~PJM6T5kA%NhTr&M?PJlG=r-5sEjbNaF>=@oQCaHsu{cb3?NxIvW$DZYr(wC@B#EQfQoZtk;Xv4??TEV^-R!iv<2L zZ(%dnDBE;5f09O3AuQCn5JwWQ|7Lb;u>8C?I;Wd|D3!&wN(xE$d})zEvt+|z`(lSa zeYDSj+{W@%q}o-AAU+bnH+Bx-9XssKER+PYX&5v7GY`(RIInr*B?n<=abD47Miku6 zvSF4oO8_`wQxRb;9L(V0}Tc+>gxX=RwF!WV?WXgtTYwaB5X z^n%eS>OY-h_(+8S{0|ti5&iK|oS#Ip3;*Y@GPSemPXLLdxD_H(z=yxjt)pKyvqoxl zV#Vc>Tc8mMZ>E^~H*aMv@0fK-hY4Z^MNZrN!{8jqd*EbhT2AU0b3CeB5cAV2=*3PJ ze+-T>LpqUG44~~RF02u<_z(Q@dbn;ei9TbuS@9I&n`knL>6Z@yx>{hbSq>1tCL7r% zmndv=f**+s1Bn(6|IwcKe9#FSRg- zAf9fMe_U`S>>a-u4{0vnppsd)Nin=sKbTbxYOC6>!PrS=(3I;KORtKL1IDV}8A<%7 z=G7mOsT0C41-OXT<*cJ_;*QYc`?WF8`;pY|p-9fo=0nh(Nm9^)USo2rgBjd)@HKe? z-OGdM&b5A-oqmrGt(SRQSr`nw!52mW@iiznhj)(-Nq*049_qfI5E*XjQ|R!kbR}fw z(4zXK6IyJtSClhEtj%gLxNMikYu(-sI=;P}4e#`;?Rfr8mkIq=B0MdjY>6{9;>T#WR9A8(bHr?-^QmgFSpLM+Qsa;H7 zSdvvMwYCzJ2@f7erY{ArEG%9&e=5xuyzS2LeB3h)8(DX>b80U_&@D4Qmsc{+mktT3 zB`AUlBv04_ga~9ZG-MYWJ%XPn?%b`O`uoaWv**_REvGTWzCcGEBgScIq$!?1@Jefi z&R*ax`MaHdqQa0v`eAQ*AXZ{l*XPZbSgYWSiEFZJYUSeN8*E)8LkUcJq}DfvY0yKx zP4rSg!@bm!0AAu6ipyq8qL1kB8srY{2H9KY4+Z}%RI9vOvp>F8<2BZKZx5X)z^dOj z+csV;49mArFbdepKdnh*bD02Io{7mc9u1cJyxpTRYs`I8@YwGK8QbQE$!Of`E2gh> z2K?6|Zj67TQz~P3byqv>JtC@9sj9d^FSOVY%RX=$Bc6vH67Fn|YA%7Ud9V>qJR%$h zq6l3m{R89rnJ-#w6@a)H^Jg|b+4yX^SK|B{G(~e?VRuRlE>RE}yco`4JT+(EzyJoX zVv`#Qy-q9c*0`=OKw){y)EL9-FX2+9^}`<0-fdeZ)$>Q}q5)xRd|6FYk&{nVI;ocK zfogOq1+2t5MleB;9>4xTRAyjI{#wXv7qdj2Ki@Y7jqmgKE6+etbnYUO$54LJ``B;t z%;2kBrW&as@Y`QX2Wl7Kp;&whcJq1P>X$_j#yeA;izSSc1>OiK6f=mlrwtN)| z6i_Xd>Q-gd!xPPM0SBkch9rdHfvb+4UDR*^lsG?b&`^z#R(O0eWVzPciK?o!5us)T zOWdbef#=a*-ApOKGjQ|`Ml1NE!_}Y%&5|G#j^hVmo^}p@lN3>fUEZrRZY?f^-dO-E zJTl|D3P!6)!8{WowucnXpRJYn0(8C;|c?>3jX6v>y15i;v-Vv77r_+aKr~Ril=Q-HCRobaJ zDmB7xUXpy$CXeO<1F}oK1e(mMQFyEzwDI+QDFj?Oy2NEVtfu3+zsly+J4OjH=m=m! z4Qb`$AUNqy-JzyFdta-93oUd!Ay?)vw>j;85m|ulS$JlUnXgMW8O^GQs4;n_PkCI3 zkjl|;rly!M>z_|Zo==xBzupJ4gU1hGhACr1TS45QT9FEOjbNutU^BUIX=4NuiS<02 zC%&A6&~~ghFrn>NNSQoNL*QbU6%&`7K-LLIu*+M79J_K^q2LJSNls>IqB_f6#k!_P z=AkY64N=Lq-Mg(hY{nNggG4V|z^6;bUhSIL`o; zaTLY)NGI9n$Gg4Bq*3#;IAfZ@A3o0;#NgWzwrH^ADWD1e6X^FW-7cAn>u{N~U%t3c zJCZlMYBTQqZ4Nrc0M0DE`lbL0pCe~Lvk{($`ArqF`ea_Dha6;-JMZKfMQ|q57Qn}I z+?x_JcI~=C-7HiS1?)YhU#lVv#3a{Mu?_HQ;L1z{B`4^H?7Ux|Ar7$}DV&u;%tN}R zaNXr?b(j>k2V*nCSl*JDMiK1upb|T30ks+oI>2A3(^B7K)?be^tf!a?xRyrzq8g;Pyu7>?LzHc!2j>gj1-I8f1-sUNMj znBf-JemY}Untj2o_Va)q`z{$vhOOW4*{}?xBHN~N_Wti;{nVQDu?Pp*afjt>Uj{Sj zIOAi()JXwmFI89#{2beCF*dOmA(dpdPi4z~z&$SSaAyx%cLIqK+@P&`5DZuppc;|~ zLOS7yMdw6_pD*{J)%Gknj z=!Q<_dtK`(N^F#0G1qf9rh`Ug!PTbTeOVNB+d?Rm7+x3Si49;K6Fz z&pRE@MqbIgjyhSKPH8iiL7lO9Gn;>lC!;_bq;-OY0FtaCA*l!f;qjfQd}jkRhnYDD z_#zn8nn1SEge_hSe^}om``YR$a|`k;jv`b>=LCmA3?=!P(ofpR+jufBG;KO!@8lPD zF!D>%`i=mArz4%HUlCW@P<()LRht@L>y1nH$xKG;mwtW&_ny(L3w70F_QQeau%0=! zZI1Ox$3p{|gjS1lB>(Mce$LlTy?$>mpAJ}p!SPV0+!I6VwX-+z*145K<`=hyGa*v4 z8hU0D>RAc}WcV!Edo#?w>YL7ls%3_K$;6~ABObRw((kPHcUX;QNte7(2Y%bP7@ylR zBi;-$*$VU(eK&o0ku2dtj6cW&~NaAO~+f*=3YA9qA}{UMSwG|7~dc11}6Ubi=S}yL)XDO zVbn@C*zT@MIOi45**w|R+ZVuS6fsS&Gju&GqTBsYx!BNavHqst|3r#<0g228hsf;Y z8YJOHrMo$nDP7UozrWKH9IZ|l0CGJ_%z;GSgd#VOgeiqV6-E_@B7)hrm&EF&I2t0A zCMkAL~18boC=^g%3-M)v_N^K(56W79-4%h9eNaXvC3%>vu^WAE+O7k@n zq9!ax)d+jLZ>a~Im1S6yBsP{CO_e4iP9Oa89rqM3r^4HmS90A$ZchYWCRwt`H^E!= zxqi0rzJWzuGl02)q23$UhgNWgZGH3DZ*@okD|M=n4{ga?9_L>TZqQ45+)~FS z9R+%%1WQ9exIMbnV9<`16Q6yZKOWhAqz@j{*0!ml*i4EisBawDu zlWy$l6AABKgcOReBwx>g#=a2Je7In|1kop*z!7|Zv+saFaaz~Y@+F5KoA){-%10!8 zwj3B&f{|oEYaX&5#Q>pOiHk2iguUX-0k84ldQXMFe%aUh;wsO72wU*4|Dt!(3wjp zMw<@enED#mKXRdS91bvGu+N|SEh9J!#7^e z-MvyYA=6fFLV?J+;EWNk9nk8PY5L0+2_kPvFY0unF~?jc0dO0umfLjj3vzEpOr~u1 z_m{UjFvR+{ zLecFs@k$-y)`pw$B?t1u8YPKECoVM36F?7?)=#-?p7ln?SSBGd8cvek3jT?Sd#^~Y z&Zh_rwTUSTEg;u1bH+6_cqh@bU%}nrxrXg_Ri2PCRX!aNztq;6&6d;DufD{X$;PR3 zUI5hM1WM^7g8)Y&m2C2*xrw41MW(aB7%HX|@`M4(<1%XbtdDE5g2|IwekIXCwO1NX zj+}Nf9rmJ+o<4eH58f1e&vk?2!Rf%6Xnm&8CSnw@uhds%1$H4O!@aE9(p zNqOuyC0QgJ_%5YT8PL+0v7xp^_%6;}m>esC)!*;P;9Jwf{`L_AUlcTh?Pc?Wh_9*t zGFQ|Wp6ohX1muW^scTS)&^q}1&zE&?aCWkAhJUvBzfleC2?$IdfY%P$9!>w`aLoqZ z1%F)C{;ZmHIeYTxM8a(E=R!jbZdV&rv!TEU zDgrX-HF6W%`DD-nQd_)UqJxdKG~tjDoyVut&J|^nQ)_?MLo=K5!54sgoLL)V_(?El z8N;f=7JkhL1%;rFTSi7B;Zl8P=6n2V0^ZDH3-p@F(7-@3tq5TQtqP8W?I(@*4v@cO zF_OdD6RFZ@3WUSjvCw6h)8Fy6D@_|_Doum^^H$`w+y)nJQv6qr=065ha$oD%s1?TR zJQgWn4?*J7VI1M{G#V#X<%s@Z70@8%JD(6aJnslpS^Q@cK(oVmXNBwyOFvxzv|`lZ zf{M_pxYO{g)pua7gva$XhbJj>{8y!RI3CA0`8#e)|0MHi(}~~p2jy%)(V*~PJK148 zjGZad@E3hQ<|8D~nWMC2c23>ZWS%8aB>0=w%6}|J6hmM6rR(eKyFoi13dc)7z)PXQ zI{`!vPTVJkONQJ2@*NUBf1LH{kWcKqP>#tc-b~F?JWVT?Z%9Ccu%^ji3~5fMj*c2Q z`Vh)re@v1cJkO>OuQ{a!z%Nq1pwq6M-=t#UQV)<|MJi|JT7FDK9?h#kxN zP~2^KxY(#moop3B1}q9tC)B9_h&7bXiLy168qNo3BDFb3P9#ywWfde6Q0mWhwv)&Hv{@gE<`pRfEbLj}`FYzxMVVl@!{Z3OCH5&yr7 z5&yiig6=|sumpRBg@svv8Oi_j&Aue4b+E90{BovVA;hQ-x2Z`Aq8_Xo$)@~LK zr<+cKz7pj2rH_AgY@qNO3&I_Z^qs$`i}iFx zyqejweDPkD07xdEJGb3V8nPt2cT>~70`vM{WS zJ^u zT5No3j@%@F^{h^9F~Q4<7I>HD4&cz)Ye#^D0$%5!N`e2|abO6`R*{joQs(rPWoKL_ zff-_%-zA>vXy{|l?CA+^qKRy}0+4+x@4e0@nU04wqBa)MYBysh(teI>0BZbV`D{CB zs)2v8Y0<$Y@b)1i@dbp>wuVa4Z2-pUZ228MQX;>**k}?xe%s6OVgj?FI6x!j^*UeJ znQTej^aPQ|d~mVepQ(ZP1f8eZXg!T$H~=;CHtZUKFS~dom8E2(Zu?Ofm{>!WBo~~Q}0OIF6 zk*|o3w}WVS=T8bcdT$;2B0|B1e5{_#q*am*IISlJKN;>$*ShPF z+xb`{V9|)0cg^6Z(d6?^fQU0#o(xBX7t$i?m90-ZWXHtHp>1=@E*K&oZR<0h~ z>~tiP8VAcyg$K-I6-o)naiIKc2QH+S1o*GHIm3YNZqwl83@ORI>b`CGRhcL#>yH=f@1x{&Z9RzQ%h403sC5 z*89p07_?dgu^F_Co{|Xvw82hMz=x^unA;hlp?09%^Q`c(O5d1L%Vr6ubK2O+r3~c* z?3AL$6<2MVK{@CrWfQvOzIWhVi13lXa4<02*zi#x8|$dA+%~+I1H7u0HwGB_DS+p` zD=ztNw^8&Bvq`BK08b)-yM4xGP8>S&EgWWo_;3FUP-vo z%p^R8A=-au z$F0f3aioo=!(EcT3BFH-2md4iIGAaO7|6a~EUE|@Y_d@?ShHLgAi1*_PewiVw=Qo^Td3 zrErNfpOdV$#b6VAZz)_{Jr1^IdA#m%2|qZeP$3e_d$=rVqf12ZG%NMNZ>cO4%VdfJ z!^JY?A&*X?Z}cgci!-R@iR$OSOc#wt?QQ1#dC0$c%{ji_`{80La;UyamW|F~{KNvv z?n0Z62Y%x`*k~Z15`Hs1zm#iRVjLU6+GdD;`H% z94nLa-uV-8+A0Fto_8-@oa+FmqjbnFK9qvk`w~vO#hHG{CGqgj80>c$AecsOYozaD zkjiRWM5hMO&*}Wc+|Jg;bO8|@HHKyY>=M+8#l?Jlj}ISIO*U7hYub0{F8H?R4yeMB zw|>0&sAEy+e5CPOPwbx;4y#R6b&MgC_#qWCx*`AJ;0tpLksEePn=~VWQe1cjiSI-U;>J(|xl0gzfUw00#<9cJ&OA6`9l-8hf6BL;(Ud_45XEqiTQa!q z>`s2$RoVFM=P}#G(Qyw6@+x&a)5EKa18slL%zmcH#={Vd*GVYVAsw`PV7^3nV2HkO zT74^N_NYnsL=G_4m!thz4tuSCvr`{P`^_OK#x~}dQspusv9N^NZHLrgeK=SG5|h*H zUi$1$1<3qkFQ>yimELDUfjZni$85l?WBgZ7o|LG=jt{vW>Fi%V(%%aFNqem)-Y51X z*CPU&L$@8THjsRDD=ch8tR7Sop}|`M|ciiFHb8GeD|u-1$hjFAQ5nax8^C-1+FY zRjx*j0hGmDL&d$<_0nfNX8Z3>W9=An8o2Pp7Bex4m0ZYcpF-?wKD|BLVPH8k*csFQ zZvV@IgDk5S)P2Tz?!LJetgI7+fKzm6DF`98b(zZgJxqkna;Yh_oB5rnM#C#gQIW>3 z@_Ei?NfH5vA+JA@I2YhJ<$&K^U?6^Aiptu&2YUf}neSNq47VZzt`2@t>Jbb$jf~%s zL<7i)ZEGV0n@<-RaPSj!iMbTXq~Z>zU-qeiaMg;Jjvi&G-E-WXoD3)i78ZNXt}_aZ ze#Xt*#s*`xyV>uootvr}!C&TU!?l>-ohR|@petb&?u zI9mMWfF3rb9sQt38O$HTk=mh`JM{2N2tozzH1@K?=c%QQOO-gNrF)HByo6;})VcN1 z-IP=$}Z?saECz@wwD-@^(Wl+s@tOXzSP0FpU+(kx7UA`{yXfjIIam%ZgJ+A&>0y zgG|zY%qbQgYf?_xO;ptY-%5}yV)vpmM%oPdGhhRCqA6rpB7+!X+idhupIUC~o$DFe zc<9bpS}e+n5~6#4>SZB2w7lHi zVmMW#@+;y;Evq_S?lN1%8L?(60 z!kXZ!Q;Yog41YY`X1SRj3Y!bH+}qsPyR*jLUf~}hr_%+}AgdFM!xV!G*=RetG~V|O zlh^k9Zc(Q*gFfXr;^Ie96T})k86MZ7X{G(8g*wx{>G2N7p^t)v%&E`%Mb(grlF%@(O^wRJq5p z9^+dScFR@A4^D@`{MY41LP_~I0wT8;Hv*=&OSt+c1R=zc1f{d4bsNaH&V^ zE%nGg{iF~MOYLKzG6a~jznUr*UpF7T6-osIc&Oi$EsT@AzgaBhQ>clVLaiiht-i___M1c3%vuz8vnjJDwErcOMQMbx1_s`Jtnu16z&+;C6n5_0NL~&jiT$+o6|} z;4)hz+<)_GA8=3({HZSIA3d*|I@oB?_px{yjqsvwJcs1kbXJFJxg%q$sA2i=CZrGK z9l@Hrukg+1oB7j(Iok?Qw2%Sra*P(LdGVv)L-UmpM|O9Nc&J_S0CfFWt<7aY_LV^d zf7QQ!mtlf?@Mo9Zoyo3t8d$UnIZVcFPMNAl0wTj>I@F>PZX^=o7Aoy;j7O3pD_5G# zM;*>)a6HJzCAp86^0qwWCc_*!_prz9<_1$kiRja_S!zr-9e8mX8TIjHGcPfo8eBim zkoI@y2|Tay4JEM6Ma68(ae!U45IgLu2dWj?Z~305LL#$z6>wxcKi#W0 zJ?{3L+2TAI-FiCobFnFlx|?7fpb1LwW&1>(Gr{#ro$|qXpklZDrc{4A1Qo@O(Ma?V z8CJzVV*jHc4;M695zBF_MW$=-tIg5nX6%WV5A2A z%}EeE%;x&p50bT(|%Kd^{1n`j!K27c5D(zlxw_KEKOQ zpduaTeZuT=kPxrrRPWLlV}g@JNU3+mvBRQGC}tSv?Kz$Xr|v{EfE+j!g^VE<)B*siI|Plf);O$;jLBY454Gz$ys?{w ztd9EuW?*xR&c!fU`V4;laeJQJlkiRJ$b%@YTsqI-7+P$V_xg_+b+S zGy8D+RHv+~d8FNLd}bPuYDGAWKzhp)ArQ7fPp0uqVInZ(Amz3yiTDD!-jyuG`;_8j z`&%MC*|Z81weewec=0r}G2nqh7EU?{j#wAU(YX4_GlRDH1KN@E+K$nDZ7pjx1GY zY(bC4=MDPX@|8pIH77aw*K&nj-jeI+Fa5?-4J+**Sfp+_QOGM~0A;D`)pT0>jvfZ@ zAE}Z*v$Fed{NEpyx{}IF81`(*M(0W zm!|Gh=~z`75OcZq^K&@#sH8a^@a-_f#)V>tq?WIe39;TK-XO$=H(KPrO(asUOS7cTjpL)`6c1ZK(4Mn?1mpB z#M%s5N_Ib?H$Z4p`;FnuvOj?iQa+SuW!Vd>48?l+SGX3(V4L^umoN-MQo7R5EOQ@w z6BE24h!wBd_cF*cRr?=pIF4AGnf@-{hYGz&oczcqTY$T|X6VlVr-i|mKygVwgfL~K zlt}4P&Y4M=!Cc^fN4$HI#}^63fFEYF>w?G@ZXt7>)a*_w4aou9WKP;%N+!g1YoPTObUfZSSn+Jyf13m3}nGuGO{yNB5OI}26YSC^4^Nsw5U`1U;LZtth0EZz80d9CW(b!-ereGjAN zOO1W+Iy$r-6#2pT?Gls=&~Px$BJdgOKF^siePbukd5%A%wKd6nV{I41u{eO-)X_&H zJLn7uS&O+N{YK_vwb$l)>S%|v%3deZLzD16;*tj$5`4UApi*XS&KwMi) zd^4FuDzO;|vtRQh7SSx!d=7G*-qgmio72|xHUr6 z%zsuz&~#ypk~$$Z)OHppk^Q;DN!+TB7pjbDCyPd53(7OI5rFHp0@T0^Vfc5d5G8-o zqkah#->Q%{)P?U7Vd%4ER^|}`v9X=RlE5eG@gT+flwD&VhVeu*y!4-nNbWW+up4ivX3k|(UU@?JQh_AkO@6gDmq24wE& z)LY~Iq()PE14DotJk@aorNqMQL^6^nk&-63mkeP?tegwiCZOz2-&Ob0^;acdd#K#QVyqe8^{JVl2IX~(K74IE9c8l%!#;(#v5Z1Nw?@>_n= zWQ?RI45sh#dn-T^dQ?vujRHMiV!mNeD{dwH2;!SMO%n~J{^JGuz%hW`=u#Wwi8}+w zVp(WE{rZ)fFNoQRXn>u4Wfp}hIf{szOro|mUh>DpLbjGC?dX%K5`k^Y;$<`t4o0(g zQBjQE!^s`KygEr?tuN}9SLJk9vJitPaaFCk@HayB_6UuotRCttYF- zjE1dIg&*Z0V1lehuY53g3Z4X3S%X}~sT^v( zA6b9UC`QQYTj1w4c9oI`8s43Bn%v*3q|v~DLWQyY4Z~R>i9|L>h0^a ze7smb_wJZcyJ5OlHok1;%>G;=ec-2^!5O&n&FHA0NhY_eLB22er>J@(I`=kjxxd3X z>SRWv@;o>j4-|18LA?|mR*>}o`PUVK> z7-gi??|MhnBuBS1w^>{lHeK${uz^11^jSj_B7z809CDO0`NoJD+vl_-YdJ^!Co!@s zwfT>2da_vWH<#_&44b0FBfFwX2U~`=OG$-$!`XiEuvd0Gx!8y2ld||9RawsLV(wk} zXUW@7lQF2j^#7;y=6nY<)<@s-%I?F2IIVkWMw~(w^BR6@RJ5^$E(@@##c7jpV7W4T zuj;cwK!`xdN{FgK+r6ELUf=#!ZrrF{yU)K;u+%g18r04bf_!HiFexq>@m2_WdC~am z>a{`sao9uJZD0{h_j?uu=Uib;p|0@|k8B{zu)_Y)IAWfC6-Y!Rxgw)s3*Vkm3>-9A zFJ`1w<50p2Fd&;v(EEh1d-p$$E{%KwF?%inslF`NQZUFf1Azrhl(SH}kHnJzlwV8(m(A{oj8ImCN)H_orK|4zx;3samafvV$v zEaq`M?J}11Tz*5KE}tC5O88{GqHRiKl-4>Re2qm zKlgPZo{v4{M2`_`W_-#zmc8{U&46^bkfMgj>Wu2TyJ)HmD7(d2BmdF)aWZ zc3!30uYwEXS%L$s%nL9qzJ;%dtKTr`MTCW@f=77^Wp)$tOk<|5W6S=`mvpZF(I5BW zhx&K(7J)o8ze76QNQDU)7Ym@RX6l74gd!dBl2df|cxpO15DJ|FQ4$Pp8g?pFn+)ZK z(ww0|RMA5KqWOoU8^~C0ku7_S(ode-*Yh;e@8O|=zRT0<)YsxHff+;1g7#;m$fjSv zB-P8WSq#2Kffpve5z2DjGz5jjjG25U zwaB4c9{aN}A=CiLn(w@0i`i+2BDWY3$9bos59>079;^`f$cg@QsHz?Sg(Z#UAvIuo zev!B7bn|{ky-v}JZaZ=|>>X4oPfj~_JXztr|5d}RGqVMw?%jSXrj*z}B65_7Fo-R9 z+r)s5Q~1?pp)oFXeWp}S{;Lejdt@dtP2$)JQd{pywUsuc)|bF3Ca^@T80h6^w70ok z#L+LXr%VaA)@4SR%W9nLC1Is>Jyov2)fkCVVJ_#207urbdB?= zS4T1VwIDxmS+M{R!+=*!Wcj$Yq)>saZay#5)tI!6g$}qIb;t`KFA-h0CqVpQa;s z)V?Vbw$0_>?Tp1m`Dj)rVOK0l5aDYSwjO;q2H!iYMI6&4f~Y5bPd`)3-ugGmev%5^ z=_5q%lN~+vak~P~cRX(=jPjWkxRUc|BDhm)eq;{{UyU}}4FspOxf`KDQnEeVQ@?VKC2Z=Z~WguL2%A z6+TwBDnJ7OB%^x&l@pyNEPg)y_DA`kVe8d?#SIo=y17RX{|oH* z-C?cjUo54Agg?FX-=0}jSDe?ZejmWi?WcFG2Rmv_aunMrnH&XoEJulF{&sB zqt4i)OZ&qK2OtoReT63kRju~ssh7d+)1qX;gi-l#+g{^UJYgxeq~i7+w6AYR-l4>p zRQD0O0@d^rK_Dq*^k$4z<%)6ZEC%>N*qQbwz}vN;mAsZ>w~-o9AdXH4v}3hme--<8 zm1P~?u+Q3 z(kd0+@F(q#N}De^=hsLpO>SL#@_N0K8Xmp;HqYZ-_FA%7Y@a7fu1y}H#pOL{jDU7v z7rBs=X~l3k7`= zX2~yO(4^zjqdTD@nLXTUW=|0&gEDC!FJb5F1~425W2KGq?U92#2A%)5KU|6GULk+6 z08nn1A@P1JHul-=;&c9_3WuT!!RrYk^uBkm9(Y}Y>~g$|#xL9$vM}Y|eofWlH4=~e zM1f18sgGf8l?43d24ssqDR<~qGU!gk5DI<)9W4^r6ZdbzS({j)YeS8~4D%A1!oPKCKF zK<)Z0v5s#Bxpl*1VM@5G_^Ds8LVM-dwU9}!$Rk!Ud&#Eg24S!5xS>69sd>ox-%bc4`dn=&1{7e&wWF$Ir(r?tETb0=j$348fN>Tf7@b39PDS%?`(O$ICS#Obi>b z1L?$K8IY-X?Uu7pb?6O`Y%~w zZouF0l!Z)O7~6skTe_6izsMjNlv4v7jUI%`UtfCwwAEO ztr#ewD&YZW_7Bg;dw^a*&aUk|&H0x3`G(tSeFecnl>o0t-uxtejhIQtV0wUhC?6HD z8}=rXs}zz^Iij0&FYpJk-?*X4=I8?vx+rNoKtDR4;7`#wMxE-{9t$TQ%dtY(Ei*93 zd-sicp?kxhR|pH>k70lYXFcMD_16^_BNdkxcNbWST3Q3)ILuPu2arm~DXo9cp2K8` zK??uw0yTjg;ZR(V_`ti{y*!t<-c4|(N@SxE=RfVHJaA$= zQXdAzX@zyDLQgZ*2Dm#3n@9plKu>_Ilxx@I?b>{Uf6D&iu9CRbdD8#sfTih#iPnDg z8l&!5W+$aHj@lQpc|77ZSt^^E0V*M$+Z(*8u~Dtx_g&Cx)=x;oS@hJaLthhZvjs#$ z4mfk2 zn*?w}G|TdJyOyycq_yjnB_LLMh){!RFTKW955E5q5Y`HQ$BBW2~YcZS8@+M^+GYoGwg zv;yCqnh9`gNT;ThL=@5+-k;^!A{?K=zgGKQj z*X@2}ENp60rw|gzg%>^`(7*DXZF%Etlj3UiM!d|)mLHxnf!E21CoZQ)deR>nZuRRI zuN4OA%60=#kMTgvi~z{(P4v-TCD&Ov6zdSso%@3TmM3H~V6>>38AQoAU2U3mWP{|MpE(8AI3r zlt)`ALuluE9L;;=LH2d$IEaaT8PLc8l^XEL)k)ZiIiQsp=--$BIN6Xf%sBA3gcwp3 zAf+n19lzsCXJ=luorxP}@u%?Z3ZOgUV!*33r%8wPoE}V}jlURm2*bH!{SIxu&&AH~ z$dXE71+xCUH)qq_;}XR0>NZlC+TFDl&OY-2#kh^zkb6~+C)x36RdnxAnDE0l1!kAC zPlBG~%+WW9=Ft#%LGf!=ijeN#&7q_@cH#Vr|0=k4u1dZDHVWR5&>tC!G!2r`uH*w~ zQ_K})vO_KGthbE?cbB-g(PYM}BtJ}8UNmZ7R--q1Oa&a>$bI$?y0H5=d1Tw*FLnxu5 zDN-T;zEkUqf#oBU3+ zRw24Y_&UkVzM^L6**c^VDfy;_6^VPAZ?m~(W;&4?{Zf_EEnXc70zMCFEO1pxPbVK z$*stTb|BHQe4Rs#dM&hh6>kyEs5f!dMrD7yX#UcdnQt_lOB51Ar~u4ub+VoYwtd=( z&_!+p47`z^8Nji3lhIk0&J?P)8Tm@@6zR_!uXE1xg;go%B>(~Y{OJ4+0A--(*vtT7 z*GGl7iT_bVhl4p-X%u@nHOcor|5+s8TjWp^^-3&EW7==^4 z_2-B^7tyuB?)>On6U*W5h#QeDKa;`OI40d@`4*Scd??R))$elV z7OE$YVOrAr6_&z_09Hxi&az}XvzyF>6#B)PX?RfCOBnk=r2O47vjQYFzjmq=DU;#% z8ltk9jTTL6Ryksg-31#rHVsC!dAtY2rlmD5P|`zRDT_|!+R84oeebX;R)megT&|<7 z0JUdEB--jlj0&h;*_lxn8VCZ8&I?{xF2j8}B&3Jj*7?~5iinMCkH9=1#)E|?*~8e( z&P~_FM>ZOcBK?>w0P#!LASm(0IRHrBc3`{v=pAXza~)oSSU<|_iN?u@@l9so1YT#Aw>&?*_fG)=|c(T z_5P~dD)ce85hl|22i;QybOpWW2Y5St7re<1W-R)TYD9YD4zGPT-GXh6@CS}yqRo!R z3MIcvleo3q-7lXqOb{vFwF&r%mt%&h76MsJ9Dh>6*49V|;Bu?zjJ%WLZS9Xk=GN|j z5&5An0!y_}G<0c@#1XAy3B%oeGv-nekX}7t>Cr5Bw`MKc-y@u0lvVW|rnb)mHJgV> zSwD`U1Q3_?m|;3GYd6>GDZs`VzypL_;mjvUa_^8JDqwf40E$w zhW2Fg7yNrr^2?Wi(J5bp9Rbb zO__E5+0I(sa^a%oThETsZI)Z(Uh8(qXfbC+@Fo1E6IH)CznCLn0y>X*Utux`V({d$ z3Zq>sQgZ*5RFv@-kw@oI9=4ru$)QW6{{cmj-wmjsECC@QE!||Ttg*UD8XRE*1`iuM zJr6uCH;|lo0m#UpNIpkFz`#2C{c<1ExsG$`9}ncW}TKkfSH2J zvWyvogsW8N7EA@C{Tx2^5(|=A%owf+M-vN{Fe5JrjfbCzm!64<5aJCV+j$(ZEJIjE z+Y7;0L2ZTWhO;;k^oEsc)s(2Zpa%+i^K^_ky5V^`XZ`*`naTl;fDKhFw`X4DQC9*e zUA2}*=(h^N0=nkn(D$tVd79+@#{1PfSBI^4bG%Li*207h^KByjk1+3H#TZ3bvV1dY zLagl%0%i6jN#Ws`nT-igw)c2KAK%k^dt5eZ&^fH|J0Ea_T;8p{b~fHk%^Z?(6XImG zIiLQ{;rk?(_>yTi)4NUIVZBjdiX@2*j==u$yzAZp$}?3Co%`doc7h5~GKN^H_gxnP zFgoU<(d>pur^YRvZeLh5JqY{U0W!`cTuF5ZX^@{I`HxK@w~5|?0adYXVvQJM8egC2 z_!g(VaR^m_KKzv=(!b1zV1G7NWB*+ z(C~@SSc*+yh)FB`XA-_mgjBUXpcdTi9PaxK7K>Id!&avw2OhV9%_Nup!FB3^D1F+F zUgPrd)ZufNtF`NfIMNRR8$S6(S{14hz?cL(;sIXh09Zw?i*ehIB|s_Y2;m)ctT zrKY3;%uVIW7qE+C`UYdY)S8My$Rh(k1|hD~cd=7!)c2Z}&Wzfgw2KsUgr?D0`7WYW z{TT!lP7ei4Vf+mnUWLdP=~oaFxU*~D0c7$Q$P0Yly&KB0{R`xU9?YWhvD~^^BbN3^ z*BIJ!k>xu8Xd0G5-hpxS?~!;M`~^E>42hJZ%O^~erF7inlV-AEG{`4ik^ORoR z&7V4~hfojZZ3wx8gBoy^WvGCYfp%ca;x5w97W5D(ABSV_cKLnw_Gj@2x$k}1`|M_P zE2Yq_7&sa!)HW^eyP#gpZvM@7k)3@4kdNb8doh!SLOkr2+{==<1YDPFf{djkTqKMC>G2Cb<= z#@H2Ryg#CW2E*huhugP;(FZ}#zMiK+Y##zp(S$Zg|^x{aae| zJzX_4G@hGVnib1h=7xXxL9e@CnGU~uh;)ToFSBdU+z)S%I}0o<`8tim%e0o+AC+w| zSE@x25BISAj_8exO*Xukbd385p@16=m6ENm#OiouN1{9R*JDJfyD8He5grZ`*v0aMLc+A9WK77wv&A<4%a801$|7{|!P=4za_KB}DYB(?8G!n-K;fCG2Fn zKq0*kqX+kf-K-eyqzX`=vxwsmBaHL?s4b=;F<~(!#n*D4u zBMeqDuF#Hz^;^Rr*EeHr<&PbPfkQ?5>nE?-vaBeqQ>c+Jm9@Lw4Y=xB8Wz5IHajo7 zI9>f#QLPetrP|@smI9uUn7ss}*2HpBU7xz{yozS{>XYBCuenhmR&Ove^& z-JO$PV7kzH5m~IoNYxZtO%v#oX<@s3W?uy$`iA0oqkc1U$=u3AUwZ}nc@{D^zvQA7 zYvO<quEaZ=S^gCxI9Etp3zd+^YHpO5ClZS&xhG8uff5i?JaQ2JY?CKlh*Mc@p5Jx( zM%v}RQ%V)eAxh?7hXsKhXony0`qu%F#}TO^D=ElfUjw1-v&0&ifClpjNt(*{&j>uq z9iM9%4NpJ&AoApTnN2FLIV>}>9t$p+)k1&&<$0W?D=_h)TA54T1GtP(b>P$ml`nq<5*>xmP!@|0e zFTMH5;FJ2v{lt;o&aqvS$vwo-FWFh8!Mk23#J{I<_P;argL1O_=UD~Qg~ zjq1lTZ}Rq+Taw4nl{)>$F@n&SM2IRz6Mv{LYPE;-DdRF%)~#n~-mp*yVoykVkxjp0wEJ6;1^mpu#?{H+&_Q zUYH%l*>Ct@n(WM_%ja56WcL`IO!_FgfHrD-L+y>KUaDg{aol}#d%4B!wuxUL4`v`s z{ZxCoFhszMqnL~D3${Twjc9?C%*BMT8r>w=?&px+K1U3E-x z2rSrre^rTOH^9Q$`33;ruZ?SHfztNP8o2K0m0q5Fk zcdzX~V%skqMpYs=ZiO=ys18IsEXPx@iCoMrxc0KomFZ=*zUJ|``favUZ@De_@oZ@a z2WkI|Y4G=RyExiCz|aVNbKre@PHhGNi}jjRN+lKn502$2-F+Xhe`^MO93kmgSV!Na ze9#7U!C%r~newMx(#qQ+K4=d)&>?}#{wj7+fWtKkDw63N?MyyycqozQ?58d=IB8BmvTJpt;FuG;Xy;{)f{@8pT5{ z-uiI4_!)lx?ZyV%~sw zjbu`nbOwto;ROlJ$43XBSFIpGJDSQqKi&Mi)`IbxC-xlw{I^5b-I%ceX@-FNbSVI( z?S*ES2WJ2E4SKdQUYa|oK8@B)EIU677hz!L*yQV-ea*p$OoA^4hKkibvRKuxVY#*$ z51r^I5$SopRscUg!)U98M%sw-mEIz77A^!@Y zkw2zgpEAGhHd77&^=I$WfZuGtT`qM+l(?wGF)!n_rL7;sfEoF-+$Tgd}K%s%Gw{n6qDD%J{jgC?d|B7D-ms_uu;n=a|xH=u4 zR2LpANXzrGtcvv^?>=tXh3fa?ksakcjy%X4W?QWt@5s(nDDlWH%vBiF@ZRI)%U5?K zf+-Dr>Ek8Yy!}ir-p(iu7EEydJzU;DK>pj)E5=dk^+$^mlUZ)gw+ZiL@d$*)`V z7^iVsB_(7^l5Edi@)dj&43bYgcQqGx+&TOXYK?L@nCLq!RxFMb2WZ@aE=U9e9rX1P zAB=kJY2U;I@Ca=jj8pz`gmo5EE7QOb4jJ4n>l1?n_*;c-141FygNR{sr24|K-hO!$ z4V$WGFMlIg8(W%*hAr|IVgii@wI3^}Z%_efrAWeEvC_&D7+#{%6TLv+6B=&b+4XoY zSP&Pqip#Lc9m}%uDE$1GtP#XTr5J}s=0vF`Lx*rf;58DKgS})r+Bnb#x2J{<>%z{) z*305vfJ=WP!}}nHS^?lF%xDPV)g`o$4JC4omA~nCp8E^T%K(pMui+~$>E;s&thDbf zR=Ud5lxd^Q5K|yu=@9YZ1l*>CGeY|NcSs3nZJja*AYj1jxp~wB0qkUC_Q$(vi z|Jz9U*uAffuZoD!HaKjSX2W?D!y$=t)avcleTJU~RX&7$<}4zb4Zl=V+IuA^EsNzva+Xxzb)Gb)Z?Q&Qq>3)Qt(EHtA9){2`Q9>OOI^T3`X%MEx^C4Y`^r>2 z9gsVlE~C6U9^i7r>$#z(vtvCUkGJqt=ErbBdeAofd|^lOpuKqHtS{?HY2^E(6bM-! zc&DsHk4Yc2)09Dc585d(Xlrl~WfJaR_o+=(_wfH6sZc^x_27N@lyWCGmog2&KB^Px zQ{S5>S7mXOhEmKROV`gJ^8>QL#2&tl+nyyPADk{2u}Z?Es^dJixB`bfkAh*6j+DHR zcoOrdfT@ji*fc3*f~8!cdy`zPS_=ovzW#(aJ1JuFU5}e@;0Z|1*c^gkWc*-cHWDys zCZS#iZ>Oo3eN46M$i#aMyUA$V1o=PK7WUv z)Es)q8AL3)#JCl@r|&j2f^tg0f=_X`y5yM}KKV`{P>e3QXhdd*=U3GG&bWi7RN-_H zz2&aYc$n2znjCYv*aVo!bnY#$*~SPuRNX)8EvKq#X=Kr7Ks;z~dIf+E+P`Ak!|X&g zC?vmOR%$&yvg@O8`qZ#jM`Yql-}KPeF6x+(67k130KUXG3yj-3O$4Xk4eVn%`Z1{( z)V??>u;tvU8Jir0=Lo>)B>u=CEy0-tM&0QIW`_daLvDn=Ekt9&=iA?)PVu8}kYF4C zmo5b3{&phWO$OQtm|R?vK_^PhKc~Bklp|njLQ1nExH{O(TgkhplC>FZDw%B+#zQfq z*QnSL3(d~H@rRi9L7+UJWuJJpyVL(jHmL=Jm)UD7sI#w>RLOj~#|&Qz-#0>B zkJqA)n%mPd1WZh|Fem9T?#ECP#L$#dSt^QxOp~OWcWV#Qwj^B>bBx!~OkW`!G0r7J zljL_R4yr0rt1>F5yXq;`;(9Apj>2RDcl~>fQDakRgK>-f9KJUp9wK$uKpFL+{OdV$O|^# zP4$1MGb*%1ET_Fjr|S707EhJ_3K84GeW|Wxq5SmCy!}WLqtjye5!>rPvAAHTG#%XK zu9;s_mu#}6BnXw6L(;czveLk1D?NZ^+#H1u$63Qg3}^mw{xbmoD-8bxb=K&;ciey% z9b`>U7Dgf+`bSpt@)SSR13E(=Qtcbz3o z>8F2i&|vN_r_c6P5y`*CUYb1&@WoR<%u&yK zRj3n=NziF*!C#QghG!?nfEH2lOGoo3G>h4yoc*b?^kQYQg)}45 zIo?bRa=7!#9`>)>j7;1qx{?Kwo_-n|bn^34;DdkE4{Xwv{&Xm{dkXM?k2o zFH*5Ft9~fvzDvMqZJ^Zbeu>TQx)xJd4<(@OPu}TLDYA;9{skudEo42rvZh3UV)Z<@$uUdjq!n??3kUXdAbk`%V?^-+^CB#)#yg*<%VGwUYlG5B$etApP&3 z2dP9LrMVEx{}s0Xr^Wr(&m!$;-~1@_D=+i^y`cYG+<*HVtKbI}ZH=u%@!uEp4{P=J zA_ErLZm=i&XO#cq^8Y7)NYf!+5&mfm|7}74^!qz8V3A&31)7WelTxCjOoc^8mLP5J+~C)QAy_9itO|2lmCb1DD*^DjhTkr%J{mEQe_ zDgDzX{2$MC5r$@C(5&lU!RG&Lt%EqgB7Y&eWF`Fn?MXF)a%bFFfYtx8i2uirvCJ=v z#5oe6{oh{h|L4Oq!2oPI{nh;c$fo{&Y%-+!@R)RvFLlu|zbEcA_Fo;qey1J{|%A z1`U`T7GSwlTMP&sB7kNfIS~|F@_*cF|8B`|$_P9_@!bLz72g7Z`D5#X*VVa14CvbX zA%)F!yuM>^G}9c2hK`TrYNwl=s8uU$iPCMh3&wVr?aZn8e$}q)m#EQR4vM3u0STlT zLLU1`fx9dALcw@9h~Xqgt9#Iisf4SE&%?3n<#@|!--ySO4XS6M5#hVBi<2a^Y zRCoM<_KH2~t|ui%DAfXmL`WyUqM=Avr@^WvjqU^8^2odE)BQ&yl|n@ucI)40s;!!= z+QG%Zb#-?%HC_Mbru#oT0aA3R;CG!<*J(c#9^{r55ueYtl}5MvA>h5&j<7}B*Mvc4ok1BvHJHxB&T8A2nF^~Q3TvvKtih~6`SF4NdEEj`&-Du z^EH6ck?tuKRSUCG_UrixOj5`iwIB!VL>vYbS+dVF*tKEUl#Id;Zy89B*;uuC^}W9A zYD8#ihZ4eQk{W;pR{QnsWXC#kLF(wP)V;$K6gx1P=WPtb(jxjb__fo6PRgNPY*zF0 zbI0>YhQk+{Buc@D#|X0uH~CSUEx{qirWF#`f#ZfW{GUITq27Z%`EJa|mg59HKC{~_ z*Pg%D0?-C{z|@X4yYhdwbP>b1%0)1QFX=OYT=j`p7}TrETGLFk!BZEbe`7nJ7GbI` zIDLyM%m(yx^4DR}NSNpOQ9O;k_AF#>E{bX@_vH3G0}=6$j#RW_caWaYPQv73+P_?) z?OclV2Q|KtRXPH1X7Crlk99TBsjOG9G)x3Hopia<`_@ZQZWxfT_@3w+V%qDF$Mcsb z$`Uhq-(7QG73>?>%l?xSQno@pPuo&~tk z><10pSA?WLbHJ1Z>SbwWfY{WmJm|fk!uYUCrW0v` z171&kwYV^t_PrZ2hcQm$w06Xer+S{zL|b^9lpa#>I|`7}ELjq#9B`+BO<4V9nD&_c zE&Y7)o%?dYag=c*l-jOuh%Jh_+q~DReb*2S75!*T(wB<(!;d1|${g>XeWP+L{L5ah z3$={}Gcr{k^|xPUX|=f6APqdC*=IPz}s>2Y;PBNo|F_sxlOnORRGCT zJmGM8Iq@JC!=REI)b-qSo&j*#PfOp8NoP315BjFa?R;xDS>MN@Z^u##z~-s~F30I?BS#<+qpwZ=N>Q3}qto;J z;j|+aC=rI}u`eXhsIW~;K%QvqPk+DEQM53nSPSF&eS5ymI$>k0?kHVyjR48K#gYBgJAJm2+aX;jIQWX7F}{>bDpnusV3{b zDltdL)BTUJkCQNjqRx@73MxK%c^=W*KRFkV-EN+5w^2`@-tv``!t(?y{gSvIA9G49{A_zK?KcTjA8@uHS0bPV~>ZzF*|4qjo`+?i9!YL8nZE!=Cw{6Xt*nG1^jID z8(>z}QtQnVod2Ccie$#>u0ndaa&86|K)MIOKx1p!H)-y|CUkenwRn%Ftg|&x!+3PDhvjtJbYE=PU5KORiWJsNzhepLWKnGzw+s2xk zxW1Fm_uwUHqtE40&`#X zZgP_%U0JL$F1&N-f~xVld+m$m_!zyVcaP|NTKb$x?7nwgpf>cLoP(BD%&+FiFsT?l z^Rd>g>h7bFPD}nN_*b#&*c13cA$f#}9q+=c>lWq2O-Z9d*SP)h!s-;{eK&b%#}HwwzC04>zxZjweIo6(d_$nbQ!iUc80Z{SdxHVnIQ(<`cVe0>RBNi z*5JVyvOu_@ON~WakbeAl`dxHGWqRk<&-rzD4%?il=1b6W>2J8y!m$)X;596ds6HH= zZ<_gD0B~rhVLx!~$$;ac-+B7&VNX1NK_)n=|K|JITlw_W1)=WR_XtadW11}1d;sE+ z*0An9QBF<2AwuB1vy;4RjtOVGT0aJ4uyghossb(PZE(=d{I`Y3HhG171ZEWKJ=cy= z?x=tAHE&nKWGil{=6&%-b?}X|7h0Lg#%8RZH|M6hrDi_%4#;gvhasn)tTYs#Df#Y2 z@I#F=krTY zzy-qvk+p|zd#DjKpMjb8X7yM4pc7=<3NW>l77>4KgXxF3Qy0rOoVmNH~;9wkZk&(~9;loaWcQmc0Pi>r!lDKcj-U4flZa3LCr7y}_253o> zimvFkzYh(FZhi_n%u)O~;gqz(BL@Q0bw|ELc|)h@hNOAHmMH zKb>~$D%k2LU?9Ou&WHKW)eDZ%8ZP9JAy(}66H*;_NniptJ*}Z#XB)>lrwPu)om0tq zs?+6$_0d%BiKpYj?3-^-jtflz(NVckQLd{iivM(OivZr21``SeX<^-Yj|k+!ZM}kP zfo9)tVDc`+FYgQP#r?Wl8|P$*zdpm4B~;;i9@`|QWaoP5%f7x&VrC*V(Y{**l2jm_ z@@vr1AL;k{WJAnqyKZ-~h}TrU!*+_74wtZ0X&&FXuFPxM^Ci zOc%P^$SzeO44g!pv9QBn(y#lpmnPKak$Uixy${E>MX#F&aq5o(oSDwVNV`2l7%aN8 z`_OLNOTo#Q9y5Q)vMZm^^$UxfBP_YIZqHdzfK^Vp{pG?RL#@arZi{c>8iK$> z%HfyOqBTP$D(SxLa=J*-oq6sM^}+UxaL;4&yT<4w8F#89`2xZ_me9^3$Ux;*%%5$u zJ0Wc|z7K~K1^GxPA>T>%m|5bfl+LSvdp@7{Dk+4>F;(^RG6#NKtQ1{}ZRp1j<%Yd# znerG>ZfW~TVN{xZwkhooy_$9o%o?`^!hM5R@7B876R*j?_$>?z2Yw{zH8Aklshjtf zHhXG4o_nJXzvHrnZ#a2D4Nq#@+Fr35`G6X@GC__sgJ6Rv@Oz{E3uWjcX%@UkM;)ng z94^0=DT3%{cb0(vz!9C%4-!#8yyp$3Mt zk;!Ma#=h)t4$1F2xbGWj4hoj(G7f{T%9a{hG_$G9yko8)sl2YjLRnmUtzCsTAWQa~ zR~0VJ##Ral<#3(Wv8pc0C~WKdb$uW9g-J_KlhZ+qiKubA&VtRsb6DYIew*BS-Iou+ZjtGXhPb-Ja;v&{Lkx!BzQoRlp$^EclgpeT=ST~`;c5gXIWS%3 zUTp_VA2+NSNv15o<0~5UOtcC}wEh-Nv617l<+RPm zLhr$EXm%=-yTtCZ+CXpJR)v-@>sP9h-&HflS7q%DG&Xc{YKBYE6*0ivF<3*jW?bu!vJ>OIr(0vItEfb$vL>hdTM*>t5Faw zWtQcN^R`xS{Q<_&D5*G5G=G19jxA4b(w3%lnUlzm3J6mt< zZWDo8wY6$_w}-)!+g_h+WCqPe#3b+r6X7vSm3{Dor%&n|OO0mZOKh8c}bI)n}*>mQw>m?~aw)i6V~n zQ;ILB@j~wkaOhmge$#hTLksln2iYAHm3^hhJd7CZwI)O2M>Nm<~X-#sIs%-${uUmzq(Uh0fk_-8YgK&6xMaE+Bt< zvvkW&-#zXKVVu|oj2h~XPtG?#$&g$8>%5a}C+yME&$=IgOIl`vsJbf%k!9`y;`0%y z$=gbBglhtro8x^~2O}$olvtC5KQ4=#-y7-WovBG&@V$iv6 z6WJv@+7GOn`o4u(9Hb@?=uK+zL+Dr*B^cEfg%-_g8 zR4HW!F(Jm~VC7E?g2B=HW4v8sC!)|mo3%Z3gWXx^x=I+)QvM?nZL2t z(T~!y4{;RkW_69M;xWd*kW^Q4!b&R`(}y7cRuVcIDxTx7ni>0xgDIa1?x5P-DsvJW zp2-V*V(Q8j#&W5jL|cMK6I&14ZW|JU7NUg z7qz?ZQ6o76v4G`#ByO$3TuR(jrhBg`-0x%!9qt!)n7M*12A59Zpf6{teL|#(~=sR_&TOY(=6S7Lhf~bx+Z?8q(;OAY(#o@cprezGwTqE=a zb=(*6{o)WRL=mly6|8Zni%Rg(JulU5IGz}>7pC#YoGw(XDDPE7r#+wHwUf`pinot> z3ph_TNjLu&J?%S{Kg}2E*==4p{q|8O#P4zQJd*eU!o+-4UKa^Il(g{z$^Tlr9)4MJ z8wtev0)TdEuFAL<0B5pQxVHq#w+77+Bu+0;Mr1pWon;VmGt4FRgK-b*gsS`7PPlJk%f1HJ`#vvF`y`*7p5Xax%{}C7Txg}L zjXiSU85PLu3#WIz~Di)1VN*}C%Y+}l_Z>InMS!M8?&8h{NRl!v0q zqh@&AtKx|z5fy#v-+tz%uda39eK@Md$gfRbe56sT#l#RnIg@}=glSRF4OiVES>>KX1C-LBTl6eW6q5B*(E*6k{s z)aAn3d%OzN->Z(;!;ypxf5yD<1z9#xsMYv)j)uD%JaEPMUE=)KtbOH)QD7+iln5O) z0SD9cs_X?sJoQSwffs&wLW_OL#a^(*dOZeQ$M2nsl`)%nhMLm(#_ObHj6!ZFdV%h< z4blk;k>MTQiT=LRI3_E?_Vz;>>jUL$6(VzUQ^pBwtcAN56Vi(9LCAd>0e#c+s8i#X z?v#LXcS$f2@3C?1OZniP@T}^_=#W(!rW0h_$zKC;470611sup|C$JmzSdjmGw}i%` zH3;kL2wuocK?u4c@#`h0?n);R-+8NyM-4G~PuyQx>a&4-LlF_zuUePKXy9OmL&c%+l2}LV(JQc&&MW?cNkTY1%RpTRL8i zNf)(}ki>WF6oO$!6IXrHazJ>Y`BPaWK!YytO9H>v<2roOIBi%WE#S%OmWJGeO}c2G z*s}Y-Ic$55de0mpOg0I<2%A>!(jitXLt^}(keX?7etcQ3)9Km6HHdY3ToFshA#dx**cw6D2Nj`1SG#cisn?>9xJ$NeB+3g(#6kMxh*{ys_Z zIYZjCNJ9A{KH*f{Yd&{x`rey)v5}~Rk>=0y8sCe(zwEmy5=1X6lyc%OjX8kO{$wrK zh7GimthG%fgJe|(Z|#M5R7Va%(~jH!zShd7J6~?T)vo_hCB^&Cdh*&EV$NeU!slyW z34#K}fT$kV+w$gi0a}m-1q~9>J?jN6ZNVRgma$YPjuWgq}BX+?*=l%U;?}=l5556~<=m@%-kB zjD^L;Pbfn)O5if|EPIKObN{PNUOX3X$P{J~75)JWaIF_Ayj?St6Vm;$!glEUh=Dur z4YS?(s0zNd0>&DIuxWFJwDPFUFO`}~@S3Fqs^08nMD%!8Xrj$D{%sY08%Bj6jAo0# z5{uwqUv6|^!)#sXgr6jp$u_v+Lq?v@kcEIW!m=hRc5gFDwx`27XGgsH@v0Pm>k1B6+2Da`QW z{3#Sz7OcxuAiP{T?Zzn|N7#jUvj*BOla^ffmz5ycit&_O=riZ?${pv&4drT`r~=&z zMAB>Za%Mso+C)G#m>b?G5@lyJy&6sYXz$P)>fY+c?P(B`i@apoYuRnUWmulX4%TAk zw5!$&?8--4hj!UJL+l!yRvx{H+w#pnXInn-{sV`rFNX}B-x;m~IkH@Uj4ng^j!Zmc z(~IwRm4nn*F*ff0G9&Uik%k7mE?vYk6Wcu7mlyK};k7b89kG-tMP(r;Ifiw%zrIY*4*s4Dk? zqA_bF(HT5>O-mz8TsJ-%eRu}Ifw_*KgE<-JTLc__t=!-5a;8AA?X_l9RIhZkWM_{3ObS`$~Ow_r}ImfFnB5=Ra9n zMe{h07@yN@*P6s-rh?W#(M+~Yw>wnqNAG^|063H>D#wWmCZ;OMhCyOod70e8{M~>` za)b_6cJ}9o>e01|8_e@&-Eh##vt>!h=ltzu<+nXpZIi5fN_2m%nU|3+3VpQi z(;fY@jp#dy_a|8ljL#yDAFu{T!K~qVq10Tee|9y}lC`1uWFgsV$Oc8Y_9YDJ+y-xg zAg%O_L6C@L;XwvBL&&`p{1&eeO{ZxylDHITLtSQ0l;jsb$G@95Vf>M|Ttl`&5GA(3 z23}~sZ})4v9CGH4YjJ7`z~95(_5GlI^q0qAbf0C~>zoan%N7#bLID2$#jyF)SVEEz*;eZjQDN~&Ok~I3}c(1f= zNC}8F;vP_myhedP?l0hpOl!eY}ElK0t6Z{qW`A>>c2b3IZx3rjR9Q5_r^@X5vRw%|E9;3J{34Vou+qda_DU| z=VCHg(V~v44kEE!e*rILf0Nwhd^I$FKKt+3=Y_FWR>h&Y!=+n&^FZCIHw$Sm`2IlY-Us^CZild86m#T$z4Gp#SNcCS>xO~;w~BV>8)S2Xfo6? zUH#v@5uxe-)#m=u8j4T#aH6uze9X9=Rx7`bF$f=*kGSp=$($F6>P02darMPN`;-@g zuW=S797g(vJ_rGIJ7*@_ob3F;DFtkjA;8J6{V&1dNb0qz;MiDK+@`%QKw#OZB z`sqngNRp()N?>-K`^E*@t|W=H`UI)8-4DXNXQK&0#~}#ECVEg4^6WR+Trm0xHU_3f zFu!r{TQw!iEmA|RVtU!oF6?!;ZYgV~d@tuiQS!Ymmu+qIfnGIMd3|VF)1yqE}>l$ z;glo?Uy1;vV-Z?6t^*HSdsA=^>5%iST4^F_I-FF%*u-ya_L5F60KK&bo=CZ~OM^Uy zxXkA^E-iQ5U1r}|7t!OZfr{U@`990;gW5#>bP@}3s(4rR-l`|Bw_>T3#kT!r_?RjZ z@v+7ueq!W&lKe8D7JEeIbC4FacT24dwtdRJBn|aWJooz}8657skzCxxpkmleEDMN@ zQ^<2X@@n#Ak8N(2qNhD9;TaVb|*)uNsbSGT+QkP!V~8Xd_BG)~^l~ zLx3d^!ih51sUe;H7qoSpa?nJ_rN(Ss;{f_dzbC^;UR;8G13!+pTrET`?|hcXlZi|ZDi%mVLxD&#p ziT>!d1SXvrQCt?kM-xBjW?2u)BaeqKA9QyiDvn>=ID~)mUNA*kbeBrN32ET7Fp2It z1QK)67_B*6YAv^MIG^7H+T7N65f^{6oo)KVCy%#(t#jlh>9=6B)kSG!*571AGM-Ym z9Zz!=9({A=-e3&l0ip@}lx)#+{{8$K$fE2K(euKQ$$h~3VFy@0%tZqP99u$cuvr5b zI`AB_yWpDN-`6{c;W}K>zTmGosNSF{)caw6$=MVDZQwCnbH7}$S%P=x{sqP$;z)=; z5}3DGEDPoZKc65GaIgzO(qC{{EV-39$o2eeo^GrZYKqGJz#&sKD(8;P~1R!66ZB_ebL5I+w&Ikp! z>vK~uiz`OL)U^?f6HeG(6He58-jgJ^rA?lcXFqq{*}qww{T+MNL0gsRJsiGd69{jk zm}jy`Jq+k36GF%NlN?OAo)#C$pL5hDv*J9Q#$!K`!;2b8+4kMIwjXllRJp^m^AnhW zwR&&-yZh*UyLsug=`y&r3ZX7%_APWc)V5JOeSHCl7QjRA{nRlHnSO;y2NHVy{e9U0 zR?zQ?0{a<;yxRb>Tp4Ys=C*oSK*ZIy^9D&N_+5tzWB%xk?+LGM++-JQbpUdNAZ@H zw%X}Gb@YI&wE&ZDtk*(5=p-kYTv)F-#1>UW5PkmUceZ80>KL)X6W4MmnJ!Xp7jpJ! zL5sS+j|ql~J~B$Li5fL@NK7dpFBE@Nx9t7+?!jT!EW_ZKaKD2X`%BJv_8V z?p2V_5UyBJQwH#z!aO9fV3B2Voyl=KSj-+w)3S?fh2q9Z#Pkw&$wc8)z&%q(VJdaL zhW-xxF$u_L&t3hZy!>PU!|sD8*&IJO^W-KW95nglRb1iOuIHgR77O1f$m>a3mkV9- zS*zeVsWNs`6ttFOX{&y`7<3&}G5>VD#IhU+{PTyA-gsm8Nd0uPQ_d8%Q`(wvFjn#^apb<>b z@$Hvj1xQ>|60HwKhJ+P~u{P-n>lbF6bw~4+s0w9oCSqpGwmnnGS?HtPREttHH8YVlc&J3J8Ecn$l z%Foi4Y;1(*5GxY+yaw(7%l}+^AwnE)MdF|=*_XzTQ>4G0a z`bfV=z$`2iDY%Ivmc+N`7cHpcQ;ruK+{0LfF~6%hZ@e^X$kNXSI5Ot58lgi8ez4P3 z?+H%VC;7W949qda3IwYcBdOaU@?Wk6ic$+{`YlzGK8U4;JPnt>s0otwt`ee<68=ChkliF9(G z3cB_qU01E6%Bb$Ro?8pb_}3NOBjs|XbSL3Ve@%N%dBPqU-eEGyzTaVCXmtMC5AC#Z zXprh%^_N9GBgQJg2K?X2`2W0+ZNUL@i^y1>xGw2YX`;^p+bAo7=1pI9S{v?~?ar+2 zK^}ap96~?pJ$8NqR2Q9|1vSm41Nq82KVw) zjdF|QK-))BF8)uYs%L(Af+>ss#cc$Q2{SgxvkmDczsb{jF!ZCRP%49lcJcaGP^ri)^9799Ocm zh^yYHGEWiJ#9Hk}2NdJDbZ z9tDAjk6kZ4Isp6fnJCP;c*>&xGhn3N+XL?(zxVmoiMr~{vv!P!3S|_PLs}cJZ(URu zBzjdx<*3>CB%K3~es#WN!2fqI!AVI6@W!%BRrTV*NYyt)Ck3bNqGRet+NVLD7r;uQ#=WJ2vKoL>MJ zzm|IDIj}ySNA12Cd+!#DC|9#uAyd22DVNUpnMYosAOa$%9MJkiRrRDC0eiMJlF^7| z5oKq(gY#0<5ISJ!g1q*k>Xw#*cdKEolZ29Pj65EPsL5^5))L(*TYA%_6R=%gCuRQ} zImHu`cyX1<#RoGd>cU^9GWPAey*-;~iOTeJ)VonKbi7B%B>EuuQWl0YY{v97$|_rr zQj@s0-_;Lb)4aeMLb0)+*-;FgqVC_U5#IP~wjo!VFt(A1nHc{NL)>~W=(M3DZ=iP1 z!HncyiY97Bs5Tj*^k@mdf~PF1EuWYSrcW(ukRDm?2y(t&onK|@W5I!0%*Dq59P>Kc z6Eo4~@4~i8(za*5cMNS6j0>h;Mq&dj!YwmP2Xl|w!h!B2Etp9DNxnwQH#rV6PA7)ST*rzMpW+75)oLnW4UAlunn;lW460L8wS`_iWe2qI$A)i3;Hg9p1WP&2rVtl9mq zl69r3_Vcv8^+Wy|@78W)`y`zACD&;!OUk|jWYMB!ON4F};mJ(_@$_1o8v`Ev=x$-n zxe6TgxC96t4}xgIjkp)Sj{aT;o~xmhSt6GX-ee6y5YgeMz)H2~qejX`yTTmFRUQ%% zVl>u_+ag@4`K#7PFYx?~0^80rwwzOvMOWP;0{}^fGdB7^XrdGk^cve=W%auY#jRV% zfTWzw*Kzwa6=(TJ0<|=^pQ#p>!~o>kJ1l>qzUPDtLTheIH1CLpDlZ1|{hzYS7&<>l z1%hC}_v>64E`1zH<%%5#QP@b-m7GV;gsEjZihlS54AlQrHLLnAkVzP3%|=qwX&c|Q z4C}b>Vu%bnm(7K)ty{h3;!k>0{ca_Xc9mKW$T= zXMaKc{@hU9jVAtv;C^`5RiLOL%(YMY-si`6l|(0zGVwV9dr~txFFdN;&Wm>f)NjW= z8$|~I-oWk_Q{L`xJIPP0zB`8D}2?v%+Vo2=n6Y5=~TIP8{wqrNnJh1?qd z!_)>EV1`*env@mY0=BV`^aNbSSvO_(pOK~VBWRQZMNyCoRKG?K7TnE$Mi7|yTc$Fa z0elpg)i;=jfq?SGprEo@@I63SIM;otX@S1qg!Fqge{9-IOv7*g;4VhwxX}n zP}2w)7?v}g#6UZDP7xm|og~vG;zmw32l`x2-xZ*s`n0~xo=1N2(tF0yS23`^v8B*j zscFSeXBEDsf)JEnd;)KmBfnV1-W+|8^NcKYl5(B8guhVfw|Lm_$8|C+EmqJ0znVdX{;5 zlqg-$9=H4;*U#`Ir+kyn5F|OotXWzX0xLFgAfCyeQg#AuDXV2uum<*tlPr$n&<3SX zlJZ4qQ94GewIqgJ66M9#O-*kpv^kTlgSt1HOAzi(2C1HErQdPMTndgr96R{8#COcS z8BVR*LFZXF=SPm8MTQ{TzbRSQR7+uzf=Qno^8x9{8WEhO@-awZ9~j)VIW30PIs*M= z`%A-Gx2>0cwvsq1(^ZZJI5O|LV)E9FhM`bJ<@4`mV6TbN`(~v)%l!n1kGceYA}^O} z>X+vuO^{=!v&{48&~6Q&Tp~1EF)8;MJtZNhXjgZHELW|~qWejmMMoC$@lbUA8tgKtr zs7BVna3`a~3I4FJBh_uzrnKfjHqf6yFL}7;^8g&7HG^3PwrqxC&@JvH`tyI%_$)X} zHf0IWCo;22<9P;w0?0JG9s7!aWPpQACY}Z*(so^$B>fFBAE&A*7JM#H!3o+9%%)Ho znx3sO$_P1xCAT5!jh+``H(=7ds4|qxJv<@Ec9XnMzTg%`Jrpfyr`( z%CxQ@mIm)fcb;?|tWC6Eu|#r|_qzpY>1tkath48l5j|O??{Km|onA%k4?mzYISOy%Cat;GGsE9wV_g$56kfsO_knx1M0>n8e-kUL#C?8<)Pcy3SjsJX`Jl z)zu%Ly=?YQaSqnMZy^tOFs1QyjxPTn zIo!SMl0~?JI8cNQRv?}e|1Id)hrwmFBt4Ez?ec{XmR~p^6=KZKZj=I8D|6L=RgaXe zM~d)Yyugr(qtnjPfM4LrD`{W?O+1|8>&)}N7|mB9f&BRE66<$hKrkYJ%g?;%CR5A_XQgVIA&#zg5eOYpy@wf8wdtqvAiEF+Hz7y zaOFNassiDo9TOf?({9-8mPPZ&1WaLA>-9Zq-%AZtAt)mJVFb?r9H5XP@V_m2M)Xar z|2&rILB^ScMmYM@?@uB2p#v?o@;%!+m+6c~D!&*eA0246fNVIJ1TCzHnzQ>rQ50`t zD^^sE98tec)1kdeD3GU+6uh7WbX5e?*j|OIWnM&4_xci3)}lu)d!%%k)~Ov|h3J>^ zm%JlE$HAJ(Jt6kd+TLcvz!~5E#P9`A5ZvVN6n%9H!rmaodg={Zu)sbGxN;etR(OwL(L| zIOnewj#P9$%!+s4FHc+30I@D(fQJl6tf(d9i?`G2E`p6ane11=IKtBUg2pDt&U!41 z?D6PJ;kAP*AkQVFHaZ?%%UfINe$J);GZow}E^2N12!Ojq)To1R!PCyDGMW& zUz59~NcG4LzB}d(zkCE;tQ~*KQ#)PZ1N{JEGz5KRvS3a~{N~hRJfd64&lr~-yn(L6 zf{IPJ$7S5XOAa%Mo(3j(OYYx7t?~YSwB;U-Hl6@oIE8OIlHoeyh~$Ls#(j1xkf}E` zko~S3Q!4hMpMY@YB*TGzcUO3l0#DLf2Iw+J7#BCO zW1_a#3DE|CjT(J9R;M!)qYxjNeurN&K!VJ^)Zwf)WHK>8qS@YcBc(t@{Gd6uC4Sa(Q{tc0 z78IP4SlJN3DSz=THXtEO%(>&tczQlDq%?2dGf}`Hf*!xPJMnCrHk_&pMO!PB({xZh z>=P-g=^D6lW*+wQ(Uy6q%OqgzD3lHzk8}hU3pR#bAs5bpn48ztW5Nzms!O8D80XxD z!U8Q&2v>PLa(wV}dX5Rrx&#DLk%*}Q3#oTa7I$j=J(a5AU#G%gyo~r9wXEmZ;4a&6 za?Q@EN`0N^>=m6ot!a9OTE(_?uNzVUS#eD|IW$5#42!8XxiG0>?N@Sp*cb|5Op+u` zG6>B4Zbn8;s7p53QiZgoc!M#ofWmV5hg?{vQ@(L1jePmlW@D=qP)+hC8nBbPFqx3U z1sh?)_#rF3TMcKUYkP-8_9;M?OP@iAihYE+EQ{FR8INY`)jR%B8+eD+CO#mcC&N@k z!NC$raxyiL2lC&pN_F=lrJCWzB(QA_@b$?8k+(!?F>CvwJbtnV0s4{wO-2uSE;eg6 zBSX~Z7plw|p;GC=512c3o@A(xK1<|IZ6DBu?gCOE=Xam%@{Noh4F$Bx-F#WBIk>#e*#CnN zce)LyGxOMs6^*2=hVA2k?*?|EO90sKuYq$P_>~(X<*uRW*2icMk?_;{B`BH(m6_Fr zkKyJFSXM!zSPbxrUJ(+pAa4|Bq3R7mTjTk!7-u5-XoM%vZn3)%*&FJ3d0QSaEks{N z5>9F9``jz{(}!E`VcoO#bH3|q8)@i;w2DHu-gi&35CMG@MkV(P0!}id4E)eXh=hxx z?VHcQgOby#kt;FRT7W0FS51Wcn{{(d)!@7%8pe}qE-xeDg% z&Z!sj$%a`JHuU}49co*avL31Wi`$~kPD)fx$Eu8x`b+$;x@BQhFLwNW+v@TEDzKtZ z%^<_|3uDu{1%zw@oi|+&@K$K10B+;VSoSkKauU;m4?{n#{sJ(!fhHX90nwCAeghmF z*VTNVOP@wAk1XJhF7uj2^U#wBuutCk4!W9*2wd54^lj2>lg28^C+Lj#=0T za4;Q~eQjp?OrbgAKMLKG7;!@JmQ_ZzE@;2;^Aqyn5}#Kl%S|fcT!eHr$|>K`VS&O; zuO8TS)1@P$LHF|}-$NKf6M!9DL7SAZr;8?TKYGiYKlL1=5fK_RI*RJBy}^d?^r{^h z{=Ns4X3U5}mx0xwYH-t_n>w#}Dkfx*D)H_!Vst&@*U`nQOYX*SxLHwL^QG37Y zkqrr)99%S8B4XpJknmSe3>xB|9Vi4hsJA~q%J#i)^X4FTDX2O|dOm;}+iS1-i}(Z_ zZ}8F0Oa$*}W^f`5!;Ql-@N*}F{PiYZ`Ja1J^1kQ3+t3d&S56Ia4^`g>`s(dCzazt8FbHE% z-JOzD*vV_X71uZ@x(>wccIm`ux-jU$#{MX}u?5w5ZhjRkq{6s-I1`WoFn~u!cTD+& zcsmS~r~ZA?vm6-7VN`hm8Ec;V^`gqH`KyA%(gnJBn_gf4e{`0{n#tcDwvPLcK z5Y;O*5tkU3gr13$wS-a3hqWuf^yW20)wB~Y0@O4%?ii|z(}Yygi|Zi`MYocRLs5`_ z`s860N5r%Mca_KVI5LwpZ%R1r=jPUT?cxZ6+0d*A4n>cj23Q09o9dB%Qv>D=$gNYE zX>t<6(OgMQ*!YvS*(y*Iz6jUayZaWaqAO`)dhV*-aVL{=Ks6 z)-+&z+GV1r*h_2lCBVXfxhR*{Qq4@MxR9EXn^#Sz0$&U(-Z|pHb;jpxTEq7j*oD&Z z+;Cenv?0Iv4<2U0+Zn-7Ca>Y@KU6NAofrxXt9;=aPnZkSn23AM-Xx{iGj>t32uG`UybjbCjgBS~05(V5M5!|0A><$?s_aO4@Rjr<#gWcS8qmA794&tYE7r$E8zCGNTFD4 zw*NK4xnDM-J}-e^7KUww3z~#o=LaE2B;Nv)LvI#s!ig2d#KV7!DSQakzun1Tp7o?^NFWtON}+j^Ln~I?q^4Kl?3}!(UpMC zxH#MPsjpH4@BNc^Ztf6(&x=JqPZS-KZZOun(0c6srE@#<&)q8LUF$Lg8+mpU)bk6k zcbhm%!7zR@bypSC8rFfD^mhmjgulr0=P{%RZt= zrFU7(C2pj#a(nK&Wf7ErlscBWyv5`5S#V#1pW$+ba(&eA)W&dDNp30uCJu1%n8H7X zmUkDp@ZLOP@I6N5do^92Qm!`<>nk(=&u;C4FC?5UKo!_d%VI{DL>X=WJ5f4;z({Mx zNmj0%Vbmo6C=S2dppI%NjUFoEFbKB1IPh#zkBkh555!aPVf--DH>BBC%LAy;z0OqZYI|e3N1ki=>kxG@*N(ZYOnxTnNSMb0bchACL36Qh8a$}`Y|b8E%O@@ zt0Bc}r<=gbP8K8){ulr+8l8QMi4J8XIWz zKd?WR;r|W$Tg-0;e4}m;%79os25v=g@%}U2?8}V0GgIc?T?d?aV72ROK5oixYR?0g zXuGFD1}rmd)4VtpF~{HjgQZMs28Ub>RYe;+P{NQ=%B53mO%rJJi2}p_rG2%L8ZJGA zn_coGzWfy2XpoF9co0Fs?gAo`NR1`$ z1AgZ?1`CZGu|iSz=lM8HfAnE_^ZG<|El)W#^XlNZ@M#+P6(oN(7cWr;5C$+)WX&jC z$oY(y@o6P~pY4?FB2P#7%D^Y2Ws1=ozO`MK4+s#+KZqX1f7Usbasf1u(jDRUuM1Dx zpy=jGcbkbGWyl9QX@5oQn{g57JwKN*rPG8B@Giuifu9x6aslw?vg>TYNsromFNrnj zJmvY&LPsuWMKS@xmZ%^6h+u3a_gbL<2pzx(m{XbjM+YKWxo06)*3sa1*}`@)7jq=3 zkxKy53s)qh`{^3W?1MGoXr<~IRm|}yAL+RcwBD~V3s4fRXTQxNjc(Qd!>|77;a5Lx zpp1(Rm2_^}8wYs%;|CuNU@|2iR1z7UMH11AWcQWAbDG@=q}=%_9Duj*175BPGZyT* ze)YKF)fv!P10UsCxS#wRcF2oHLqk)x1%QFJUZ#yMSdFLaPyQcR!Ka2)o{#1z`?bKE zvO6LFkt%3t+xm-`gf9yt1|tL@fn^dN3MnMhed_q$o+;8cX+#B)t#?`(o~blmNlk`n z41Fpcg$3O@Ej=~-6_ds_R-_b*x8nJu`=0}S(!bDc9~lz=FR^qW5WrzVRiu>r=g;$> z|1()Mp9jL9@y?0f{~f(BDj4{BNMtQZ{?otv&wu~_zq$WYy$k_?Ze`lHf}Hw3%DbQd Oe_kkQC|1dvhW; + + + + +Worktree Manager + + + +
+ + +
+
+ + + + + + + + + + +
+
+

Worktree Manager

+
Native git worktree management for JetBrains IDEs.
Atomic symlink switching — zero restarts, sub-second context changes.
+ wt CLI companion +
+
+ + + +
+
+
+ +
+
+
Switch Worktree Ctrl+Alt+W
+
Swap active worktree instantly via symlink
+
+
+
+
+ +
+
+
Create Worktree Ctrl+Alt+Shift+W
+
New worktree from any branch or commit
+
+
+
+
+ +
+
+
Provision
+
Set up IDE metadata & Bazel symlinks
+
+
+
+
+ +
+
+
Metadata Vault
+
Export & import .idea, .ijwb, .vscode
+
+
+
+
+ +
+
+
Agent Detection
+
Track Claude & AI agents in worktrees
+
+
+
+
+ +
+
+
Status Bar & Tool Window
+
Active context visible at a glance
+
+
+
+ + +
+ +
+
+
+
~/dev/myrepo
+
symlink
+
+
+
+ +
points to
+
+
+
+
feature-a/
+
active worktree
+
+
+
+ +
atomic swap
+
+
+
+
feature-b/
+
other worktree
+
+
+
+
+ + +
+ +
+ Worktree Manager tool window showing worktree list with branch, status, agent, and provision columns +
Worktrees tool window — branch, status, agent activity, and provision state at a glance
+
+
+ + +
+ +
    +
  1. +
    1
    +
    +
    Register your repository
    +
    Go to VCS > Worktrees > Add Context to connect a repo managed by the wt CLI. This links the plugin to your ~/.wt/ configuration.
    +
    +
  2. +
  3. +
    2
    +
    +
    Create or switch worktrees
    +
    Press Ctrl+Alt+Shift+W to create a new worktree, or Ctrl+Alt+W to switch. Switching atomically swaps the symlink — all open editors reload instantly.
    +
    +
  4. +
  5. +
    3
    +
    +
    Browse in the tool window
    +
    Open the Worktrees panel (bottom bar) to see all worktrees with branch, status, agent activity, and provision state. Double-click to switch.
    +
    +
  6. +
  7. +
    4
    +
    +
    Provision on first switch
    +
    When switching to a worktree for the first time, the plugin offers to provision it — importing IDE metadata from the vault and installing Bazel symlinks. This is one-time per worktree.
    +
    +
  8. +
  9. +
    5
    +
    +
    Keep metadata in sync
    +
    Use VCS > Worktrees > Export Metadata to save your current IDE settings to the vault before switching, so other worktrees can import them.
    +
    +
  10. +
+
+ + +
+ +
+
+
Shortcut
+
The status bar widget (bottom-right) shows your active context. Click it to quickly switch between repositories.
+
+
+
CLI Sync
+
Changes from the wt CLI are detected automatically via file watchers. No manual refresh needed.
+
+
+
Provision
+
If a worktree already has project files, provisioning offers to keep existing or overwrite from vault — nothing is lost silently.
+
+
+
Right-Click
+
Right-click any worktree row for Open Terminal, Reveal in Finder, Copy Path, and more.
+
+
+
+ + + + +
+ + diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt new file mode 100644 index 0000000..af1f9ea --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt @@ -0,0 +1,538 @@ +package com.block.wt.git + +import com.block.wt.model.ContextConfig +import com.block.wt.testutil.TestFileHelper.deleteRecursive +import com.block.wt.util.ProcessHelper +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class GitConfigHelperTest { + + private fun createTempGitRepo(): Path { + val dir = Files.createTempDirectory("gitconfig-test") + ProcessHelper.runGit(listOf("init"), workingDir = dir) + return dir + } + + private fun setWtConfig(repoDir: Path, enabled: Boolean = true, extra: Map = emptyMap()) { + ProcessHelper.runGit( + listOf("config", "--local", "wt.enabled", enabled.toString()), + workingDir = repoDir, + ) + for ((key, value) in extra) { + ProcessHelper.runGit( + listOf("config", "--local", "wt.$key", value), + workingDir = repoDir, + ) + } + } + + @Test + fun testReadConfigReturnsContextConfigWhenAllRequiredKeysPresent() { + val dir = createTempGitRepo() + try { + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt/worktrees", + "ideaFilesBase" to "/tmp/wt/idea-files", + "baseBranch" to "main", + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNotNull(config) + assertEquals(Path.of("/tmp/wt/worktrees"), config!!.worktreesBase) + assertEquals(Path.of("/tmp/wt/idea-files"), config.ideaFilesBase) + assertEquals("main", config.baseBranch) + assertEquals(dir, config.mainRepoRoot) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigReturnsNullWhenEnabledIsFalse() { + val dir = createTempGitRepo() + try { + setWtConfig( + dir, + enabled = false, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNull(config) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigReturnsNullWhenEnabledIsMissing() { + val dir = createTempGitRepo() + try { + // Don't set wt.enabled at all + val config = GitConfigHelper.readConfig(dir) + assertNull(config) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigReturnsNullWhenRequiredKeyMissing() { + val dir = createTempGitRepo() + try { + // Missing ideaFilesBase + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "baseBranch" to "main", + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNull(config) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigReturnsNullForNonGitDirectory() { + val dir = Files.createTempDirectory("gitconfig-test-nogit") + try { + val config = GitConfigHelper.readConfig(dir) + assertNull(config) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigHandlesOptionalActiveWorktree() { + val dir = createTempGitRepo() + try { + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + "activeWorktree" to "/tmp/active", + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNotNull(config) + assertEquals(Path.of("/tmp/active"), config!!.activeWorktree) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigDefaultsActiveWorktreeToMainRepoRoot() { + val dir = createTempGitRepo() + try { + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNotNull(config) + // activeWorktree defaults to mainRepoRoot when absent + assertEquals(dir, config!!.activeWorktree) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigHandlesMetadataPatterns() { + val dir = createTempGitRepo() + try { + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + "metadataPatterns" to ".idea .ijwb .vscode", + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNotNull(config) + assertEquals(listOf(".idea", ".ijwb", ".vscode"), config!!.metadataPatterns) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigDefaultsEmptyMetadataPatterns() { + val dir = createTempGitRepo() + try { + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNotNull(config) + assertEquals(emptyList(), config!!.metadataPatterns) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteConfigSetsAllKeys() { + val dir = createTempGitRepo() + try { + val config = ContextConfig( + name = "test", + mainRepoRoot = dir, + worktreesBase = Path.of("/tmp/wt"), + ideaFilesBase = Path.of("/tmp/idea"), + baseBranch = "main", + activeWorktree = Path.of("/tmp/active"), + metadataPatterns = listOf(".idea", ".vscode"), + ) + + GitConfigHelper.writeConfig(dir, config) + + // Verify by reading back with git config + fun gitGet(key: String): String? { + val result = ProcessHelper.runGit( + listOf("config", "--local", "--get", key), + workingDir = dir, + ) + return if (result.isSuccess) result.stdout.trim() else null + } + + assertEquals("true", gitGet("wt.enabled")) + assertEquals("test", gitGet("wt.contextName")) + assertEquals("/tmp/wt", gitGet("wt.worktreesBase")) + assertEquals("/tmp/idea", gitGet("wt.ideaFilesBase")) + assertEquals("main", gitGet("wt.baseBranch")) + assertEquals("/tmp/active", gitGet("wt.activeWorktree")) + assertEquals(".idea .vscode", gitGet("wt.metadataPatterns")) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteAndReadRoundTrip() { + val dir = createTempGitRepo() + try { + // Use a name different from the dir name to verify wt.contextName round-trip + val original = ContextConfig( + name = "my-custom-context", + mainRepoRoot = dir, + worktreesBase = Path.of("/tmp/wt"), + ideaFilesBase = Path.of("/tmp/idea"), + baseBranch = "develop", + activeWorktree = Path.of("/tmp/active"), + metadataPatterns = listOf(".idea", ".ijwb"), + ) + + GitConfigHelper.writeConfig(dir, original) + val read = GitConfigHelper.readConfig(dir) + + assertNotNull(read) + assertEquals(original.name, read!!.name) + assertEquals(original.worktreesBase, read.worktreesBase) + assertEquals(original.ideaFilesBase, read.ideaFilesBase) + assertEquals(original.baseBranch, read.baseBranch) + assertEquals(original.activeWorktree, read.activeWorktree) + assertEquals(original.metadataPatterns, read.metadataPatterns) + assertEquals(original.mainRepoRoot, read.mainRepoRoot) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testIsEnabledReturnsTrueWhenEnabled() { + val dir = createTempGitRepo() + try { + setWtConfig(dir, enabled = true) + assertTrue(GitConfigHelper.isEnabled(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testIsEnabledReturnsFalseWhenDisabled() { + val dir = createTempGitRepo() + try { + setWtConfig(dir, enabled = false) + assertFalse(GitConfigHelper.isEnabled(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testIsEnabledReturnsFalseWhenAbsent() { + val dir = createTempGitRepo() + try { + assertFalse(GitConfigHelper.isEnabled(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testIsEnabledReturnsFalseForNonGitDir() { + val dir = Files.createTempDirectory("gitconfig-test-nogit") + try { + assertFalse(GitConfigHelper.isEnabled(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigFromLinkedWorktree() { + val mainDir = createTempGitRepo() + try { + // Create an initial commit so we have a branch + val readmeFile = mainDir.resolve("README.md").toFile() + readmeFile.writeText("hello") + ProcessHelper.runGit(listOf("add", "README.md"), workingDir = mainDir) + ProcessHelper.runGit(listOf("commit", "-m", "initial"), workingDir = mainDir) + + // Set wt config on the main repo + setWtConfig( + mainDir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + ), + ) + + // Simulate a linked worktree: create a dir with .git file pointing back + val worktreeDir = Files.createTempDirectory("gitconfig-linked-wt") + val mainGitDir = mainDir.resolve(".git") + val worktreeGitDir = mainGitDir.resolve("worktrees").resolve("feature") + Files.createDirectories(worktreeGitDir) + Files.writeString(worktreeDir.resolve(".git"), "gitdir: $worktreeGitDir") + + val config = GitConfigHelper.readConfig(worktreeDir) + assertNotNull(config) + assertEquals(Path.of("/tmp/wt"), config!!.worktreesBase) + assertEquals(mainDir, config.mainRepoRoot) + + deleteRecursive(worktreeDir) + } finally { + deleteRecursive(mainDir) + } + } + + @Test + fun testPartialRequiredKeysReturnsNull() { + val dir = createTempGitRepo() + try { + // Set only 2 of 3 required keys + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "baseBranch" to "main", + // Missing ideaFilesBase + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNull(config) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testContextNameDerivesFromRepoBasenameStrippingSuffix() { + val baseDir = Files.createTempDirectory("gitconfig-test") + try { + // Create a repo in a directory named "java-master" + val repoDir = baseDir.resolve("java-master") + Files.createDirectory(repoDir) + ProcessHelper.runGit(listOf("init"), workingDir = repoDir) + + setWtConfig( + repoDir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "master", + ), + ) + + val config = GitConfigHelper.readConfig(repoDir) + assertNotNull(config) + assertEquals("java", config!!.name) + } finally { + deleteRecursive(baseDir) + } + } + + @Test + fun testRemoveAllConfigClearsAllKeys() { + val dir = createTempGitRepo() + try { + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + "activeWorktree" to "/tmp/active", + "metadataPatterns" to ".idea .vscode", + ), + ) + + // Verify config is present + assertNotNull(GitConfigHelper.readConfig(dir)) + assertTrue(GitConfigHelper.isEnabled(dir)) + + // Remove all config + GitConfigHelper.removeAllConfig(dir) + + // Verify everything is gone + assertNull(GitConfigHelper.readConfig(dir)) + assertFalse(GitConfigHelper.isEnabled(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigUsesContextNameFromGitConfig() { + val dir = createTempGitRepo() + try { + setWtConfig( + dir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + "contextName" to "mycontext", + ), + ) + + val config = GitConfigHelper.readConfig(dir) + assertNotNull(config) + // Name should come from wt.contextName, not dirname + assertEquals("mycontext", config!!.name) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigFallsBackToDirnameWhenContextNameAbsent() { + val baseDir = Files.createTempDirectory("gitconfig-test") + try { + val repoDir = baseDir.resolve("myrepo-main") + Files.createDirectory(repoDir) + ProcessHelper.runGit(listOf("init"), workingDir = repoDir) + + setWtConfig( + repoDir, + extra = mapOf( + "worktreesBase" to "/tmp/wt", + "ideaFilesBase" to "/tmp/idea", + "baseBranch" to "main", + ), + ) + + val config = GitConfigHelper.readConfig(repoDir) + assertNotNull(config) + // Falls back to dirname with -main stripped + assertEquals("myrepo", config!!.name) + } finally { + deleteRecursive(baseDir) + } + } + + @Test + fun testRemoveAllConfigOnNonGitDirIsNoOp() { + val dir = Files.createTempDirectory("gitconfig-test-nogit") + try { + // Should not throw + GitConfigHelper.removeAllConfig(dir) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteConfigClearsMetadataPatternsWhenEmpty() { + val dir = createTempGitRepo() + try { + // First write with patterns + val configWithPatterns = ContextConfig( + name = dir.fileName.toString(), + mainRepoRoot = dir, + worktreesBase = Path.of("/tmp/wt"), + ideaFilesBase = Path.of("/tmp/idea"), + baseBranch = "main", + activeWorktree = dir, + metadataPatterns = listOf(".idea", ".vscode"), + ) + GitConfigHelper.writeConfig(dir, configWithPatterns) + + // Verify patterns are set + var result = ProcessHelper.runGit( + listOf("config", "--local", "--get", "wt.metadataPatterns"), + workingDir = dir, + ) + assertEquals(".idea .vscode", result.stdout.trim()) + + // Now write with empty patterns — should clear the key + val configNoPatterns = configWithPatterns.copy(metadataPatterns = emptyList()) + GitConfigHelper.writeConfig(dir, configNoPatterns) + + result = ProcessHelper.runGit( + listOf("config", "--local", "--get", "wt.metadataPatterns"), + workingDir = dir, + ) + // Key should be unset (git config --get returns exit code 1 for missing keys) + assertFalse(result.isSuccess) + + // Round-trip: readConfig should return empty list + val read = GitConfigHelper.readConfig(dir) + assertNotNull(read) + assertEquals(emptyList(), read!!.metadataPatterns) + } finally { + deleteRecursive(dir) + } + } +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt new file mode 100644 index 0000000..d026586 --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt @@ -0,0 +1,172 @@ +package com.block.wt.git + +import com.block.wt.testutil.TestFileHelper.deleteRecursive +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class GitDirResolverTest { + + @Test + fun testMainWorktreeWithDotGitDirectory() { + val dir = Files.createTempDirectory("gitdir-test") + try { + val dotGit = dir.resolve(".git") + Files.createDirectory(dotGit) + + val result = GitDirResolver.resolveGitDir(dir) + assertNotNull(result) + assertEquals(dotGit, result) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testLinkedWorktreeWithAbsoluteGitdir() { + val dir = Files.createTempDirectory("gitdir-test") + try { + val gitWorktreeDir = dir.resolve("main-repo").resolve(".git").resolve("worktrees").resolve("feature") + Files.createDirectories(gitWorktreeDir) + + val worktree = dir.resolve("worktree") + Files.createDirectory(worktree) + Files.writeString(worktree.resolve(".git"), "gitdir: $gitWorktreeDir") + + val result = GitDirResolver.resolveGitDir(worktree) + assertNotNull(result) + assertEquals(gitWorktreeDir, result) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testLinkedWorktreeWithRelativeGitdir() { + val dir = Files.createTempDirectory("gitdir-test") + try { + val mainRepo = dir.resolve("repo") + val gitWorktreeDir = mainRepo.resolve(".git").resolve("worktrees").resolve("feature") + Files.createDirectories(gitWorktreeDir) + + val worktree = dir.resolve("worktree") + Files.createDirectory(worktree) + + // Write a relative gitdir path + val relativePath = worktree.relativize(gitWorktreeDir) + Files.writeString(worktree.resolve(".git"), "gitdir: $relativePath") + + val result = GitDirResolver.resolveGitDir(worktree) + assertNotNull(result) + // The resolved path should be normalized and point to the same directory + assertEquals(gitWorktreeDir.toRealPath(), result!!.toRealPath()) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testNoDotGitReturnsNull() { + val dir = Files.createTempDirectory("gitdir-test") + try { + val result = GitDirResolver.resolveGitDir(dir) + assertNull(result) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testMalformedGitFileReturnsNull() { + val dir = Files.createTempDirectory("gitdir-test") + try { + Files.writeString(dir.resolve(".git"), "not a gitdir pointer") + val result = GitDirResolver.resolveGitDir(dir) + assertNull(result) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testUnreadableGitFileReturnsNull() { + val dir = Files.createTempDirectory("gitdir-test") + try { + val dotGit = dir.resolve(".git") + Files.writeString(dotGit, "gitdir: /some/path") + dotGit.toFile().setReadable(false) + + val result = GitDirResolver.resolveGitDir(dir) + assertNull(result) + } finally { + // Restore permissions for cleanup + dir.resolve(".git").toFile().setReadable(true) + deleteRecursive(dir) + } + } + + @Test + fun testEmptyGitFileReturnsNull() { + val dir = Files.createTempDirectory("gitdir-test") + try { + Files.writeString(dir.resolve(".git"), "") + val result = GitDirResolver.resolveGitDir(dir) + assertNull(result) + } finally { + deleteRecursive(dir) + } + } + + // --- resolveMainGitDir tests --- + + @Test + fun testResolveMainGitDirForMainRepoReturnsOwnDotGit() { + val dir = Files.createTempDirectory("gitdir-test") + try { + val dotGit = dir.resolve(".git") + Files.createDirectory(dotGit) + + val result = GitDirResolver.resolveMainGitDir(dir) + assertNotNull(result) + assertEquals(dotGit, result) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testResolveMainGitDirForLinkedWorktreeWalksUpToDotGit() { + val dir = Files.createTempDirectory("gitdir-test") + try { + val mainRepo = dir.resolve("main-repo") + val mainGitDir = mainRepo.resolve(".git") + val worktreeGitDir = mainGitDir.resolve("worktrees").resolve("feature") + Files.createDirectories(worktreeGitDir) + + val worktree = dir.resolve("worktree") + Files.createDirectory(worktree) + Files.writeString(worktree.resolve(".git"), "gitdir: $worktreeGitDir") + + val result = GitDirResolver.resolveMainGitDir(worktree) + assertNotNull(result) + assertEquals(mainGitDir, result) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testResolveMainGitDirForNonGitPathReturnsNull() { + val dir = Files.createTempDirectory("gitdir-test") + try { + val result = GitDirResolver.resolveMainGitDir(dir) + assertNull(result) + } finally { + deleteRecursive(dir) + } + } + +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt new file mode 100644 index 0000000..ba788a3 --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt @@ -0,0 +1,230 @@ +package com.block.wt.model + +import com.block.wt.git.GitBranchHelper +import com.block.wt.git.GitParser +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Path + +class WorktreeInfoTest { + + @Test + fun testParsePorcelainBasic() { + val output = """ + worktree /Users/test/repo + HEAD abc123def456 + branch refs/heads/main + + worktree /Users/test/worktrees/feature + HEAD def456abc123 + branch refs/heads/feature/foo + + """.trimIndent() + + val result = GitParser.parsePorcelainOutput(output) + assertEquals(2, result.size) + + val main = result[0] + assertEquals(Path.of("/Users/test/repo"), main.path) + assertEquals("main", main.branch) + assertEquals("abc123def456", main.head) + assertTrue(main.isMain) + + val feature = result[1] + assertEquals(Path.of("/Users/test/worktrees/feature"), feature.path) + assertEquals("feature/foo", feature.branch) + assertFalse(feature.isMain) + } + + @Test + fun testParsePorcelainDetachedHead() { + val output = """ + worktree /Users/test/repo + HEAD abc123 + branch refs/heads/main + + worktree /Users/test/worktrees/detached + HEAD def456 + detached + + """.trimIndent() + + val result = GitParser.parsePorcelainOutput(output) + assertEquals(2, result.size) + + val detached = result[1] + assertNull(detached.branch) + assertEquals("def456", detached.head) + } + + @Test + fun testParsePorcelainPrunable() { + val output = """ + worktree /Users/test/repo + HEAD abc123 + branch refs/heads/main + + worktree /Users/test/worktrees/old + HEAD def456 + branch refs/heads/old-branch + prunable + + """.trimIndent() + + val result = GitParser.parsePorcelainOutput(output) + assertEquals(2, result.size) + assertTrue(result[1].isPrunable) + } + + @Test + fun testParsePorcelainEmpty() { + val result = GitParser.parsePorcelainOutput("") + assertTrue(result.isEmpty()) + } + + @Test + fun testParsePorcelainWithLinkedWorktree() { + val output = """ + worktree /Users/test/repo + HEAD abc123 + branch refs/heads/main + + worktree /Users/test/worktrees/feature + HEAD def456 + branch refs/heads/feature + + """.trimIndent() + + val result = GitParser.parsePorcelainOutput( + output, + linkedWorktreePath = Path.of("/Users/test/worktrees/feature") + ) + assertEquals(2, result.size) + assertFalse(result[0].isLinked) + assertTrue(result[1].isLinked) + } + + @Test + fun testDisplayName() { + val withBranch = WorktreeInfo( + path = Path.of("/test"), + branch = "feature/foo", + head = "abc123", + ) + assertEquals("feature/foo", withBranch.displayName) + + val detached = WorktreeInfo( + path = Path.of("/test2"), + branch = null, + head = "abc123def456", + ) + assertEquals("abc123de", detached.displayName) + } + + @Test + fun testHasActiveAgentDerived() { + val noAgent = WorktreeInfo( + path = Path.of("/test"), + branch = "main", + head = "abc123", + ) + assertFalse(noAgent.hasActiveAgent) + + val withAgent = WorktreeInfo( + path = Path.of("/test2"), + branch = "main", + head = "abc123", + activeAgentSessionIds = listOf("session-1", "session-2"), + ) + assertTrue(withAgent.hasActiveAgent) + } + + @Test + fun testIsDirtyWithNotLoaded() { + val wt = WorktreeInfo( + path = Path.of("/test"), + branch = "main", + head = "abc123", + ) + assertNull(wt.isDirty) + } + + @Test + fun testIsDirtyWithCleanStatus() { + val wt = WorktreeInfo( + path = Path.of("/test"), + branch = "main", + head = "abc123", + status = WorktreeStatus.Loaded( + staged = 0, modified = 0, untracked = 0, conflicts = 0, + ahead = null, behind = null, + ), + ) + assertEquals(false, wt.isDirty) + } + + @Test + fun testIsDirtyWithDirtyStatus() { + val wt = WorktreeInfo( + path = Path.of("/test"), + branch = "main", + head = "abc123", + status = WorktreeStatus.Loaded( + staged = 2, modified = 1, untracked = 0, conflicts = 0, + ahead = null, behind = null, + ), + ) + assertEquals(true, wt.isDirty) + } + + @Test + fun testWorktreeStatusLoadedIsDirty() { + val clean = WorktreeStatus.Loaded(staged = 0, modified = 0, untracked = 0, conflicts = 0, ahead = null, behind = null) + assertFalse(clean.isDirty) + + val dirty = WorktreeStatus.Loaded(staged = 1, modified = 0, untracked = 0, conflicts = 0, ahead = null, behind = null) + assertTrue(dirty.isDirty) + } + + @Test + fun testSanitizeBranchName() { + assertEquals("feature/foo", GitBranchHelper.sanitizeBranchName("feature/foo")) + assertEquals("simple", GitBranchHelper.sanitizeBranchName(" simple ")) + } + + @Test(expected = IllegalArgumentException::class) + fun testSanitizeBranchNamePathTraversal() { + GitBranchHelper.sanitizeBranchName("../evil") + } + + @Test(expected = IllegalArgumentException::class) + fun testSanitizeBranchNameBlank() { + GitBranchHelper.sanitizeBranchName(" ") + } + + @Test(expected = IllegalArgumentException::class) + fun testSanitizeBranchNameDash() { + GitBranchHelper.sanitizeBranchName("-flag") + } + + @Test(expected = IllegalArgumentException::class) + fun testSanitizeBranchNameDashWithLeadingWhitespace() { + GitBranchHelper.sanitizeBranchName(" -flag") + } + + @Test + fun testWorktreePathForBranch() { + val base = Path.of("/worktrees") + assertEquals( + Path.of("/worktrees/feature-foo"), + GitBranchHelper.worktreePathForBranch(base, "feature/foo") + ) + assertEquals( + Path.of("/worktrees/simple"), + GitBranchHelper.worktreePathForBranch(base, "simple") + ) + } +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt new file mode 100644 index 0000000..32c3b92 --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt @@ -0,0 +1,247 @@ +package com.block.wt.provision + +import com.block.wt.testutil.TestFileHelper.deleteRecursive +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class ProvisionMarkerServiceTest { + + @Test + fun testIsProvisionedReturnsFalseWhenNoGitDir() { + val dir = Files.createTempDirectory("provision-test") + try { + assertFalse(ProvisionMarkerService.isProvisioned(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testIsProvisionedReturnsFalseWhenNoMarker() { + val dir = Files.createTempDirectory("provision-test") + try { + // Create a .git directory (main worktree style) + Files.createDirectory(dir.resolve(".git")) + assertFalse(ProvisionMarkerService.isProvisioned(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteAndReadProvisionMarker() { + val dir = Files.createTempDirectory("provision-test") + try { + Files.createDirectory(dir.resolve(".git")) + + val result = ProvisionMarkerService.writeProvisionMarker(dir, "java") + assertTrue(result.isSuccess) + assertTrue(ProvisionMarkerService.isProvisioned(dir)) + assertTrue(ProvisionMarkerService.isProvisionedByContext(dir, "java")) + assertFalse(ProvisionMarkerService.isProvisionedByContext(dir, "kotlin")) + + val marker = ProvisionMarkerService.readProvisionMarker(dir) + assertNotNull(marker) + assertEquals("java", marker!!.current) + assertEquals(1, marker.provisions.size) + assertEquals("java", marker.provisions[0].context) + assertEquals("intellij-plugin", marker.provisions[0].provisionedBy) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteMultipleContextsPreservesHistory() { + val dir = Files.createTempDirectory("provision-test") + try { + Files.createDirectory(dir.resolve(".git")) + + ProvisionMarkerService.writeProvisionMarker(dir, "java") + ProvisionMarkerService.writeProvisionMarker(dir, "kotlin") + + val marker = ProvisionMarkerService.readProvisionMarker(dir) + assertNotNull(marker) + assertEquals("kotlin", marker!!.current) + assertEquals(2, marker.provisions.size) + + val contexts = marker.provisions.map { it.context }.toSet() + assertTrue(contexts.contains("java")) + assertTrue(contexts.contains("kotlin")) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReProvisionSameContextUpdatesEntry() { + val dir = Files.createTempDirectory("provision-test") + try { + Files.createDirectory(dir.resolve(".git")) + + ProvisionMarkerService.writeProvisionMarker(dir, "java") + ProvisionMarkerService.writeProvisionMarker(dir, "java") + + val marker = ProvisionMarkerService.readProvisionMarker(dir) + assertNotNull(marker) + assertEquals("java", marker!!.current) + assertEquals(1, marker.provisions.size) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testRemoveProvisionMarkerDeletesFile() { + val dir = Files.createTempDirectory("provision-test") + try { + Files.createDirectory(dir.resolve(".git")) + ProvisionMarkerService.writeProvisionMarker(dir, "java") + assertTrue(ProvisionMarkerService.isProvisioned(dir)) + + val result = ProvisionMarkerService.removeProvisionMarker(dir) + assertTrue(result.isSuccess) + assertFalse(ProvisionMarkerService.isProvisioned(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testRemoveSpecificContextKeepsOthers() { + val dir = Files.createTempDirectory("provision-test") + try { + Files.createDirectory(dir.resolve(".git")) + ProvisionMarkerService.writeProvisionMarker(dir, "java") + ProvisionMarkerService.writeProvisionMarker(dir, "kotlin") + + val result = ProvisionMarkerService.removeProvisionMarker(dir, "java") + assertTrue(result.isSuccess) + + val marker = ProvisionMarkerService.readProvisionMarker(dir) + assertNotNull(marker) + assertEquals(1, marker!!.provisions.size) + assertEquals("kotlin", marker.provisions[0].context) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testRemoveCurrentContextUpdatesCurrent() { + val dir = Files.createTempDirectory("provision-test") + try { + Files.createDirectory(dir.resolve(".git")) + ProvisionMarkerService.writeProvisionMarker(dir, "java") + ProvisionMarkerService.writeProvisionMarker(dir, "kotlin") + + // "kotlin" is current, remove it + ProvisionMarkerService.removeProvisionMarker(dir, "kotlin") + + val marker = ProvisionMarkerService.readProvisionMarker(dir) + assertNotNull(marker) + assertEquals("java", marker!!.current) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testRemoveLastContextDeletesFile() { + val dir = Files.createTempDirectory("provision-test") + try { + Files.createDirectory(dir.resolve(".git")) + ProvisionMarkerService.writeProvisionMarker(dir, "java") + + ProvisionMarkerService.removeProvisionMarker(dir, "java") + assertFalse(ProvisionMarkerService.isProvisioned(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testRemoveNonexistentMarkerReturnsTrue() { + val dir = Files.createTempDirectory("provision-test") + try { + Files.createDirectory(dir.resolve(".git")) + assertTrue(ProvisionMarkerService.removeProvisionMarker(dir).isSuccess) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadIncompleteJsonReturnsNull() { + val dir = Files.createTempDirectory("provision-test") + try { + val gitDir = dir.resolve(".git") + Files.createDirectory(gitDir) + + // Empty JSON object — Gson sets non-null fields to null via Unsafe + Files.writeString(gitDir.resolve("wt-provisioned"), "{}") + assertNull(ProvisionMarkerService.readProvisionMarker(dir)) + + // Missing provisions field + Files.writeString(gitDir.resolve("wt-provisioned"), """{"current":"test"}""") + assertNull(ProvisionMarkerService.readProvisionMarker(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadMalformedJsonReturnsNull() { + val dir = Files.createTempDirectory("provision-test") + try { + val gitDir = dir.resolve(".git") + Files.createDirectory(gitDir) + Files.writeString(gitDir.resolve("wt-provisioned"), "not valid json") + + assertNull(ProvisionMarkerService.readProvisionMarker(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testHasExistingMetadata() { + val dir = Files.createTempDirectory("provision-test") + try { + assertFalse(ProvisionMarkerService.hasExistingMetadata(dir)) + + Files.createDirectory(dir.resolve(".idea")) + assertTrue(ProvisionMarkerService.hasExistingMetadata(dir)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testLinkedWorktreeGitFile() { + val dir = Files.createTempDirectory("provision-test") + try { + // Simulate a linked worktree: .git is a file pointing to a gitdir + val gitWorktreeDir = dir.resolve("gitworktrees").resolve("mybranch") + Files.createDirectories(gitWorktreeDir) + + val worktree = dir.resolve("worktree") + Files.createDirectory(worktree) + Files.writeString(worktree.resolve(".git"), "gitdir: ${gitWorktreeDir}") + + val result = ProvisionMarkerService.writeProvisionMarker(worktree, "test-context") + assertTrue(result.isSuccess) + assertTrue(Files.exists(gitWorktreeDir.resolve("wt-provisioned"))) + assertTrue(ProvisionMarkerService.isProvisionedByContext(worktree, "test-context")) + } finally { + deleteRecursive(dir) + } + } + +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt new file mode 100644 index 0000000..d211b6f --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt @@ -0,0 +1,145 @@ +package com.block.wt.services + +import com.block.wt.model.ContextConfig +import com.block.wt.util.PathHelper +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Path + +class ExternalChangeWatcherTest { + + private fun makeConfig( + activeWorktree: Path = Path.of("/wt/repos/java/worktrees/guodong"), + mainRepoRoot: Path = Path.of("/wt/repos/java/base"), + ) = ContextConfig( + name = "java", + mainRepoRoot = mainRepoRoot, + worktreesBase = Path.of("/wt/repos/java/worktrees"), + activeWorktree = activeWorktree, + ideaFilesBase = Path.of("/wt/repos/java/idea-files"), + baseBranch = "master", + metadataPatterns = emptyList(), + ) + + // --- isRelevantEvent tests --- + + @Test + fun testConfFileChangeIsRelevant() { + assertTrue(ExternalChangeWatcher.isRelevantEvent("java.conf", PathHelper.reposDir, null)) + } + + @Test + fun testConfFileChangeInWrongDirIsNotRelevant() { + val config = makeConfig() + val gitDir = config.mainRepoRoot.resolve(".git") + assertFalse(ExternalChangeWatcher.isRelevantEvent("something.conf", gitDir, config)) + } + + @Test + fun testCurrentFileChangeIsRelevant() { + assertTrue(ExternalChangeWatcher.isRelevantEvent("current", PathHelper.wtRoot, null)) + } + + @Test + fun testCurrentFileChangeInWrongDirIsNotRelevant() { + assertFalse(ExternalChangeWatcher.isRelevantEvent("current", Path.of("/some/dir"), null)) + } + + @Test + fun testActiveWorktreeSymlinkChangeIsRelevant() { + val config = makeConfig() + assertTrue( + ExternalChangeWatcher.isRelevantEvent("guodong", config.activeWorktree.parent, config) + ) + } + + @Test + fun testActiveWorktreeSymlinkChangeInWrongDirIsNotRelevant() { + val config = makeConfig() + assertFalse( + ExternalChangeWatcher.isRelevantEvent("guodong", Path.of("/some/other/dir"), config) + ) + } + + @Test + fun testUnrelatedFileChangeIsNotRelevant() { + val config = makeConfig() + assertFalse( + ExternalChangeWatcher.isRelevantEvent("unrelated.txt", Path.of("/some/dir"), config) + ) + } + + @Test + fun testGitWorktreesDirChangeIsRelevant() { + val config = makeConfig() + val gitWorktreesDir = config.mainRepoRoot.resolve(".git/worktrees") + assertTrue( + ExternalChangeWatcher.isRelevantEvent("feature-branch", gitWorktreesDir, config) + ) + } + + @Test + fun testGitConfigChangeIsRelevant() { + val config = makeConfig() + val gitDir = config.mainRepoRoot.resolve(".git") + assertTrue( + ExternalChangeWatcher.isRelevantEvent("config", gitDir, config) + ) + } + + @Test + fun testGitDirNonConfigChangeIsNotRelevant() { + val config = makeConfig() + val gitDir = config.mainRepoRoot.resolve(".git") + assertFalse( + ExternalChangeWatcher.isRelevantEvent("HEAD", gitDir, config) + ) + } + + @Test + fun testNullConfigStillDetectsConfAndCurrentChanges() { + assertTrue(ExternalChangeWatcher.isRelevantEvent("foo.conf", PathHelper.reposDir, null)) + assertTrue(ExternalChangeWatcher.isRelevantEvent("current", PathHelper.wtRoot, null)) + assertFalse(ExternalChangeWatcher.isRelevantEvent("random.txt", null, null)) + } + + // --- buildWatchState tests --- + + @Test + fun testBuildWatchStateIncludesActiveWorktreeFileName() { + val config = makeConfig() + val paths = setOf(Path.of("/wt"), Path.of("/wt/repos/java/worktrees")) + val state = ExternalChangeWatcher.buildWatchState(config, paths) + assertEquals("guodong", state.activeWorktreeFileName) + assertEquals(paths, state.paths) + } + + @Test + fun testBuildWatchStateNullConfigReturnsNullFileName() { + val state = ExternalChangeWatcher.buildWatchState(null, emptySet()) + assertEquals(null, state.activeWorktreeFileName) + } + + @Test + fun testBuildWatchStateDiffersWhenActiveWorktreeChanges() { + val config1 = makeConfig(activeWorktree = Path.of("/wt/repos/java/worktrees/guodong")) + val config2 = makeConfig(activeWorktree = Path.of("/wt/repos/java/worktrees/feature")) + val paths = setOf(Path.of("/wt")) + // Same paths, different activeWorktree filename → different state + val state1 = ExternalChangeWatcher.buildWatchState(config1, paths) + val state2 = ExternalChangeWatcher.buildWatchState(config2, paths) + assertNotEquals(state1, state2) + } + + @Test + fun testBuildWatchStateSameWhenNothingChanges() { + val config = makeConfig() + val paths = setOf(Path.of("/wt")) + val state1 = ExternalChangeWatcher.buildWatchState(config, paths) + val state2 = ExternalChangeWatcher.buildWatchState(config, paths) + assertEquals(state1, state2) + } +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt new file mode 100644 index 0000000..3dd21f0 --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt @@ -0,0 +1,101 @@ +package com.block.wt.services + +import com.block.wt.testutil.TestFileHelper.deleteRecursive +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class MetadataServiceStaticTest { + + @Test + fun testExportCreatesSymlinksInVault() { + val dir = Files.createTempDirectory("metadata-test") + try { + val source = dir.resolve("repo") + val vault = dir.resolve("vault") + Files.createDirectories(source) + + // Create metadata directories + Files.createDirectories(source.resolve(".idea")) + Files.createFile(source.resolve(".idea").resolve("workspace.xml")) + Files.createDirectories(source.resolve(".vscode")) + Files.createFile(source.resolve(".vscode").resolve("settings.json")) + + val result = MetadataService.exportMetadataStatic(source, vault, listOf(".idea", ".vscode")) + assertTrue(result.isSuccess) + assertEquals(2, result.getOrThrow()) + + // Vault should contain symlinks + assertTrue(Files.isSymbolicLink(vault.resolve(".idea"))) + assertTrue(Files.isSymbolicLink(vault.resolve(".vscode"))) + + // Symlinks should point to the source directories + assertEquals(source.resolve(".idea"), Files.readSymbolicLink(vault.resolve(".idea"))) + assertEquals(source.resolve(".vscode"), Files.readSymbolicLink(vault.resolve(".vscode"))) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testExportWithNoMatchingPatternsReturnsZero() { + val dir = Files.createTempDirectory("metadata-test") + try { + val source = dir.resolve("repo") + val vault = dir.resolve("vault") + Files.createDirectories(source) + + val result = MetadataService.exportMetadataStatic(source, vault, listOf(".idea")) + assertTrue(result.isSuccess) + assertEquals(0, result.getOrThrow()) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testExportReplacesExistingSymlinks() { + val dir = Files.createTempDirectory("metadata-test") + try { + val source = dir.resolve("repo") + val vault = dir.resolve("vault") + Files.createDirectories(source) + Files.createDirectories(vault) + Files.createDirectories(source.resolve(".idea")) + + // Create an existing symlink pointing somewhere else + val oldTarget = dir.resolve("old-target") + Files.createDirectories(oldTarget) + Files.createSymbolicLink(vault.resolve(".idea"), oldTarget) + + val result = MetadataService.exportMetadataStatic(source, vault, listOf(".idea")) + assertTrue(result.isSuccess) + assertEquals(1, result.getOrThrow()) + + // Symlink should now point to the new source + assertEquals(source.resolve(".idea"), Files.readSymbolicLink(vault.resolve(".idea"))) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testExportCreatesVaultDirectoryIfNotExists() { + val dir = Files.createTempDirectory("metadata-test") + try { + val source = dir.resolve("repo") + val vault = dir.resolve("vault").resolve("nested") + Files.createDirectories(source) + Files.createDirectories(source.resolve(".idea")) + + val result = MetadataService.exportMetadataStatic(source, vault, listOf(".idea")) + assertTrue(result.isSuccess) + assertTrue(Files.isDirectory(vault)) + } finally { + deleteRecursive(dir) + } + } + +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt new file mode 100644 index 0000000..9679a03 --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt @@ -0,0 +1,97 @@ +package com.block.wt.services + +import com.block.wt.testutil.TestFileHelper.deleteRecursive +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class MetadataServiceTest { + + @Test + fun testDeduplicateNested() { + // Use a standalone test for the deduplication logic + val paths = listOf( + Path.of("/repo/.ijwb"), + Path.of("/repo/.ijwb/.idea"), + Path.of("/repo/.vscode"), + Path.of("/repo/subdir/.idea"), + ) + + val result = deduplicateNested(paths) + + assertEquals(3, result.size) + assertTrue(result.contains(Path.of("/repo/.ijwb"))) + assertTrue(result.contains(Path.of("/repo/.vscode"))) + assertTrue(result.contains(Path.of("/repo/subdir/.idea"))) + // .ijwb/.idea should be removed because it's nested under .ijwb + assertTrue(!result.contains(Path.of("/repo/.ijwb/.idea"))) + } + + @Test + fun testDeduplicateNestedEmpty() { + assertEquals(emptyList(), deduplicateNested(emptyList())) + } + + @Test + fun testDeduplicateNestedSingle() { + val paths = listOf(Path.of("/repo/.idea")) + assertEquals(paths, deduplicateNested(paths)) + } + + @Test + fun testCopyDirectoryStructure() { + val sourceDir = Files.createTempDirectory("meta-source") + val targetDir = Files.createTempDirectory("meta-target") + try { + // Create source structure + val subDir = sourceDir.resolve("subdir") + Files.createDirectory(subDir) + Files.writeString(sourceDir.resolve("file1.txt"), "content1") + Files.writeString(subDir.resolve("file2.txt"), "content2") + + // Copy + copyDirectory(sourceDir, targetDir) + + // Verify + assertTrue(Files.exists(targetDir.resolve("file1.txt"))) + assertTrue(Files.exists(targetDir.resolve("subdir/file2.txt"))) + assertEquals("content1", Files.readString(targetDir.resolve("file1.txt"))) + assertEquals("content2", Files.readString(targetDir.resolve("subdir/file2.txt"))) + } finally { + deleteRecursive(sourceDir) + deleteRecursive(targetDir) + } + } + + // Standalone implementations of the logic for testing without IntelliJ platform + private fun deduplicateNested(paths: List): List { + val sorted = paths.sortedBy { it.nameCount } + val kept = mutableListOf() + for (path in sorted) { + val isNested = kept.any { path.startsWith(it) } + if (!isNested) { + kept.add(path) + } + } + return kept + } + + private fun copyDirectory(source: Path, target: Path) { + Files.walkFileTree(source, object : java.nio.file.SimpleFileVisitor() { + override fun preVisitDirectory(dir: Path, attrs: java.nio.file.attribute.BasicFileAttributes): java.nio.file.FileVisitResult { + val targetDir = target.resolve(source.relativize(dir)) + Files.createDirectories(targetDir) + return java.nio.file.FileVisitResult.CONTINUE + } + + override fun visitFile(file: Path, attrs: java.nio.file.attribute.BasicFileAttributes): java.nio.file.FileVisitResult { + val targetFile = target.resolve(source.relativize(file)) + Files.copy(file, targetFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING) + return java.nio.file.FileVisitResult.CONTINUE + } + }) + } + +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt new file mode 100644 index 0000000..25f1d7b --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt @@ -0,0 +1,141 @@ +package com.block.wt.services + +import com.block.wt.git.GitParser +import com.block.wt.model.WorktreeInfo +import com.block.wt.testutil.FakeAgentDetection +import com.block.wt.testutil.FakeProcessRunner +import com.block.wt.util.ProcessHelper +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Path + +class WorktreeServiceTest { + + @Test + fun testParseValidPorcelainOutput() { + val output = """ + worktree /Users/test/repo + HEAD abc123def456 + branch refs/heads/main + + worktree /Users/test/worktrees/feature + HEAD def456abc123 + branch refs/heads/feature/foo + + """.trimIndent() + + val result = GitParser.parsePorcelainOutput(output) + assertEquals(2, result.size) + + assertEquals(Path.of("/Users/test/repo"), result[0].path) + assertEquals("main", result[0].branch) + assertTrue(result[0].isMain) + + assertEquals(Path.of("/Users/test/worktrees/feature"), result[1].path) + assertEquals("feature/foo", result[1].branch) + assertFalse(result[1].isMain) + } + + @Test + fun testParseEmptyOutputReturnsEmptyList() { + val result = GitParser.parsePorcelainOutput("") + assertTrue(result.isEmpty()) + } + + @Test + fun testParseMalformedOutputReturnsEmptyList() { + val result = GitParser.parsePorcelainOutput("garbage\ndata\nhere") + assertTrue(result.isEmpty()) + } + + @Test + fun testAgentEnrichmentWithActiveAgents() { + val featurePath = Path.of("/Users/test/worktrees/feature") + val agentDetection = FakeAgentDetection( + activeDirs = setOf(featurePath), + sessionIds = mapOf(featurePath to listOf("session-abc", "session-def")), + ) + + val worktrees = listOf( + WorktreeInfo( + path = Path.of("/Users/test/repo"), + branch = "main", + head = "abc123", + isMain = true, + ), + WorktreeInfo( + path = featurePath, + branch = "feature/foo", + head = "def456", + ), + ) + + val enriched = enrichAgentStatusWith(worktrees, agentDetection) + + assertFalse(enriched[0].hasActiveAgent) + assertTrue(enriched[0].activeAgentSessionIds.isEmpty()) + + assertTrue(enriched[1].hasActiveAgent) + assertEquals(listOf("session-abc", "session-def"), enriched[1].activeAgentSessionIds) + } + + @Test + fun testAgentEnrichmentWithNoAgents() { + val agentDetection = FakeAgentDetection() + + val worktrees = listOf( + WorktreeInfo( + path = Path.of("/Users/test/repo"), + branch = "main", + head = "abc123", + ), + ) + + val enriched = enrichAgentStatusWith(worktrees, agentDetection) + assertFalse(enriched[0].hasActiveAgent) + } + + @Test + fun testFakeProcessRunnerReturnsConfiguredResponses() { + val runner = FakeProcessRunner( + responses = mapOf( + listOf("git", "worktree", "list", "--porcelain") to ProcessHelper.ProcessResult( + exitCode = 0, + stdout = "worktree /test\nHEAD abc123\nbranch refs/heads/main\n", + stderr = "", + ), + ), + ) + + val result = runner.runGit(listOf("worktree", "list", "--porcelain")) + assertTrue(result.isSuccess) + assertTrue(result.stdout.contains("worktree /test")) + } + + @Test + fun testFakeProcessRunnerReturnsFailureForUnknownCommands() { + val runner = FakeProcessRunner() + val result = runner.runGit(listOf("unknown", "command")) + assertFalse(result.isSuccess) + } + + /** + * Mirrors WorktreeService.enrichAgentStatus using a given AgentDetection. + * Allows testing agent enrichment without a Project/service. + */ + private fun enrichAgentStatusWith( + worktrees: List, + agentDetection: FakeAgentDetection, + ): List { + val agentDirs = agentDetection.detectActiveAgentDirs() + return worktrees.map { wt -> + val hasAgent = agentDirs.any { agentDir -> + agentDir == wt.path + } + val sessionIds = if (hasAgent) agentDetection.detectActiveSessionIds(wt.path) else emptyList() + wt.copy(activeAgentSessionIds = sessionIds) + } + } +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt new file mode 100644 index 0000000..ba6d87c --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt @@ -0,0 +1,15 @@ +package com.block.wt.testutil + +import com.block.wt.agent.AgentDetection +import java.nio.file.Path + +class FakeAgentDetection( + private val activeDirs: Set = emptySet(), + private val sessionIds: Map> = emptyMap(), +) : AgentDetection { + + override fun detectActiveAgentDirs(): Set = activeDirs + + override fun detectActiveSessionIds(worktreePath: Path): List = + sessionIds[worktreePath] ?: emptyList() +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt new file mode 100644 index 0000000..73305ed --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt @@ -0,0 +1,20 @@ +package com.block.wt.testutil + +import com.block.wt.util.ProcessHelper +import com.block.wt.util.ProcessRunner +import java.nio.file.Path + +class FakeProcessRunner( + private val responses: Map, ProcessHelper.ProcessResult> = emptyMap(), +) : ProcessRunner { + + private val defaultResult = ProcessHelper.ProcessResult(exitCode = 1, stdout = "", stderr = "not configured") + + override fun run(command: List, workingDir: Path?, timeoutSeconds: Long): ProcessHelper.ProcessResult { + return responses[command] ?: defaultResult + } + + override fun runGit(args: List, workingDir: Path?): ProcessHelper.ProcessResult { + return responses[listOf("git") + args] ?: responses[args] ?: defaultResult + } +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt new file mode 100644 index 0000000..7accc5e --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt @@ -0,0 +1,16 @@ +package com.block.wt.testutil + +import java.nio.file.Files +import java.nio.file.Path + +object TestFileHelper { + + fun deleteRecursive(path: Path) { + if (Files.isDirectory(path) && !Files.isSymbolicLink(path)) { + Files.list(path).use { stream -> + stream.forEach { deleteRecursive(it) } + } + } + Files.deleteIfExists(path) + } +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt new file mode 100644 index 0000000..1bcba86 --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt @@ -0,0 +1,230 @@ +package com.block.wt.util + +import com.block.wt.model.ContextConfig +import com.block.wt.testutil.TestFileHelper.deleteRecursive +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class ConfigFileHelperTest { + + @Test + fun testReadConfig() { + val dir = Files.createTempDirectory("config-test") + try { + val confFile = dir.resolve("java.conf") + Files.writeString( + confFile, + """ + WT_MAIN_REPO_ROOT="/Users/test/.wt/repos/java/base" + WT_WORKTREES_BASE="/Users/test/.wt/repos/java/worktrees" + WT_ACTIVE_WORKTREE="/Users/test/java" + WT_IDEA_FILES_BASE="/Users/test/.wt/repos/java/idea-files" + WT_BASE_BRANCH="master" + WT_METADATA_PATTERNS=".bazelbsp .ijwb .vscode .run .idea" + """.trimIndent() + ) + + val config = ConfigFileHelper.readConfig(confFile) + assertNotNull(config) + assertEquals("java", config!!.name) + assertEquals(Path.of("/Users/test/.wt/repos/java/base"), config.mainRepoRoot) + assertEquals(Path.of("/Users/test/.wt/repos/java/worktrees"), config.worktreesBase) + assertEquals(Path.of("/Users/test/java"), config.activeWorktree) + assertEquals(Path.of("/Users/test/.wt/repos/java/idea-files"), config.ideaFilesBase) + assertEquals("master", config.baseBranch) + assertEquals(listOf(".bazelbsp", ".ijwb", ".vscode", ".run", ".idea"), config.metadataPatterns) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigWithDoubleQuotes() { + val dir = Files.createTempDirectory("config-test") + try { + val confFile = dir.resolve("test.conf") + Files.writeString( + confFile, + """ + WT_MAIN_REPO_ROOT="/path/with spaces/repo" + WT_WORKTREES_BASE="/path/worktrees" + WT_ACTIVE_WORKTREE="/path/active" + WT_IDEA_FILES_BASE="/path/vault" + WT_BASE_BRANCH="main" + WT_METADATA_PATTERNS=".idea" + """.trimIndent() + ) + + val config = ConfigFileHelper.readConfig(confFile) + assertNotNull(config) + assertEquals(Path.of("/path/with spaces/repo"), config!!.mainRepoRoot) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigMissingFile() { + val config = ConfigFileHelper.readConfig(Path.of("/nonexistent/path.conf")) + assertNull(config) + } + + @Test + fun testReadConfigMissingRequiredField() { + val dir = Files.createTempDirectory("config-test") + try { + val confFile = dir.resolve("bad.conf") + Files.writeString( + confFile, + """ + WT_BASE_BRANCH="main" + """.trimIndent() + ) + + val config = ConfigFileHelper.readConfig(confFile) + assertNull(config) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteAndReadRoundTrip() { + val dir = Files.createTempDirectory("config-test") + try { + val confFile = dir.resolve("roundtrip.conf") + + val original = ContextConfig( + name = "roundtrip", + mainRepoRoot = Path.of("/Users/test/repo"), + worktreesBase = Path.of("/Users/test/worktrees"), + activeWorktree = Path.of("/Users/test/active"), + ideaFilesBase = Path.of("/Users/test/vault"), + baseBranch = "main", + metadataPatterns = listOf(".idea", ".vscode"), + ) + + ConfigFileHelper.writeConfig(confFile, original) + val read = ConfigFileHelper.readConfig(confFile) + + assertNotNull(read) + // Name is derived from .conf filename, not from ContextConfig.name + assertEquals("roundtrip", read!!.name) + assertEquals(original.mainRepoRoot, read.mainRepoRoot) + assertEquals(original.worktreesBase, read.worktreesBase) + assertEquals(original.activeWorktree, read.activeWorktree) + assertEquals(original.ideaFilesBase, read.ideaFilesBase) + assertEquals(original.baseBranch, read.baseBranch) + assertEquals(original.metadataPatterns, read.metadataPatterns) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadConfigDefaultsBaseBranchToMaster() { + val dir = Files.createTempDirectory("config-test") + try { + val confFile = dir.resolve("nobase.conf") + Files.writeString( + confFile, + """ + WT_MAIN_REPO_ROOT="/Users/test/repo" + WT_WORKTREES_BASE="/Users/test/worktrees" + WT_ACTIVE_WORKTREE="/Users/test/active" + WT_IDEA_FILES_BASE="/Users/test/vault" + """.trimIndent() + ) + + val config = ConfigFileHelper.readConfig(confFile) + assertNotNull(config) + assertEquals("master", config!!.baseBranch) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadCurrentContextReturnsNullWhenFileMissing() { + val file = Path.of("/tmp/nonexistent-wt-test-current") + assertNull(ConfigFileHelper.readCurrentContext(file)) + } + + @Test + fun testReadCurrentContextReturnsNullOnEmptyFile() { + val dir = Files.createTempDirectory("current-test") + try { + val file = dir.resolve("current") + Files.writeString(file, "") + assertNull(ConfigFileHelper.readCurrentContext(file)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteAndReadCurrentContextRoundTrip() { + val dir = Files.createTempDirectory("current-test") + try { + val file = dir.resolve("current") + ConfigFileHelper.writeCurrentContext("java", file) + assertEquals("java", ConfigFileHelper.readCurrentContext(file)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadCurrentContextReadsOnlyFirstLine() { + val dir = Files.createTempDirectory("current-test") + try { + val file = dir.resolve("current") + Files.writeString(file, "java\nextra-line\nmore-stuff") + assertEquals("java", ConfigFileHelper.readCurrentContext(file)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteCurrentContextCreatesParentDirectory() { + val dir = Files.createTempDirectory("current-test") + try { + val file = dir.resolve("subdir").resolve("current") + ConfigFileHelper.writeCurrentContext("go", file) + assertEquals("go", ConfigFileHelper.readCurrentContext(file)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testWriteConfigEmptyPatterns() { + val dir = Files.createTempDirectory("config-test") + try { + val confFile = dir.resolve("empty.conf") + + val config = ContextConfig( + name = "empty", + mainRepoRoot = Path.of("/repo"), + worktreesBase = Path.of("/wt"), + activeWorktree = Path.of("/active"), + ideaFilesBase = Path.of("/vault"), + baseBranch = "main", + metadataPatterns = emptyList(), + ) + + ConfigFileHelper.writeConfig(confFile, config) + val read = ConfigFileHelper.readConfig(confFile) + assertNotNull(read) + assertEquals(emptyList(), read!!.metadataPatterns) + } finally { + deleteRecursive(dir) + } + } + +} diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/util/PathHelperTest.kt b/wt-intellij-plugin/src/test/kotlin/com/block/wt/util/PathHelperTest.kt new file mode 100644 index 0000000..d1699dd --- /dev/null +++ b/wt-intellij-plugin/src/test/kotlin/com/block/wt/util/PathHelperTest.kt @@ -0,0 +1,147 @@ +package com.block.wt.util + +import com.block.wt.testutil.TestFileHelper.deleteRecursive +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class PathHelperTest { + + @Test + fun testExpandTildeWithSubpath() { + val result = PathHelper.expandTilde("~/foo/bar") + val home = System.getProperty("user.home") + assertEquals(Path.of(home, "foo", "bar"), result) + } + + @Test + fun testExpandTildeAlone() { + val result = PathHelper.expandTilde("~") + val home = System.getProperty("user.home") + assertEquals(Path.of(home), result) + } + + @Test + fun testExpandTildeAbsolutePath() { + val result = PathHelper.expandTilde("/absolute/path") + assertEquals(Path.of("/absolute/path"), result) + } + + @Test + fun testAtomicSetSymlinkCreate() { + val dir = Files.createTempDirectory("pathhelper-test") + try { + val link = dir.resolve("link") + val target = dir.resolve("target") + Files.createDirectory(target) + + PathHelper.atomicSetSymlink(link, target) + + assertTrue(Files.isSymbolicLink(link)) + assertEquals(target, Files.readSymbolicLink(link)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testAtomicSetSymlinkReplace() { + val dir = Files.createTempDirectory("pathhelper-test") + try { + val link = dir.resolve("link") + val target1 = dir.resolve("target1") + val target2 = dir.resolve("target2") + Files.createDirectory(target1) + Files.createDirectory(target2) + + PathHelper.atomicSetSymlink(link, target1) + assertEquals(target1, Files.readSymbolicLink(link)) + + // Atomic replace + PathHelper.atomicSetSymlink(link, target2) + assertEquals(target2, Files.readSymbolicLink(link)) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testAtomicSetSymlinkCleanupOnError() { + val dir = Files.createTempDirectory("pathhelper-test") + try { + val link = dir.resolve("nonexistent-parent/link") + + try { + PathHelper.atomicSetSymlink(link, dir) + } catch (_: Exception) { + // Expected + } + + // Verify no temp files left behind in parent + val parentDir = link.parent + if (Files.isDirectory(parentDir)) { + val tempFiles = Files.list(parentDir).use { stream -> + stream.filter { it.fileName.toString().contains(".tmp") }.count() + } + assertEquals(0L, tempFiles) + } + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testIsSymlink() { + val dir = Files.createTempDirectory("pathhelper-test") + try { + val target = dir.resolve("target") + Files.createDirectory(target) + val link = dir.resolve("link") + Files.createSymbolicLink(link, target) + + assertTrue(PathHelper.isSymlink(link)) + assertFalse(PathHelper.isSymlink(target)) + assertFalse(PathHelper.isSymlink(dir.resolve("nonexistent"))) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testReadSymlink() { + val dir = Files.createTempDirectory("pathhelper-test") + try { + val target = dir.resolve("target") + Files.createDirectory(target) + val link = dir.resolve("link") + Files.createSymbolicLink(link, target) + + val read = PathHelper.readSymlink(link) + assertNotNull(read) + assertEquals(target, read) + + // Non-symlink returns null + val notLink = PathHelper.readSymlink(target) + assertEquals(null, notLink) + } finally { + deleteRecursive(dir) + } + } + + @Test + fun testNormalizeSafe() { + val dir = Files.createTempDirectory("pathhelper-test") + try { + val normalized = PathHelper.normalizeSafe(dir) + assertNotNull(normalized) + assertFalse(normalized.toString().contains("..")) + } finally { + deleteRecursive(dir) + } + } + +} From e7aab784074a73afa1b01111da315f49064e554f Mon Sep 17 00:00:00 2001 From: Guodong Zhu Date: Mon, 9 Mar 2026 21:52:11 -0400 Subject: [PATCH 2/2] create jetbrains plugin Co-authored-by: Claude Code Ai-assisted: true --- .github/workflows/plugin-build.yml | 25 ++++++++++++++++++ README.md | 1 + .../.gitignore | 0 .../README.md | 0 .../build.gradle.kts | 0 .../gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../gradlew | 0 .../gradlew.bat | 0 .../install-plugin-to-intellij.sh | 0 .../settings.gradle.kts | 0 .../kotlin/com/block/wt/actions/WtAction.kt | 0 .../wt/actions/context/AddContextAction.kt | 0 .../wt/actions/context/DeleteContextAction.kt | 0 .../context/ReprovisionContextAction.kt | 0 .../context/ShowContextConfigAction.kt | 0 .../actions/metadata/ExportMetadataAction.kt | 0 .../actions/metadata/ImportMetadataAction.kt | 0 .../metadata/RefreshBazelTargetsAction.kt | 0 .../wt/actions/util/CopyWorktreePathAction.kt | 0 .../wt/actions/util/OpenInTerminalAction.kt | 0 .../actions/util/RefreshWorktreeListAction.kt | 0 .../wt/actions/util/RevealInFinderAction.kt | 0 .../block/wt/actions/util/WelcomeAction.kt | 0 .../actions/worktree/CreateWorktreeAction.kt | 0 .../worktree/ProvisionWorktreeAction.kt | 0 .../wt/actions/worktree/RemoveMergedAction.kt | 0 .../actions/worktree/RemoveWorktreeAction.kt | 0 .../actions/worktree/SwitchWorktreeAction.kt | 0 .../com/block/wt/agent/AgentDetection.kt | 0 .../com/block/wt/agent/AgentDetector.kt | 0 .../com/block/wt/git/GitBranchHelper.kt | 0 .../com/block/wt/git/GitConfigHelper.kt | 0 .../kotlin/com/block/wt/git/GitDirResolver.kt | 0 .../main/kotlin/com/block/wt/git/GitParser.kt | 0 .../com/block/wt/model/ContextConfig.kt | 0 .../com/block/wt/model/MetadataPattern.kt | 0 .../com/block/wt/model/ProvisionMarker.kt | 0 .../kotlin/com/block/wt/model/WorktreeInfo.kt | 0 .../com/block/wt/progress/ProgressScope.kt | 0 .../com/block/wt/progress/RemovalProgress.kt | 0 .../com/block/wt/provision/ProvisionHelper.kt | 0 .../wt/provision/ProvisionMarkerService.kt | 0 .../wt/provision/ProvisionSwitchHelper.kt | 0 .../com/block/wt/services/BazelService.kt | 0 .../com/block/wt/services/ContextService.kt | 0 .../wt/services/CreateWorktreeUseCase.kt | 0 .../wt/services/ExternalChangeWatcher.kt | 0 .../kotlin/com/block/wt/services/GitClient.kt | 0 .../com/block/wt/services/MetadataService.kt | 0 .../block/wt/services/SymlinkSwitchService.kt | 0 .../com/block/wt/services/WorktreeEnricher.kt | 0 .../wt/services/WorktreeRefreshScheduler.kt | 0 .../com/block/wt/services/WorktreeService.kt | 0 .../block/wt/services/WtStartupActivity.kt | 0 .../com/block/wt/settings/WtPluginSettings.kt | 0 .../block/wt/settings/WtSettingsComponent.kt | 0 .../wt/settings/WtSettingsConfigurable.kt | 0 .../com/block/wt/ui/AddContextDialog.kt | 0 .../com/block/wt/ui/ContextSetupDialog.kt | 0 .../com/block/wt/ui/CreateWorktreeDialog.kt | 0 .../kotlin/com/block/wt/ui/Notifications.kt | 0 .../com/block/wt/ui/WelcomePageHelper.kt | 0 .../kotlin/com/block/wt/ui/WorktreePanel.kt | 0 .../wt/ui/WorktreeStatusBarWidgetFactory.kt | 0 .../com/block/wt/ui/WorktreeTableModel.kt | 0 .../block/wt/ui/WorktreeToolWindowFactory.kt | 0 .../com/block/wt/util/ConfigFileHelper.kt | 0 .../com/block/wt/util/PathExtensions.kt | 0 .../kotlin/com/block/wt/util/PathHelper.kt | 0 .../kotlin/com/block/wt/util/ProcessHelper.kt | 0 .../kotlin/com/block/wt/util/ProcessRunner.kt | 0 .../src/main/resources/META-INF/plugin.xml | 0 .../src/main/resources/icons/worktree.svg | 0 .../src/main/resources/ui.png | Bin .../src/main/resources/welcome.html | 0 .../com/block/wt/git/GitConfigHelperTest.kt | 0 .../com/block/wt/git/GitDirResolverTest.kt | 0 .../com/block/wt/model/WorktreeInfoTest.kt | 0 .../provision/ProvisionMarkerServiceTest.kt | 0 .../wt/services/ExternalChangeWatcherTest.kt | 0 .../wt/services/MetadataServiceStaticTest.kt | 0 .../block/wt/services/MetadataServiceTest.kt | 0 .../block/wt/services/WorktreeServiceTest.kt | 0 .../block/wt/testutil/FakeAgentDetection.kt | 0 .../block/wt/testutil/FakeProcessRunner.kt | 0 .../com/block/wt/testutil/TestFileHelper.kt | 0 .../com/block/wt/util/ConfigFileHelperTest.kt | 0 .../com/block/wt/util/PathHelperTest.kt | 0 89 files changed, 26 insertions(+) create mode 100644 .github/workflows/plugin-build.yml rename {wt-intellij-plugin => wt-jetbrains-plugin}/.gitignore (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/README.md (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/build.gradle.kts (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/gradle.properties (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/gradle/wrapper/gradle-wrapper.properties (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/gradlew (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/gradlew.bat (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/install-plugin-to-intellij.sh (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/settings.gradle.kts (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/WtAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/agent/AgentDetection.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/agent/AgentDetector.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/git/GitDirResolver.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/git/GitParser.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/model/ContextConfig.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/model/MetadataPattern.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/progress/ProgressScope.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/BazelService.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/ContextService.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/GitClient.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/MetadataService.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/WorktreeService.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/Notifications.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/util/PathExtensions.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/util/PathHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/util/ProcessHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/kotlin/com/block/wt/util/ProcessRunner.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/resources/META-INF/plugin.xml (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/resources/icons/worktree.svg (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/resources/ui.png (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/main/resources/welcome.html (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt (100%) rename {wt-intellij-plugin => wt-jetbrains-plugin}/src/test/kotlin/com/block/wt/util/PathHelperTest.kt (100%) diff --git a/.github/workflows/plugin-build.yml b/.github/workflows/plugin-build.yml new file mode 100644 index 0000000..aaf73ba --- /dev/null +++ b/.github/workflows/plugin-build.yml @@ -0,0 +1,25 @@ +name: Plugin Build + +on: + pull_request: + paths: + - 'wt-intellij-plugin/**' + push: + branches: [main] + paths: + - 'wt-intellij-plugin/**' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Build and test + working-directory: wt-intellij-plugin + run: ./gradlew build --no-build-cache diff --git a/README.md b/README.md index 8e600f1..9556e17 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Tests](https://github.com/block/wt/actions/workflows/test.yml/badge.svg)](https://github.com/block/wt/actions/workflows/test.yml) [![ShellCheck](https://github.com/block/wt/actions/workflows/lint.yml/badge.svg)](https://github.com/block/wt/actions/workflows/lint.yml) +[![Plugin Build](https://github.com/block/wt/actions/workflows/plugin-build.yml/badge.svg)](https://github.com/block/wt/actions/workflows/plugin-build.yml) A streamlined workflow for developing in large Bazel + IntelliJ monorepos using Git worktrees. diff --git a/wt-intellij-plugin/.gitignore b/wt-jetbrains-plugin/.gitignore similarity index 100% rename from wt-intellij-plugin/.gitignore rename to wt-jetbrains-plugin/.gitignore diff --git a/wt-intellij-plugin/README.md b/wt-jetbrains-plugin/README.md similarity index 100% rename from wt-intellij-plugin/README.md rename to wt-jetbrains-plugin/README.md diff --git a/wt-intellij-plugin/build.gradle.kts b/wt-jetbrains-plugin/build.gradle.kts similarity index 100% rename from wt-intellij-plugin/build.gradle.kts rename to wt-jetbrains-plugin/build.gradle.kts diff --git a/wt-intellij-plugin/gradle.properties b/wt-jetbrains-plugin/gradle.properties similarity index 100% rename from wt-intellij-plugin/gradle.properties rename to wt-jetbrains-plugin/gradle.properties diff --git a/wt-intellij-plugin/gradle/wrapper/gradle-wrapper.properties b/wt-jetbrains-plugin/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from wt-intellij-plugin/gradle/wrapper/gradle-wrapper.properties rename to wt-jetbrains-plugin/gradle/wrapper/gradle-wrapper.properties diff --git a/wt-intellij-plugin/gradlew b/wt-jetbrains-plugin/gradlew similarity index 100% rename from wt-intellij-plugin/gradlew rename to wt-jetbrains-plugin/gradlew diff --git a/wt-intellij-plugin/gradlew.bat b/wt-jetbrains-plugin/gradlew.bat similarity index 100% rename from wt-intellij-plugin/gradlew.bat rename to wt-jetbrains-plugin/gradlew.bat diff --git a/wt-intellij-plugin/install-plugin-to-intellij.sh b/wt-jetbrains-plugin/install-plugin-to-intellij.sh similarity index 100% rename from wt-intellij-plugin/install-plugin-to-intellij.sh rename to wt-jetbrains-plugin/install-plugin-to-intellij.sh diff --git a/wt-intellij-plugin/settings.gradle.kts b/wt-jetbrains-plugin/settings.gradle.kts similarity index 100% rename from wt-intellij-plugin/settings.gradle.kts rename to wt-jetbrains-plugin/settings.gradle.kts diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/WtAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/WtAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/WtAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/WtAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/context/AddContextAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/context/DeleteContextAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/context/ReprovisionContextAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/context/ShowContextConfigAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/metadata/ExportMetadataAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/metadata/ImportMetadataAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/metadata/RefreshBazelTargetsAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/CopyWorktreePathAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/OpenInTerminalAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/RefreshWorktreeListAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/RevealInFinderAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/util/WelcomeAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/CreateWorktreeAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/ProvisionWorktreeAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveMergedAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/RemoveWorktreeAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/actions/worktree/SwitchWorktreeAction.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetection.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/agent/AgentDetection.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetection.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/agent/AgentDetection.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetector.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/agent/AgentDetector.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/agent/AgentDetector.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/agent/AgentDetector.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/git/GitBranchHelper.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/git/GitConfigHelper.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitDirResolver.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/git/GitDirResolver.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitDirResolver.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/git/GitDirResolver.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitParser.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/git/GitParser.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/git/GitParser.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/git/GitParser.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ContextConfig.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/model/ContextConfig.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ContextConfig.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/model/ContextConfig.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/MetadataPattern.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/model/MetadataPattern.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/model/MetadataPattern.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/model/MetadataPattern.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/model/ProvisionMarker.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/model/WorktreeInfo.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/ProgressScope.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/progress/ProgressScope.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/ProgressScope.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/progress/ProgressScope.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/progress/RemovalProgress.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/provision/ProvisionHelper.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/provision/ProvisionMarkerService.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/provision/ProvisionSwitchHelper.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/BazelService.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/BazelService.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/BazelService.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/BazelService.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ContextService.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/ContextService.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ContextService.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/ContextService.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/CreateWorktreeUseCase.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/ExternalChangeWatcher.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/GitClient.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/GitClient.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/GitClient.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/GitClient.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/MetadataService.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/MetadataService.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/MetadataService.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/MetadataService.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/SymlinkSwitchService.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/WorktreeEnricher.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/WorktreeRefreshScheduler.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeService.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/WorktreeService.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WorktreeService.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/WorktreeService.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/services/WtStartupActivity.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/settings/WtPluginSettings.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsComponent.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/settings/WtSettingsConfigurable.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/AddContextDialog.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/ContextSetupDialog.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/CreateWorktreeDialog.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/Notifications.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/Notifications.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/Notifications.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/Notifications.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WelcomePageHelper.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WorktreePanel.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WorktreeStatusBarWidgetFactory.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WorktreeTableModel.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/ui/WorktreeToolWindowFactory.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/ConfigFileHelper.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathExtensions.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/PathExtensions.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathExtensions.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/PathExtensions.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathHelper.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/PathHelper.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/util/PathHelper.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/PathHelper.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessHelper.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/ProcessHelper.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessHelper.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/ProcessHelper.kt diff --git a/wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessRunner.kt b/wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/ProcessRunner.kt similarity index 100% rename from wt-intellij-plugin/src/main/kotlin/com/block/wt/util/ProcessRunner.kt rename to wt-jetbrains-plugin/src/main/kotlin/com/block/wt/util/ProcessRunner.kt diff --git a/wt-intellij-plugin/src/main/resources/META-INF/plugin.xml b/wt-jetbrains-plugin/src/main/resources/META-INF/plugin.xml similarity index 100% rename from wt-intellij-plugin/src/main/resources/META-INF/plugin.xml rename to wt-jetbrains-plugin/src/main/resources/META-INF/plugin.xml diff --git a/wt-intellij-plugin/src/main/resources/icons/worktree.svg b/wt-jetbrains-plugin/src/main/resources/icons/worktree.svg similarity index 100% rename from wt-intellij-plugin/src/main/resources/icons/worktree.svg rename to wt-jetbrains-plugin/src/main/resources/icons/worktree.svg diff --git a/wt-intellij-plugin/src/main/resources/ui.png b/wt-jetbrains-plugin/src/main/resources/ui.png similarity index 100% rename from wt-intellij-plugin/src/main/resources/ui.png rename to wt-jetbrains-plugin/src/main/resources/ui.png diff --git a/wt-intellij-plugin/src/main/resources/welcome.html b/wt-jetbrains-plugin/src/main/resources/welcome.html similarity index 100% rename from wt-intellij-plugin/src/main/resources/welcome.html rename to wt-jetbrains-plugin/src/main/resources/welcome.html diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/git/GitConfigHelperTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/git/GitDirResolverTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/model/WorktreeInfoTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/provision/ProvisionMarkerServiceTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/services/ExternalChangeWatcherTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceStaticTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/services/MetadataServiceTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/services/WorktreeServiceTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/testutil/FakeAgentDetection.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/testutil/FakeProcessRunner.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/testutil/TestFileHelper.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/util/ConfigFileHelperTest.kt diff --git a/wt-intellij-plugin/src/test/kotlin/com/block/wt/util/PathHelperTest.kt b/wt-jetbrains-plugin/src/test/kotlin/com/block/wt/util/PathHelperTest.kt similarity index 100% rename from wt-intellij-plugin/src/test/kotlin/com/block/wt/util/PathHelperTest.kt rename to wt-jetbrains-plugin/src/test/kotlin/com/block/wt/util/PathHelperTest.kt