kube_cluster 1.9.0 → 2.0.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.
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ # Reads the file under inspection with Prism, which ships with Ruby, instead of through the parser gem syntax tree RuboCop hands a cop.
7
+ #
8
+ # A cop which includes this does not use the `on_<type>` callbacks: those are driven by the parser gem tree, so a cop which reads a different tree walks it itself from `on_new_investigation`. What it gains is one parse of the file by the same Prism which runs the program, and a uniform walk over every node.
9
+ #
10
+ # Offences are still reported against `Parser::Source::Range`, because that is what RuboCop and its correctors take. THE TWO DO NOT MEASURE THE SAME WAY: a Prism location counts BYTES and a `Parser::Source::Range` counts CHARACTERS, so a range built from `start_offset` is correct only until the first multibyte character in the file and silently wrong after it. Prism gives both, and `prism_range` takes the character pair.
11
+ #
12
+ # Prism does not track parentage - a node does not name the node it is written inside - so the walk records the parent of each node as it descends.
13
+
14
+ require "prism"
15
+
16
+ module RuboCop
17
+ module Cop
18
+ module PrismSource
19
+ # @returns [Prism::ParseResult] the file, parsed once however many times this is called.
20
+ def prism_result
21
+ self.prism_state.fetch(:result)
22
+ end
23
+
24
+ # Every node in the file, outermost first, in source order.
25
+ #
26
+ # @returns [Array(Prism::Node)] the nodes.
27
+ def prism_nodes
28
+ self.prism_state.fetch(:nodes)
29
+ end
30
+
31
+ # @parameter classes [Array(Class)] the Prism node classes wanted.
32
+ # @returns [Array(Prism::Node)] the nodes which are one of them.
33
+ def prism_each(*classes)
34
+ if classes.empty?
35
+ self.prism_nodes
36
+ else
37
+ self.prism_nodes.select{|node| classes.any?{|klass| node.is_a?(klass)}}
38
+ end
39
+ end
40
+
41
+ # @parameter node [Prism::Node] the node whose parent is wanted.
42
+ # @returns [Prism::Node | nil] the node it is written inside, or nil at the root.
43
+ def prism_parent(node)
44
+ self.prism_state.fetch(:parents)[node]
45
+ end
46
+
47
+ # The nodes a node is written inside, innermost first.
48
+ #
49
+ # @returns [Array(Prism::Node)] the ancestors.
50
+ def prism_ancestors(node)
51
+ [].tap do |found|
52
+ current = self.prism_parent(node)
53
+
54
+ while current
55
+ found << current
56
+ current = self.prism_parent(current)
57
+ end
58
+ end
59
+ end
60
+
61
+ # @returns [Array(Prism::Node)] the nodes written directly inside the given node.
62
+ def prism_children(node)
63
+ node.compact_child_nodes
64
+ end
65
+
66
+ # Everything written inside the given node, at any depth, outermost first.
67
+ #
68
+ # @parameter classes [Array(Class)] the Prism node classes wanted, or none for all of them.
69
+ # @returns [Array(Prism::Node)] the nodes.
70
+ def prism_descendants(node, *classes)
71
+ [].tap do |found|
72
+ pending = node.compact_child_nodes.dup
73
+
74
+ until pending.empty?
75
+ current = pending.shift
76
+
77
+ if classes.empty? or classes.any?{|klass| current.is_a?(klass)}
78
+ found << current
79
+ end
80
+
81
+ pending.concat(current.compact_child_nodes)
82
+ end
83
+ end
84
+ end
85
+
86
+ # Whether the given node contains a heredoc, whose content lives below the line it opens on.
87
+ def prism_heredoc?(node)
88
+ [node].concat(self.prism_descendants(node)).any? do |current|
89
+ case current
90
+ when ::Prism::StringNode, ::Prism::InterpolatedStringNode,
91
+ ::Prism::XStringNode, ::Prism::InterpolatedXStringNode
92
+ current.heredoc?
93
+ else
94
+ false
95
+ end
96
+ end
97
+ end
98
+
99
+ # The statements of a body, whether it holds one or many.
100
+ #
101
+ # @parameter body [Prism::StatementsNode | nil] the body of a method, module, or block.
102
+ # @returns [Array(Prism::Node)] its statements, which is empty when there is no body.
103
+ def prism_statements(body)
104
+ if body.nil?
105
+ []
106
+ else
107
+ body.body
108
+ end
109
+ end
110
+
111
+ # A Prism location as the range RuboCop reports an offence against.
112
+ #
113
+ # @parameter location [Prism::Location] where the offence is.
114
+ # @returns [Parser::Source::Range] the same place, measured for RuboCop.
115
+ def prism_range(location)
116
+ # CHARACTER OFFSETS, NOT `start_offset`. A `Parser::Source::Range` indexes the buffer by character and a Prism location counts bytes, so a range built from the byte pair lands further and further past where it should as the multibyte characters above it accumulate. A corrector handed such a range replaces the wrong text and writes a file which no longer parses.
117
+ ::Parser::Source::Range.new(
118
+ processed_source.buffer,
119
+ location.start_character_offset,
120
+ location.end_character_offset,
121
+ )
122
+ end
123
+
124
+ # One range reaching from the start of the first location to the end of the last, for an offence over a run of statements rather than a single node.
125
+ #
126
+ # @returns [Parser::Source::Range] the span.
127
+ def prism_span(first, last)
128
+ ::Parser::Source::Range.new(
129
+ processed_source.buffer,
130
+ first.start_character_offset,
131
+ last.end_character_offset,
132
+ )
133
+ end
134
+
135
+ # @returns [Parser::Source::Range] the whole of the given node.
136
+ def prism_node_range(node)
137
+ self.prism_range(node.location)
138
+ end
139
+
140
+ # The column a node starts at, counted the way a Ruby String is indexed.
141
+ #
142
+ # @returns [Integer] the character column.
143
+ def prism_column(node)
144
+ # `Prism::Location#start_column` counts bytes, so indexing a line from `processed_source.lines` with it cuts in the wrong place once anything above or before it on that line is multibyte.
145
+ node.location.start_character_column
146
+ end
147
+
148
+ # @returns [String] the source of the given node.
149
+ def prism_source(node)
150
+ node.location.slice
151
+ end
152
+
153
+ # @returns [Array(Prism::InlineComment)] every comment in the file, in source order.
154
+ def prism_comments
155
+ self.prism_result.comments
156
+ end
157
+
158
+ # The run of comment lines written directly above a declaration, which is what documents it.
159
+ #
160
+ # @returns [Array(Prism::InlineComment)] the comments, in source order.
161
+ def prism_leading_comments(node)
162
+ self.prism_state.fetch(:leading)[node] ||= self.contiguous_above(node)
163
+ end
164
+
165
+ # Every comment a declaration owns, which is the same run: a comment belongs to whatever it is written above.
166
+ #
167
+ # @returns [Array(Prism::InlineComment)] the comments, in source order.
168
+ def prism_owned_comments(node)
169
+ self.prism_leading_comments(node)
170
+ end
171
+
172
+ private
173
+
174
+ # Everything read from the file, discarded when RuboCop moves to the next one.
175
+ def prism_state
176
+ source = processed_source
177
+
178
+ unless @prism_state and @prism_source.equal?(source)
179
+ @prism_source = source
180
+ @prism_state = self.read(source)
181
+ end
182
+
183
+ @prism_state
184
+ end
185
+
186
+ def read(source)
187
+ result = ::Prism.parse(source.buffer.source)
188
+
189
+ nodes = []
190
+ parents = {}.compare_by_identity
191
+
192
+ self.descend(result.value, nil, nodes, parents)
193
+
194
+ by_line = {}
195
+
196
+ result.comments.each do |comment|
197
+ by_line[comment.location.start_line] = comment
198
+ end
199
+
200
+ {
201
+ result: result,
202
+ nodes: nodes,
203
+ parents: parents,
204
+ by_line: by_line,
205
+ leading: {}.compare_by_identity,
206
+ }
207
+ end
208
+
209
+ # The tree, recorded flat with the parent of each node, since Prism does not say.
210
+ def descend(node, parent, nodes, parents)
211
+ nodes << node
212
+ parents[node] = parent
213
+
214
+ node.compact_child_nodes.each do |child|
215
+ self.descend(child, node, nodes, parents)
216
+ end
217
+ end
218
+
219
+ # The run of comment lines ending on the line above the node, stopping at the first gap.
220
+ def contiguous_above(node)
221
+ by_line = self.prism_state.fetch(:by_line)
222
+ found = []
223
+ line = node.location.start_line - 1
224
+
225
+ while comment = by_line[line]
226
+ found.unshift(comment)
227
+ line -= 1
228
+ end
229
+
230
+ found
231
+ end
232
+ end
233
+ end
234
+ end
data/cops/use_tap.rb ADDED
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requires `tap` when a method opens by assigning a local variable and closes by
4
+ # returning that same variable:
5
+ #
6
+ # def require_existing_column! # NO
7
+ # column = params[:id].to_s
8
+ # unless connection.columns(@table).any? { |c| c.name == column }
9
+ # render json: { error: "unknown column" }, status: :not_found
10
+ # end
11
+ # column
12
+ # end
13
+ #
14
+ # def require_existing_column! # YES
15
+ # params[:id].to_s.tap do |column|
16
+ # unless connection.columns(@table).any? { |c| c.name == column }
17
+ # render json: { error: "unknown column" }, status: :not_found
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ # The assign-work-return shape states the return value twice — once as the
23
+ # assignment target, once as a bare mention on the last line — and puts the two
24
+ # far enough apart that the trailing `column` reads as a stray expression.
25
+ # `tap` names the value once, scopes it to the block that works on it, and makes
26
+ # "the value flows through unchanged" structural rather than something you check
27
+ # by comparing the first and last lines.
28
+ #
29
+ # A body that REASSIGNS the variable is left alone: `tap` yields the original
30
+ # object and returns it, so that rewrite would change the result.
31
+ module RuboCop
32
+ module Cop
33
+ module Local
34
+ class UseTap < Base
35
+ MSG = "Assigning `%<name>s` and returning it at the end is `tap`; " \
36
+ "write `<expression>.tap do |%<name>s| ... end`."
37
+
38
+ # Every way a local can be written to after the opening assignment.
39
+ REASSIGNMENTS = %i[lvasgn op_asgn or_asgn and_asgn masgn].freeze
40
+
41
+ def on_def(node)
42
+ check(node.body)
43
+ end
44
+
45
+ def on_defs(node)
46
+ check(node.body)
47
+ end
48
+
49
+ private
50
+
51
+ def check(body)
52
+ if tap_shaped?(body)
53
+ add_offense(body, message: format(MSG, name: assigned_name(body)))
54
+ end
55
+ end
56
+
57
+ # First statement assigns a local; last statement is that same local,
58
+ # read and nothing else.
59
+ def tap_shaped?(body)
60
+ body&.begin_type? &&
61
+ body.children.size >= 2 &&
62
+ body.children.first.lvasgn_type? &&
63
+ returns_assigned?(body) &&
64
+ !reassigned?(body)
65
+ end
66
+
67
+ def assigned_name(body)
68
+ body.children.first.name
69
+ end
70
+
71
+ def returns_assigned?(body)
72
+ last = body.children.last
73
+
74
+ last.lvar_type? && last.children.first == assigned_name(body)
75
+ end
76
+
77
+ # `tap` yields the object the receiver evaluated to, so a body that
78
+ # points the name at something else partway through is a different
79
+ # program and must not be flagged.
80
+ def reassigned?(body)
81
+ name = assigned_name(body)
82
+
83
+ body.children[1..-2].any? do |statement|
84
+ statement.each_node(*REASSIGNMENTS).any? do |write|
85
+ writes_name?(write, name)
86
+ end
87
+ end
88
+ end
89
+
90
+ def writes_name?(node, name)
91
+ if node.masgn_type?
92
+ node.assignments.any? { |target| target == name }
93
+ else
94
+ node.children.first == name
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
data/flake.lock CHANGED
@@ -8,11 +8,11 @@
8
8
  "systems": "systems"
