Changelog¶
All notable changes to this project will be documented in this file.
This project adheres to Semantic Versioning.
1.6.0 - 2026-10-27¶
Fixed¶
- A failed
switch_tenantannounced the tenant it had failed to reach. Switching from one tenant to another whose switch failed (an unregistered shard, or any connect or verify failure) printed the error, thenTenant initialized: <new tenant>, and labelled the prompt with the new tenant while every backend stayed on the previous one.TenantOrchestratorjudged success byconfiguration_success, which still described the previous tenant, instead of by the result of the switch it had just attempted. It now usesTenantConfigurator.configure_tenant's return value: a failed switch leaves the previous tenant current, configured and verifiable, with no banner. The first selection at boot was never affected, because there is no previous tenant there.
Added¶
- Documentation site at https://soumyadeep-ai.github.io/console_kit/, built with MkDocs Material and deployed by a new
Docsworkflow that fails a pull request on any broken link or anchor. Its pages: - Architecture
- Console boot and tenant selection
- Configuration and validation
- Backends
- Writing a backend handler
- Public API reference
- Development
- Glossary
- A Try it page with an in-browser console that simulates boot, tenant selection and the console helpers
The site includes the README and this changelog, so the two cannot drift. None of it ships in the gem. The gem's RubyGems page links to it through documentation_uri.
Changed¶
- Ruby 3.1 is no longer supported; the minimum is now Ruby 3.2. Ruby 3.1 reached end of life on 2025-03-26 and CI runners no longer provide it. Rails 6.1-8.1 support is unchanged, and the CI matrix is now 24 cells (Rails 6.1-8.1 x Ruby 3.2-4.0).
- README corrections.
- The switch pipeline is
validate -> prepare -> snapshot -> ...;prepareruns before the snapshot. - Releases are a
vX.Y.Ztag push handled by the release workflow, notrake release. - Instrumentation subscribers receive switch, connect, verify and diagnostics events; rollbacks and failures are counted only.
- The code-of-conduct and changelog links now point at
master. They pointed at amainbranch that does not exist.
1.5.1 - 2026-09-24¶
Fixed¶
- The IRB prompt still did not name the tenant. 1.5.0's live prompt label was never installed in a stock Rails console. Rails runs
consoleblocks before it requires IRB, soIRB::Contextwas not yet defined when the label was applied and the step was silently skipped - the prompt stayed as the plain IRB prompt through every switch. It only worked in apps that happened to load IRB earlier, such as throughgem "irb"in the Gemfile. ConsoleKit now loads IRB itself before labelling the prompt (Pry sessions are unaffected). Verified in a real Rails 7.2.1 console: selecting a tenant and runningswitch_tenantnow shows[beta] ... >and then[alpha] ... >.
1.5.0 - 2026-09-24¶
Hardening release. Tenant switching is now transactional: a switch either completes fully or leaves the previous tenant exactly as it was.
Added¶
- Atomic tenant switching.
TenantSwitchrunsvalidate -> snapshot -> prepare -> apply context -> connect -> verify -> commit. The new tenant is not observable as current until the final commit, so a partially applied tenant state cannot survive a failure. - Rollback. Any failure restores every touched component - the context attributes and all four backends - in the reverse of the order they were applied. Each component is attempted even when an earlier one fails, so one broken backend cannot strand the rest.
- Connection identity verification. A successful connection is no longer accepted as proof. SQL compares
current_shard/db_config.name, Mongoid the effective client or database name, Redis the client's cached logical DB, Elasticsearch the effective index prefix. All are local reads with no network round trip; a mismatch raisesConnectionVerificationErrorand fails the switch. - Exception taxonomy.
ConfigurationError,TenantNotFoundError,ConnectionError,ConnectionVerificationError,TenantSwitchError,RollbackError,UnsupportedBackendError, all underConsoleKit::Error.TenantSwitchErrorcarries#original_error,#rollback_failuresand#rollback_succeeded?, so a rollback failure never replaces the root cause. TenantStateandStateStore. All per-thread tenant state lives in one thread-local holding one immutable value object, replacing five independentThread.currentkeys.ConsoleKit.switch_tenant(:acme)- programmatic, raising counterpart to the interactive console flow.ConsoleKit.with_tenant(:acme) { ... }- nested, exception-safe tenant scope that restores the enclosing tenant on exit, including after an exception or a failed inner switch.ConsoleKit.verify_tenant!- re-verify that every available backend still points at the current tenant, and report any backend the switch could not drive.- Handlers declare their own backend.
backend :key, display_name:, context_attribute:, constants_key:, detail_label:plus.target_erroras the single validation rule, called by bothprepareandvalidate!. Adding a backend means adding one file; nothing outside a handler names a backend. Registration is explicit and keyed by backend, so two live reload generations of one backend cannot be represented. - Diagnostic levels.
dashboard(level: :basic)(the default) performs no network calls;level: :fullkeeps the version and health probes. Tenant switching triggers no diagnostics at all. - Diagnostic caching.
:fullrows are cached per thread for a couple of seconds. A row is reused only while the TTL holds, the calling thread is on the same tenant state, and the backend still reports the same observed identity - so another thread moving a process-global backend drops the row rather than serving it stale.:errorrows are never cached. - Instrumentation hook.
ConsoleKit::Instrumentation.subscribe { |name, duration_ms, payload| ... }plus counters for switches, verifications, rollbacks, dropped handlers and diagnostic timeouts. No external dependency. - Strict configuration validation.
ConsoleKit.configuration.validate!reports every problem in one pass: tenant structure, identifiers, duplicates colliding by case or type, required constants, Redis DB numbers, Elasticsearch prefixes, shard and Mongoid client names. Unrecognised keys and acontext_classmissing writers are reported as warnings. -
Isolation reporting.
RedisConnectionHandler#isolation_model/#thread_isolated?and the Elasticsearch equivalents report at runtime whether a backend is genuinely per-thread or process-global. -
BaseConnectionHandler#unavailable_reason. A handler that exists but cannot be driven returns a reason instead of merely answeringavailable? => false, and is recorded as a dropped backend rather than skipped like an optional gem that is not installed. - Uneven backend coverage is reported.
validate!warns when tenants disagree about which backend keys they name, stating that switching to a tenant which omits one resets that backend.
Changed¶
- Connection pool churn removed. The SQL handler uses the native
connecting_to(shard:)path when the target is a registered shard, detected by capability check rather than Rails version, and keeps ConsoleKit to exactly one entry on theconnected_tostack. Theestablish_connectionfallback re-establishes only when the resolved configuration actually changes, and no longer disconnects a pool Rails is about to replace anyway. Switching to the shard already in use performs no pool work. TenantOrchestrator.reapply(and the deprecatedSetup.reapply) no longer re-establishes a connection already on the target shard.- Diagnostics run in the calling thread. A backend's tenant lives in thread-local state, so a check run anywhere else inspects a different tenant than the caller is on.
ContextWrapper#assignreturns a Hash of applied values rather than triples, and owns the case-mismatch warning. It gainedcurrent_valuesandrestore.TenantConfigurator.validate_constants!moved toConsoleKit::TenantPlan.configuration_successis derived fromStateStoreand joined by aconfiguration_success?predicate.- An unknown tenant key raises
TenantNotFoundErrornaming the configured tenant keys, or saying that none are configured. - A successful console switch prints one success line,
Tenant initialized: <key>, instead of two near-identical ones.
Deprecated¶
Still working, each warning once per process; all are removed in 2.0.
ConsoleKit::Setup.setup->ConsoleKit::TenantOrchestrator.runConsoleKit::Setup.current_tenant->ConsoleKit.current_tenantConsoleKit::Setup.current_tenant=->ConsoleKit.switch_tenant, which also switches the connectionsConsoleKit::Setup.tenant_setup_successful?->ConsoleKit.current_tenantConsoleKit::Setup.reapply->ConsoleKit::TenantOrchestrator.reapplyConsoleKit::Setup.reset_current_tenant->ConsoleKit.reset_current_tenantConsoleKit::Setup.auto_select?->ConsoleKit::TenantOrchestrator.auto_select?ConsoleKit.pretty_output/pretty_output=->ConsoleKit.configuration.pretty_output/pretty_output=ConsoleKit.tenants=->ConsoleKit.configuration.tenants=ConsoleKit.context_class/context_class=->ConsoleKit.configuration.context_class/context_class=ConsoleKit.show_dashboard/show_dashboard=->ConsoleKit.configuration.show_dashboard/show_dashboard=ConsoleKit::Configuration#validate->#validate!, which raises instead of returningfalse
Fixed¶
- Diagnostic threads leaked without bound. Every diagnostic call spawned a thread, and a timed-out one was abandoned - deliberately, since 1.3.0 removed
Thread.killto avoid corrupting a connection mid-operation - so each dashboard render could leak another. Diagnostics no longer spawn threads at all, so there is nothing left to leak or to kill. - A failed Mongoid, Redis or Elasticsearch switch reported success. Each handler rescued
NoMethodErrorand printed a warning while the caller carried on, so a tenant switch that had not actually happened looked like one that had. Failures now propagate and roll back, and a client that genuinely cannot support the request is refused before anything is mutated. - Elasticsearch
:fulldiagnostics reportedConnectedfor an unreachable cluster. The ping failure was swallowed andcluster.healthwas called anyway. - A misconfigured
sql_base_classsilently removed SQL from every switch. An unresolvable class name madeavailable?return false, which is indistinguishable from ActiveRecord not being loaded, so SQL was never switched while the switch reported success. A non-default class name that cannot be resolved now warns. - The gem did not require the ActiveSupport core extensions it uses.
Object#tryandTime.currentare called on ordinary paths but were only available if the host application had already loaded more of Rails. -
Errors printed to the console were not scrubbed. A tenant constant can carry a connection URI, so an authentication failure could put a plaintext password into the console and the logs.
-
The prompt named the wrong tenant after a switch. The tenant label was baked into the prompt when it was installed, so
switch_tenant- and choosing no tenant - left the prompt naming the tenant the console started on, whiletenant_inforeported the real one. Under IRB the label never appeared at all without Pry, because Rails'IRB.setupresetsIRB.confafter the railtie installs it. The label is now computed each time the prompt is drawn, under both IRB and Pry, so it always names the current tenant and survives whatever Rails does toIRB.conf. Rails' own prompt and environment colouring are kept, and a%in a tenant name is escaped rather than read by IRB as a format directive. - An interactive switch was not atomic.
switch_tenantfrom the console cleared the current tenant before switching, so a switch that failed left the console on no tenant at all rather than the one it was on, and every switch printed three lines of reset noise and reset SQL once for nothing. It now switches directly, so a failure rolls back to the previous tenant; clearing happens only when you choose no tenant. -
The banner listed backends as
Sql, Mongorather than the names the handlers give themselves. -
Tenant state was fiber-local while the ActiveRecord shard frame was thread-local.
Thread.current[]is fiber-scoped in Ruby, so a second Fiber or an Enumerator on the same thread saw an emptyTenantStatewhile sharing that thread's SQL and context state, and the diagnostics cache could serve a:fullrow cached against a different tenant. Both now use the same thread-level primitive the shard frame uses. - The undo bundle was only shallow-frozen, so a caller could edit a handler snapshot after commit and make a later rollback restore altered state.
- Rollback resolved the context class when it ran rather than retaining the object the switch moved, so replacing or reloading
context_classinside an openwith_tenantscope wrote the saved values to the new object and left the original on the inner tenant. - A configured but unresolvable
sql_base_classwas indistinguishable from an optional gem that is not loaded. SQL was dropped from the switch with no record, so the other backends committed while the SQL pool stayed on the previous tenant andverify_tenant!reported it fully verified. - A fallback SQL rollback could not restore "there was no pool". A base class with no pool before the switch was left holding the tenant pool the failed switch established.
- Two handlers could claim one context attribute, so the context said one thing while the live connection said another. A collision now raises at declaration.
- A malformed tenant entry raised
NoMethodErrororTypeErrorfrom outside the transactional error boundary instead ofConfigurationError.
Security¶
- Credentials are scrubbed from error messages, diagnostic rows and console output: connection URIs,
key=valueandkey => valuefragments,Authorization: Bearer <token>style auth headers, barepassword <value>phrases andfor user <name>principals. Hostnames and ports are deliberately kept - they are not secrets, and removing them would gut the diagnostic value of a connection error. Treat this as defence in depth, not a boundary: it is shape-matching over strings ConsoleKit did not produce. - Elasticsearch cross-thread prefix conflicts are detected.
Elasticsearch::Model.index_name_prefixis process-wide; when live threads hold different prefixes ConsoleKit warns once, naming both, instead of silently letting one thread read another tenant's indices. - Broad
rescue StandardErrorblocks were narrowed. Programming errors (NoMethodError,NameError,ArgumentError,TypeError) are no longer converted into ordinary "dependency unavailable" results.
Performance¶
Measured, not asserted. bundle exec rake benchmark runs entirely against fakes, so it needs no database, Redis, Mongo or Elasticsearch. Counts are facts; timings are indicative.
- Connection pool churn, per switch:
| Path | Pools replaced |
|---|---|
| native shard - same, different, or reset | 0 |
| fallback - same shard | 0 |
| fallback - different shard or reset | 1 |
Before 1.5.0 every switch performed one disconnect! plus one establish_connection unconditionally, including a switch to the shard already in use.
- Queries and health probes per tenant switch: 0, including verification. The only round trip a switch makes is the one that moves the tenant, such as a Redis SELECT. Asserted, not just measured: every handler runs this check in the shared contract, spec/support/shared_examples/connection_handler_contract.rb. dashboard(level: :basic) performs 0 cold and cached; level: :full performs 8 cold and 0 within the cache window.
- Allocations per switch: 195 objects alternating between two tenants, 192 repeating the same tenant.
- The ActiveRecord connected_to stack holds exactly one ConsoleKit entry regardless of switch count or reload! count.
- The 1.3.0 base_class memoization is retained.
level: :fulldiagnostics no longer time out. They previously ran on a thread ConsoleKit could abandon after two seconds. That thread could not see the caller's tenant - a backend's tenant lives in thread-local state - so it inspected a different tenant than the one being reported on, and could check out a second pool connection doing it. Reporting the wrong tenant is worse than reporting slowly, so the check now runs in the calling thread. A backend that hangs holds the dashboard until its own client gives up;Ctrl-Cinterrupts it,:fullis never the default, andtimeout:now counts overruns rather than cutting them off. Configure a timeout on the client to bound it - that is the only layer that can cancel its own call safely.
Breaking changes¶
- redis-rb 5 and redis-client: these expose no process-wide client handle, so a non-default
redis_dbnow raisesUnsupportedBackendErrorinstead of printing a warning and silently running against the wrong DB. Give each tenant its own Redis URL, for exampleredis://redis.internal:6379/<db>. - Elasticsearch prefixes must be Strings or Symbols, and may not contain uppercase characters, whitespace, a leading
_,-or+, or any of\ / * ? " < > | , #. Previously such values were passed through and produced unusable index names. - Blank tenant constants are rejected.
shard: '',mongo_db: ''andredis_db: ''now raiseConfigurationErrorat bothvalidate!and switch time, rather than silently meaning "use the default". - Incomplete tenant entries now fail
validate!. A tenant with no:constants, or missingshard/partner_code, previously validated as fine and only broke at switch time. - A failed backend switch now raises rather than warning.
ConsoleKit.switch_tenantraises; the interactive console flow still reports throughOutputand returnsfalse. Failures found before anything is applied - an unknown tenant, malformed constants, an unsupported client - raiseTenantNotFoundError,ConfigurationErrororUnsupportedBackendErrordirectly, since there is nothing to roll back. - A Mongoid whose overrides cannot be read back is refused at
prepare. Such a switch could not be verified, and its rollback would have cleared the override rather than restoring it.
Internal API notes¶
Not part of the documented public surface, but visible to anyone who reached for them:
TenantConfigurator::CONTEXT_MAPPINGandContextWrapper::HANDLER_ATTRIBUTESare gone; backends declare their own mapping.BaseConnectionHandlergained.backend,#diagnostic_identity, and a keyedHandlerRegistryin place ofClass#descendants.ConnectionManager.available_handlerstakes an optional collector for backends it had to drop, and returns handlers in declaration order.RedisConnectionHandler#isolation_modelcan return:unknownwhen the probe could not observe the client.- New instrumentation counters:
console_kit.handler_dropped,console_kit.handler_collision,console_kit.incomplete_verification,console_kit.sql_frame_reasserted.
Compatibility¶
- CI covers every Ruby × Rails combination the gemspec permits - 28 cells, all of them gating.
railtiesdeclares>= 2.5(6.1),>= 2.7(7.0, 7.1),>= 3.1(7.2) and>= 3.2(8.0, 8.1), none of them declare an upper bound, and this gem requires Ruby>= 3.1.
| Rails | Ruby |
|---|---|
| 6.1 | 3.1, 3.2, 3.3, 3.4, 4.0 |
| 7.0 | 3.1, 3.2, 3.3, 3.4, 4.0 |
| 7.1 | 3.1, 3.2, 3.3, 3.4, 4.0 |
| 7.2 | 3.1, 3.2, 3.3, 3.4, 4.0 |
| 8.0 | 3.2, 3.3, 3.4, 4.0 |
| 8.1 | 3.2, 3.3, 3.4, 4.0 |
- Rails 8.1 and Ruby 4.0 are covered. Rails 8.1 is what the default lockfile resolves to, so the suite had been running against it without saying so. Ruby 4.0 has been the current stable release since July 2026.
required_ruby_versionremains>= 3.1.0and the Rails dependencies remain>= 6.1, both without an upper bound.
Preserved¶
- The 1.3.0 Mongoid named-client fixes -
override_clientfor named clients,override_databasefor database names, and clearing both on reset - are intact and covered by explicit regression tests.
1.4.0 - 2026-06-24¶
- Minor Bug Fixes
1.3.0 - 2026-05-25¶
Added¶
- Mongoid Named Client Support:
MongoConnectionHandlernow detects named Mongoid clients (configured inmongoid.yml) and callsMongoid.override_clientinstead ofMongoid.override_database. This correctly handles multi-tenant setups where each tenant has a separate Mongoid client URI rather than a shared client with a different database name override. - Partner Identifier Case-Mismatch Warning: When ConsoleKit writes a
partner_identifierthat differs from the existing context value only in case (e.g., app set"DAHABDEV_SO", config has"dahabdev_so"), a warning is now printed so misconfigured tenant constants are immediately visible. - Mongoid Reset on Clear:
TenantConfigurator.clearnow resets bothMongoid.override_clientandMongoid.override_databaseto nil, ensuring a clean state regardless of which override path was used.
Fixed¶
- Mongoid Wrong Database Bug: Tenants using named Mongoid clients (separate URIs per tenant) were previously routed to a non-existent database because ConsoleKit called
Mongoid.override_databasewith the client name instead of switching the client. Fixed by detecting named clients and usingMongoid.override_client. - Test Suite Stability: Resolved 3 pre-existing failures in
tenant_orchestrator_spec.rbcaused byauto_select?returning true in non-TTY environments (CI/test), bypassing stubbed tenant selection. Fixed by stubbingauto_select?in affected examples. - Pending Tests Eliminated: 12 pending tests in
output_spec.rbcaused by conditionalskipin shared examples consolidated into a single unconditional assertion per example.
Security¶
safe_constantizein Configuration:Configuration#resolve_context_classnow usessafe_constantize(returns nil on unknown constant) instead ofconstantize(raises on NameError), consistent withSqlConnectionHandler. Removes an inconsistency in constant resolution across the codebase.- Removed
Thread.killon Diagnostic Timeout: Diagnostic threads that exceed the 2-second timeout are no longer forcibly killed viaThread#kill. Forcibly killing a thread mid-operation can leave database connections in a corrupt state. Timed-out threads now finish naturally while the main thread proceeds with a timeout diagnostic result.
Performance¶
base_classMemoization in SQL Handler:SqlConnectionHandler#base_classnow memoizes the resolved constant with@base_class ||=, eliminating repeatedsafe_constantizecalls acrossconnect,diagnostics, anddisconnect_existing_poolwithin the same handler instance.
1.2.0 - 2026-05-13¶
Added¶
- Connection Dashboard: New
dashboardconsole helper displaying a Unicode table with connection status, latency, and service-specific details (adapter, DB version, pool size, memory, cluster health) for all active handlers. show_dashboardConfig Option: Opt-in auto-display of the dashboard on tenant switch (config.show_dashboard = true). Off by default to keep tenant switching fast.- Handler Diagnostics: Each connection handler now exposes a
diagnosticsmethod with status, latency, and details. - Per-Handler Timeout: Diagnostics calls are wrapped in a 2-second timeout to prevent slow services from blocking.
- Rails 6.1+ Support: Lowered minimum Rails version from 7.2.1 to 6.1, enabling use in older Rails applications.
- Pry Prompt Fallback: Graceful fallback for Pry versions < 0.13 that lack
Pry::Prompt.new. - IRB Fallback: Console helpers now work when
IRB::ExtendCommandBundleis not available. - Multi-version CI: Test matrix covering Rails 6.1, 7.0, 7.1, 7.2, and 8.0.
Changed¶
- Lightweight Dependencies: Replaced full
railsgem dependency withrailties,activerecord, andactivesupportonly. - Smaller Gem Package: Excluded
.mdfiles anddocs/from the gem package.
1.1.0 - 2026-03-14¶
Added¶
- Redis Connection Handler: Automatic Redis DB selection per tenant via
Redis.current.select, with graceful fallback for Redis v5+ whereRedis.currentis deprecated. - Elasticsearch Connection Handler: Sets a per-tenant Elasticsearch index name prefix via thread-local storage and
Elasticsearch::Model.index_name_prefix=(when available). - Console Helpers: New
switch_tenant,tenant_info, andtenantsmethods available in the Rails console for quick tenant management. - Custom Console Prompt: IRB and Pry prompts now display the active tenant name (e.g.,
[acme] main:001>). - Tenant Banner: On successful tenant initialization, a banner now shows the tenant name, environment safety warnings (production in red, staging in yellow), and a summary of active connections.
- Environment Safety Warnings: Production and staging environments are flagged with color-coded warnings at tenant setup time.
- New tenant configuration keys:
redis_db,elasticsearch_prefix, andenvironment.
Changed¶
TenantConfiguratornow managestenant_redis_dbandtenant_elasticsearch_prefixcontext attributes alongside existing ones.- Generator template updated with examples for the new configuration keys.
1.0.0 - 2026-03-01¶
Added¶
- Global Configuration Persistence: ConsoleKit settings now persist across the entire session and across multiple threads.
- Isolated Tenant Selection: Each thread maintains its own tenant selection for safety, while sharing the global configuration.
- Seamless Rails Reloading: Full support for Rails
reload!; your selected tenant and context are now automatically preserved after code reloads. - Reliable Tenant Switching: Switching or clearing tenants now correctly resets all database connections (SQL and MongoDB) to their default state.
- Flexible Tenant Selection: Users can now select tenants by typing their names (case-insensitive) in addition to index numbers.
- Session Control: Added support for
exitorquitcommands directly at the selection prompt to terminate the console session. - Safe Mode: Added a "Skip" option (0) to load the console without any tenant configuration.
- Improved Configuration Validation: Enhanced startup checks to provide clearer feedback if the configuration or context class is incorrectly defined.
- Custom SQL Base Class: New configuration option to specify a custom base class for SQL connections.
Changed¶
- Modernized CLI Interface: Redesigned the tenant selection menu and prompts for a cleaner, more intuitive user experience.
- Enhanced Error Feedback: Improved messaging for invalid selections and missing configurations.
- Optimized Performance: Refactored internal discovery and configuration logic for better reliability in large applications.
Fixed¶
- Fixed a bug where tenant context was lost after running
reload!in the Rails console. - Fixed an issue where database connections could remain tied to a previous tenant after the context was cleared.
- Resolved all stability and code quality warnings.
- Fixed timestamp formatting in console output.
0.1.5 - 2025-10-12¶
Added¶
- Minor Bug Fixes
0.1.4 - 2025-09-30¶
Added¶
- Minor Fixes and Improvements
0.1.3 - 2025-08-12¶
Added¶
ConsoleKit.current_tenantmethod to retrieve the current tenant at runtime.ConsoleKit.reset_current_tenantto reset tenant selection.pretty_outputconfiguration added with ability to manually toggle CLI verbosity.
Changed¶
- Refactored internal logic for improved maintainability and future extensibility.
- Enhanced test coverage for better reliability and edge case handling.
0.1.2 - 2025-07-23¶
Added¶
- Changelog added.
- Readme and installation instructions added.
0.1.1 - 2025-07-21¶
Added¶
- Initial generator:
console_kit:installto scaffold configuration. - RSpec test suite to support core features.
Changed¶
- Applied RuboCop fixes for code consistency and style.
0.1.0 - 2025-07-09¶
- Initial release
Added¶
- Core setup logic for ConsoleKit:
ConsoleKit.setup- Tenant selection via CLI.
- Tenant-specific database configuration.
- Colorized console output for improved UX.