railbow 0.1.0 โ†’ 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 783c8ab9f75f889cd1c54cd36dbc258e06b411b9ae5132af6815564f83d73312
4
- data.tar.gz: 1200e9edbd432786044d0ac0992773de513390e787500eecd48dc745a473da4e
3
+ metadata.gz: c42f0f6d8162662344a6ef8fb12388e8dd6a2a579384cab3829a85d806cb1571
4
+ data.tar.gz: 064fb908f8cd7567e9d8be53800e6972aa48faf65348aff7338c6088aa7427dd
5
5
  SHA512:
6
- metadata.gz: be4c900efd96d11e1d896eb0facd145a928fc93b2b08d30b02cf14a3638e0cf5f133f0c929d037cf828cb3862387ad63725cbbc1cdad4fb84afa353b77ef32fe
7
- data.tar.gz: 5231639b5beaefce4ee54e9d0e81b0309d8370b66e4cb7ca9a0e496ad64d0bebcd8b8a69458fe056789947a216e6b6cbdd3c8ad0fb442ddfebf38d25cbf196b8
6
+ metadata.gz: f7af392359a26db27919f6bac5d31ce22d6736615bbbe8d514c794e5114a2a71ff61081160340a3177fd4b172207b8c07ae2948fcfdaeaccbc0498b28370fb1c
7
+ data.tar.gz: e5a9364c62c93fa530cbde3222c2249b3f251043514fda4d4cf51a33cfaf27ad54bffed5d5abae43befe065634faee0d96ec60581adc22004cd45790e7f87534
data/CHANGELOG.md CHANGED
@@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2026-07-23
9
+
10
+ ### Added
11
+
12
+ - Ghost rows in `db:migrate:status` now surface mighost 0.5's classification:
13
+ a superseded ghost (re-timestamped migration) shows a calmer ๐Ÿชฆ status with
14
+ a `โ‰ก <version>` badge pointing at its successor, and a ghost deleted with no
15
+ surviving branch shows `โœ‚ deleted in:<sha>`. Branch badges are unchanged and
16
+ take precedence right after supersession.
17
+ - `RBW_FORCE=1` forces Railbow formatting past every auto-detection
18
+ (`NO_COLOR`, CI, piped output, LLM agent detection). An explicit
19
+ `RBW_PLAIN=1` still wins.
20
+
21
+ ### Changed
22
+
23
+ - Ghost data now comes from `Mighost::API.orphaned_migrations`, so
24
+ `db:migrate:status` honors mighost dismissals and `hide_superseded` for the
25
+ first time: a dismissed or hidden ghost renders as plain `NO FILE` instead
26
+ of ๐Ÿ‘ป. Live git/worktree recovery still kicks in for versions without a
27
+ stored snapshot, so fresh clones keep working with zero setup.
28
+
29
+ ### Fixed
30
+
31
+ - A ghost row without a branch no longer inherits the previous ghost row's
32
+ branch badge (a stale local leaked across loop iterations; unreachable
33
+ before mighost 0.5 made branchless ghosts the normal case).
34
+
35
+ ## [0.2.0] - 2026-07-23
36
+
37
+ ### Added
38
+
39
+ - Affected-table detection now recognizes tables passed via keyword
40
+ arguments (`table:`, `to_table:`, `from_table:`), covering project-level
41
+ migration helpers and DSLs that the ActiveRecord method allow-list missed.
42
+ - `change_column_default` and `change_column_null` are recognized, and
43
+ foreign-key methods contribute their first table even when the target is
44
+ given as `to_table:`.
45
+
46
+ ### Fixed
47
+
48
+ - Ruby comments no longer produce phantom tables: content is stripped of
49
+ `#` comments (preserving `#{}` interpolation) before scanning, so a
50
+ comment mentioning `ALTER TABLE` or a commented-out `create_table` does
51
+ not count.
52
+
53
+ ### Changed
54
+
55
+ - Ghost recovery integration targets [mighost](https://github.com/amberpixels/mighost)
56
+ 0.5, which classifies superseded ghosts and reports accurate branch
57
+ attribution.
58
+
8
59
  ## [0.1.0] - 2026-07-08
9
60
 
10
61
  First public release.
data/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  - **About** - polished `rails about` output
23
23
  - **Git Integration** - authors, diffs, branch origin, landing dates, uncommitted file indicators
24
24
  - **Calendar View** - month separators and week tick markers for migration timelines
25
- - **Ghost Recovery** - pairs with the optional [mighost](https://github.com/amberpixels/mighost) gem to recover names, authors, and branch badges for `NO FILE` migrations
25
+ - **Ghost Recovery** - uses the [mighost](https://github.com/amberpixels/mighost) gem (optional) to recover names, authors, and branch badges for `NO FILE` migrations
26
26
  - **Smart Defaults** - auto-disables in CI, piped output, `NO_COLOR`, and LLM agents
27
27
 
28
28
  ## Installation
@@ -97,16 +97,16 @@ becomes a rich, information-dense dashboard (with default config):
97
97
 
98
98
  Out of the box you get:
99
99
 
100
- - **Calendar separators** โ€” month + ISO week headers to orient you in time
101
- - **Status aliases** โ€” `โ†‘โ†‘` / `โ†“โ†“` instead of `up` / `down` (customizable)
102
- - **Created At** โ€” timestamp parsed from the migration ID
103
- - **Landing dates** โ€” `โคป Mar 04` badge when a migration was merged to main after its creation
104
- - **Branch badges** โ€” `โއ PS-142` showing the source branch/ticket
105
- - **Affected tables** โ€” color-coded table names extracted from migration files
106
- - **Time filtering** โ€” only the last 70 days shown by default (`since: 70d`)
107
- - **Your migrations highlighted** โ€” rows authored by you are visually distinct
100
+ - **Calendar separators** - month + ISO week headers to orient you in time
101
+ - **Status aliases** - `โ†‘โ†‘` / `โ†“โ†“` instead of `up` / `down` (customizable)
102
+ - **Created At** - timestamp parsed from the migration ID
103
+ - **Landing dates** - `โคป Mar 04` badge when a migration was merged to main after its creation
104
+ - **Branch badges** - `โއ PS-142` showing the source branch/ticket
105
+ - **Affected tables** - color-coded table names extracted from migration files
106
+ - **Time filtering** - only the last 70 days shown by default (`since: 70d`)
107
+ - **Your migrations highlighted** - rows authored by you are visually distinct
108
108
 
109
- If the [mighost](https://github.com/amberpixels/mighost) gem is installed, migrations whose files were deleted (e.g. after switching branches) show up with a ๐Ÿ‘ป status and their recovered name and origin branch instead of a bare `********** NO FILE **********` row.
109
+ If the [mighost](https://github.com/amberpixels/mighost) gem is installed (`gem "mighost", group: [:development, :test]`), migrations whose files were deleted (e.g. after switching branches) show up with a ๐Ÿ‘ป status and their recovered name instead of a bare `********** NO FILE **********` row - Railbow talks to it through the stable `Mighost::API`. Each ghost row carries the most informative badge mighost can provide: `โ‰ก <version>` when the migration was re-timestamped and lives on under another version (shown with a calmer ๐Ÿชฆ status), `โŒฅ <branch>` when a branch still holds the file, or `โœ‚ deleted in:<sha>` pointing at the commit that removed it. Ghosts dismissed via `mighost:dismiss` (or hidden by `hide_superseded`) render as plain `NO FILE`.
110
110
 
111
111
  ### `rails db:migrate:down`
112
112
 
@@ -126,7 +126,7 @@ Code statistics rendered as a colorful table with highlighted totals and code-to
126
126
 
127
127
  ### `rails notes`
128
128
 
129
- Annotations enriched with git blame data โ€” author names, commit dates, and color-coded tags (TODO=yellow, FIXME=red, OPTIMIZE=cyan, HACK=red, NOTE=green).
129
+ Annotations enriched with git blame data - author names, commit dates, and color-coded tags (TODO=yellow, FIXME=red, OPTIMIZE=cyan, HACK=red, NOTE=green).
130
130
 
131
131
  ## Configuration
132
132
 
@@ -176,6 +176,7 @@ Every option can also be set via `RBW_*` environment variables, which override c
176
176
  | Variable | Example | Description |
177
177
  |---|---|---|
178
178
  | `RBW_PLAIN` | `1` | Disable all formatting |
179
+ | `RBW_FORCE` | `1` | Force formatting even when piped, in CI, or run by an LLM agent (`RBW_PLAIN=1` still wins) |
179
180
  | `RBW_SINCE` | `2mo`, `70d`, `1y`, `all` | Filter migrations by time period |
180
181
  | `RBW_DATE` | `full`, `rel`, `short`, `custom(%b %d)` | Date display format |
181
182
  | `RBW_GIT` | `author:me,diff,mask:auto` | Git integration options |
@@ -207,7 +208,7 @@ RBW_SORT=date rails notes
207
208
 
208
209
  ## How It Works
209
210
 
210
- Railbow integrates through a Rails Railtie โ€” it prepends formatter modules onto existing Rails classes without modifying your code:
211
+ Railbow integrates through a Rails Railtie - it prepends formatter modules onto existing Rails classes without modifying your code:
211
212
 
212
213
  - `ActiveRecord::Migration` - migration output
213
214
  - `ActiveRecord::Tasks::DatabaseTasks` - migration status
@@ -221,6 +222,10 @@ Formatting auto-disables when:
221
222
  - Output is piped or redirected (non-TTY)
222
223
  - Running inside an LLM agent (`CLAUDECODE` env var)
223
224
 
225
+ `RBW_FORCE=1` overrides all of the auto-detection above (useful for capturing
226
+ formatted output to a file, or letting an agent inspect the real rendering).
227
+ An explicit `RBW_PLAIN=1` always wins over `RBW_FORCE`.
228
+
224
229
  ## Requirements
225
230
 
226
231
  - Ruby >= 3.1.0
data/lib/railbow/init.rb CHANGED
@@ -26,16 +26,16 @@ module Railbow
26
26
  # sort: "file"
27
27
 
28
28
  # Git integration (comma-separated compound value):
29
- # author โ€” add Author column (same as author:all)
30
- # author:me โ€” highlight your own migrations
31
- # author:all โ€” show all authors
32
- # diff โ€” tag migrations by git origin branch
33
- # base:<branch> โ€” base branch for diff (default: auto-detected)
34
- # mask:auto โ€” auto-extract ticket id from branch name
35
- # mask:<re> โ€” custom regex to extract branch label, e.g. mask:(WS-[^/]+)/
29
+ # author - add Author column (same as author:all)
30
+ # author:me - highlight your own migrations
31
+ # author:all - show all authors
32
+ # diff - tag migrations by git origin branch
33
+ # base:<branch> - base branch for diff (default: auto-detected)
34
+ # mask:auto - auto-extract ticket id from branch name
35
+ # mask:<re> - custom regex to extract branch label, e.g. mask:(WS-[^/]+)/
36
36
  git: "author:all,diff,mask:auto"
37
37
 
38
- # Author display format โ€” preset or custom pattern (FF L, FFF LL, etc.):
38
+ # Author display format - preset or custom pattern (FF L, FFF LL, etc.):
39
39
  # initials (J D), short (Jo D), first_name (John), last_name (Doe),
40
40
  # full_name (John Doe), full_name_short (John D.)
41
41
  author_format: "short"
@@ -50,11 +50,11 @@ module Railbow
50
50
  # date: "full"
51
51
 
52
52
  # Compact mode (comma-separated):
53
- # oneline โ€” one line per migration
54
- # dense โ€” reduce padding
55
- # noheader โ€” hide table headers
56
- # maxw:<N> โ€” max column width
57
- # hide:<col> โ€” hide a column (repeatable)
53
+ # oneline - one line per migration
54
+ # dense - reduce padding
55
+ # noheader - hide table headers
56
+ # maxw:<N> - max column width
57
+ # hide:<col> - hide a column (repeatable)
58
58
  # compact: ""
59
59
 
60
60
  # Rename column headers and cell values in table output
@@ -8,6 +8,7 @@ module Railbow
8
8
  SINGLE_TABLE_METHODS = %w[
9
9
  create_table drop_table change_table
10
10
  add_column remove_column rename_column change_column
11
+ change_column_default change_column_null
11
12
  add_index remove_index
12
13
  add_reference remove_reference
13
14
  add_belongs_to remove_belongs_to
@@ -19,7 +20,9 @@ module Railbow
19
20
  add_foreign_key remove_foreign_key
20
21
  ].freeze
21
22
 
22
- SINGLE_TABLE_PATTERN = /\b(?:#{SINGLE_TABLE_METHODS.join("|")})\s+[:"](\w+)/
23
+ # Dual-table methods are included here too: their first argument is always a
24
+ # table, even when the second is passed as `to_table:` instead of positionally.
25
+ SINGLE_TABLE_PATTERN = /\b(?:#{(SINGLE_TABLE_METHODS + DUAL_TABLE_METHODS).join("|")})\s+[:"](\w+)/
23
26
  DUAL_TABLE_PATTERN = /\b(?:#{DUAL_TABLE_METHODS.join("|")})\s+[:"](\w+)["\s,]+[:"](\w+)/
24
27
 
25
28
  # SQL keywords followed by a table name
@@ -33,6 +36,17 @@ module Railbow
33
36
  /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i
34
37
  ].freeze
35
38
 
39
+ # Helper/DSL calls that name their target table via a keyword argument, e.g.
40
+ # `convert_to_monthly_partitions(table: :apples)` or
41
+ # `remove_foreign_key :accounts, to_table: :owners`. Catches project-level
42
+ # migration helpers regardless of the method name. Bare `from:`/`to:` are
43
+ # deliberately not matched โ€” `change_column_default :users, :status,
44
+ # from: nil, to: "active"` would yield phantom tables.
45
+ KEYWORD_TABLE_PATTERN = /\b(?:table|to_table|from_table):\s*[:"']?(\w+)/
46
+
47
+ # Ruby `#` comments up to end of line, but not `#{}` interpolation.
48
+ COMMENT_PATTERN = /#(?!\{).*/
49
+
36
50
  # AR class-level query/mutation methods that strongly signal a constant is a model.
37
51
  # Kept narrow on purpose โ€” methods like `find`, `all`, `first`, `count`, `create`,
38
52
  # `new`, `transaction`, `order`, `includes` collide with stdlib/non-model constants
@@ -60,6 +74,11 @@ module Railbow
60
74
  def self.extract_tables_from_content(content)
61
75
  return [] if content.nil? || content.empty?
62
76
 
77
+ # Comments would otherwise produce phantom tables โ€” an explanatory
78
+ # "# ALTER TABLE foo ..." or a commented-out create_table reads exactly
79
+ # like the real thing to the patterns below.
80
+ content = content.gsub(COMMENT_PATTERN, "")
81
+
63
82
  tables = []
64
83
 
65
84
  content.scan(SINGLE_TABLE_PATTERN) { |match| tables << match[0] }
@@ -67,6 +86,7 @@ module Railbow
67
86
  SQL_TABLE_PATTERNS.each do |pattern|
68
87
  content.scan(pattern) { |match| tables << match[0] }
69
88
  end
89
+ content.scan(KEYWORD_TABLE_PATTERN) { |match| tables << match[0] }
70
90
  tables.uniq!
71
91
 
72
92
  # Model-based detection is heuristic (a constant that happens to respond
@@ -256,17 +256,17 @@ module Railbow
256
256
 
257
257
  \e[1mOptions:\e[0m
258
258
  RBW_GIT=<options> Git integration (comma-separated):
259
- author โ€” show all authors (same as author:all)
260
- author:all โ€” show author + date on each annotation
261
- author:me โ€” highlight your own annotations
259
+ author - show all authors (same as author:all)
260
+ author:all - show author + date on each annotation
261
+ author:me - highlight your own annotations
262
262
 
263
263
  RBW_SINCE=<period> Filter annotations by blame date (default: all)
264
264
  Values: all, 2mo, 1w, 30d, 1y, etc.
265
265
  Units: d (days), w (weeks), mo/m (months), y (years)
266
266
 
267
267
  RBW_SORT=<mode> Sort order (default: file)
268
- file โ€” group by file (default Rails order)
269
- date โ€” sort by blame date (newest first)
268
+ file - group by file (default Rails order)
269
+ date - sort by blame date (newest first)
270
270
 
271
271
  RBW_PLAIN=1 Disable Railbow formatting (plain Rails output)
272
272
 
@@ -71,6 +71,10 @@ module Railbow
71
71
  truthy?(ENV["RBW_PLAIN"])
72
72
  end
73
73
 
74
+ def force?
75
+ truthy?(ENV["RBW_FORCE"])
76
+ end
77
+
74
78
  def since
75
79
  (ENV["RBW_SINCE"] || Config.load["since"] || "all").strip.downcase
76
80
  end
@@ -12,8 +12,113 @@ require_relative "../logo"
12
12
  # db:migrate:status and db:migrate:status:<database_name> tasks.
13
13
  module Railbow
14
14
  module MigrateStatusFormatter
15
+ # Ghost migration data normalized for rendering, whether it came from
16
+ # mighost's orphan classification or a live snapshot recovery.
17
+ class GhostRow
18
+ attr_reader :filename, :branch_name, :source, :superseded_by, :deleted_in_sha,
19
+ :author_name, :author_email, :content
20
+
21
+ def initialize(filename: nil, branch_name: nil, source: nil, superseded_by: nil,
22
+ deleted_in_sha: nil, author_name: nil, author_email: nil, content: nil)
23
+ @filename = filename
24
+ @branch_name = branch_name
25
+ @source = source
26
+ @superseded_by = superseded_by
27
+ @deleted_in_sha = deleted_in_sha
28
+ @author_name = author_name
29
+ @author_email = author_email
30
+ @content = content
31
+ end
32
+ end
33
+
15
34
  private
16
35
 
36
+ def mighost_attr(obj, name)
37
+ obj.respond_to?(name) ? obj.public_send(name) : nil
38
+ end
39
+
40
+ def mighost_snapshot_content(version)
41
+ Mighost::API.find_snapshot(version)&.content
42
+ rescue
43
+ nil
44
+ end
45
+
46
+ def load_ghost_rows(versions, with_content: false)
47
+ # Detect once: OrphanedMigration carries the classification (supersession,
48
+ # deletion commit) that a bare snapshot doesn't, and already honors
49
+ # dismissals and hide_superseded.
50
+ orphans = begin
51
+ Mighost::API.orphaned_migrations.to_h { |o| [o.version.to_s, o] }
52
+ rescue
53
+ return {}
54
+ end
55
+
56
+ rows = {}
57
+ versions.each do |v|
58
+ # Absent from detect = deliberately suppressed (dismissed, or superseded
59
+ # with hide_superseded on) - render as plain NO FILE, don't re-recover.
60
+ next unless (orphan = orphans[v])
61
+
62
+ if orphan.filename && !orphan.filename.empty?
63
+ rows[v] = GhostRow.new(
64
+ filename: orphan.filename,
65
+ branch_name: orphan.branch_name,
66
+ source: mighost_attr(orphan, :source),
67
+ superseded_by: mighost_attr(orphan, :superseded_by),
68
+ deleted_in_sha: mighost_attr(orphan, :deleted_in_sha),
69
+ author_name: mighost_attr(orphan, :author_name),
70
+ author_email: mighost_attr(orphan, :author_email),
71
+ content: with_content ? mighost_snapshot_content(v) : nil
72
+ )
73
+ else
74
+ # Detect reads stored snapshots only. A version it lists without a
75
+ # filename has no snapshot yet, so fall back to live git/worktree
76
+ # recovery - keeps fresh clones working with zero setup.
77
+ snapshot = begin
78
+ Mighost::API.find_or_recover_snapshot(v)
79
+ rescue
80
+ nil
81
+ end
82
+ next unless snapshot&.filename && !snapshot.filename.empty?
83
+
84
+ rows[v] = GhostRow.new(
85
+ filename: snapshot.filename,
86
+ branch_name: snapshot.branch_name,
87
+ source: mighost_attr(snapshot, :source),
88
+ superseded_by: api_superseded_by(v),
89
+ deleted_in_sha: mighost_attr(snapshot, :deleted_in_sha),
90
+ author_name: mighost_attr(snapshot, :author_name),
91
+ author_email: mighost_attr(snapshot, :author_email),
92
+ content: with_content ? snapshot.content : nil
93
+ )
94
+ end
95
+ end
96
+ rows
97
+ end
98
+
99
+ def api_superseded_by(version)
100
+ return nil unless Mighost::API.respond_to?(:superseded_by)
101
+
102
+ Mighost::API.superseded_by(version)
103
+ rescue
104
+ nil
105
+ end
106
+
107
+ # One tag slot per ghost row; most informative wins.
108
+ def ghost_tag(ghost)
109
+ if ghost.superseded_by
110
+ "\e[38;5;245mโ‰ก #{ghost.superseded_by}\e[38;5;217m"
111
+ elsif ghost.branch_name
112
+ if ghost.source == "worktree"
113
+ "\e[38;5;222mโŒฅโ‚œ#{ghost.branch_name}\e[38;5;217m"
114
+ else
115
+ "\e[38;5;222mโŒฅ #{ghost.branch_name}\e[38;5;217m"
116
+ end
117
+ elsif ghost.deleted_in_sha
118
+ "\e[38;5;245mโœ‚ deleted in:#{ghost.deleted_in_sha[0, 8]}\e[38;5;217m"
119
+ end
120
+ end
121
+
17
122
  def git_migration_authors(migrate_dir)
18
123
  output, _status = Railbow::GitUtils.capture2(
19
124
  "log", "--format=COMMIT:%aN\t%aE", "--diff-filter=AR", "--name-status", "--", migrate_dir
@@ -304,6 +409,9 @@ module Railbow
304
409
 
305
410
  RBW_PLAIN=1 Disable Railbow formatting (plain Rails output)
306
411
 
412
+ RBW_FORCE=1 Force Railbow formatting even when piped, in CI,
413
+ or called by an LLM agent (RBW_PLAIN=1 still wins)
414
+
307
415
  RBW_HELP=1 Show this help message
308
416
 
309
417
  \e[2mAuto-disabled when piped, in CI, or when called by an LLM agent.\e[0m
@@ -428,19 +536,12 @@ module Railbow
428
536
  uncommitted_files.each { |f| branch_origins[f] ||= current_branch }
429
537
  end
430
538
 
431
- # Load mighost snapshots for "NO FILE" migrations (if mighost gem is available)
539
+ # Load mighost ghost data for "NO FILE" migrations (if mighost gem is available)
432
540
  mighost_snapshots = {}
433
541
  mighost_available = defined?(Mighost::API) && Mighost.enabled?
434
542
  if mighost_available
435
543
  no_file_versions = db_list.select { |_, _, n| n.include?("NO FILE") }.map { |_, v, _| v.to_s }
436
- no_file_versions.each do |v|
437
- snapshot = begin
438
- Mighost::API.find_or_recover_snapshot(v)
439
- rescue
440
- nil
441
- end
442
- mighost_snapshots[v] = snapshot if snapshot&.filename && !snapshot.filename.empty?
443
- end
544
+ mighost_snapshots = load_ghost_rows(no_file_versions, with_content: tables_enabled) if no_file_versions.any?
444
545
  end
445
546
 
446
547
  # Build columns
@@ -501,29 +602,25 @@ module Railbow
501
602
  ghost_snapshot = name.include?("NO FILE") ? mighost_snapshots[version.to_s] : nil
502
603
  if name.include?("NO FILE") && ghost_snapshot
503
604
  ghost_rows << idx
504
- # Mighost recovered this ghost migration โ€” show ๐Ÿ‘ป status + name + branch badge
505
- colored_status = "๐Ÿ‘ป"
605
+ # Mighost recovered this ghost migration โ€” show ghost status + name + badge.
606
+ # A superseded ghost lives on under another version: stale bookkeeping,
607
+ # not a lost migration, so it gets a calmer glyph.
608
+ colored_status = ghost_snapshot.superseded_by ? "๐Ÿชฆ" : "๐Ÿ‘ป"
506
609
  ghost_name = ghost_snapshot.filename
507
610
  .sub(/\A\d+_/, "") # strip version prefix
508
611
  .sub(/\.rb\z/, "") # strip extension
509
612
  .tr("_", " ")
510
613
  .gsub(/\b\w/, &:upcase) # titleize
511
- if ghost_snapshot.branch_name
512
- branch_tag = if ghost_snapshot.respond_to?(:source) && ghost_snapshot.source == "worktree"
513
- "\e[38;5;222mโŒฅโ‚œ#{ghost_snapshot.branch_name}\e[38;5;217m"
514
- else
515
- "\e[38;5;222mโŒฅ #{ghost_snapshot.branch_name}\e[38;5;217m"
516
- end
517
- end
518
- if branch_tag && name_col_width
519
- tag_width = formatter.display_width(formatter.strip_ansi(branch_tag))
614
+ ghost_badge = ghost_tag(ghost_snapshot)
615
+ if ghost_badge && name_col_width
616
+ tag_width = formatter.display_width(formatter.strip_ansi(ghost_badge))
520
617
  available = name_col_width - tag_width - 2
521
618
  ghost_name = formatter.truncate_str(ghost_name, available)
522
619
  name_width = formatter.display_width(ghost_name)
523
620
  padding = name_col_width - name_width - tag_width
524
- display_name = "#{ghost_name}#{" " * [padding, 2].max}#{branch_tag}"
525
- elsif branch_tag
526
- display_name = "#{ghost_name} #{branch_tag}"
621
+ display_name = "#{ghost_name}#{" " * [padding, 2].max}#{ghost_badge}"
622
+ elsif ghost_badge
623
+ display_name = "#{ghost_name} #{ghost_badge}"
527
624
  else
528
625
  display_name = ghost_name
529
626
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railbow
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/railbow.rb CHANGED
@@ -9,8 +9,10 @@ module Railbow
9
9
 
10
10
  # Returns true when Railbow formatting should be disabled.
11
11
  # Checks for explicit opt-out, standard conventions, CI, and LLM agents.
12
+ # RBW_PLAIN always wins; RBW_FORCE beats every auto-detection below it.
12
13
  def self.plain?
13
14
  return true if Params.plain?
15
+ return false if Params.force?
14
16
  return true if ENV.key?("NO_COLOR")
15
17
  return true if ENV.key?("CLAUDECODE")
16
18
  return true if ENV.key?("CI")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railbow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eugene M