9
9
  },
10
10
  "locked": {
11
- "lastModified": 1790556091,
12
- "narHash": "sha256-+kgvg6vuaxjEUUad6pL+pxa3LZFveq3XvWGSZ+Z9gOA=",
11
+ "lastModified": 1790610895,
12
+ "narHash": "sha256-sansiZlrb60MrGFzTXNCVVMTU5EsGM6KWvZkA/aNp14=",
13
13
  "owner": "n-at-han-k",
14
14
  "repo": "flake.nix",
15
- "rev": "d023f40398596a5d80bf03e429c678a99b3f259a",
15
+ "rev": "0f3388742debac3e78751796f4568f441c0dee57",
16
16
  "type": "github"
17
17
  },
18
18
  "original": {
data/flake.nix CHANGED
@@ -10,34 +10,35 @@
10
10
  outputs = { self, nixpkgs, utils, mine }:
11
11
  utils.lib.eachDefaultSystem (system:
12
12
  let
13
- # No mine.overlays.default: it overrides pkg-config, which stdenv
14
- # splices, so the overlay rebuilds stdenv and all of nixpkgs from
15
- # source. mine.inputs.nixpkgs.follows makes it redundant anyway.
16
13
  pkgs = nixpkgs.legacyPackages.${system};
17
- ruby = pkgs.ruby_3_4;
18
- kubectlWithKubeconfig = pkgs.writeShellScriptBin "kubectl" ''
19
- #!${pkgs.bash}/bin/bash
20
- KUBECONFIG="$PWD/kubeconfig.yaml" ${pkgs.kubectl}/bin/kubectl "$@"
21
- '';
14
+ lib = mine.lib.${system};
15
+
16
+ gems = pkgs.bundlerEnv {
17
+ name = "kube_cluster-gems";
18
+ ruby = pkgs.ruby_3_4;
19
+ gemfile = ./Gemfile;
20
+ lockfile = ./Gemfile.lock;
21
+ gemset = ./gemset.nix;
22
+ };
22
23
  in
23
24
  {
24
- # pkg-config, bundix, libyaml, openssl, overmind, tmux and `bundix -l`
25
- # come from mkRubyShell.
26
- devShells.default = mine.lib.${system}.mkRubyShell {
25
+ devShells.default = lib.mkRubyShell {
27
26
  buildInputs = [
28
- pkgs.trufflehog
29
- ruby
30
- kubectlWithKubeconfig
27
+ gems
28
+ gems.wrappedRuby
29
+ pkgs.ripgrep # scampi shells out to `rg` to discover __END__ spec sections
31
30
  ];
32
31
 
33
- shellHook = ''
34
- export GEM_HOME="$HOME/.gem-${ruby.version}"
35
- export GEM_PATH="$GEM_HOME"
36
- export PATH="$GEM_HOME/bin:$PATH"
37
- export BUNDLE_PATH="$GEM_HOME"
32
+ shellHook = /* bash */ ''
33
+ unset RUBYOPT
34
+ export BUNDLE_FORCE_RUBY_PLATFORM=false
38
35
  export BUNDLE_GEMFILE="$PWD/Gemfile"
39
- export BUNDLE_BIN="$GEM_HOME/bin"
40
- export KUBECONFIG="$PWD/kubeconfig.yaml"
36
+ export BUNDLE_FROZEN=false
37
+
38
+ export LANG="''${LANG:-C.UTF-8}"
39
+ export LC_ALL="''${LC_ALL:-$LANG}"
40
+
41
+ export RUBYLIB="$PWD/lib''${RUBYLIB:+:$RUBYLIB}"
41
42
  '';
42
43
  };
43
44
  }
data/gemset.nix CHANGED
@@ -10,6 +10,16 @@
10
10
  };
11
11
  version = "8.1.3";
12
12
  };
13
+ ast = {
14
+ groups = ["default"];
15
+ platforms = [];
16
+ source = {
17
+ remotes = ["https://rubygems.org"];
18
+ sha256 = "10yknjyn0728gjn6b5syynvrvrwm66bhssbxq8mkhshxghaiailm";
19
+ type = "gem";
20
+ };
21
+ version = "2.4.3";
22
+ };
13
23
  async = {
14
24
  dependencies = ["console" "fiber-annotation" "io-event" "metrics" "traces"];
15
25
  groups = ["default" "maintenance"];
@@ -168,10 +178,10 @@
168
178
  platforms = [];
169
179
  source = {
170
180
  remotes = ["https://rubygems.org"];
171
- sha256 = "1g9zi8c4i7g8zz0c3hxrw6mblrjvgn7akys60clb9si7c1k1gljk";
181
+ sha256 = "0rng45p4vc3f6nac44cnylwch7rj4zsff3icdz6c7garwpjy3sv1";
172
182
  type = "gem";
173
183
  };
174
- version = "4.1.2";
184
+ version = "4.1.3";
175
185
  };
176
186
  build-files = {
177
187
  groups = ["default" "maintenance"];
@@ -308,6 +318,27 @@
308
318
  };
309
319
  version = "1.0.1";
310
320
  };
