ros-apartment 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +93 -2
- data/.ruby-version +1 -1
- data/Appraisals +15 -0
- data/CLAUDE.md +210 -0
- data/README.md +1 -1
- data/Rakefile +6 -5
- data/docs/adapters.md +177 -0
- data/docs/architecture.md +274 -0
- data/docs/elevators.md +226 -0
- data/docs/images/log_example.png +0 -0
- data/lib/apartment/CLAUDE.md +300 -0
- data/lib/apartment/adapters/CLAUDE.md +314 -0
- data/lib/apartment/adapters/abstract_adapter.rb +24 -15
- data/lib/apartment/adapters/jdbc_mysql_adapter.rb +1 -1
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
- data/lib/apartment/adapters/mysql2_adapter.rb +2 -2
- data/lib/apartment/adapters/postgresql_adapter.rb +42 -19
- data/lib/apartment/adapters/sqlite3_adapter.rb +7 -7
- data/lib/apartment/console.rb +1 -1
- data/lib/apartment/custom_console.rb +7 -7
- data/lib/apartment/elevators/CLAUDE.md +292 -0
- data/lib/apartment/elevators/domain.rb +1 -1
- data/lib/apartment/elevators/generic.rb +1 -1
- data/lib/apartment/elevators/host_hash.rb +3 -3
- data/lib/apartment/elevators/subdomain.rb +9 -5
- data/lib/apartment/log_subscriber.rb +1 -1
- data/lib/apartment/migrator.rb +2 -2
- data/lib/apartment/model.rb +1 -1
- data/lib/apartment/railtie.rb +3 -3
- data/lib/apartment/tasks/enhancements.rb +1 -1
- data/lib/apartment/tasks/task_helper.rb +4 -4
- data/lib/apartment/tenant.rb +3 -3
- data/lib/apartment/version.rb +1 -1
- data/lib/apartment.rb +15 -9
- data/lib/generators/apartment/install/install_generator.rb +1 -1
- data/lib/generators/apartment/install/templates/apartment.rb +2 -2
- data/lib/tasks/apartment.rake +25 -25
- data/ros-apartment.gemspec +3 -3
- metadata +22 -11
|
@@ -5,6 +5,7 @@ module Apartment
|
|
|
5
5
|
# Abstract adapter from which all the Apartment DB related adapters will inherit the base logic
|
|
6
6
|
class AbstractAdapter
|
|
7
7
|
include ActiveSupport::Callbacks
|
|
8
|
+
|
|
8
9
|
define_callbacks :create, :switch
|
|
9
10
|
|
|
10
11
|
attr_writer :default_tenant
|
|
@@ -21,7 +22,7 @@ module Apartment
|
|
|
21
22
|
# @param {String} tenant Tenant name
|
|
22
23
|
#
|
|
23
24
|
def create(tenant)
|
|
24
|
-
run_callbacks
|
|
25
|
+
run_callbacks(:create) do
|
|
25
26
|
create_tenant(tenant)
|
|
26
27
|
|
|
27
28
|
switch(tenant) do
|
|
@@ -72,7 +73,7 @@ module Apartment
|
|
|
72
73
|
# @param {String} tenant name
|
|
73
74
|
#
|
|
74
75
|
def switch!(tenant = nil)
|
|
75
|
-
run_callbacks
|
|
76
|
+
run_callbacks(:switch) do
|
|
76
77
|
connect_to_new(tenant).tap do
|
|
77
78
|
Apartment.connection.clear_query_cache
|
|
78
79
|
end
|
|
@@ -88,9 +89,11 @@ module Apartment
|
|
|
88
89
|
switch!(tenant)
|
|
89
90
|
yield
|
|
90
91
|
ensure
|
|
92
|
+
# Always attempt rollback to previous tenant, even if block raised
|
|
91
93
|
begin
|
|
92
94
|
switch!(previous_tenant)
|
|
93
95
|
rescue StandardError => _e
|
|
96
|
+
# If rollback fails (tenant was dropped, connection lost), fall back to default
|
|
94
97
|
reset
|
|
95
98
|
end
|
|
96
99
|
end
|
|
@@ -99,7 +102,7 @@ module Apartment
|
|
|
99
102
|
#
|
|
100
103
|
def each(tenants = Apartment.tenant_names)
|
|
101
104
|
tenants.each do |tenant|
|
|
102
|
-
switch(tenant) { yield
|
|
105
|
+
switch(tenant) { yield(tenant) }
|
|
103
106
|
end
|
|
104
107
|
end
|
|
105
108
|
|
|
@@ -116,7 +119,7 @@ module Apartment
|
|
|
116
119
|
# Reset the tenant connection to the default
|
|
117
120
|
#
|
|
118
121
|
def reset
|
|
119
|
-
Apartment.establish_connection
|
|
122
|
+
Apartment.establish_connection(@config)
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
# Load the rails seed file into the db
|
|
@@ -147,7 +150,7 @@ module Apartment
|
|
|
147
150
|
protected
|
|
148
151
|
|
|
149
152
|
def process_excluded_model(excluded_model)
|
|
150
|
-
excluded_model.constantize.establish_connection
|
|
153
|
+
excluded_model.constantize.establish_connection(@config)
|
|
151
154
|
end
|
|
152
155
|
|
|
153
156
|
def drop_command(conn, tenant)
|
|
@@ -178,11 +181,14 @@ module Apartment
|
|
|
178
181
|
def connect_to_new(tenant)
|
|
179
182
|
return reset if tenant.nil?
|
|
180
183
|
|
|
184
|
+
# Preserve query cache state across tenant switches
|
|
185
|
+
# Rails disables it during connection establishment
|
|
181
186
|
query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled
|
|
182
187
|
|
|
183
|
-
Apartment.establish_connection
|
|
184
|
-
Apartment.connection.verify! #
|
|
188
|
+
Apartment.establish_connection(multi_tenantify(tenant))
|
|
189
|
+
Apartment.connection.verify! # Explicitly validate connection is live
|
|
185
190
|
|
|
191
|
+
# Restore query cache if it was previously enabled
|
|
186
192
|
Apartment.connection.enable_query_cache! if query_cache_enabled
|
|
187
193
|
rescue *rescuable_exceptions => e
|
|
188
194
|
Apartment::Tenant.reset if reset_on_connection_exception?
|
|
@@ -216,7 +222,7 @@ module Apartment
|
|
|
216
222
|
# Load a file or raise error if it doesn't exists
|
|
217
223
|
#
|
|
218
224
|
def load_or_raise(file)
|
|
219
|
-
raise
|
|
225
|
+
raise(FileNotFound, "#{file} doesn't exist yet") unless File.exist?(file)
|
|
220
226
|
|
|
221
227
|
load(file)
|
|
222
228
|
end
|
|
@@ -239,15 +245,16 @@ module Apartment
|
|
|
239
245
|
Apartment.db_config_for(tenant).dup
|
|
240
246
|
end
|
|
241
247
|
|
|
242
|
-
def with_neutral_connection(tenant, &
|
|
248
|
+
def with_neutral_connection(tenant, &)
|
|
243
249
|
if Apartment.with_multi_server_setup
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
#
|
|
250
|
+
# Multi-server setup requires separate connection handler to avoid polluting
|
|
251
|
+
# the main connection pool. For example: connecting to postgres 'template1'
|
|
252
|
+
# database to CREATE/DROP tenant databases without affecting app connections.
|
|
247
253
|
SeparateDbConnectionHandler.establish_connection(multi_tenantify(tenant, false))
|
|
248
254
|
yield(SeparateDbConnectionHandler.connection)
|
|
249
255
|
SeparateDbConnectionHandler.connection.close
|
|
250
256
|
else
|
|
257
|
+
# Single-server: reuse existing connection (safe for most operations)
|
|
251
258
|
yield(Apartment.connection)
|
|
252
259
|
end
|
|
253
260
|
end
|
|
@@ -257,17 +264,19 @@ module Apartment
|
|
|
257
264
|
end
|
|
258
265
|
|
|
259
266
|
def raise_drop_tenant_error!(tenant, exception)
|
|
260
|
-
raise
|
|
267
|
+
raise(TenantNotFound, "Error while dropping tenant #{environmentify(tenant)}: #{exception.message}")
|
|
261
268
|
end
|
|
262
269
|
|
|
263
270
|
def raise_create_tenant_error!(tenant, exception)
|
|
264
|
-
raise
|
|
271
|
+
raise(TenantExists, "Error while creating tenant #{environmentify(tenant)}: #{exception.message}")
|
|
265
272
|
end
|
|
266
273
|
|
|
267
274
|
def raise_connect_error!(tenant, exception)
|
|
268
|
-
raise
|
|
275
|
+
raise(TenantNotFound, "Error while connecting to tenant #{environmentify(tenant)}: #{exception.message}")
|
|
269
276
|
end
|
|
270
277
|
|
|
278
|
+
# Dedicated AR connection class for neutral connections (admin operations like CREATE/DROP DATABASE).
|
|
279
|
+
# Prevents admin commands from polluting the main application connection pool.
|
|
271
280
|
class SeparateDbConnectionHandler < ::ActiveRecord::Base
|
|
272
281
|
end
|
|
273
282
|
end
|
|
@@ -38,12 +38,12 @@ module Apartment
|
|
|
38
38
|
#
|
|
39
39
|
def connect_to_new(tenant = nil)
|
|
40
40
|
return reset if tenant.nil?
|
|
41
|
-
raise
|
|
41
|
+
raise(ActiveRecord::StatementInvalid, "Could not find schema #{tenant}") unless schema_exists?(tenant)
|
|
42
42
|
|
|
43
43
|
@current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
|
|
44
44
|
Apartment.connection.schema_search_path = full_search_path
|
|
45
45
|
rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError
|
|
46
|
-
raise
|
|
46
|
+
raise(TenantNotFound, "One of the following schema(s) is invalid: #{full_search_path}")
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
private
|
|
@@ -51,7 +51,7 @@ module Apartment
|
|
|
51
51
|
def tenant_exists?(tenant)
|
|
52
52
|
return true unless Apartment.tenant_presence_check
|
|
53
53
|
|
|
54
|
-
Apartment.connection.all_schemas.include?
|
|
54
|
+
Apartment.connection.all_schemas.include?(tenant)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def rescue_from
|
|
@@ -44,7 +44,7 @@ module Apartment
|
|
|
44
44
|
def reset
|
|
45
45
|
return unless default_tenant
|
|
46
46
|
|
|
47
|
-
Apartment.connection.execute
|
|
47
|
+
Apartment.connection.execute("use `#{default_tenant}`")
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
protected
|
|
@@ -54,7 +54,7 @@ module Apartment
|
|
|
54
54
|
def connect_to_new(tenant)
|
|
55
55
|
return reset if tenant.nil?
|
|
56
56
|
|
|
57
|
-
Apartment.connection.execute
|
|
57
|
+
Apartment.connection.execute("use `#{environmentify(tenant)}`")
|
|
58
58
|
rescue ActiveRecord::StatementInvalid => e
|
|
59
59
|
Apartment::Tenant.reset
|
|
60
60
|
raise_connect_error!(tenant, e)
|
|
@@ -57,7 +57,8 @@ module Apartment
|
|
|
57
57
|
|
|
58
58
|
def process_excluded_model(excluded_model)
|
|
59
59
|
excluded_model.constantize.tap do |klass|
|
|
60
|
-
#
|
|
60
|
+
# Strip any existing schema qualifier (handles "schema.table" → "table")
|
|
61
|
+
# Then explicitly set to default schema to prevent tenant-based queries
|
|
61
62
|
table_name = klass.table_name.split('.', 2).last
|
|
62
63
|
|
|
63
64
|
klass.table_name = "#{default_tenant}.#{table_name}"
|
|
@@ -72,7 +73,7 @@ module Apartment
|
|
|
72
73
|
#
|
|
73
74
|
def connect_to_new(tenant = nil)
|
|
74
75
|
return reset if tenant.nil?
|
|
75
|
-
raise
|
|
76
|
+
raise(ActiveRecord::StatementInvalid, "Could not find schema #{tenant}") unless schema_exists?(tenant)
|
|
76
77
|
|
|
77
78
|
@current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
|
|
78
79
|
Apartment.connection.schema_search_path = full_search_path
|
|
@@ -89,7 +90,8 @@ module Apartment
|
|
|
89
90
|
end
|
|
90
91
|
|
|
91
92
|
def create_tenant_command(conn, tenant)
|
|
92
|
-
#
|
|
93
|
+
# Avoid nested transactions: if already in transaction (e.g., RSpec tests),
|
|
94
|
+
# execute directly. Otherwise, wrap in explicit transaction for atomicity.
|
|
93
95
|
if ActiveRecord::Base.connection.open_transactions.positive?
|
|
94
96
|
conn.execute(%(CREATE SCHEMA "#{tenant}"))
|
|
95
97
|
else
|
|
@@ -101,7 +103,7 @@ module Apartment
|
|
|
101
103
|
end
|
|
102
104
|
rescue *rescuable_exceptions => e
|
|
103
105
|
rollback_transaction(conn)
|
|
104
|
-
raise
|
|
106
|
+
raise(e)
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
def rollback_transaction(conn)
|
|
@@ -131,7 +133,7 @@ module Apartment
|
|
|
131
133
|
end
|
|
132
134
|
|
|
133
135
|
def raise_schema_connect_to_new(tenant, exception)
|
|
134
|
-
raise
|
|
136
|
+
raise(TenantNotFound, <<~EXCEPTION_MESSAGE)
|
|
135
137
|
Could not set search path to schemas, they may be invalid: "#{tenant}" #{full_search_path}.
|
|
136
138
|
Original error: #{exception.class}: #{exception}
|
|
137
139
|
EXCEPTION_MESSAGE
|
|
@@ -152,6 +154,26 @@ module Apartment
|
|
|
152
154
|
|
|
153
155
|
].freeze
|
|
154
156
|
|
|
157
|
+
# PostgreSQL meta-commands (backslash commands) that appear in pg_dump output
|
|
158
|
+
# but are not valid SQL when passed to ActiveRecord's execute().
|
|
159
|
+
# These must be filtered out to prevent syntax errors during schema import.
|
|
160
|
+
PSQL_META_COMMANDS = [
|
|
161
|
+
/^\\connect/i,
|
|
162
|
+
/^\\set/i,
|
|
163
|
+
/^\\unset/i,
|
|
164
|
+
/^\\copyright/i,
|
|
165
|
+
/^\\echo/i,
|
|
166
|
+
/^\\warn/i,
|
|
167
|
+
/^\\o/i,
|
|
168
|
+
/^\\t/i,
|
|
169
|
+
/^\\q/i,
|
|
170
|
+
/^\\./i, # Catch-all for any backslash command (e.g., \. for COPY delimiter,
|
|
171
|
+
# \restrict/\unrestrict in PostgreSQL 17.6+, and future meta-commands)
|
|
172
|
+
].freeze
|
|
173
|
+
|
|
174
|
+
# Combined blacklist: SQL statements and psql meta-commands to filter from pg_dump output
|
|
175
|
+
PSQL_DUMP_GLOBAL_BLACKLIST = (PSQL_DUMP_BLACKLISTED_STATEMENTS + PSQL_META_COMMANDS).freeze
|
|
176
|
+
|
|
155
177
|
def import_database_schema
|
|
156
178
|
preserving_search_path do
|
|
157
179
|
clone_pg_schema
|
|
@@ -161,9 +183,9 @@ module Apartment
|
|
|
161
183
|
|
|
162
184
|
private
|
|
163
185
|
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
186
|
+
# PostgreSQL's pg_dump clears search_path in the dump output, which would
|
|
187
|
+
# leave us with an empty path after import. Capture current path, execute
|
|
188
|
+
# import, then restore it to maintain tenant context.
|
|
167
189
|
#
|
|
168
190
|
def preserving_search_path
|
|
169
191
|
search_path = Apartment.connection.execute('show search_path').first['search_path']
|
|
@@ -209,13 +231,14 @@ module Apartment
|
|
|
209
231
|
end
|
|
210
232
|
# rubocop:enable Layout/LineLength
|
|
211
233
|
|
|
212
|
-
#
|
|
213
|
-
#
|
|
234
|
+
# Temporarily set PostgreSQL environment variables for pg_dump shell commands.
|
|
235
|
+
# Must preserve and restore existing ENV values to avoid polluting global state.
|
|
236
|
+
# pg_dump reads these instead of passing connection params as CLI args.
|
|
214
237
|
def with_pg_env
|
|
215
|
-
pghost = ENV
|
|
216
|
-
pgport = ENV
|
|
217
|
-
pguser = ENV
|
|
218
|
-
pgpassword = ENV
|
|
238
|
+
pghost = ENV.fetch('PGHOST', nil)
|
|
239
|
+
pgport = ENV.fetch('PGPORT', nil)
|
|
240
|
+
pguser = ENV.fetch('PGUSER', nil)
|
|
241
|
+
pgpassword = ENV.fetch('PGPASSWORD', nil)
|
|
219
242
|
|
|
220
243
|
ENV['PGHOST'] = @config[:host] if @config[:host]
|
|
221
244
|
ENV['PGPORT'] = @config[:port].to_s if @config[:port]
|
|
@@ -224,6 +247,7 @@ module Apartment
|
|
|
224
247
|
|
|
225
248
|
yield
|
|
226
249
|
ensure
|
|
250
|
+
# Always restore original ENV state (might be nil)
|
|
227
251
|
ENV['PGHOST'] = pghost
|
|
228
252
|
ENV['PGPORT'] = pgport
|
|
229
253
|
ENV['PGUSER'] = pguser
|
|
@@ -239,16 +263,15 @@ module Apartment
|
|
|
239
263
|
|
|
240
264
|
swap_schema_qualifier(sql)
|
|
241
265
|
.split("\n")
|
|
242
|
-
.
|
|
266
|
+
.grep_v(Regexp.union(PSQL_DUMP_GLOBAL_BLACKLIST))
|
|
243
267
|
.prepend(search_path)
|
|
244
268
|
.join("\n")
|
|
245
269
|
end
|
|
246
270
|
|
|
247
271
|
def swap_schema_qualifier(sql)
|
|
248
272
|
sql.gsub(/#{default_tenant}\.\w*/) do |match|
|
|
249
|
-
if Apartment.pg_excluded_names.any? { |name| match.include?
|
|
250
|
-
|
|
251
|
-
elsif Apartment.pg_exclude_clone_tables && excluded_tables.any?(match)
|
|
273
|
+
if Apartment.pg_excluded_names.any? { |name| match.include?(name) } ||
|
|
274
|
+
(Apartment.pg_exclude_clone_tables && excluded_tables.any?(match))
|
|
252
275
|
match
|
|
253
276
|
else
|
|
254
277
|
match.gsub("#{default_tenant}.", %("#{current}".))
|
|
@@ -259,7 +282,7 @@ module Apartment
|
|
|
259
282
|
# Checks if any of regexps matches against input
|
|
260
283
|
#
|
|
261
284
|
def check_input_against_regexps(input, regexps)
|
|
262
|
-
regexps.select { |c| input.match
|
|
285
|
+
regexps.select { |c| input.match(c) }
|
|
263
286
|
end
|
|
264
287
|
|
|
265
288
|
# Convenience method for excluded table names
|
|
@@ -19,8 +19,8 @@ module Apartment
|
|
|
19
19
|
|
|
20
20
|
def drop(tenant)
|
|
21
21
|
unless File.exist?(database_file(tenant))
|
|
22
|
-
raise
|
|
23
|
-
"The tenant #{environmentify(tenant)} cannot be found."
|
|
22
|
+
raise(TenantNotFound,
|
|
23
|
+
"The tenant #{environmentify(tenant)} cannot be found.")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
File.delete(database_file(tenant))
|
|
@@ -36,17 +36,17 @@ module Apartment
|
|
|
36
36
|
return reset if tenant.nil?
|
|
37
37
|
|
|
38
38
|
unless File.exist?(database_file(tenant))
|
|
39
|
-
raise
|
|
40
|
-
"The tenant #{environmentify(tenant)} cannot be found."
|
|
39
|
+
raise(TenantNotFound,
|
|
40
|
+
"The tenant #{environmentify(tenant)} cannot be found.")
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
super
|
|
43
|
+
super(database_file(tenant))
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def create_tenant(tenant)
|
|
47
47
|
if File.exist?(database_file(tenant))
|
|
48
|
-
raise
|
|
49
|
-
"The tenant #{environmentify(tenant)} already exists."
|
|
48
|
+
raise(TenantExists,
|
|
49
|
+
"The tenant #{environmentify(tenant)} already exists.")
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
begin
|
data/lib/apartment/console.rb
CHANGED
|
@@ -4,7 +4,7 @@ def st(schema_name = nil)
|
|
|
4
4
|
if schema_name.nil?
|
|
5
5
|
tenant_list.each { |t| puts t }
|
|
6
6
|
|
|
7
|
-
elsif tenant_list.include?
|
|
7
|
+
elsif tenant_list.include?(schema_name)
|
|
8
8
|
Apartment::Tenant.switch!(schema_name)
|
|
9
9
|
else
|
|
10
10
|
puts "Tenant #{schema_name} is not part of the tenant list"
|
|
@@ -5,7 +5,7 @@ require_relative 'console'
|
|
|
5
5
|
module Apartment
|
|
6
6
|
module CustomConsole
|
|
7
7
|
begin
|
|
8
|
-
require
|
|
8
|
+
require('pry-rails')
|
|
9
9
|
rescue LoadError
|
|
10
10
|
# rubocop:disable Layout/LineLength
|
|
11
11
|
puts '[Failed to load pry-rails] If you want to use Apartment custom prompt you need to add pry-rails to your gemfile'
|
|
@@ -13,17 +13,17 @@ module Apartment
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
desc = "Includes the current Rails environment and project folder name.\n" \
|
|
16
|
-
|
|
16
|
+
'[1] [project_name][Rails.env][Apartment::Tenant.current] pry(main)>'
|
|
17
17
|
|
|
18
18
|
prompt_procs = [
|
|
19
19
|
proc { |target_self, nest_level, pry| prompt_contents(pry, target_self, nest_level, '>') },
|
|
20
|
-
proc { |target_self, nest_level, pry| prompt_contents(pry, target_self, nest_level, '*') }
|
|
20
|
+
proc { |target_self, nest_level, pry| prompt_contents(pry, target_self, nest_level, '*') },
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
if Gem::Version.new(Pry::VERSION) >= Gem::Version.new('0.13')
|
|
24
|
-
Pry.config.prompt = Pry::Prompt.new
|
|
24
|
+
Pry.config.prompt = Pry::Prompt.new('ros', desc, prompt_procs)
|
|
25
25
|
else
|
|
26
|
-
Pry::Prompt.add
|
|
26
|
+
Pry::Prompt.add('ros', desc, %w[> *]) do |target_self, nest_level, pry, sep|
|
|
27
27
|
prompt_contents(pry, target_self, nest_level, sep)
|
|
28
28
|
end
|
|
29
29
|
Pry.config.prompt = Pry::Prompt[:ros][:value]
|
|
@@ -35,8 +35,8 @@ module Apartment
|
|
|
35
35
|
|
|
36
36
|
def self.prompt_contents(pry, target_self, nest_level, sep)
|
|
37
37
|
"[#{pry.input_ring.size}] [#{PryRails::Prompt.formatted_env}][#{Apartment::Tenant.current}] " \
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
"#{pry.config.prompt_name}(#{Pry.view_clip(target_self)})" \
|
|
39
|
+
"#{":#{nest_level}" unless nest_level.zero?}#{sep} "
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|