321
+ gem_kit = {
322
+ groups = ["default"];
323
+ platforms = [];
324
+ source = {
325
+ remotes = ["https://rubygems.org"];
326
+ sha256 = "0dx9w041lqzr4jd0m9as0v5v6n3fj2hcg74p2gyswr2y2a6zc5v9";
327
+ type = "gem";
328
+ };
329
+ version = "0.2.0";
330
+ };
331
+ gem_kit-release = {
332
+ dependencies = ["gem_kit" "thor"];
333
+ groups = ["release"];
334
+ platforms = [];
335
+ source = {
336
+ remotes = ["https://rubygems.org"];
337
+ sha256 = "1cnl3p4y9jfag8q908ih28q3hll21b3ban5j004cra7l58hg3rrq";
338
+ type = "gem";
339
+ };
340
+ version = "0.3.2";
341
+ };
311
342
  hana = {
312
343
  groups = ["default"];
313
344
  platforms = [];
@@ -411,16 +442,6 @@
411
442
  };
412
443
  version = "2.5.0";
413
444
  };
414
- kube_cluster = {
415
- dependencies = ["activesupport" "kube_kubectl" "kube_schema"];
416
- groups = ["default"];
417
- platforms = [];
418
- source = {
419
- path = ./.;
420
- type = "path";
421
- };
422
- version = "1.8.0";
423
- };
424
445
  kube_kubectl = {
425
446
  dependencies = ["debug" "json_schemer" "rubyshell" "shellwords" "string_builder"];
426
447
  groups = ["default"];
@@ -433,15 +454,25 @@
433
454
  version = "2.0.9";
434
455
  };
435
456
  kube_schema = {
436
- dependencies = ["json_schemer" "rubyshell"];
457
+ dependencies = ["gem_kit" "json_schemer" "rubyshell"];
437
458
  groups = ["default"];
438
459
  platforms = [];
439
460
  source = {
440
461
  remotes = ["https://rubygems.org"];
441
- sha256 = "1f8nazb6glzxkms0paqsn9x0zihmgj739l2shnwafhzaxgxcqy4z";
462
+ sha256 = "03nzn7mx1cmkbvbiiyk7qcw2npwhdi1g19i6l5m5p17iz4zlcrj0";
442
463
  type = "gem";
443
464
  };
444
- version = "1.11.0";
465
+ version = "2.0.0";
466
+ };
467
+ language_server-protocol = {
468
+ groups = ["default"];
469
+ platforms = [];
470
+ source = {
471
+ remotes = ["https://rubygems.org"];
472
+ sha256 = "1w5p8c2145lmqzr25bxh4ikzjm6k8y1k5lriqqdpw9pq730w1wjy";
473
+ type = "gem";
474
+ };
475
+ version = "3.17.0.6";
445
476
  };
446
477
  lefthook = {
447
478
  groups = ["development"];
@@ -453,6 +484,16 @@
453
484
  };
454
485
  version = "2.1.10";
455
486
  };
487
+ lint_roller = {
488
+ groups = ["default"];
489
+ platforms = [];
490
+ source = {
491
+ remotes = ["https://rubygems.org"];
492
+ sha256 = "11yc0d84hsnlvx8cpk4cbj6a4dz9pk0r1k29p0n1fz9acddq831c";
493
+ type = "gem";
494
+ };
495
+ version = "1.1.0";
496
+ };
456
497
  localhost = {
457
498
  dependencies = ["bake"];
458
499
  groups = ["default" "maintenance"];
@@ -621,6 +662,27 @@
621
662
  };
622
663
  version = "4.0.2";
623
664
  };
665
+ parallel = {
666
+ groups = ["default"];
667
+ platforms = [];
668
+ source = {
669
+ remotes = ["https://rubygems.org"];
670
+ sha256 = "0vvs2sar7wnw70j1jc1kv1k23m5grc0ci3bbrcf6mkh18683wnpp";
671
+ type = "gem";
672
+ };
673
+ version = "2.3.0";
674
+ };
675
+ parser = {
676
+ dependencies = ["ast" "racc"];
677
+ groups = ["default"];
678
+ platforms = [];
679
+ source = {
680
+ remotes = ["https://rubygems.org"];
681
+ sha256 = "0a4q5h2hcihk79dbr20scgkm56l79qp7fsvfvkxlv8nmapvxg9i1";
682
+ type = "gem";
683
+ };
684
+ version = "3.3.12.0";
685
+ };
624
686
  pp = {
625
687
  dependencies = ["prettyprint"];
626
688
  groups = ["default" "maintenance"];
@@ -726,6 +788,16 @@
726
788
  };
727
789
  version = "5.3.1";
728
790
  };
791
+ racc = {
792
+ groups = ["default"];
793
+ platforms = [];
794
+ source = {
795
+ remotes = ["https://rubygems.org"];
796
+ sha256 = "0byn0c9nkahsl93y9ln5bysq4j31q8xkf2ws42swighxd4lnjzsa";
797
+ type = "gem";
798
+ };
799
+ version = "1.8.1";
800
+ };
729
801
  rack = {
730
802
  groups = ["default" "maintenance"];
731
803
  platforms = [];
@@ -747,6 +819,16 @@
747
819
  };
748
820
  version = "1.4.1";
749
821
  };
822
+ rainbow = {
823
+ groups = ["default"];
824
+ platforms = [];
825
+ source = {
826
+ remotes = ["https://rubygems.org"];
827
+ sha256 = "0smwg4mii0fm38pyb5fddbmrdpifwv22zv3d3px2xx497am93503";
828
+ type = "gem";
829
+ };
830
+ version = "3.1.1";
831
+ };
750
832
  rake = {
751
833
  groups = ["development"];
752
834
  platforms = [];
@@ -784,10 +866,10 @@
784
866
  platforms = [];
785
867
  source = {
786
868
  remotes = ["https://rubygems.org"];
787
- sha256 = "1fwfw26a32rps78920nn29shqg2zmqv72i89j1fap41isshida9m";
869
+ sha256 = "0jxl6c5cagjz5i5n9jplhdy2izad6wapmwgfhqggb22nqfvvdvas";
788
870
  type = "gem";
789
871
  };
790
- version = "2.12.0";
872
+ version = "2.13.1";
791
873
  };
792
874
  reline = {
793
875
  dependencies = ["io-console"];
@@ -800,6 +882,38 @@
800
882
  };
801
883
  version = "0.6.3";
802
884
  };
885
+ rubocop = {
886
+ dependencies = ["json" "language_server-protocol" "lint_roller" "parallel" "parser" "rainbow" "regexp_parser" "rubocop-ast" "ruby-progressbar" "unicode-display_width"];
887
+ groups = ["default"];
888
+ platforms = [];
889
+ source = {
890
+ remotes = ["https://rubygems.org"];
891
+ sha256 = "1q2nih91fjbv6j1qqxfhlc480zyhz94rniyh469vsw1fzd0q1xia";
892
+ type = "gem";
893
+ };
894
+ version = "1.92.0";
895
+ };
896
+ rubocop-ast = {
897
+ dependencies = ["parser" "prism"];
898
+ groups = ["default"];
899
+ platforms = [];
900
+ source = {
901
+ remotes = ["https://rubygems.org"];
902
+ sha256 = "1nw84xk6vc2ls8sxqvyhxs2agh4l0jrws85d4bi3x0501lq8ijmr";
903
+ type = "gem";
904
+ };
905
+ version = "1.50.0";
906
+ };
907
+ ruby-progressbar = {
908
+ groups = ["default"];
909
+ platforms = [];
910
+ source = {
911
+ remotes = ["https://rubygems.org"];
912
+ sha256 = "0cwvyb7j47m7wihpfaq7rc47zwwx9k4v7iqd9s1xch5nm53rrz40";
913
+ type = "gem";
914
+ };
915
+ version = "1.13.0";
916
+ };
803
917
  rubyshell = {
804
918
  groups = ["default"];
805
919
  platforms = [];
@@ -866,10 +980,10 @@
866
980
  platforms = [];
867
981
  source = {
868
982
  remotes = ["https://rubygems.org"];
869
- sha256 = "0a9c1mdy12y81ck7mcn9f9i2s2wwzjh1nr92ps354q517zq9dkh8";
983
+ sha256 = "00sdym5q3d9zg7lv47mjamzv67z1lfyd3gz9256v0g9gxl5p7jhj";
870
984
  type = "gem";
871
985
  };
872
- version = "0.2.3";
986
+ version = "0.3.0";
873
987
  };
874
988
  string-format = {
875
989
  groups = ["default" "maintenance"];
@@ -901,6 +1015,16 @@
901
1015
  };
902
1016
  version = "3.2.0";
903
1017
  };
1018
+ thor = {
1019
+ groups = ["default" "release"];
1020
+ platforms = [];
1021
+ source = {
1022
+ remotes = ["https://rubygems.org"];
1023
+ sha256 = "0wsy88vg2mazl039392hqrcwvs5nb9kq8jhhrrclir2px1gybag3";
1024
+ type = "gem";
1025
+ };
1026
+ version = "1.5.0";
1027
+ };
904
1028
  thread-local = {
905
1029
  groups = ["default" "maintenance"];
906
1030
  platforms = [];
@@ -952,6 +1076,27 @@
952
1076
  };
953
1077
  version = "2.0.6";
954
1078
  };
1079
+ unicode-display_width = {
1080
+ dependencies = ["unicode-emoji"];
1081
+ groups = ["default"];
1082
+ platforms = [];
1083
+ source = {
1084
+ remotes = ["https://rubygems.org"];
1085
+ sha256 = "04xnl0zwpjjvxipd6rdnxkm11pfhbhhi3179yzv51nqi9dmacyjb";
1086
+ type = "gem";
1087
+ };
1088
+ version = "3.3.0";
1089
+ };
1090
+ unicode-emoji = {
1091
+ groups = ["default"];
1092
+ platforms = [];
1093
+ source = {
1094
+ remotes = ["https://rubygems.org"];
1095
+ sha256 = "1kp89lja8ii7l6f0bq5vdxq1p3kxr3a2qmmhdc38qdwh6akjzh0i";
1096
+ type = "gem";
1097
+ };
1098
+ version = "4.3.0";
1099
+ };
955
1100
  uri = {
956
1101
  groups = ["default"];
957
1102
  platforms = [];