├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── setup └── update-deps ├── exe └── gorails ├── gorails.gemspec ├── lib ├── gorails.rb └── gorails │ ├── commands.rb │ ├── commands │ ├── episodes.rb │ ├── example.rb │ ├── help.rb │ ├── jobs.rb │ ├── jumpstart.rb │ ├── railsbytes.rb │ └── version.rb │ ├── entry_point.rb │ └── version.rb ├── sig └── gorails.rbs ├── test ├── gorails_test.rb └── test_helper.rb └── vendor └── deps ├── cli-kit ├── REVISION └── lib │ └── cli │ ├── kit.rb │ └── kit │ ├── args.rb │ ├── args │ ├── definition.rb │ ├── evaluation.rb │ ├── parser.rb │ ├── parser │ │ └── node.rb │ └── tokenizer.rb │ ├── base_command.rb │ ├── command_help.rb │ ├── command_registry.rb │ ├── config.rb │ ├── core_ext.rb │ ├── error_handler.rb │ ├── executor.rb │ ├── ini.rb │ ├── levenshtein.rb │ ├── logger.rb │ ├── opts.rb │ ├── resolver.rb │ ├── sorbet_runtime_stub.rb │ ├── support.rb │ ├── support │ └── test_helper.rb │ ├── system.rb │ ├── util.rb │ └── version.rb └── cli-ui ├── REVISION └── lib └── cli ├── ui.rb └── ui ├── ansi.rb ├── color.rb ├── formatter.rb ├── frame.rb ├── frame ├── frame_stack.rb ├── frame_style.rb └── frame_style │ ├── box.rb │ └── bracket.rb ├── glyph.rb ├── os.rb ├── printer.rb ├── progress.rb ├── prompt.rb ├── prompt ├── interactive_options.rb └── options_handler.rb ├── sorbet_runtime_stub.rb ├── spinner.rb ├── spinner ├── async.rb └── spin_group.rb ├── stdout_router.rb ├── terminal.rb ├── truncater.rb ├── version.rb ├── widgets.rb ├── widgets ├── base.rb └── status.rb └── wrap.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.0.3' 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Run the default task 27 | run: bundle exec rake 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.1.4] - 2022-03-13 4 | 5 | - Add `version` command 6 | 7 | ## [0.1.3] - 2022-03-13 8 | 9 | - Vendor cli-kit and cli-ui for speed 10 | This also skips loading bundler/setup which looks for a Gemfile 11 | 12 | ## [0.1.2] - 2022-03-13 13 | 14 | - Add ability to view and apply individual Railsbytes 15 | 16 | ## [0.1.1] - 2022-03-13 17 | 18 | - Initial release 19 | 20 | ## [0.1.0] - 2021-12-16 21 | 22 | - Empty release 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at excid3@gmail.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in gorails.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | 10 | gem "mocha", "~> 1.5.0", require: false 11 | gem "minitest", ">= 5.0.0", require: false 12 | gem "minitest-reporters", require: false 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gorails (0.1.5) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ansi (1.5.0) 10 | ast (2.4.2) 11 | builder (3.2.4) 12 | metaclass (0.0.4) 13 | minitest (5.15.0) 14 | minitest-reporters (1.5.0) 15 | ansi 16 | builder 17 | minitest (>= 5.0) 18 | ruby-progressbar 19 | mocha (1.5.0) 20 | metaclass (~> 0.0.1) 21 | parallel (1.21.0) 22 | parser (3.1.1.0) 23 | ast (~> 2.4.1) 24 | rainbow (3.1.1) 25 | rake (13.0.6) 26 | regexp_parser (2.2.1) 27 | rexml (3.2.5) 28 | rubocop (1.25.1) 29 | parallel (~> 1.10) 30 | parser (>= 3.1.0.0) 31 | rainbow (>= 2.2.2, < 4.0) 32 | regexp_parser (>= 1.8, < 3.0) 33 | rexml 34 | rubocop-ast (>= 1.15.1, < 2.0) 35 | ruby-progressbar (~> 1.7) 36 | unicode-display_width (>= 1.4.0, < 3.0) 37 | rubocop-ast (1.16.0) 38 | parser (>= 3.1.1.0) 39 | rubocop-performance (1.13.3) 40 | rubocop (>= 1.7.0, < 2.0) 41 | rubocop-ast (>= 0.4.0) 42 | ruby-progressbar (1.11.0) 43 | standard (1.7.3) 44 | rubocop (= 1.25.1) 45 | rubocop-performance (= 1.13.3) 46 | unicode-display_width (2.1.0) 47 | 48 | PLATFORMS 49 | arm64-darwin-21 50 | 51 | DEPENDENCIES 52 | gorails! 53 | minitest (>= 5.0.0) 54 | minitest-reporters 55 | mocha (~> 1.5.0) 56 | rake (~> 13.0) 57 | standard 58 | 59 | BUNDLED WITH 60 | 2.3.8 61 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Chris Oliver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoRails CLI 2 | 3 | [![Gem Version](https://badge.fury.io/rb/gorails.svg)](https://badge.fury.io/rb/gorails) 4 | 5 | An interactive CLI for [GoRails](https://gorails.com), [Railsbytes](https://railsbytes.com), [Jumpstart](https://github.com/excid3/jumpstart), [Ruby on Rails jobs](https://jobs.gorails.com), and more. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'gorails' 13 | ``` 14 | 15 | And then run 16 | 17 | ```bash 18 | gorails 19 | ``` 20 | 21 | ## Usage 22 | 23 | #### GoRails episodes 24 | 25 | To see a list of the recent GoRails screencasts, you can run: 26 | 27 | ```sh 28 | gorails episodes 29 | ``` 30 | 31 | #### Ruby on Rails Jobs 32 | 33 | To see a list of Ruby on Rails job posts, you can run: 34 | 35 | ```sh 36 | gorails jobs 37 | ``` 38 | 39 | #### Railsbytes 40 | 41 | To see a list of Railsbytes.com templates, you can run: 42 | 43 | ```sh 44 | gorails railsbytes 45 | gorails railsbytes x7msKX 46 | ``` 47 | 48 | #### Jumpstart 49 | 50 | To create a Ruby on Rails app using the [Jumpstart template](https://github.com/excid3/jumpstart), you can run: 51 | 52 | ```sh 53 | gorails jumpstart myapp 54 | ``` 55 | 56 | ## Development 57 | 58 | After checking out the repo, run `bundle` to install dependencies. Then, run `rake test` to run the tests. You can also run `exe/gorails` to test the gem. 59 | 60 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 61 | 62 | ## Contributing 63 | 64 | Bug reports and pull requests are welcome on GitHub at https://github.com/excid3/gorails-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/excid3/gorails-ruby/blob/master/CODE_OF_CONDUCT.md). 65 | 66 | ## License 67 | 68 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 69 | 70 | ## Code of Conduct 71 | 72 | Everyone interacting in the GoRails project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/excid3/gorails-ruby/blob/master/CODE_OF_CONDUCT.md). 73 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList["test/**/*_test.rb"] 10 | end 11 | 12 | task default: :test 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "gorails" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/update-deps: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift(File.expand_path("../../vendor/deps/cli-ui/lib", __FILE__)) 4 | require 'open3' 5 | require 'fileutils' 6 | 7 | def fmt(tag, msg) 8 | # Not really CLI::UI.fmt compatible: no nesting support, for example. 9 | # While we could pull in CLI::UI here, it makes it more difficult to 10 | # bootstrap a new project and to fix a broken vendor. 11 | fmt_msg = msg 12 | .gsub(/{{yellow:(.*?)}}/, "\x1b[33m\\1\x1b[31m") 13 | .gsub(/{{green:(.*?)}}/, "\x1b[32m\\1\x1b[31m") 14 | .gsub(/{{blue:(.*?)}}/, "\x1b[34m\\1\x1b[31m") 15 | .gsub(/{{bold_blue:(.*?)}}/, "\x1b[1;34m\\1\x1b[0;31m") 16 | .gsub(/{{bold_green:(.*?)}}/, "\x1b[1;32m\\1\x1b[0;31m") 17 | "\x1b[1;31m[#{tag}] \x1b[0;31m#{fmt_msg}\x1b[0m" 18 | end 19 | 20 | def bail(msg) 21 | STDERR.puts(fmt("ERROR", msg)) 22 | exit 1 23 | end 24 | 25 | def warn(msg) 26 | STDERR.puts(fmt("WARNING", msg)) 27 | end 28 | 29 | def source_path 30 | File.expand_path(ENV.fetch('SOURCE_ROOT', File.expand_path('../../..', __FILE__))) 31 | end 32 | 33 | deps = %w(cli-ui cli-kit) 34 | 35 | deps.each do |dep| 36 | path = File.expand_path(dep, source_path) 37 | 38 | unless Dir.exist?(path) 39 | bail( 40 | "dependency is not checked out: {{yellow:#{dep}}}.\n" \ 41 | " This repo {{bold_blue:(github.com/shopify/#{dep})}} must be cloned at {{bold_blue:#{path}}} for this script to succeed.\n" \ 42 | " Currently, SOURCE_ROOT is set to {{bold_blue:#{source_path}}}.\n" \ 43 | " Alternatively, you can set {{bold_blue:SOURCE_ROOT}} to a directory containing {{yellow:#{dep}}}.\n" \ 44 | " {{bold_blue:SOURCE_ROOT}} defaults to {{bold_blue:../}}." 45 | ) 46 | end 47 | 48 | head_sha = nil 49 | dirty = false 50 | 51 | Dir.chdir(path) do 52 | _, _, stat = Open3.capture3('git fetch origin main') 53 | bail("couldn't git fetch in dependency: {{yellow:#{dep}}}") unless stat.success? 54 | 55 | head_sha, stat = Open3.capture2('git rev-parse HEAD') 56 | bail("couldn't determine HEAD: {{yellow:#{dep}}}") unless stat.success? 57 | head_sha.chomp! 58 | 59 | fetch_head_sha, stat = Open3.capture2('git rev-parse FETCH_HEAD') 60 | bail("couldn't determine FETCH_HEAD: {{yellow:#{dep}}}") unless stat.success? 61 | fetch_head_sha.chomp! 62 | 63 | git_status, stat = Open3.capture2('git status --porcelain') 64 | bail("couldn't determine git status: {{yellow:#{dep}}}") unless stat.success? 65 | 66 | if head_sha != fetch_head_sha 67 | warn( 68 | "Copying files from {{yellow:#{path}}} to satisfy dependency {{yellow:#{dep}}}.\n" \ 69 | " However, the repo at {{yellow:#{path}}} isn't up to date.\n" \ 70 | " The checked-out revision is {{yellow:#{head_sha[0..8]}}}, and "\ 71 | "{{yellow:origin/master}} is {{yellow:#{fetch_head_sha[0..8]}}}.\n" \ 72 | " Unless you know what you're doing, you should {{green:cd}} to that repo and {{green:git pull}}, then run this again." 73 | ) 74 | end 75 | 76 | unless git_status.chomp.empty? 77 | dirty = true 78 | warn("importing uncommitted changes from dependency: {{yellow:#{dep}}}") 79 | end 80 | end 81 | 82 | depdir = File.expand_path("../../vendor/deps/#{dep}", __FILE__) 83 | FileUtils.rm_rf(depdir) 84 | FileUtils.mkdir_p(depdir) 85 | dstlib = File.expand_path('lib', depdir) 86 | srclib = File.expand_path('lib', path) 87 | 88 | FileUtils.cp_r(srclib, dstlib) 89 | 90 | rev = head_sha 91 | rev << " (dirty)" if dirty 92 | rev << "\n" 93 | 94 | File.write("#{depdir}/REVISION", rev) 95 | end 96 | -------------------------------------------------------------------------------- /exe/gorails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | Encoding.default_external = Encoding::UTF_8 4 | Encoding.default_internal = Encoding::UTF_8 5 | 6 | unshift_path = ->(path) { 7 | p = File.expand_path("../../#{path}", __FILE__) 8 | $LOAD_PATH.unshift(p) unless $LOAD_PATH.include?(p) 9 | } 10 | unshift_path.call("vendor/deps/cli-ui/lib") 11 | unshift_path.call("vendor/deps/cli-kit/lib") 12 | unshift_path.call("lib") 13 | 14 | require "gorails" 15 | 16 | exit(Gorails::ErrorHandler.call do 17 | Gorails::EntryPoint.call(ARGV.dup) 18 | end) 19 | -------------------------------------------------------------------------------- /gorails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/gorails/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "gorails" 7 | spec.version = Gorails::VERSION 8 | spec.authors = ["Chris Oliver"] 9 | spec.email = ["excid3@gmail.com"] 10 | 11 | spec.summary = "GoRails rubygem" 12 | spec.description = "The GoRails rubygem" 13 | spec.homepage = "https://github.com/excid3/gorails-ruby" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 2.6.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = spec.homepage 19 | # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 24 | `git ls-files -z`.split("\x0").reject do |f| 25 | (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) 26 | end 27 | end 28 | spec.bindir = "exe" 29 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 30 | spec.require_paths = ["lib"] 31 | 32 | spec.add_development_dependency "standard" 33 | 34 | # For more information and examples about making a new gem, checkout our 35 | # guide at: https://bundler.io/guides/creating_gem.html 36 | end 37 | -------------------------------------------------------------------------------- /lib/gorails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "gorails/version" 4 | require "cli/ui" 5 | require "cli/kit" 6 | 7 | CLI::UI::StdoutRouter.enable 8 | 9 | module Gorails 10 | class Error < StandardError; end 11 | 12 | TOOL_NAME = "gorails" 13 | ROOT = File.expand_path("../..", __FILE__) 14 | LOG_FILE = "/tmp/gorails.log" 15 | 16 | autoload(:EntryPoint, "gorails/entry_point") 17 | autoload(:Commands, "gorails/commands") 18 | 19 | Config = CLI::Kit::Config.new(tool_name: TOOL_NAME) 20 | Command = CLI::Kit::BaseCommand 21 | 22 | Executor = CLI::Kit::Executor.new(log_file: LOG_FILE) 23 | Resolver = CLI::Kit::Resolver.new( 24 | tool_name: TOOL_NAME, 25 | command_registry: Gorails::Commands::Registry 26 | ) 27 | 28 | ErrorHandler = CLI::Kit::ErrorHandler.new(log_file: LOG_FILE) 29 | end 30 | -------------------------------------------------------------------------------- /lib/gorails/commands.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | 3 | module Gorails 4 | module Commands 5 | Registry = CLI::Kit::CommandRegistry.new(default: "help") 6 | 7 | def self.register(const, cmd, path) 8 | autoload(const, path) 9 | Registry.add(-> { const_get(const) }, cmd) 10 | end 11 | 12 | register :Episodes, "episodes", "gorails/commands/episodes" 13 | register :Jobs, "jobs", "gorails/commands/jobs" 14 | register :Jumpstart, "jumpstart", "gorails/commands/jumpstart" 15 | register :Railsbytes, "railsbytes", "gorails/commands/railsbytes" 16 | register :Version, "version", "gorails/commands/version" 17 | register :Help, "help", "gorails/commands/help" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/gorails/commands/episodes.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | require "net/http" 3 | require "json" 4 | 5 | module Gorails 6 | module Commands 7 | class Episodes < Gorails::Command 8 | def call(_args, _name) 9 | episodes = JSON.parse Net::HTTP.get(URI("https://gorails.com/episodes.json")) 10 | 11 | CLI::UI::Frame.open("Latest GoRails episodes") do 12 | episodes.each do |episode| 13 | puts CLI::UI.fmt "##{episode["number"]} {{green:#{episode["name"]}}}" 14 | puts episode["url"] 15 | puts 16 | end 17 | end 18 | end 19 | 20 | def self.help 21 | "View the latest GoRails episodes.\nUsage: {{command:#{Gorails::TOOL_NAME} episodes}}" 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gorails/commands/example.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | 3 | module Gorails 4 | module Commands 5 | class Example < Gorails::Command 6 | def call(_args, _name) 7 | puts "neato" 8 | 9 | if rand < 0.05 10 | raise(CLI::Kit::Abort, "you got unlucky!") 11 | end 12 | end 13 | 14 | def self.help 15 | "A dummy command.\nUsage: {{command:#{Gorails::TOOL_NAME} example}}" 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/gorails/commands/help.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | 3 | module Gorails 4 | module Commands 5 | class Help < Gorails::Command 6 | def call(args, _name) 7 | puts CLI::UI.fmt("{{bold:Available commands}}") 8 | puts "" 9 | 10 | Gorails::Commands::Registry.resolved_commands.each do |name, klass| 11 | next if name == "help" 12 | puts CLI::UI.fmt("{{command:#{Gorails::TOOL_NAME} #{name}}}") 13 | if (help = klass.help) 14 | puts CLI::UI.fmt(help) 15 | end 16 | puts "" 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gorails/commands/jobs.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | require "net/http" 3 | require "json" 4 | 5 | module Gorails 6 | module Commands 7 | class Jobs < Gorails::Command 8 | def call(_args, _name) 9 | jobs = JSON.parse Net::HTTP.get(URI("https://jobs.gorails.com/jobs.json")) 10 | 11 | CLI::UI::Frame.open("Ruby on Rails jobs") do 12 | jobs.each do |job| 13 | puts CLI::UI.fmt "{{green:#{job["title"]}}} at {{blue:#{job.dig("company", "name")}}}" 14 | puts job["url"] 15 | puts 16 | end 17 | end 18 | end 19 | 20 | def self.help 21 | "View the latest Ruby on Rails jobs.\nUsage: {{command:#{Gorails::TOOL_NAME} jobs}}" 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gorails/commands/jumpstart.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | require "net/http" 3 | require "json" 4 | 5 | module Gorails 6 | module Commands 7 | class Jumpstart < Gorails::Command 8 | def call(args, _name) 9 | name = args.first 10 | 11 | if name.nil? 12 | puts CLI::UI.fmt "{{red:Application name is required.}}" 13 | puts "Try again with \"gorails jumpstart myapp\"" 14 | exit 1 15 | end 16 | 17 | CLI::UI::Frame.open("Generating Jumpstart application \"#{name}\"") do 18 | Bundler.with_original_env do 19 | system "rails new #{name} -d postgresql -m https://raw.githubusercontent.com/excid3/jumpstart/master/template.rb" 20 | end 21 | end 22 | end 23 | 24 | def self.help 25 | "Create a new Ruby on Rails application with the Jumpstart template. https://github.com/excid3/jumpstart\nUsage: {{command:#{Gorails::TOOL_NAME} jumpstart myapp}}" 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/gorails/commands/railsbytes.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | require "json" 3 | require "net/http" 4 | 5 | module Gorails 6 | module Commands 7 | class Railsbytes < Gorails::Command 8 | def call(args, _name) 9 | if args.none? 10 | list 11 | else 12 | apply(args.first) 13 | end 14 | end 15 | 16 | def self.help 17 | <<~EOF 18 | View the latest Railsbytes templates or load a template by ID. 19 | Usage: 20 | {{command:#{Gorails::TOOL_NAME} railsbytes}} 21 | {{command:#{Gorails::TOOL_NAME} railsbytes x7msKX}} 22 | EOF 23 | end 24 | 25 | def list 26 | bytes = JSON.parse Net::HTTP.get(URI("https://railsbytes.com/public/templates.json")) 27 | 28 | CLI::UI::Frame.open("Railsbytes") do 29 | bytes.each do |byte| 30 | puts CLI::UI.fmt "{{green:#{byte["name"]}}} by #{byte["created_by"]}" 31 | puts byte["short_description"] 32 | puts "#{byte["id"]} - #{byte["url"]}" 33 | puts 34 | end 35 | end 36 | end 37 | 38 | def apply(id) 39 | byte = JSON.parse Net::HTTP.get(URI("https://railsbytes.com/public/templates/#{id}.json")) 40 | 41 | CLI::UI::Frame.open("Railsbyte") do 42 | puts CLI::UI.fmt "{{green:#{byte["name"]}}} by #{byte["created_by"]}" 43 | puts byte["short_description"] 44 | puts 45 | 46 | CLI::UI::Prompt.ask("What would you like to do?") do |handler| 47 | handler.option("Apply Railsbyte") do |selection| 48 | puts 49 | puts "Running Railsbyte..." 50 | puts 51 | 52 | system "rails app:template LOCATION=\"https://railsbytes.com/script/#{id}\"" 53 | end 54 | 55 | handler.option("View source") do |selection| 56 | puts 57 | puts byte["script"] 58 | end 59 | 60 | handler.option("Exit") { |selection| exit 0 } 61 | end 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/gorails/commands/version.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | 3 | module Gorails 4 | module Commands 5 | class Version < Gorails::Command 6 | def call(_args, _name) 7 | puts Gorails::VERSION 8 | end 9 | 10 | def self.help 11 | "Prints the version.\nUsage: {{command:#{Gorails::TOOL_NAME} version}}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/gorails/entry_point.rb: -------------------------------------------------------------------------------- 1 | require "gorails" 2 | 3 | module Gorails 4 | module EntryPoint 5 | def self.call(args) 6 | cmd, command_name, args = Gorails::Resolver.call(args) 7 | Gorails::Executor.call(cmd, command_name, args) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/gorails/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Gorails 4 | VERSION = "0.1.5" 5 | end 6 | -------------------------------------------------------------------------------- /sig/gorails.rbs: -------------------------------------------------------------------------------- 1 | module Gorails 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /test/gorails_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class GorailsTest < Minitest::Test 6 | def test_that_it_has_a_version_number 7 | refute_nil ::Gorails::VERSION 8 | end 9 | 10 | def test_it_does_something_useful 11 | assert false 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 4 | require "gorails" 5 | 6 | require "minitest/autorun" 7 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/REVISION: -------------------------------------------------------------------------------- 1 | 635eab627526f34c7ce15d2c894633246493b1a4 2 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui' 3 | 4 | unless defined?(T) 5 | require('cli/kit/sorbet_runtime_stub') 6 | end 7 | 8 | require 'cli/kit/core_ext' 9 | 10 | module CLI 11 | module Kit 12 | extend T::Sig 13 | 14 | autoload :Args, 'cli/kit/args' 15 | autoload :BaseCommand, 'cli/kit/base_command' 16 | autoload :CommandRegistry, 'cli/kit/command_registry' 17 | autoload :CommandHelp, 'cli/kit/command_help' 18 | autoload :Config, 'cli/kit/config' 19 | autoload :ErrorHandler, 'cli/kit/error_handler' 20 | autoload :Executor, 'cli/kit/executor' 21 | autoload :Ini, 'cli/kit/ini' 22 | autoload :Levenshtein, 'cli/kit/levenshtein' 23 | autoload :Logger, 'cli/kit/logger' 24 | autoload :Opts, 'cli/kit/opts' 25 | autoload :Resolver, 'cli/kit/resolver' 26 | autoload :Support, 'cli/kit/support' 27 | autoload :System, 'cli/kit/system' 28 | autoload :Util, 'cli/kit/util' 29 | 30 | EXIT_FAILURE_BUT_NOT_BUG = 30 31 | EXIT_BUG = 1 32 | EXIT_SUCCESS = 0 33 | 34 | # Abort, Bug, AbortSilent, and BugSilent are four ways of immediately bailing 35 | # on command-line execution when an unrecoverable error occurs. 36 | # 37 | # Note that these don't inherit from StandardError, and so are not caught by 38 | # a bare `rescue => e`. 39 | # 40 | # * Abort prints its message in red and exits 1; 41 | # * Bug additionally submits the exception to the exception_reporter passed to 42 | # `CLI::Kit::ErrorHandler.new` 43 | # * AbortSilent and BugSilent do the same as above, but do not print 44 | # messages before exiting. 45 | # 46 | # Treat these like panic() in Go: 47 | # * Don't rescue them. Use a different Exception class if you plan to recover; 48 | # * Provide a useful message, since it will be presented in brief to the 49 | # user, and will be useful for debugging. 50 | # * Avoid using it if it does actually make sense to recover from an error. 51 | # 52 | # Additionally: 53 | # * Do not subclass these. 54 | # * Only use AbortSilent or BugSilent if you prefer to print a more 55 | # contextualized error than Abort or Bug would present to the user. 56 | # * In general, don't attach a message to AbortSilent or BugSilent. 57 | # * Never raise GenericAbort directly. 58 | # * Think carefully about whether Abort or Bug is more appropriate. Is this 59 | # a bug in the tool? Or is it just user error, transient network 60 | # failure, etc.? 61 | # * One case where it's ok to rescue (cli-kit internals or tests aside): 62 | # 1. rescue Abort or Bug 63 | # 2. Print a contextualized error message 64 | # 3. Re-raise AbortSilent or BugSilent respectively. 65 | # 66 | # These aren't the only exceptions that can carry this 'bug' and 'silent' 67 | # metadata, however: 68 | # 69 | # If you raise an exception with `CLI::Kit.raise(..., bug: x, silent: y)`, 70 | # those last two (optional) keyword arguments will attach the metadata to 71 | # whatever exception you raise. This is interpreted later in the 72 | # ErrorHandler to decide how to print output and whether to submit the 73 | # exception to bugsnag. 74 | GenericAbort = Class.new(Exception) # rubocop:disable Lint/InheritException 75 | 76 | class Abort < GenericAbort # bug:false; silent: false 77 | extend(T::Sig) 78 | 79 | sig { returns(T::Boolean) } 80 | def bug? 81 | false 82 | end 83 | end 84 | 85 | class Bug < GenericAbort # bug:true; silent:false 86 | end 87 | 88 | class BugSilent < GenericAbort # bug:true; silent:true 89 | extend(T::Sig) 90 | 91 | sig { returns(T::Boolean) } 92 | def silent? 93 | true 94 | end 95 | end 96 | 97 | class AbortSilent < GenericAbort # bug:false; silent:true 98 | extend(T::Sig) 99 | 100 | sig { returns(T::Boolean) } 101 | def bug? 102 | false 103 | end 104 | 105 | sig { returns(T::Boolean) } 106 | def silent? 107 | true 108 | end 109 | end 110 | 111 | # Mirrors the API of Kernel#raise, but with the addition of a few new 112 | # optional keyword arguments. `bug` and `silent` attach metadata to the 113 | # exception being raised, which is interpreted later in the ErrorHandler to 114 | # decide what to print and whether to submit to bugsnag. 115 | # 116 | # `depth` is used to trim leading elements of the backtrace. If you wrap 117 | # this method in your own wrapper, you'll want to pass `depth: 2`, for 118 | # example. 119 | sig do 120 | params( 121 | exception: T.any(Class, String, Exception), 122 | string: T.untyped, 123 | array: T.nilable(T::Array[String]), 124 | cause: T.nilable(Exception), 125 | bug: T.nilable(T::Boolean), 126 | silent: T.nilable(T::Boolean), 127 | depth: Integer, 128 | ).returns(T.noreturn) 129 | end 130 | def self.raise( 131 | # default arguments 132 | exception = T.unsafe(nil), string = T.unsafe(nil), array = T.unsafe(nil), cause: $ERROR_INFO, 133 | # new arguments 134 | bug: nil, silent: nil, depth: 1 135 | ) 136 | if array 137 | T.unsafe(Kernel).raise(exception, string, array, cause: cause) 138 | elsif string 139 | T.unsafe(Kernel).raise(exception, string, Kernel.caller(depth), cause: cause) 140 | elsif exception.is_a?(String) 141 | T.unsafe(Kernel).raise(RuntimeError, exception, Kernel.caller(depth), cause: cause) 142 | else 143 | T.unsafe(Kernel).raise(exception, exception.message, Kernel.caller(depth), cause: cause) 144 | end 145 | rescue Exception => e # rubocop:disable Lint/RescueException 146 | e.bug!(bug) unless bug.nil? 147 | e.silent!(silent) unless silent.nil? 148 | Kernel.raise(e, cause: cause) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/args.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module Args 7 | Error = Class.new(StandardError) 8 | 9 | autoload :Definition, 'cli/kit/args/definition' 10 | autoload :Parser, 'cli/kit/args/parser' 11 | autoload :Evaluation, 'cli/kit/args/evaluation' 12 | autoload :Tokenizer, 'cli/kit/args/tokenizer' 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/args/definition.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module Args 7 | class Definition 8 | extend T::Sig 9 | 10 | Error = Class.new(Args::Error) 11 | ConflictingFlag = Class.new(Error) 12 | InvalidFlag = Class.new(Error) 13 | InvalidLookup = Class.new(Error) 14 | InvalidPosition = Class.new(Error) 15 | 16 | sig { returns(T::Array[Flag]) } 17 | attr_reader :flags 18 | 19 | sig { returns(T::Array[Option]) } 20 | attr_reader :options 21 | 22 | sig { returns(T::Array[Position]) } 23 | attr_reader :positions 24 | 25 | sig { params(name: Symbol, short: T.nilable(String), long: T.nilable(String), desc: T.nilable(String)).void } 26 | def add_flag(name, short: nil, long: nil, desc: nil) 27 | short, long = strip_prefixes_and_validate(short, long) 28 | flag = Flag.new(name: name, short: short, long: long, desc: desc) 29 | add_resolution(flag) 30 | @flags << flag 31 | end 32 | 33 | sig do 34 | params( 35 | name: Symbol, short: T.nilable(String), long: T.nilable(String), 36 | desc: T.nilable(String), default: T.any(NilClass, String, T.proc.returns(String)), 37 | required: T::Boolean, multi: T::Boolean, 38 | ).void 39 | end 40 | def add_option(name, short: nil, long: nil, desc: nil, default: nil, required: false, multi: false) 41 | short, long = strip_prefixes_and_validate(short, long) 42 | option = Option.new( 43 | name: name, short: short, long: long, desc: desc, default: default, 44 | required: required, multi: multi, 45 | ) 46 | add_resolution(option) 47 | @options << option 48 | end 49 | 50 | sig { params(name: Symbol, required: T::Boolean, multiple: T::Boolean, desc: T.nilable(String)).void } 51 | def add_position(name, required:, multiple:, desc: nil) 52 | index = @positions.size 53 | position = Position.new(name: name, desc: desc, required: required, multiple: multiple, index: index) 54 | validate_order(position) 55 | add_name_resolution(position) 56 | @positions << position 57 | end 58 | 59 | sig { void } 60 | def initialize 61 | @flags = [] 62 | @options = [] 63 | @by_short = {} 64 | @by_long = {} 65 | @by_name = {} 66 | @positions = [] 67 | end 68 | 69 | class Flag 70 | extend T::Sig 71 | 72 | sig { returns(Symbol) } 73 | attr_reader :name 74 | 75 | sig { returns(T.nilable(String)) } 76 | attr_reader :short 77 | 78 | sig { returns(T.nilable(String)) } 79 | attr_reader :long 80 | 81 | sig { returns(T.nilable(String)) } 82 | attr_reader :desc 83 | 84 | sig { returns(String) } 85 | def as_written_by_user 86 | long ? "--#{long}" : "-#{short}" 87 | end 88 | 89 | sig { params(name: Symbol, short: T.nilable(String), long: T.nilable(String), desc: T.nilable(String)).void } 90 | def initialize(name:, short: nil, long: nil, desc: nil) 91 | if long&.start_with?('-') || short&.start_with?('-') 92 | raise(ArgumentError, 'invalid - prefix') 93 | end 94 | 95 | @name = name 96 | @short = short 97 | @long = long 98 | @desc = desc 99 | end 100 | end 101 | 102 | class Position 103 | extend T::Sig 104 | 105 | sig { returns(Symbol) } 106 | attr_reader :name 107 | 108 | sig { returns(T.nilable(String)) } 109 | attr_reader :desc 110 | 111 | sig { returns(Integer) } 112 | attr_reader :index 113 | 114 | sig do 115 | params(name: Symbol, desc: T.nilable(String), required: T::Boolean, multiple: T::Boolean, index: Integer) 116 | .void 117 | end 118 | def initialize(name:, desc:, required:, multiple:, index:) 119 | raise(ArgumentError, 'Cannot be required and multiple') if required && multiple 120 | 121 | @name = name 122 | @desc = desc 123 | @required = required 124 | @multiple = multiple 125 | @index = index 126 | end 127 | 128 | sig { returns(T::Boolean) } 129 | def required? 130 | @required 131 | end 132 | 133 | sig { returns(T::Boolean) } 134 | def multiple? 135 | @multiple 136 | end 137 | 138 | sig { returns(T::Boolean) } 139 | def optional? 140 | !required? 141 | end 142 | end 143 | 144 | class Option < Flag 145 | extend T::Sig 146 | 147 | sig { returns(T.nilable(String)) } 148 | def default 149 | if @default.is_a?(Proc) 150 | @default.call 151 | else 152 | @default 153 | end 154 | end 155 | 156 | sig { returns(T::Boolean) } 157 | def dynamic_default? 158 | @default.is_a?(Proc) 159 | end 160 | 161 | sig { returns(T::Boolean) } 162 | attr_reader :required 163 | 164 | sig { returns(T::Boolean) } 165 | attr_reader :multi 166 | 167 | sig do 168 | params( 169 | name: Symbol, short: T.nilable(String), long: T.nilable(String), 170 | desc: T.nilable(String), default: T.any(NilClass, String, T.proc.returns(String)), 171 | required: T::Boolean, multi: T::Boolean, 172 | ).void 173 | end 174 | def initialize(name:, short: nil, long: nil, desc: nil, default: nil, required: false, multi: false) 175 | if multi && (default || required) 176 | raise(ArgumentError, 'multi-valued options cannot have a default or required value') 177 | end 178 | 179 | super(name: name, short: short, long: long, desc: desc) 180 | @default = default 181 | @required = required 182 | @multi = multi 183 | end 184 | end 185 | 186 | sig { params(name: Symbol).returns(T.nilable(Flag)) } 187 | def lookup_flag(name) 188 | flagopt = @by_name[name] 189 | if flagopt.class == Flag 190 | flagopt 191 | end 192 | end 193 | 194 | sig { params(name: Symbol).returns(T.nilable(Option)) } 195 | def lookup_option(name) 196 | flagopt = @by_name[name] 197 | if flagopt.class == Option 198 | flagopt 199 | end 200 | end 201 | 202 | sig { params(name: String).returns(T.any(Flag, Option, NilClass)) } 203 | def lookup_short(name) 204 | raise(InvalidLookup, "invalid '-' prefix") if name.start_with?('-') 205 | 206 | @by_short[name] 207 | end 208 | 209 | sig { params(name: String).returns(T.any(Flag, Option, NilClass)) } 210 | def lookup_long(name) 211 | raise(InvalidLookup, "invalid '-' prefix") if name.start_with?('-') 212 | 213 | @by_long[name] 214 | end 215 | 216 | sig { params(name: Symbol).returns(T.nilable(Position)) } 217 | def lookup_position(name) 218 | position = @by_name[name] 219 | if position.class == Position 220 | position 221 | end 222 | end 223 | 224 | private 225 | 226 | sig { params(position: Position).void } 227 | def validate_order(position) 228 | if @positions.last&.multiple? 229 | raise(InvalidPosition, 'Cannot have any more positional arguments after multiple') 230 | elsif @positions.last&.optional? && !position.optional? 231 | raise(InvalidPosition, 'Cannot have any more required positional arguments after optional ones') 232 | end 233 | end 234 | 235 | sig { params(short: String).returns(String) } 236 | def strip_short_prefix(short) 237 | unless short.match?(/^-[^-]/) 238 | raise(InvalidFlag, "Short flag '#{short}' does not start with '-'") 239 | end 240 | if short.size != 2 241 | raise(InvalidFlag, 'Short flag must be a single character') 242 | end 243 | 244 | short.sub(/^-/, '') 245 | end 246 | 247 | sig { params(long: String).returns(String) } 248 | def strip_long_prefix(long) 249 | unless long.match?(/^--[^-]/) 250 | raise(InvalidFlag, "Long flag '#{long}' does not start with '--'") 251 | end 252 | 253 | long.sub(/^--/, '') 254 | end 255 | 256 | sig do 257 | params(short: T.nilable(String), long: T.nilable(String)) 258 | .returns([T.nilable(String), T.nilable(String)]) 259 | end 260 | def strip_prefixes_and_validate(short, long) 261 | if short.nil? && long.nil? 262 | raise(Error, 'One or more of short and long must be specified') 263 | end 264 | 265 | short = strip_short_prefix(short) if short 266 | long = strip_long_prefix(long) if long 267 | 268 | [short, long] 269 | end 270 | 271 | sig { params(flagopt: Flag).void } 272 | def add_resolution(flagopt) 273 | if flagopt.short 274 | if (existing = @by_short[flagopt.short]) 275 | raise(ConflictingFlag, "Short flag '#{flagopt.short}' already defined by #{existing.name}") 276 | end 277 | 278 | @by_short[flagopt.short] = flagopt 279 | end 280 | if flagopt.long 281 | if (existing = @by_long[flagopt.long]) 282 | raise(ConflictingFlag, "Long flag '#{flagopt.long}' already defined by #{existing.name}") 283 | end 284 | 285 | @by_long[flagopt.long] = flagopt 286 | end 287 | add_name_resolution(flagopt) 288 | end 289 | 290 | sig { params(arg: T.any(Flag, Position)).void } 291 | def add_name_resolution(arg) 292 | if (existing = @by_name[arg.name]) 293 | raise(ConflictingFlag, "Flag '#{arg.name}' already defined by #{existing.name}") 294 | end 295 | 296 | @by_name[arg.name] = arg 297 | end 298 | end 299 | end 300 | end 301 | end 302 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module Args 7 | class Evaluation 8 | extend T::Sig 9 | 10 | Error = Class.new(Args::Error) 11 | 12 | class MissingRequiredOption < Error 13 | extend T::Sig 14 | sig { params(name: String).void } 15 | def initialize(name) 16 | super("missing required option `#{name}'") 17 | end 18 | end 19 | 20 | class MissingRequiredPosition < Error 21 | extend T::Sig 22 | sig { void } 23 | def initialize 24 | super('more arguments required') 25 | end 26 | end 27 | 28 | class TooManyPositions < Error 29 | extend T::Sig 30 | sig { void } 31 | def initialize 32 | super('too many arguments') 33 | end 34 | end 35 | 36 | class FlagProxy 37 | extend T::Sig 38 | 39 | sig { params(sym: Symbol).returns(T::Boolean) } 40 | def method_missing(sym) 41 | flag = @evaluation.defn.lookup_flag(sym) 42 | unless flag 43 | raise NoMethodError, "undefined flag `#{sym}' for #{self}" 44 | end 45 | 46 | @evaluation.send(:lookup_flag, flag) 47 | end 48 | 49 | sig { params(sym: Symbol, include_private: T::Boolean).returns(T::Boolean) } 50 | def respond_to_missing?(sym, include_private = false) 51 | !!@evaluation.defn.lookup_flag(sym) 52 | end 53 | 54 | sig { params(evaluation: Evaluation).void } 55 | def initialize(evaluation) 56 | @evaluation = evaluation 57 | end 58 | end 59 | 60 | class OptionProxy 61 | extend T::Sig 62 | 63 | sig { params(sym: Symbol).returns(T.any(NilClass, String, T::Array[String])) } 64 | def method_missing(sym) 65 | opt = @evaluation.defn.lookup_option(sym) 66 | unless opt 67 | raise NoMethodError, "undefined option `#{sym}' for #{self}" 68 | end 69 | 70 | @evaluation.send(:lookup_option, opt) 71 | end 72 | 73 | sig { params(sym: Symbol, include_private: T::Boolean).returns(T::Boolean) } 74 | def respond_to_missing?(sym, include_private = false) 75 | !!@evaluation.defn.lookup_option(sym) 76 | end 77 | 78 | sig { params(evaluation: Evaluation).void } 79 | def initialize(evaluation) 80 | @evaluation = evaluation 81 | end 82 | end 83 | 84 | class PositionProxy 85 | extend T::Sig 86 | 87 | sig { params(sym: Symbol).returns(T.any(NilClass, String, T::Array[String])) } 88 | def method_missing(sym) 89 | position = @evaluation.defn.lookup_position(sym) 90 | unless position 91 | raise NoMethodError, "undefined position `#{sym}' for #{self}" 92 | end 93 | 94 | @evaluation.send(:lookup_position, position) 95 | end 96 | 97 | sig { params(sym: Symbol, include_private: T::Boolean).returns(T::Boolean) } 98 | def respond_to_missing?(sym, include_private = false) 99 | !!@evaluation.defn.lookup_position(sym) 100 | end 101 | 102 | sig { params(evaluation: Evaluation).void } 103 | def initialize(evaluation) 104 | @evaluation = evaluation 105 | end 106 | end 107 | 108 | sig { returns(FlagProxy) } 109 | def flag 110 | @flag_proxy ||= FlagProxy.new(self) 111 | end 112 | 113 | sig { returns(OptionProxy) } 114 | def opt 115 | @option_proxy ||= OptionProxy.new(self) 116 | end 117 | 118 | sig { returns(PositionProxy) } 119 | def position 120 | @position_proxy ||= PositionProxy.new(self) 121 | end 122 | 123 | sig { returns(Definition) } 124 | attr_reader :defn 125 | 126 | sig { returns(T::Array[Parser::Node]) } 127 | attr_reader :parse 128 | 129 | sig { returns(T::Array[String]) } 130 | def unparsed 131 | @unparsed ||= begin 132 | nodes = T.cast( 133 | parse.select { |node| node.is_a?(Parser::Node::Unparsed) }, 134 | T::Array[Parser::Node::Unparsed], 135 | ) 136 | nodes.flat_map(&:value) 137 | end 138 | end 139 | 140 | sig { params(defn: Definition, parse: T::Array[Parser::Node]).void } 141 | def initialize(defn, parse) 142 | @defn = defn 143 | @parse = parse 144 | check_required! 145 | end 146 | 147 | sig { void } 148 | def check_required! 149 | @defn.options.each do |opt| 150 | next unless opt.required 151 | 152 | node = @parse.detect do |node| 153 | node.is_a?(Parser::Node::Option) && node.name == opt.name 154 | end 155 | if !node || T.cast(node, Parser::Node::Option).value.nil? 156 | raise(MissingRequiredOption, opt.as_written_by_user) 157 | end 158 | end 159 | min_positions = @defn.positions.count(&:required?) 160 | max_positions = if @defn.positions.last&.multiple? 161 | Float::INFINITY 162 | else 163 | min_positions + @defn.positions.count(&:optional?) 164 | end 165 | raise(MissingRequiredPosition) if args.size < min_positions 166 | raise(TooManyPositions) if args.size > max_positions 167 | end 168 | 169 | sig { params(flag: Definition::Flag).returns(T::Boolean) } 170 | def lookup_flag(flag) 171 | if flag.short 172 | flags = T.cast( 173 | parse.select { |node| node.is_a?(Parser::Node::ShortFlag) }, 174 | T::Array[Parser::Node::ShortFlag], 175 | ) 176 | return true if flags.any? { |node| node.value == flag.short } 177 | end 178 | if flag.long 179 | flags = T.cast( 180 | parse.select { |node| node.is_a?(Parser::Node::LongFlag) }, 181 | T::Array[Parser::Node::LongFlag], 182 | ) 183 | return true if flags.any? { |node| node.value == flag.long } 184 | end 185 | false 186 | end 187 | 188 | sig { params(opt: Definition::Option).returns(T.any(NilClass, String, T::Array[String])) } 189 | def lookup_option(opt) 190 | if opt.short 191 | opts = T.cast( 192 | parse.select { |node| node.is_a?(Parser::Node::ShortOption) }, 193 | T::Array[Parser::Node::ShortOption], 194 | ) 195 | matches = opts.reverse.select { |node| node.name == opt.short } 196 | if (first = matches.first) 197 | return(opt.multi ? matches.map(&:value) : first.value) 198 | end 199 | end 200 | if opt.long 201 | opts = T.cast( 202 | parse.select { |node| node.is_a?(Parser::Node::LongOption) }, 203 | T::Array[Parser::Node::LongOption], 204 | ) 205 | matches = opts.reverse.select { |node| node.name == opt.long } 206 | if (first = matches.first) 207 | return(opt.multi ? matches.map(&:value) : first.value) 208 | end 209 | end 210 | opt.multi ? [] : opt.default 211 | end 212 | 213 | sig { params(position: Definition::Position).returns(T.any(NilClass, String, T::Array[String])) } 214 | def lookup_position(position) 215 | if position.multiple? 216 | args[position.index..] 217 | else 218 | args[position.index] 219 | end 220 | end 221 | 222 | private 223 | 224 | sig { returns(T::Array[String]) } 225 | def args 226 | @args ||= begin 227 | nodes = T.cast( 228 | parse.select { |node| node.is_a?(Parser::Node::Argument) }, 229 | T::Array[Parser::Node::Argument], 230 | ) 231 | nodes.map(&:value) 232 | end 233 | end 234 | end 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/args/parser.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module Args 7 | class Parser 8 | extend T::Sig 9 | 10 | autoload :Node, 'cli/kit/args/parser/node' 11 | 12 | Error = Class.new(Args::Error) 13 | 14 | class InvalidOptionError < Error 15 | extend T::Sig 16 | sig { params(option: String).void } 17 | def initialize(option) 18 | super("invalid option -- '#{option}'") 19 | end 20 | end 21 | 22 | class OptionRequiresAnArgumentError < Error 23 | extend T::Sig 24 | sig { params(option: String).void } 25 | def initialize(option) 26 | super("option requires an argument -- '#{option}'") 27 | end 28 | end 29 | 30 | sig { params(tokens: T::Array[Tokenizer::Token]).returns(T::Array[Node]) } 31 | def parse(tokens) 32 | nodes = T.let([], T::Array[Node]) 33 | args = T.let(tokens, T::Array[T.nilable(Tokenizer::Token)]) 34 | args << nil # to make each_cons pass (args.last, nil) on the final round. 35 | state = :init 36 | # TODO: test that "--height -- 3" is parsed correctly. 37 | args.each_cons(2) do |(arg, next_arg)| 38 | case state 39 | when :skip 40 | state = :init 41 | when :init 42 | state, val = parse_token(T.must(arg), next_arg) 43 | nodes << val 44 | when :unparsed 45 | unless arg.is_a?(Tokenizer::Token::UnparsedArgument) 46 | raise(Error, 'bug: non-unparsed argument after unparsed argument') 47 | end 48 | 49 | unparsed = nodes.last 50 | unless unparsed.is_a?(Node::Unparsed) 51 | # :nocov: not actually possible, in theory 52 | raise(Error, 'bug: parser failed to recognize first unparsed argument') 53 | # :nocov: 54 | end 55 | 56 | unparsed.value << arg.value 57 | end 58 | end 59 | nodes 60 | end 61 | 62 | sig { params(definition: Definition).void } 63 | def initialize(definition) 64 | @defn = definition 65 | end 66 | 67 | private 68 | 69 | sig do 70 | params(token: Tokenizer::Token, next_token: T.nilable(Tokenizer::Token)) 71 | .returns([Symbol, Parser::Node]) 72 | end 73 | def parse_token(token, next_token) 74 | case token 75 | when Tokenizer::Token::LongOptionName 76 | case @defn.lookup_long(token.value) 77 | when Definition::Option 78 | [:skip, parse_option(token, next_token)] 79 | when Definition::Flag 80 | [:init, Node::LongFlag.new(token.value)] 81 | else 82 | raise(InvalidOptionError, token.value) 83 | end 84 | when Tokenizer::Token::ShortOptionName 85 | case @defn.lookup_short(token.value) 86 | when Definition::Option 87 | [:skip, parse_option(token, next_token)] 88 | when Definition::Flag 89 | [:init, Node::ShortFlag.new(token.value)] 90 | else 91 | raise(InvalidOptionError, token.value) 92 | end 93 | when Tokenizer::Token::OptionValue 94 | raise(Error, "bug: unexpected option value in argument parse sequence: #{token.value}") 95 | when Tokenizer::Token::PositionalArgument 96 | [:init, Node::Argument.new(token.value)] 97 | when Tokenizer::Token::OptionValueOrPositionalArgument 98 | [:init, Node::Argument.new(token.value)] 99 | when Tokenizer::Token::UnparsedArgument 100 | [:unparsed, Node::Unparsed.new([token.value])] 101 | else 102 | raise(Error, "bug: unexpected token type: #{token.class}") 103 | end 104 | end 105 | 106 | sig { params(arg: Tokenizer::Token::OptionName, next_arg: T.nilable(Tokenizer::Token)).returns(Node) } 107 | def parse_option(arg, next_arg) 108 | case next_arg 109 | when nil, Tokenizer::Token::LongOptionName, 110 | Tokenizer::Token::ShortOptionName, Tokenizer::Token::PositionalArgument 111 | raise(OptionRequiresAnArgumentError, arg.value) 112 | when Tokenizer::Token::OptionValue, Tokenizer::Token::OptionValueOrPositionalArgument 113 | case arg 114 | when Tokenizer::Token::LongOptionName 115 | Node::LongOption.new(arg.value, next_arg.value) 116 | when Tokenizer::Token::ShortOptionName 117 | Node::ShortOption.new(arg.value, next_arg.value) 118 | else 119 | raise(Error, "bug: unexpected token type: #{arg.class}") 120 | end 121 | else 122 | raise(Error, "bug: unexpected argument type: #{next_arg.class}") 123 | end 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module Args 7 | class Parser 8 | class Node 9 | extend T::Sig 10 | 11 | sig { void } 12 | def initialize 13 | end 14 | 15 | sig { params(other: T.untyped).returns(T::Boolean) } 16 | def ==(other) 17 | self.class == other.class 18 | end 19 | 20 | class Option < Node 21 | extend T::Sig 22 | 23 | sig { returns(String) } 24 | attr_reader :name 25 | 26 | sig { returns(String) } 27 | attr_reader :value 28 | 29 | sig { params(name: String, value: String).void } 30 | def initialize(name, value) 31 | @name = name 32 | @value = value 33 | super() 34 | end 35 | private_class_method(:new) # don't instantiate this class directly 36 | 37 | sig { returns(String) } 38 | def inspect 39 | "#<#{self.class.name} #{@name}=#{@value}>" 40 | end 41 | 42 | sig { params(other: T.untyped).returns(T::Boolean) } 43 | def ==(other) 44 | !!(super(other) && @value == other.value && @name == other.name) 45 | end 46 | end 47 | 48 | class LongOption < Option 49 | public_class_method(:new) 50 | end 51 | 52 | class ShortOption < Option 53 | public_class_method(:new) 54 | end 55 | 56 | class Flag < Node 57 | sig { returns(String) } 58 | attr_reader :value 59 | 60 | sig { params(value: String).void } 61 | def initialize(value) 62 | @value = value 63 | super() 64 | end 65 | private_class_method(:new) # don't instantiate this class directly 66 | 67 | sig { returns(String) } 68 | def inspect 69 | "#<#{self.class.name} #{@value}>" 70 | end 71 | 72 | sig { params(other: T.untyped).returns(T::Boolean) } 73 | def ==(other) 74 | !!(super(other) && @value == other.value) 75 | end 76 | end 77 | 78 | class LongFlag < Flag 79 | public_class_method(:new) 80 | end 81 | 82 | class ShortFlag < Flag 83 | public_class_method(:new) 84 | end 85 | 86 | class Argument < Node 87 | sig { returns(String) } 88 | attr_reader :value 89 | 90 | sig { params(value: String).void } 91 | def initialize(value) 92 | @value = value 93 | super() 94 | end 95 | 96 | sig { returns(String) } 97 | def inspect 98 | "#<#{self.class.name} #{@value}>" 99 | end 100 | 101 | sig { params(other: T.untyped).returns(T::Boolean) } 102 | def ==(other) 103 | !!(super(other) && @value == other.value) 104 | end 105 | end 106 | 107 | class Unparsed < Node 108 | sig { returns(T::Array[String]) } 109 | attr_reader :value 110 | 111 | sig { params(value: T::Array[String]).void } 112 | def initialize(value) 113 | @value = value 114 | super() 115 | end 116 | 117 | sig { returns(String) } 118 | def inspect 119 | "#<#{self.class.name} #{@value.join(" ")}>" 120 | end 121 | 122 | sig { params(other: T.untyped).returns(T::Boolean) } 123 | def ==(other) 124 | !!(super(other) && @value == other.value) 125 | end 126 | end 127 | end 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module Args 7 | module Tokenizer 8 | extend T::Sig 9 | 10 | Error = Class.new(Args::Error) 11 | 12 | class InvalidShortOption < Error 13 | extend T::Sig 14 | sig { params(short_option: String).void } 15 | def initialize(short_option) 16 | super("invalid short option: '-#{short_option}'") 17 | end 18 | end 19 | 20 | class InvalidCharInShortOption < Error 21 | extend T::Sig 22 | sig { params(short_option: String, char: String).void } 23 | def initialize(short_option, char) 24 | super("invalid character '#{char}' in short option: '-#{short_option}'") 25 | end 26 | end 27 | 28 | class Token 29 | extend T::Sig 30 | 31 | sig { returns(String) } 32 | attr_reader :value 33 | 34 | sig { params(value: String).void } 35 | def initialize(value) 36 | @value = value 37 | end 38 | 39 | sig { returns(String) } 40 | def inspect 41 | "#<#{self.class.name} #{@value}>" 42 | end 43 | 44 | sig { params(other: T.untyped).returns(T::Boolean) } 45 | def ==(other) 46 | self.class == other.class && @value == other.value 47 | end 48 | 49 | OptionName = Class.new(Token) 50 | LongOptionName = Class.new(OptionName) 51 | ShortOptionName = Class.new(OptionName) 52 | 53 | OptionValue = Class.new(Token) 54 | PositionalArgument = Class.new(Token) 55 | OptionValueOrPositionalArgument = Class.new(Token) 56 | UnparsedArgument = Class.new(Token) 57 | end 58 | 59 | class << self 60 | extend T::Sig 61 | 62 | sig { params(raw_args: T::Array[String]).returns(T::Array[Token]) } 63 | def tokenize(raw_args) 64 | args = [] 65 | 66 | mode = :init 67 | 68 | raw_args.each do |arg| 69 | case mode 70 | when :unparsed 71 | args << Token::UnparsedArgument.new(arg) 72 | when :init 73 | case arg 74 | when '--' 75 | mode = :unparsed 76 | when /\A--/ 77 | name, value = arg.split('=', 2) 78 | args << Token::LongOptionName.new(T.must(T.must(name)[2..-1])) 79 | if value 80 | args << Token::OptionValue.new(value) 81 | end 82 | when /\A-/ 83 | args.concat(tokenize_short_option(T.must(arg[1..-1]))) 84 | else 85 | args << if args.last.is_a?(Token::OptionName) 86 | Token::OptionValueOrPositionalArgument.new(arg) 87 | else 88 | Token::PositionalArgument.new(arg) 89 | end 90 | end 91 | end 92 | end 93 | 94 | args 95 | end 96 | 97 | sig { params(arg: String).returns(T::Array[Token]) } 98 | def tokenize_short_option(arg) 99 | args = [] 100 | mode = :init 101 | number = +'' 102 | arg.each_char do |char| 103 | case mode 104 | when :numeric 105 | case char 106 | when /[0-9]/ 107 | number << char 108 | else 109 | raise(InvalidShortOption, arg) 110 | end 111 | when :init 112 | case char 113 | when /[a-zA-Z]/ 114 | args << Token::ShortOptionName.new(char) 115 | when /[0-9]/ 116 | mode = :numeric 117 | number << char 118 | else 119 | raise(InvalidCharInShortOption.new(arg, char)) 120 | end 121 | end 122 | end 123 | if number != '' 124 | args << Token::OptionValue.new(number) 125 | end 126 | args 127 | end 128 | end 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/base_command.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | class BaseCommand 7 | extend T::Sig 8 | extend T::Helpers 9 | include CLI::Kit::CommandHelp 10 | extend CLI::Kit::CommandHelp::ClassMethods 11 | abstract! 12 | 13 | sig { returns(T::Boolean) } 14 | def self.defined? 15 | true 16 | end 17 | 18 | sig { params(args: T::Array[String], command_name: String).void } 19 | def self.call(args, command_name) 20 | new.call(args, command_name) 21 | end 22 | 23 | sig { returns(T::Boolean) } 24 | def has_subcommands? 25 | false 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/command_help.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module CommandHelp 7 | extend T::Sig 8 | include Kernel # for sorbet 9 | 10 | sig { params(args: T::Array[String], name: String).void } 11 | def call(args, name) 12 | begin 13 | defn = Args::Definition.new 14 | opts = self.class.opts_class 15 | opts.new(defn).install_to_definition 16 | tokens = Args::Tokenizer.tokenize(args) 17 | parse = Args::Parser.new(defn).parse(tokens) 18 | result = Args::Evaluation.new(defn, parse) 19 | opts_inst = opts.new(result) 20 | rescue Args::Evaluation::TooManyPositions, Args::Evaluation::MissingRequiredPosition => e 21 | STDERR.puts CLI::UI.fmt("{{red:{{bold:Error: #{e.message}}}}}") 22 | STDERR.puts 23 | STDERR.puts self.class.build_help 24 | raise(AbortSilent) 25 | rescue Args::Error => e 26 | raise(Abort, e) 27 | end 28 | 29 | if opts_inst.helpflag 30 | puts self.class.build_help 31 | else 32 | res = begin 33 | opts.new(result) 34 | rescue Args::Error => e 35 | raise(Abort, e) 36 | end 37 | invoke_wrapper(res, name) 38 | end 39 | end 40 | 41 | # use to implement error handling 42 | sig { params(op: T.untyped, name: String).void } 43 | def invoke_wrapper(op, name) 44 | invoke(op, name) 45 | end 46 | 47 | sig { params(op: T.untyped, name: String).void } 48 | def invoke(op, name) 49 | raise(NotImplementedError, '#invoke must be implemented, or #call overridden') 50 | end 51 | 52 | sig { params(name: String).void } 53 | def self.tool_name=(name) 54 | @tool_name = name 55 | end 56 | 57 | sig { returns(String) } 58 | def self._tool_name 59 | unless @tool_name 60 | raise 'You must set CLI::Kit::CommandHelp.tool_name=' 61 | end 62 | 63 | @tool_name 64 | end 65 | 66 | module ClassMethods 67 | extend T::Sig 68 | include Kernel # for sorbet 69 | 70 | DEFAULT_HELP_SECTIONS = [ 71 | :desc, 72 | :long_desc, 73 | :usage, 74 | :examples, 75 | :options, 76 | ] 77 | 78 | sig { returns(String) } 79 | def build_help 80 | h = (@help_sections || DEFAULT_HELP_SECTIONS).map do |section| 81 | case section 82 | when :desc 83 | build_desc 84 | when :long_desc 85 | @long_desc 86 | when :usage 87 | @usage_section ||= build_usage 88 | when :examples 89 | @examples_section ||= build_examples 90 | when :options 91 | @options_section ||= build_options 92 | else 93 | raise "Unknown help section: #{section}" 94 | end 95 | end.compact.map(&:chomp).join("\n\n") + "\n" 96 | CLI::UI.fmt(h) 97 | end 98 | 99 | sig { returns(String) } 100 | def _command_name 101 | return @command_name if @command_name 102 | 103 | last_camel = send(:name).split('::').last 104 | last_camel.gsub(/([a-z])([A-Z])/, '\1-\2').downcase 105 | end 106 | 107 | sig { returns(String) } 108 | def _desc 109 | @desc 110 | end 111 | 112 | sig { returns(String) } 113 | def build_desc 114 | out = +"{{command:#{CommandHelp._tool_name} #{_command_name}}}" 115 | if @desc 116 | out << ": #{@desc}" 117 | end 118 | "{{bold:#{out}}}" 119 | end 120 | 121 | sig { returns(T.untyped) } 122 | def opts_class 123 | T.unsafe(self).const_get(:Opts) # rubocop:disable Sorbet/ConstantsFromStrings 124 | rescue NameError 125 | Class.new(CLI::Kit::Opts) 126 | end 127 | 128 | sig { returns(T.nilable(String)) } 129 | def build_options 130 | opts = opts_class 131 | return(nil) unless opts 132 | 133 | methods = [] 134 | loop do 135 | methods.concat(opts.public_instance_methods(false)) 136 | break if opts.superclass == CLI::Kit::Opts 137 | 138 | opts = opts.superclass 139 | end 140 | 141 | @defn = Args::Definition.new 142 | o = opts.new(@defn) 143 | o.install_to_definition 144 | 145 | return nil if @defn.options.empty? && @defn.flags.empty? 146 | 147 | merged = T.let(@defn.options, T::Array[T.any(Args::Definition::Option, Args::Definition::Flag)]) 148 | merged += @defn.flags 149 | merged.sort_by!(&:name) 150 | "{{bold:Options:}}\n" + merged.map do |o| 151 | if o.is_a?(Args::Definition::Option) 152 | z = ' ' + [o.short&.prepend('-'), o.long&.prepend('--')].compact.join(', ') + ' VALUE' 153 | default = if o.dynamic_default? 154 | '(generated default)' 155 | elsif o.default.nil? 156 | '(no default)' 157 | else 158 | "(default: #{o.default.inspect})" 159 | end 160 | z << if o.desc 161 | " {{italic:{{gray:# #{o.desc} #{default}}}}}" 162 | else 163 | " {{italic:{{gray:# #{default}}}}}" 164 | end 165 | else 166 | z = ' ' + [o.short&.prepend('-'), o.long&.prepend('--')].compact.join(', ') 167 | if o.desc 168 | z << " {{italic:{{gray:# #{o.desc}}}}}" 169 | end 170 | end 171 | z 172 | end.join("\n") 173 | end 174 | 175 | sig { params(sections: T::Array[Symbol]).void } 176 | def help_sections(sections) 177 | @help_sections = sections 178 | end 179 | 180 | sig { params(command_name: String).void } 181 | def command_name(command_name) 182 | if @command_name 183 | raise(ArgumentError, "Command name already set to #{@command_name}") 184 | end 185 | 186 | @command_name = command_name 187 | end 188 | 189 | sig { params(desc: String).void } 190 | def desc(desc) 191 | if desc.size > 80 192 | raise(ArgumentError, 'description must be 80 characters or less') 193 | end 194 | if @desc 195 | raise(ArgumentError, 'description already set') 196 | end 197 | 198 | @desc = desc 199 | end 200 | 201 | sig { params(long_desc: String).void } 202 | def long_desc(long_desc) 203 | if @long_desc 204 | raise(ArgumentError, 'long description already set') 205 | end 206 | 207 | @long_desc = long_desc 208 | end 209 | 210 | sig { returns(String) } 211 | def build_usage 212 | '{{bold:Usage:}}' + case (@usage || []).size 213 | when 0 214 | " {{command:#{CommandHelp._tool_name} #{_command_name}}} [options]\n" 215 | when 1 216 | " {{command:#{CommandHelp._tool_name} #{_command_name}}} #{@usage.first}\n" 217 | else 218 | "\n" + @usage.map do |usage| 219 | " {{command:#{CommandHelp._tool_name} #{_command_name}}} #{usage}\n" 220 | end.join 221 | end 222 | end 223 | 224 | sig { returns(T.nilable(String)) } 225 | def build_examples 226 | return nil unless @examples 227 | 228 | cmd_prefix = " {{command:#{CommandHelp._tool_name} #{_command_name}}}" 229 | "{{bold:Examples:}}\n" + @examples.map do |command, explanation| 230 | cmd = "#{cmd_prefix} #{command}" 231 | exp = "{{italic:{{gray:# #{explanation}}}}}" 232 | 233 | width = CLI::UI::ANSI.printing_width(CLI::UI.fmt("#{cmd} #{exp}")) 234 | if width > CLI::UI::Terminal.width 235 | " #{exp}\n#{cmd}" 236 | else 237 | "#{cmd} #{exp}" 238 | end 239 | end.join("\n\n") 240 | end 241 | 242 | sig { params(usage: String).void } 243 | def usage(usage) 244 | @usage ||= [] 245 | @usage << usage 246 | end 247 | 248 | sig { params(command: String, explanation: T.nilable(String)).void } 249 | def example(command, explanation) 250 | @examples ||= [] 251 | @examples << [command, explanation] 252 | end 253 | end 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/command_registry.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | class CommandRegistry 7 | extend T::Sig 8 | 9 | CommandOrProc = T.type_alias do 10 | T.any(T.class_of(CLI::Kit::BaseCommand), T.proc.returns(T.class_of(CLI::Kit::BaseCommand))) 11 | end 12 | 13 | sig { returns(T::Hash[String, CommandOrProc]) } 14 | attr_reader :commands 15 | 16 | sig { returns(T::Hash[String, String]) } 17 | attr_reader :aliases 18 | 19 | module ContextualResolver 20 | extend T::Sig 21 | extend T::Helpers 22 | interface! 23 | 24 | sig { abstract.returns(T::Array[String]) } 25 | def command_names; end 26 | 27 | sig { abstract.returns(T::Hash[String, String]) } 28 | def aliases; end 29 | 30 | sig { abstract.params(_name: String).returns(T.class_of(CLI::Kit::BaseCommand)) } 31 | def command_class(_name); end 32 | end 33 | 34 | module NullContextualResolver 35 | extend T::Sig 36 | extend ContextualResolver 37 | 38 | sig { override.returns(T::Array[String]) } 39 | def self.command_names 40 | [] 41 | end 42 | 43 | sig { override.returns(T::Hash[String, String]) } 44 | def self.aliases 45 | {} 46 | end 47 | 48 | sig { override.params(_name: String).returns(T.class_of(CLI::Kit::BaseCommand)) } 49 | def self.command_class(_name) 50 | raise(CLI::Kit::Abort, 'Cannot be called on the NullContextualResolver since command_names is empty') 51 | end 52 | end 53 | 54 | sig { params(default: String, contextual_resolver: ContextualResolver).void } 55 | def initialize(default:, contextual_resolver: NullContextualResolver) 56 | @commands = {} 57 | @aliases = {} 58 | @default = default 59 | @contextual_resolver = contextual_resolver 60 | end 61 | 62 | sig { returns(T::Hash[String, T.class_of(CLI::Kit::BaseCommand)]) } 63 | def resolved_commands 64 | @commands.each_with_object({}) do |(k, v), a| 65 | a[k] = resolve_class(v) 66 | end 67 | end 68 | 69 | sig { params(const: CommandOrProc, name: String).void } 70 | def add(const, name) 71 | commands[name] = const 72 | end 73 | 74 | sig { params(name: T.nilable(String)).returns([T.nilable(T.class_of(CLI::Kit::BaseCommand)), String]) } 75 | def lookup_command(name) 76 | name = @default if name.to_s.empty? 77 | resolve_command(T.must(name)) 78 | end 79 | 80 | sig { params(from: String, to: String).void } 81 | def add_alias(from, to) 82 | aliases[from] = to unless aliases[from] 83 | end 84 | 85 | sig { returns(T::Array[String]) } 86 | def command_names 87 | @contextual_resolver.command_names + commands.keys 88 | end 89 | 90 | sig { params(name: String).returns(T::Boolean) } 91 | def exist?(name) 92 | !resolve_command(name).first.nil? 93 | end 94 | 95 | private 96 | 97 | sig { params(name: String).returns(String) } 98 | def resolve_alias(name) 99 | aliases[name] || @contextual_resolver.aliases.fetch(name, name) 100 | end 101 | 102 | sig { params(name: String).returns([T.nilable(T.class_of(CLI::Kit::BaseCommand)), String]) } 103 | def resolve_command(name) 104 | name = resolve_alias(name) 105 | resolve_global_command(name) || \ 106 | resolve_contextual_command(name) || \ 107 | [nil, name] 108 | end 109 | 110 | sig { params(name: String).returns(T.nilable([T.class_of(CLI::Kit::BaseCommand), String])) } 111 | def resolve_global_command(name) 112 | klass = resolve_class(commands.fetch(name, nil)) 113 | return nil unless klass 114 | 115 | [klass, name] 116 | rescue NameError 117 | nil 118 | end 119 | 120 | sig { params(name: String).returns(T.nilable([T.class_of(CLI::Kit::BaseCommand), String])) } 121 | def resolve_contextual_command(name) 122 | found = @contextual_resolver.command_names.include?(name) 123 | return nil unless found 124 | 125 | [@contextual_resolver.command_class(name), name] 126 | end 127 | 128 | sig { params(class_or_proc: T.nilable(CommandOrProc)).returns(T.nilable(T.class_of(CLI::Kit::BaseCommand))) } 129 | def resolve_class(class_or_proc) 130 | case class_or_proc 131 | when nil 132 | nil 133 | when Proc 134 | class_or_proc.call 135 | else 136 | class_or_proc 137 | end 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/config.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | require 'fileutils' 4 | 5 | module CLI 6 | module Kit 7 | class Config 8 | extend T::Sig 9 | 10 | XDG_CONFIG_HOME = 'XDG_CONFIG_HOME' 11 | 12 | sig { params(tool_name: String).void } 13 | def initialize(tool_name:) 14 | @tool_name = tool_name 15 | end 16 | 17 | # Returns the config corresponding to `name` from the config file 18 | # `false` is returned if it doesn't exist 19 | # 20 | # #### Parameters 21 | # `section` : the section of the config value you are looking for 22 | # `name` : the name of the config value you are looking for 23 | # 24 | # #### Returns 25 | # `value` : the value of the config variable (nil if none) 26 | # 27 | # #### Example Usage 28 | # `config.get('name.of.config')` 29 | # 30 | sig { params(section: String, name: String, default: T.nilable(String)).returns(T.nilable(String)) } 31 | def get(section, name, default: nil) 32 | all_configs.dig("[#{section}]", name) || default 33 | end 34 | 35 | # Coalesce and enforce the value of a config to a boolean 36 | sig { params(section: String, name: String, default: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) } 37 | def get_bool(section, name, default: false) 38 | case get(section, name) 39 | when 'true' 40 | true 41 | when 'false' 42 | false 43 | when nil 44 | default 45 | else 46 | raise CLI::Kit::Abort, "Invalid config: #{section}.#{name} is expected to be true or false" 47 | end 48 | end 49 | 50 | # Sets the config value in the config file 51 | # 52 | # #### Parameters 53 | # `section` : the section of the config you are setting 54 | # `name` : the name of the config you are setting 55 | # `value` : the value of the config you are setting 56 | # 57 | # #### Example Usage 58 | # `config.set('section', 'name.of.config', 'value')` 59 | # 60 | sig { params(section: String, name: String, value: T.nilable(T.any(String, T::Boolean))).void } 61 | def set(section, name, value) 62 | all_configs["[#{section}]"] ||= {} 63 | case value 64 | when nil 65 | T.must(all_configs["[#{section}]"]).delete(name) 66 | else 67 | T.must(all_configs["[#{section}]"])[name] = value.to_s 68 | end 69 | write_config 70 | end 71 | 72 | # Unsets a config value in the config file 73 | # 74 | # #### Parameters 75 | # `section` : the section of the config you are deleting 76 | # `name` : the name of the config you are deleting 77 | # 78 | # #### Example Usage 79 | # `config.unset('section', 'name.of.config')` 80 | # 81 | sig { params(section: String, name: String).void } 82 | def unset(section, name) 83 | set(section, name, nil) 84 | end 85 | 86 | # Gets the hash for the entire section 87 | # 88 | # #### Parameters 89 | # `section` : the section of the config you are getting 90 | # 91 | # #### Example Usage 92 | # `config.get_section('section')` 93 | # 94 | sig { params(section: String).returns(T::Hash[String, String]) } 95 | def get_section(section) 96 | (all_configs["[#{section}]"] || {}).dup 97 | end 98 | 99 | sig { returns(String) } 100 | def to_s 101 | ini.to_s 102 | end 103 | 104 | # The path on disk at which the configuration is stored: 105 | # `$XDG_CONFIG_HOME//config` 106 | # if ENV['XDG_CONFIG_HOME'] is not set, we default to ~/.config, e.g.: 107 | # ~/.config/tool/config 108 | # 109 | sig { returns(String) } 110 | def file 111 | config_home = ENV.fetch(XDG_CONFIG_HOME, '~/.config') 112 | File.expand_path(File.join(@tool_name, 'config'), config_home) 113 | end 114 | 115 | private 116 | 117 | sig { returns(T::Hash[String, T::Hash[String, String]]) } 118 | def all_configs 119 | ini.ini 120 | end 121 | 122 | sig { returns(CLI::Kit::Ini) } 123 | def ini 124 | @ini ||= CLI::Kit::Ini.new(file).tap(&:parse) 125 | end 126 | 127 | sig { void } 128 | def write_config 129 | all_configs.each do |section, sub_config| 130 | all_configs.delete(section) if sub_config.empty? 131 | end 132 | FileUtils.mkdir_p(File.dirname(file)) 133 | File.write(file, to_s) 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/core_ext.rb: -------------------------------------------------------------------------------- 1 | # typed: strong 2 | # frozen_string_literal: true 3 | 4 | class Exception 5 | extend(T::Sig) 6 | 7 | # You'd think instance variables @bug and @silent would work here. They 8 | # don't. I'm not sure why. If you, the reader, want to take some time to 9 | # figure it out, go ahead and refactor to that. 10 | 11 | sig { returns(T::Boolean) } 12 | def bug? 13 | true 14 | end 15 | 16 | sig { returns(T::Boolean) } 17 | def silent? 18 | false 19 | end 20 | 21 | sig { params(bug: T::Boolean).void } 22 | def bug!(bug = true) 23 | singleton_class.define_method(:bug?) { bug } 24 | end 25 | 26 | sig { params(silent: T::Boolean).void } 27 | def silent!(silent = true) 28 | singleton_class.define_method(:silent?) { silent } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/error_handler.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | require 'English' 4 | 5 | module CLI 6 | module Kit 7 | class ErrorHandler 8 | extend T::Sig 9 | 10 | ExceptionReporterOrProc = T.type_alias do 11 | T.any(T.class_of(ExceptionReporter), T.proc.returns(T.class_of(ExceptionReporter))) 12 | end 13 | 14 | sig { params(override_exception_handler: T.proc.params(arg0: Exception).returns(Integer)).void } 15 | attr_writer :override_exception_handler 16 | 17 | sig do 18 | params( 19 | log_file: T.nilable(String), 20 | exception_reporter: ExceptionReporterOrProc, 21 | tool_name: T.nilable(String), 22 | dev_mode: T::Boolean, 23 | ).void 24 | end 25 | def initialize(log_file: nil, exception_reporter: NullExceptionReporter, tool_name: nil, dev_mode: false) 26 | @log_file = log_file 27 | @exception_reporter_or_proc = exception_reporter 28 | @tool_name = tool_name 29 | @dev_mode = dev_mode 30 | end 31 | 32 | class ExceptionReporter 33 | extend T::Sig 34 | extend T::Helpers 35 | abstract! 36 | 37 | sig { abstract.params(exception: T.nilable(Exception), logs: T.nilable(String)).void } 38 | def self.report(exception, logs = nil); end 39 | end 40 | 41 | class NullExceptionReporter < ExceptionReporter 42 | extend T::Sig 43 | 44 | sig { override.params(_exception: T.nilable(Exception), _logs: T.nilable(String)).void } 45 | def self.report(_exception, _logs = nil) 46 | nil 47 | end 48 | end 49 | 50 | sig { params(block: T.proc.void).returns(Integer) } 51 | def call(&block) 52 | # @at_exit_exception is set if handle_abort decides to submit an error. 53 | # $ERROR_INFO is set if we terminate because of a signal. 54 | at_exit { report_exception(@at_exit_exception || $ERROR_INFO) } 55 | triage_all_exceptions(&block) 56 | end 57 | 58 | sig { params(error: T.nilable(Exception)).void } 59 | def report_exception(error) 60 | if (notify_with = exception_for_submission(error)) 61 | logs = nil 62 | if @log_file 63 | logs = begin 64 | File.read(@log_file) 65 | rescue => e 66 | "(#{e.class}: #{e.message})" 67 | end 68 | end 69 | exception_reporter.report(notify_with, logs) 70 | end 71 | end 72 | 73 | SIGNALS_THAT_ARENT_BUGS = [ 74 | 'SIGTERM', 'SIGHUP', 'SIGINT', 75 | ].freeze 76 | 77 | private 78 | 79 | # Run the program, handling any errors that occur. 80 | # 81 | # Errors are printed to stderr unless they're #silent?, and are reported 82 | # to bugsnag (by setting @at_exit_exeption for our at_exit handler) if 83 | # they're #bug? 84 | # 85 | # Returns an exit status for the program. 86 | sig { params(block: T.proc.void).returns(Integer) } 87 | def triage_all_exceptions(&block) 88 | begin 89 | block.call 90 | CLI::Kit::EXIT_SUCCESS 91 | rescue Interrupt => e # Ctrl-C 92 | # transform message, prevent bugsnag 93 | exc = e.exception('Interrupt') 94 | CLI::Kit.raise(exc, bug: false) 95 | rescue Errno::ENOSPC => e 96 | # transform message, prevent bugsnag 97 | message = if @tool_name 98 | "Your disk is full - {{command:#{@tool_name}}} requires free space to operate" 99 | else 100 | 'Your disk is full - free space is required to operate' 101 | end 102 | exc = e.exception(message) 103 | CLI::Kit.raise(exc, bug: false) 104 | end 105 | rescue Exception => e # rubocop:disable Lint/RescueException 106 | @at_exit_exception = e if e.bug? 107 | 108 | if (eh = @override_exception_handler) 109 | return eh.call(e) 110 | end 111 | 112 | raise(e) if @dev_mode && e.bug? 113 | 114 | stderr_puts(e.message) unless e.silent? 115 | e.bug? ? CLI::Kit::EXIT_BUG : CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG 116 | end 117 | 118 | sig { params(error: T.nilable(Exception)).returns(T.nilable(Exception)) } 119 | def exception_for_submission(error) 120 | # happens on normal non-error termination 121 | return(nil) if error.nil? 122 | 123 | return(nil) unless error.bug? 124 | 125 | case error 126 | when SignalException 127 | SIGNALS_THAT_ARENT_BUGS.include?(error.message) ? nil : error 128 | when SystemExit # "exit N" called 129 | case error.status 130 | when CLI::Kit::EXIT_SUCCESS # submit nothing if it was `exit 0` 131 | nil 132 | when CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG 133 | # if it was `exit 30`, translate the exit code to 1, and submit 134 | # nothing. 30 is used to signal normal failures that are not 135 | # indicative of bugs. However, users should see it presented as 1. 136 | exit(1) 137 | else 138 | # A weird termination status happened. `error.exception "message"` 139 | # will maintain backtrace but allow us to set a message 140 | error.exception("abnormal termination status: #{error.status}") 141 | end 142 | else 143 | error 144 | end 145 | end 146 | 147 | sig { params(message: String).void } 148 | def stderr_puts(message) 149 | $stderr.puts(CLI::UI.fmt("{{red:#{message}}}")) 150 | rescue Errno::EPIPE 151 | nil 152 | end 153 | 154 | sig { returns(T.class_of(ExceptionReporter)) } 155 | def exception_reporter 156 | case @exception_reporter_or_proc 157 | when Proc 158 | @exception_reporter_or_proc.call 159 | else 160 | @exception_reporter_or_proc 161 | end 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/executor.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | require 'English' 4 | require 'fileutils' 5 | 6 | module CLI 7 | module Kit 8 | class Executor 9 | extend T::Sig 10 | 11 | sig { params(log_file: String).void } 12 | def initialize(log_file:) 13 | FileUtils.mkpath(File.dirname(log_file)) 14 | @log_file = log_file 15 | end 16 | 17 | sig { params(command: T.class_of(CLI::Kit::BaseCommand), command_name: String, args: T::Array[String]).void } 18 | def call(command, command_name, args) 19 | with_traps do 20 | with_logging do |id| 21 | command.call(args, command_name) 22 | rescue => e 23 | begin 24 | $stderr.puts "This command ran with ID: #{id}" 25 | $stderr.puts 'Please include this information in any issues/report along with relevant logs' 26 | rescue SystemCallError 27 | # Outputting to stderr is best-effort. Avoid raising another error when outputting debug info so that 28 | # we can detect and log the original error, which may even be the source of this error. 29 | nil 30 | end 31 | raise e 32 | end 33 | end 34 | end 35 | 36 | private 37 | 38 | sig do 39 | type_parameters(:T).params(block: T.proc.params(id: String).returns(T.type_parameter(:T))) 40 | .returns(T.type_parameter(:T)) 41 | end 42 | def with_logging(&block) 43 | CLI::UI.log_output_to(@log_file) do 44 | CLI::UI::StdoutRouter.with_id(on_streams: [CLI::UI::StdoutRouter.duplicate_output_to].compact) do |id| 45 | block.call(id) 46 | end 47 | end 48 | end 49 | 50 | sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) } 51 | def with_traps(&block) 52 | twrap('QUIT', method(:quit_handler)) do 53 | twrap('INFO', method(:info_handler), &block) 54 | end 55 | end 56 | 57 | sig do 58 | type_parameters(:T).params(signal: String, handler: Method, 59 | block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) 60 | end 61 | def twrap(signal, handler, &block) 62 | return yield unless Signal.list.key?(signal) 63 | 64 | begin 65 | begin 66 | prev_handler = trap(signal, handler) 67 | installed = true 68 | rescue ArgumentError 69 | # If we couldn't install a signal handler because the signal is 70 | # reserved, remember not to uninstall it later. 71 | installed = false 72 | end 73 | yield 74 | ensure 75 | trap(signal, prev_handler) if installed 76 | end 77 | end 78 | 79 | sig { params(_sig: T.untyped).void } 80 | def quit_handler(_sig) 81 | z = caller 82 | CLI::UI.raw do 83 | $stderr.puts('SIGQUIT: quit') 84 | $stderr.puts(z) 85 | end 86 | exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG) 87 | end 88 | 89 | sig { params(_sig: T.untyped).void } 90 | def info_handler(_sig) 91 | z = caller 92 | CLI::UI.raw do 93 | $stderr.puts('SIGINFO:') 94 | $stderr.puts(z) 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/ini.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | # INI is a language similar to JSON or YAML, but simplied 7 | # The spec is here: https://en.wikipedia.org/wiki/INI_file 8 | # This parser includes supports for 2 very basic uses 9 | # - Sections 10 | # - Key Value Pairs (within and outside of the sections) 11 | # 12 | # [global] 13 | # key = val 14 | # 15 | # Nothing else is supported right now 16 | # See the ini_test.rb file for more examples 17 | # 18 | class Ini 19 | extend T::Sig 20 | 21 | sig { returns(T::Hash[String, T::Hash[String, String]]) } 22 | attr_accessor :ini 23 | 24 | sig do 25 | params(path: T.nilable(String), config: T.nilable(String), default_section: String).void 26 | end 27 | def initialize(path = nil, config: nil, default_section: '[global]') 28 | @config = if path && File.exist?(path) 29 | File.readlines(path) 30 | elsif config 31 | config.lines 32 | end 33 | @ini = {} 34 | @current_key = default_section 35 | end 36 | 37 | sig { returns(T::Hash[String, T::Hash[String, String]]) } 38 | def parse 39 | return @ini if @config.nil? 40 | 41 | @config.each do |l| 42 | l.strip! 43 | 44 | if section_designator?(l) 45 | @current_key = l 46 | else 47 | k, v = l.split('=', 2).map(&:strip) 48 | set_val(k, v) if k && v 49 | end 50 | end 51 | 52 | @ini 53 | end 54 | 55 | sig { returns(String) } 56 | def git_format 57 | to_ini(git_format: true) 58 | end 59 | 60 | sig { returns(String) } 61 | def to_s 62 | to_ini 63 | end 64 | 65 | private 66 | 67 | sig { params(git_format: T::Boolean).returns(String) } 68 | def to_ini(git_format: false) 69 | optional_tab = git_format ? "\t" : '' 70 | str = [] 71 | @ini.each do |section_designator, section| 72 | str << '' unless str.empty? || git_format 73 | str << section_designator 74 | section.each do |k, v| 75 | str << "#{optional_tab}#{k} = #{v}" 76 | end 77 | end 78 | str.join("\n") 79 | end 80 | 81 | sig { params(key: String, val: String).void } 82 | def set_val(key, val) 83 | current_key = @current_key 84 | @ini[current_key] ||= {} 85 | @ini[current_key][key] = val 86 | end 87 | 88 | sig { params(k: String).returns(T::Boolean) } 89 | def section_designator?(k) 90 | k.start_with?('[') && k.end_with?(']') 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # Copyright (c) 2014-2016 Yuki Nishijima 3 | 4 | # MIT License 5 | 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | require 'cli/kit' 26 | 27 | module CLI 28 | module Kit 29 | module Levenshtein 30 | extend T::Sig 31 | 32 | # This code is based directly on the Text gem implementation 33 | # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. 34 | # 35 | # Returns a value representing the "cost" of transforming str1 into str2 36 | sig { params(str1: String, str2: String).returns(Integer) } 37 | def distance(str1, str2) 38 | n = str1.length 39 | m = str2.length 40 | return m if n.zero? 41 | return n if m.zero? 42 | 43 | d = (0..m).to_a 44 | x = 0 45 | 46 | # to avoid duplicating an enumerable object, create it outside of the loop 47 | str2_codepoints = str2.codepoints 48 | 49 | str1.each_codepoint.with_index(1) do |char1, i| 50 | j = 0 51 | while j < m 52 | cost = char1 == str2_codepoints[j] ? 0 : 1 53 | x = min3( 54 | T.must(d[j + 1]) + 1, # insertion 55 | i + 1, # deletion 56 | T.must(d[j]) + cost # substitution 57 | ) 58 | d[j] = i 59 | i = x 60 | 61 | j += 1 62 | end 63 | d[m] = x 64 | end 65 | 66 | x 67 | end 68 | module_function :distance 69 | 70 | private 71 | 72 | # detects the minimum value out of three arguments. This method is 73 | # faster than `[a, b, c].min` and puts less GC pressure. 74 | # See https://github.com/yuki24/did_you_mean/pull/1 for a performance 75 | # benchmark. 76 | sig { params(a: Integer, b: Integer, c: Integer).returns(Integer) } 77 | def min3(a, b, c) 78 | if a < b && a < c 79 | a 80 | elsif b < c 81 | b 82 | else 83 | c 84 | end 85 | end 86 | module_function :min3 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/logger.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | require 'logger' 4 | require 'fileutils' 5 | 6 | module CLI 7 | module Kit 8 | class Logger 9 | extend T::Sig 10 | 11 | MAX_LOG_SIZE = 5 * 1024 * 1000 # 5MB 12 | MAX_NUM_LOGS = 10 13 | 14 | # Constructor for CLI::Kit::Logger 15 | # 16 | # @param debug_log_file [String] path to the file where debug logs should be stored 17 | sig { params(debug_log_file: String, env_debug_name: String).void } 18 | def initialize(debug_log_file:, env_debug_name: 'DEBUG') 19 | FileUtils.mkpath(File.dirname(debug_log_file)) 20 | @debug_logger = ::Logger.new(debug_log_file, MAX_NUM_LOGS, MAX_LOG_SIZE) 21 | @env_debug_name = env_debug_name 22 | end 23 | 24 | # Functionally equivalent to Logger#info 25 | # Also logs to the debug file, taking into account CLI::UI::StdoutRouter.current_id 26 | # 27 | # @param msg [String] the message to log 28 | # @param debug [Boolean] determines if the debug logger will receive the log (default true) 29 | sig { params(msg: String, debug: T::Boolean).void } 30 | def info(msg, debug: true) 31 | $stdout.puts CLI::UI.fmt(msg) 32 | @debug_logger.info(format_debug(msg)) if debug 33 | end 34 | 35 | # Functionally equivalent to Logger#warn 36 | # Also logs to the debug file, taking into account CLI::UI::StdoutRouter.current_id 37 | # 38 | # @param msg [String] the message to log 39 | # @param debug [Boolean] determines if the debug logger will receive the log (default true) 40 | sig { params(msg: String, debug: T::Boolean).void } 41 | def warn(msg, debug: true) 42 | $stdout.puts CLI::UI.fmt("{{yellow:#{msg}}}") 43 | @debug_logger.warn(format_debug(msg)) if debug 44 | end 45 | 46 | # Functionally equivalent to Logger#error 47 | # Also logs to the debug file, taking into account CLI::UI::StdoutRouter.current_id 48 | # 49 | # @param msg [String] the message to log 50 | # @param debug [Boolean] determines if the debug logger will receive the log (default true) 51 | sig { params(msg: String, debug: T::Boolean).void } 52 | def error(msg, debug: true) 53 | $stderr.puts CLI::UI.fmt("{{red:#{msg}}}") 54 | @debug_logger.error(format_debug(msg)) if debug 55 | end 56 | 57 | # Functionally equivalent to Logger#fatal 58 | # Also logs to the debug file, taking into account CLI::UI::StdoutRouter.current_id 59 | # 60 | # @param msg [String] the message to log 61 | # @param debug [Boolean] determines if the debug logger will receive the log (default true) 62 | sig { params(msg: String, debug: T::Boolean).void } 63 | def fatal(msg, debug: true) 64 | $stderr.puts CLI::UI.fmt("{{red:{{bold:Fatal:}} #{msg}}}") 65 | @debug_logger.fatal(format_debug(msg)) if debug 66 | end 67 | 68 | # Similar to Logger#debug, however will not output to STDOUT unless DEBUG env var is set 69 | # Logs to the debug file, taking into account CLI::UI::StdoutRouter.current_id 70 | # 71 | # @param msg [String] the message to log 72 | sig { params(msg: String).void } 73 | def debug(msg) 74 | $stdout.puts CLI::UI.fmt(msg) if debug? 75 | @debug_logger.debug(format_debug(msg)) 76 | end 77 | 78 | private 79 | 80 | sig { params(msg: String).returns(String) } 81 | def format_debug(msg) 82 | msg = CLI::UI.fmt(msg) 83 | return msg unless CLI::UI::StdoutRouter.current_id 84 | 85 | "[#{CLI::UI::StdoutRouter.current_id&.fetch(:id, nil)}] #{msg}" 86 | end 87 | 88 | sig { returns(T::Boolean) } 89 | def debug? 90 | val = ENV[@env_debug_name] 91 | !!val && val != '0' && val != '' 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/opts.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | class Opts 7 | extend T::Sig 8 | 9 | module Mixin 10 | extend T::Sig 11 | include Kernel 12 | 13 | module MixinClassMethods 14 | extend T::Sig 15 | 16 | sig { params(included_module: Module).void } 17 | def include(included_module) 18 | super 19 | return unless included_module.is_a?(MixinClassMethods) 20 | 21 | included_module.tracked_methods.each { |m| track_method(m) } 22 | end 23 | 24 | # No signature - Sorbet uses method_added internally, so can't verify it 25 | def method_added(method_name) # rubocop:disable Sorbet/EnforceSignatures 26 | super 27 | track_method(method_name) 28 | end 29 | 30 | sig { params(method_name: Symbol).void } 31 | def track_method(method_name) 32 | @tracked_methods ||= [] 33 | @tracked_methods << method_name unless @tracked_methods.include?(method_name) 34 | end 35 | 36 | sig { returns(T::Array[Symbol]) } 37 | def tracked_methods 38 | @tracked_methods || [] 39 | end 40 | end 41 | 42 | sig { params(klass: Module).void } 43 | def self.included(klass) 44 | klass.extend(MixinClassMethods) 45 | end 46 | 47 | sig do 48 | params( 49 | name: Symbol, 50 | short: T.nilable(String), 51 | long: T.nilable(String), 52 | desc: T.nilable(String), 53 | default: T.any(NilClass, String, T.proc.returns(String)), 54 | ).returns(T.nilable(String)) 55 | end 56 | def option(name: infer_name, short: nil, long: nil, desc: nil, default: nil) 57 | unless default.nil? 58 | raise(ArgumentError, 'declare options with non-nil defaults using `option!` instead of `option`') 59 | end 60 | 61 | case @obj 62 | when Args::Definition 63 | @obj.add_option( 64 | name, short: short, long: long, desc: desc, default: default, 65 | ) 66 | '(result unavailable)' 67 | when Args::Evaluation 68 | @obj.opt.send(name) 69 | end 70 | end 71 | 72 | sig do 73 | params( 74 | name: Symbol, 75 | short: T.nilable(String), 76 | long: T.nilable(String), 77 | desc: T.nilable(String), 78 | default: T.any(NilClass, String, T.proc.returns(String)), 79 | ).returns(String) 80 | end 81 | def option!(name: infer_name, short: nil, long: nil, desc: nil, default: nil) 82 | case @obj 83 | when Args::Definition 84 | @obj.add_option( 85 | name, short: short, long: long, desc: desc, default: default, 86 | ) 87 | '(result unavailable)' 88 | when Args::Evaluation 89 | @obj.opt.send(name) 90 | end 91 | end 92 | 93 | sig do 94 | params( 95 | name: Symbol, 96 | short: T.nilable(String), 97 | long: T.nilable(String), 98 | desc: T.nilable(String), 99 | ).returns(T::Array[String]) 100 | end 101 | def multi_option(name: infer_name, short: nil, long: nil, desc: nil) 102 | case @obj 103 | when Args::Definition 104 | @obj.add_option( 105 | name, short: short, long: long, desc: desc, multi: true, 106 | ) 107 | ['(result unavailable)'] 108 | when Args::Evaluation 109 | @obj.opt.send(name) 110 | end 111 | end 112 | 113 | sig do 114 | params( 115 | name: Symbol, 116 | short: T.nilable(String), 117 | long: T.nilable(String), 118 | desc: T.nilable(String), 119 | ).returns(T::Boolean) 120 | end 121 | def flag(name: infer_name, short: nil, long: nil, desc: nil) 122 | case @obj 123 | when Args::Definition 124 | @obj.add_flag(name, short: short, long: long, desc: desc) 125 | false 126 | when Args::Evaluation 127 | @obj.flag.send(name) 128 | end 129 | end 130 | 131 | sig { params(name: Symbol, desc: T.nilable(String)).returns(String) } 132 | def position!(name: infer_name, desc: nil) 133 | case @obj 134 | when Args::Definition 135 | @obj.add_position(name, desc: desc, required: true, multiple: false) 136 | '(result unavailable)' 137 | when Args::Evaluation 138 | @obj.position.send(name) 139 | end 140 | end 141 | 142 | sig { params(name: Symbol, desc: T.nilable(String)).returns(T.nilable(String)) } 143 | def position(name: infer_name, desc: nil) 144 | case @obj 145 | when Args::Definition 146 | @obj.add_position(name, desc: desc, required: false, multiple: false) 147 | '(result unavailable)' 148 | when Args::Evaluation 149 | @obj.position.send(name) 150 | end 151 | end 152 | 153 | sig { params(name: Symbol, desc: T.nilable(String)).returns(T::Array[String]) } 154 | def rest(name: infer_name, desc: nil) 155 | case @obj 156 | when Args::Definition 157 | @obj.add_position(name, desc: desc, required: false, multiple: true) 158 | ['(result unavailable)'] 159 | when Args::Evaluation 160 | @obj.position.send(name) 161 | end 162 | end 163 | 164 | private 165 | 166 | sig { returns(Symbol) } 167 | def infer_name 168 | to_skip = 1 169 | Kernel.caller_locations&.each do |loc| 170 | next if loc.path =~ /sorbet-runtime/ 171 | 172 | if to_skip > 0 173 | to_skip -= 1 174 | next 175 | end 176 | return(T.must(loc.label&.to_sym)) 177 | end 178 | raise(ArgumentError, 'could not infer name') 179 | end 180 | end 181 | include(Mixin) 182 | 183 | DEFAULT_OPTIONS = [:helpflag] 184 | 185 | sig { returns(T::Boolean) } 186 | def helpflag 187 | flag(name: :help, short: '-h', long: '--help', desc: 'Show this help message') 188 | end 189 | 190 | sig { params(obj: T.any(Args::Definition, Args::Evaluation)).void } 191 | def initialize(obj) 192 | @obj = obj 193 | end 194 | 195 | sig { returns(T::Array[String]) } 196 | def unparsed 197 | obj = assert_result! 198 | obj.unparsed 199 | end 200 | 201 | sig do 202 | params( 203 | block: T.nilable( 204 | T.proc.params(arg0: Symbol, arg1: T.nilable(String)).void, 205 | ), 206 | ).returns(T.untyped) 207 | end 208 | def each_option(&block) 209 | return(enum_for(:each_option)) unless block_given? 210 | 211 | obj = assert_result! 212 | obj.defn.options.each do |opt| 213 | name = opt.name 214 | value = obj.opt.send(name) 215 | yield(name, value) 216 | end 217 | end 218 | 219 | sig do 220 | params( 221 | block: T.nilable( 222 | T.proc.params(arg0: Symbol, arg1: T::Boolean).void, 223 | ), 224 | ).returns(T.untyped) 225 | end 226 | def each_flag(&block) 227 | return(enum_for(:each_flag)) unless block_given? 228 | 229 | obj = assert_result! 230 | obj.defn.flags.each do |flag| 231 | name = flag.name 232 | value = obj.flag.send(name) 233 | yield(name, value) 234 | end 235 | end 236 | 237 | sig { params(name: String).returns(T.nilable(T.any(String, T::Boolean))) } 238 | def [](name) 239 | obj = assert_result! 240 | if obj.opt.respond_to?(name) 241 | obj.opt.send(name) 242 | elsif obj.flag.respond_to?(name) 243 | obj.flag.send(name) 244 | end 245 | end 246 | 247 | sig { params(name: String).returns(T.nilable(String)) } 248 | def lookup_option(name) 249 | obj = assert_result! 250 | obj.opt.send(name) 251 | rescue NoMethodError 252 | # TODO: should we raise a KeyError? 253 | nil 254 | end 255 | 256 | sig { params(name: String).returns(T::Boolean) } 257 | def lookup_flag(name) 258 | obj = assert_result! 259 | obj.flag.send(name) 260 | rescue NoMethodError 261 | false 262 | end 263 | 264 | sig { returns(Args::Evaluation) } 265 | def assert_result! 266 | raise(NotImplementedError, 'not implemented') if @obj.is_a?(Args::Definition) 267 | 268 | @obj 269 | end 270 | 271 | sig { void } 272 | def install_to_definition 273 | raise('not a Definition') unless @obj.is_a?(Args::Definition) 274 | 275 | T.cast(self.class, Mixin::MixinClassMethods).tracked_methods.each do |m| 276 | send(m) 277 | end 278 | DEFAULT_OPTIONS.each do |m| 279 | send(m) 280 | end 281 | end 282 | end 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/resolver.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | class Resolver 7 | extend T::Sig 8 | 9 | sig { params(tool_name: String, command_registry: CLI::Kit::CommandRegistry).void } 10 | def initialize(tool_name:, command_registry:) 11 | @tool_name = tool_name 12 | @command_registry = command_registry 13 | end 14 | 15 | sig { params(args: T::Array[String]).returns([T.class_of(CLI::Kit::BaseCommand), String, T::Array[String]]) } 16 | def call(args) 17 | args = args.dup 18 | command_name = args.shift 19 | 20 | command, resolved_name = @command_registry.lookup_command(command_name) 21 | 22 | if command.nil? 23 | command_not_found(command_name) 24 | raise CLI::Kit::AbortSilent # Already output message 25 | end 26 | 27 | [command, resolved_name, args] 28 | end 29 | 30 | private 31 | 32 | sig { params(name: T.nilable(String)).void } 33 | def command_not_found(name) 34 | CLI::UI::Frame.open('Command not found', color: :red, timing: false) do 35 | $stderr.puts(CLI::UI.fmt("{{command:#{@tool_name} #{name}}} was not found")) 36 | end 37 | 38 | cmds = commands_and_aliases 39 | if cmds.all? { |cmd| cmd.is_a?(String) } 40 | possible_matches = cmds.min_by(2) do |cmd| 41 | CLI::Kit::Levenshtein.distance(cmd, name) 42 | end 43 | 44 | # We don't want to match against any possible command 45 | # so reject anything that is too far away 46 | possible_matches.reject! do |possible_match| 47 | CLI::Kit::Levenshtein.distance(possible_match, name) > 3 48 | end 49 | 50 | # If we have any matches left, tell the user 51 | if possible_matches.any? 52 | CLI::UI::Frame.open('{{bold:Did you mean?}}', timing: false, color: :blue) do 53 | possible_matches.each do |possible_match| 54 | $stderr.puts CLI::UI.fmt("{{command:#{@tool_name} #{possible_match}}}") 55 | end 56 | end 57 | end 58 | end 59 | end 60 | 61 | sig { returns(T::Array[String]) } 62 | def commands_and_aliases 63 | @command_registry.command_names + @command_registry.aliases.keys 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb: -------------------------------------------------------------------------------- 1 | # typed: ignore 2 | # frozen_string_literal: true 3 | 4 | module T 5 | class << self 6 | def absurd(value); end 7 | def all(type_a, type_b, *types); end 8 | def any(type_a, type_b, *types); end 9 | def attached_class; end 10 | def class_of(klass); end 11 | def enum(values); end 12 | def nilable(type); end 13 | def noreturn; end 14 | def self_type; end 15 | def type_alias(type = nil, &_blk); end 16 | def type_parameter(name); end 17 | def untyped; end 18 | 19 | def assert_type!(value, _type, _checked: true) 20 | value 21 | end 22 | 23 | def cast(value, _type, _checked: true) 24 | value 25 | end 26 | 27 | def let(value, _type, _checked: true) 28 | value 29 | end 30 | 31 | def must(arg, _msg = nil) 32 | arg 33 | end 34 | 35 | def proc 36 | T::Proc.new 37 | end 38 | 39 | def reveal_type(value) 40 | value 41 | end 42 | 43 | def unsafe(value) 44 | value 45 | end 46 | end 47 | 48 | module Sig 49 | def sig(arg0 = nil, &blk); end 50 | end 51 | 52 | module Helpers 53 | def abstract!; end 54 | def interface!; end 55 | def final!; end 56 | def sealed!; end 57 | def mixes_in_class_methods(mod); end 58 | end 59 | 60 | module Generic 61 | include(T::Helpers) 62 | 63 | def type_parameters(*params); end 64 | def type_member(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end 65 | def type_template(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end 66 | 67 | def [](*types) 68 | self 69 | end 70 | end 71 | 72 | module Array 73 | def self.[](type); end 74 | end 75 | 76 | Boolean = Object.new.freeze 77 | 78 | module Configuration 79 | def self.call_validation_error_handler(signature, opts); end 80 | def self.call_validation_error_handler=(value); end 81 | def self.default_checked_level=(default_checked_level); end 82 | def self.enable_checking_for_sigs_marked_checked_tests; end 83 | def self.enable_final_checks_on_hooks; end 84 | def self.enable_legacy_t_enum_migration_mode; end 85 | def self.reset_final_checks_on_hooks; end 86 | def self.hard_assert_handler(str, extra); end 87 | def self.hard_assert_handler=(value); end 88 | def self.inline_type_error_handler(error); end 89 | def self.inline_type_error_handler=(value); end 90 | def self.log_info_handler(str, extra); end 91 | def self.log_info_handler=(value); end 92 | def self.scalar_types; end 93 | def self.scalar_types=(values); end 94 | # rubocop:disable Naming/InclusiveLanguage 95 | def self.sealed_violation_whitelist; end 96 | def self.sealed_violation_whitelist=(sealed_violation_whitelist); end 97 | # rubocop:enable Naming/InclusiveLanguage 98 | def self.sig_builder_error_handler=(value); end 99 | def self.sig_validation_error_handler(error, opts); end 100 | def self.sig_validation_error_handler=(value); end 101 | def self.soft_assert_handler(str, extra); end 102 | def self.soft_assert_handler=(value); end 103 | end 104 | 105 | module Enumerable 106 | def self.[](type); end 107 | end 108 | 109 | module Enumerator 110 | def self.[](type); end 111 | end 112 | 113 | module Hash 114 | def self.[](keys, values); end 115 | end 116 | 117 | class Proc 118 | def bind(*_) 119 | self 120 | end 121 | 122 | def params(*_param) 123 | self 124 | end 125 | 126 | def void 127 | self 128 | end 129 | 130 | def returns(_type) 131 | self 132 | end 133 | end 134 | 135 | module Range 136 | def self.[](type); end 137 | end 138 | 139 | module Set 140 | def self.[](type); end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/support.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module Support 7 | autoload :TestHelper, 'cli/kit/support/test_helper' 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'cli/kit' 2 | 3 | module CLI 4 | module Kit 5 | module Support 6 | module TestHelper 7 | def setup 8 | super 9 | CLI::Kit::System.reset! 10 | end 11 | 12 | def assert_all_commands_run(should_raise: true) 13 | errors = CLI::Kit::System.error_message 14 | CLI::Kit::System.reset! 15 | # this is in minitest, but sorbet doesn't know that. probably we 16 | # could structure this better. 17 | T.unsafe(self).assert(false, errors) if should_raise && !errors.nil? 18 | errors 19 | end 20 | 21 | def teardown 22 | super 23 | assert_all_commands_run 24 | end 25 | 26 | module FakeConfig 27 | require 'tmpdir' 28 | require 'fileutils' 29 | 30 | def setup 31 | super 32 | @tmpdir = Dir.mktmpdir 33 | @prev_xdg = ENV['XDG_CONFIG_HOME'] 34 | ENV['XDG_CONFIG_HOME'] = @tmpdir 35 | end 36 | 37 | def teardown 38 | FileUtils.rm_rf(@tmpdir) 39 | ENV['XDG_CONFIG_HOME'] = @prev_xdg 40 | super 41 | end 42 | end 43 | 44 | class FakeSuccess 45 | def initialize(success) 46 | @success = success 47 | end 48 | 49 | def success? 50 | @success 51 | end 52 | end 53 | 54 | module ::CLI 55 | module Kit 56 | module System 57 | class << self 58 | alias_method :original_system, :system 59 | def system(cmd, *a, sudo: false, env: {}, **kwargs) 60 | a.unshift(cmd) 61 | expected_command = expected_command(a, sudo: sudo, env: env) 62 | 63 | # In the case of an unexpected command, expected_command will be nil 64 | return FakeSuccess.new(false) if expected_command.nil? 65 | 66 | # Otherwise handle the command 67 | if expected_command[:allow] 68 | T.unsafe(self).original_system(*a, sudo: sudo, env: env, **kwargs) 69 | else 70 | FakeSuccess.new(expected_command[:success]) 71 | end 72 | end 73 | 74 | alias_method :original_capture2, :capture2 75 | def capture2(cmd, *a, sudo: false, env: {}, **kwargs) 76 | a.unshift(cmd) 77 | expected_command = expected_command(a, sudo: sudo, env: env) 78 | 79 | # In the case of an unexpected command, expected_command will be nil 80 | return [nil, FakeSuccess.new(false)] if expected_command.nil? 81 | 82 | # Otherwise handle the command 83 | if expected_command[:allow] 84 | T.unsafe(self).original_capture2(*a, sudo: sudo, env: env, **kwargs) 85 | else 86 | [ 87 | expected_command[:stdout], 88 | FakeSuccess.new(expected_command[:success]), 89 | ] 90 | end 91 | end 92 | 93 | alias_method :original_capture2e, :capture2e 94 | def capture2e(cmd, *a, sudo: false, env: {}, **kwargs) 95 | a.unshift(cmd) 96 | expected_command = expected_command(a, sudo: sudo, env: env) 97 | 98 | # In the case of an unexpected command, expected_command will be nil 99 | return [nil, FakeSuccess.new(false)] if expected_command.nil? 100 | 101 | # Otherwise handle the command 102 | if expected_command[:allow] 103 | T.unsafe(self).original_capture2e(*a, sudo: sudo, env: env, **kwargs) 104 | else 105 | [ 106 | expected_command[:stdout], 107 | FakeSuccess.new(expected_command[:success]), 108 | ] 109 | end 110 | end 111 | 112 | alias_method :original_capture3, :capture3 113 | def capture3(cmd, *a, sudo: false, env: {}, **kwargs) 114 | a.unshift(cmd) 115 | expected_command = expected_command(a, sudo: sudo, env: env) 116 | 117 | # In the case of an unexpected command, expected_command will be nil 118 | return [nil, nil, FakeSuccess.new(false)] if expected_command.nil? 119 | 120 | # Otherwise handle the command 121 | if expected_command[:allow] 122 | T.unsafe(self).original_capture3(*a, sudo: sudo, env: env, **kwargs) 123 | else 124 | [ 125 | expected_command[:stdout], 126 | expected_command[:stderr], 127 | FakeSuccess.new(expected_command[:success]), 128 | ] 129 | end 130 | end 131 | 132 | # Sets up an expectation for a command and stubs out the call (unless allow is true) 133 | # 134 | # #### Parameters 135 | # `*a` : the command, represented as a splat 136 | # `stdout` : stdout to stub the command with (defaults to empty string) 137 | # `stderr` : stderr to stub the command with (defaults to empty string) 138 | # `allow` : allow determines if the command will be actually run, or stubbed. Defaults to nil (stub) 139 | # `success` : success status to stub the command with (Defaults to nil) 140 | # `sudo` : expectation of sudo being set or not (defaults to false) 141 | # `env` : expectation of env being set or not (defaults to {}) 142 | # 143 | # Note: Must set allow or success 144 | # 145 | def fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {}) 146 | raise ArgumentError, 'success or allow must be set' if success.nil? && allow.nil? 147 | 148 | @delegate_open3 ||= {} 149 | @delegate_open3[a.join(' ')] = { 150 | expected: { 151 | sudo: sudo, 152 | env: env, 153 | }, 154 | actual: { 155 | sudo: nil, 156 | env: nil, 157 | }, 158 | stdout: stdout, 159 | stderr: stderr, 160 | allow: allow, 161 | success: success, 162 | run: false, 163 | } 164 | end 165 | 166 | # Resets the faked commands 167 | # 168 | def reset! 169 | @delegate_open3 = {} 170 | end 171 | 172 | # Returns the errors associated to a test run 173 | # 174 | # #### Returns 175 | # `errors` (String) a string representing errors found on this run, nil if none 176 | def error_message 177 | errors = { 178 | unexpected: [], 179 | not_run: [], 180 | other: {}, 181 | } 182 | 183 | @delegate_open3.each do |cmd, opts| 184 | if opts[:unexpected] 185 | errors[:unexpected] << cmd 186 | elsif opts[:run] 187 | error = [] 188 | 189 | if opts[:expected][:sudo] != opts[:actual][:sudo] 190 | error << "- sudo was supposed to be #{opts[:expected][:sudo]} but was #{opts[:actual][:sudo]}" 191 | end 192 | 193 | if opts[:expected][:env] != opts[:actual][:env] 194 | error << "- env was supposed to be #{opts[:expected][:env]} but was #{opts[:actual][:env]}" 195 | end 196 | 197 | errors[:other][cmd] = error.join("\n") unless error.empty? 198 | else 199 | errors[:not_run] << cmd 200 | end 201 | end 202 | 203 | final_error = [] 204 | 205 | unless errors[:unexpected].empty? 206 | final_error << CLI::UI.fmt(<<~EOF) 207 | {{bold:Unexpected command invocations:}} 208 | {{command:#{errors[:unexpected].join("\n")}}} 209 | EOF 210 | end 211 | 212 | unless errors[:not_run].empty? 213 | final_error << CLI::UI.fmt(<<~EOF) 214 | {{bold:Expected commands were not run:}} 215 | {{command:#{errors[:not_run].join("\n")}}} 216 | EOF 217 | end 218 | 219 | unless errors[:other].empty? 220 | final_error << CLI::UI.fmt(<<~EOF) 221 | {{bold:Commands were not run as expected:}} 222 | #{errors[:other].map { |cmd, msg| "{{command:#{cmd}}}\n#{msg}" }.join("\n\n")} 223 | EOF 224 | end 225 | 226 | return nil if final_error.empty? 227 | 228 | "\n" + final_error.join("\n") # Initial new line for formatting reasons 229 | end 230 | 231 | private 232 | 233 | def expected_command(a, sudo: raise, env: raise) 234 | expected_cmd = @delegate_open3[a.join(' ')] 235 | 236 | if expected_cmd.nil? 237 | @delegate_open3[a.join(' ')] = { unexpected: true } 238 | return nil 239 | end 240 | 241 | expected_cmd[:run] = true 242 | expected_cmd[:actual][:sudo] = sudo 243 | expected_cmd[:actual][:env] = env 244 | expected_cmd 245 | end 246 | end 247 | end 248 | end 249 | end 250 | end 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/util.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/kit' 3 | 4 | module CLI 5 | module Kit 6 | module Util 7 | class << self 8 | extend T::Sig 9 | # 10 | # Converts an integer representing bytes into a human readable format 11 | # 12 | sig { params(bytes: Integer, precision: Integer, space: T::Boolean).returns(String) } 13 | def to_filesize(bytes, precision: 2, space: false) 14 | to_si_scale(bytes, 'B', precision: precision, space: space, factor: 1024) 15 | end 16 | 17 | # Converts a number to a human readable format on the SI scale 18 | # 19 | sig do 20 | params(number: Numeric, unit: String, factor: Integer, precision: Integer, 21 | space: T::Boolean).returns(String) 22 | end 23 | def to_si_scale(number, unit = '', factor: 1000, precision: 2, space: false) 24 | raise ArgumentError, 'factor should only be 1000 or 1024' unless [1000, 1024].include?(factor) 25 | 26 | small_scale = ['m', 'µ', 'n', 'p', 'f', 'a', 'z', 'y'] 27 | big_scale = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] 28 | negative = number < 0 29 | number = number.abs.to_f 30 | 31 | if number == 0.0 || number.between?(1, factor) 32 | prefix = '' 33 | scale = 0 34 | else 35 | scale = Math.log(number, factor).floor 36 | if number < 1 37 | index = [-scale - 1, small_scale.length].min 38 | scale = -(index + 1) 39 | prefix = T.must(small_scale[index]) 40 | else 41 | index = [scale - 1, big_scale.length].min 42 | scale = index + 1 43 | prefix = T.must(big_scale[index]) 44 | end 45 | end 46 | 47 | divider = (factor**scale) 48 | fnum = (number / divider.to_f).round(precision) 49 | 50 | # Trim useless decimal 51 | fnum = fnum.to_i if (fnum.to_i.to_f * divider.to_f) == number 52 | 53 | fnum = -fnum if negative 54 | if space 55 | prefix = ' ' + prefix 56 | end 57 | 58 | "#{fnum}#{prefix}#{unit}" 59 | end 60 | 61 | # Dir.chdir, when invoked in block form, complains when we call chdir 62 | # again recursively. There's no apparent good reason for this, so we 63 | # simply implement our own block form of Dir.chdir here. 64 | sig do 65 | type_parameters(:T).params(dir: String, block: T.proc.returns(T.type_parameter(:T))) 66 | .returns(T.type_parameter(:T)) 67 | end 68 | def with_dir(dir, &block) 69 | prev = Dir.pwd 70 | begin 71 | Dir.chdir(dir) 72 | yield 73 | ensure 74 | Dir.chdir(prev) 75 | end 76 | end 77 | 78 | # Must call retry_after on the result in order to execute the block 79 | # 80 | # Example usage: 81 | # 82 | # CLI::Kit::Util.begin do 83 | # might_raise_if_costly_prep_not_done() 84 | # end.retry_after(ExpectedError) do 85 | # costly_prep() 86 | # end 87 | sig do 88 | type_parameters(:T).params(block_that_might_raise: T.proc.returns(T.type_parameter(:T))) 89 | .returns(Retrier[T.type_parameter(:T)]) 90 | end 91 | def begin(&block_that_might_raise) 92 | Retrier.new(block_that_might_raise) 93 | end 94 | end 95 | 96 | class Retrier 97 | extend T::Sig 98 | extend T::Generic 99 | 100 | BlockReturnType = type_member 101 | 102 | sig { params(block_that_might_raise: T.proc.returns(BlockReturnType)).void } 103 | def initialize(block_that_might_raise) 104 | @block_that_might_raise = block_that_might_raise 105 | end 106 | 107 | sig do 108 | params( 109 | exception: T.class_of(Exception), 110 | retries: Integer, 111 | before_retry: T.nilable(T.proc.params(e: Exception).void) 112 | ).returns(BlockReturnType) 113 | end 114 | def retry_after(exception = StandardError, retries: 1, &before_retry) 115 | @block_that_might_raise.call 116 | rescue exception => e 117 | raise if (retries -= 1) < 0 118 | 119 | if before_retry 120 | if before_retry.arity == 0 121 | T.cast(before_retry, T.proc.void).call 122 | else 123 | T.cast(before_retry, T.proc.params(e: Exception).void).call(e) 124 | end 125 | end 126 | retry 127 | end 128 | end 129 | 130 | private_constant :Retrier 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /vendor/deps/cli-kit/lib/cli/kit/version.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | module CLI 4 | module Kit 5 | VERSION = '4.0.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/REVISION: -------------------------------------------------------------------------------- 1 | c9b77b8073f9242ba8d0c8c5651264810b23cb0d 2 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/ansi.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui' 3 | 4 | module CLI 5 | module UI 6 | module ANSI 7 | extend T::Sig 8 | 9 | ESC = "\x1b" 10 | 11 | # ANSI escape sequences (like \x1b[31m) have zero width. 12 | # when calculating the padding width, we must exclude them. 13 | # This also implements a basic version of utf8 character width calculation like 14 | # we could get for real from something like utf8proc. 15 | # 16 | sig { params(str: String).returns(Integer) } 17 | def self.printing_width(str) 18 | zwj = T.let(false, T::Boolean) 19 | strip_codes(str).codepoints.reduce(0) do |acc, cp| 20 | if zwj 21 | zwj = false 22 | next acc 23 | end 24 | case cp 25 | when 0x200d # zero-width joiner 26 | zwj = true 27 | acc 28 | when "\n" 29 | acc 30 | else 31 | acc + 1 32 | end 33 | end 34 | end 35 | 36 | # Strips ANSI codes from a str 37 | # 38 | # ==== Attributes 39 | # 40 | # - +str+ - The string from which to strip codes 41 | # 42 | sig { params(str: String).returns(String) } 43 | def self.strip_codes(str) 44 | str.gsub(/\x1b\[[\d;]+[A-z]|\r/, '') 45 | end 46 | 47 | # Returns an ANSI control sequence 48 | # 49 | # ==== Attributes 50 | # 51 | # - +args+ - Argument to pass to the ANSI control sequence 52 | # - +cmd+ - ANSI control sequence Command 53 | # 54 | sig { params(args: String, cmd: String).returns(String) } 55 | def self.control(args, cmd) 56 | ESC + '[' + args + cmd 57 | end 58 | 59 | # https://en.wikipedia.org/wiki/ANSI_escape_code#graphics 60 | sig { params(params: String).returns(String) } 61 | def self.sgr(params) 62 | control(params, 'm') 63 | end 64 | 65 | # Cursor Movement 66 | 67 | # Move the cursor up n lines 68 | # 69 | # ==== Attributes 70 | # 71 | # * +n+ - number of lines by which to move the cursor up 72 | # 73 | sig { params(n: Integer).returns(String) } 74 | def self.cursor_up(n = 1) 75 | return '' if n.zero? 76 | 77 | control(n.to_s, 'A') 78 | end 79 | 80 | # Move the cursor down n lines 81 | # 82 | # ==== Attributes 83 | # 84 | # * +n+ - number of lines by which to move the cursor down 85 | # 86 | sig { params(n: Integer).returns(String) } 87 | def self.cursor_down(n = 1) 88 | return '' if n.zero? 89 | 90 | control(n.to_s, 'B') 91 | end 92 | 93 | # Move the cursor forward n columns 94 | # 95 | # ==== Attributes 96 | # 97 | # * +n+ - number of columns by which to move the cursor forward 98 | # 99 | sig { params(n: Integer).returns(String) } 100 | def self.cursor_forward(n = 1) 101 | return '' if n.zero? 102 | 103 | control(n.to_s, 'C') 104 | end 105 | 106 | # Move the cursor back n columns 107 | # 108 | # ==== Attributes 109 | # 110 | # * +n+ - number of columns by which to move the cursor back 111 | # 112 | sig { params(n: Integer).returns(String) } 113 | def self.cursor_back(n = 1) 114 | return '' if n.zero? 115 | 116 | control(n.to_s, 'D') 117 | end 118 | 119 | # Move the cursor to a specific column 120 | # 121 | # ==== Attributes 122 | # 123 | # * +n+ - The column to move to 124 | # 125 | sig { params(n: Integer).returns(String) } 126 | def self.cursor_horizontal_absolute(n = 1) 127 | cmd = control(n.to_s, 'G') 128 | cmd += cursor_back if CLI::UI::OS.current.shift_cursor_back_on_horizontal_absolute? 129 | cmd 130 | end 131 | 132 | # Show the cursor 133 | # 134 | sig { returns(String) } 135 | def self.show_cursor 136 | control('', '?25h') 137 | end 138 | 139 | # Hide the cursor 140 | # 141 | sig { returns(String) } 142 | def self.hide_cursor 143 | control('', '?25l') 144 | end 145 | 146 | # Save the cursor position 147 | # 148 | sig { returns(String) } 149 | def self.cursor_save 150 | control('', 's') 151 | end 152 | 153 | # Restore the saved cursor position 154 | # 155 | sig { returns(String) } 156 | def self.cursor_restore 157 | control('', 'u') 158 | end 159 | 160 | # Move to the next line 161 | # 162 | sig { returns(String) } 163 | def self.next_line 164 | cursor_down + cursor_horizontal_absolute 165 | end 166 | 167 | # Move to the previous line 168 | # 169 | sig { returns(String) } 170 | def self.previous_line 171 | cursor_up + cursor_horizontal_absolute 172 | end 173 | 174 | sig { returns(String) } 175 | def self.clear_to_end_of_line 176 | control('', 'K') 177 | end 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/color.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui' 3 | 4 | module CLI 5 | module UI 6 | class Color 7 | extend T::Sig 8 | 9 | sig { returns(String) } 10 | attr_reader :sgr, :code 11 | 12 | sig { returns(Symbol) } 13 | attr_reader :name 14 | 15 | # Creates a new color mapping 16 | # Signatures can be found here: 17 | # https://en.wikipedia.org/wiki/ANSI_escape_code#Colors 18 | # 19 | # ==== Attributes 20 | # 21 | # * +sgr+ - The color signature 22 | # * +name+ - The name of the color 23 | # 24 | sig { params(sgr: String, name: Symbol).void } 25 | def initialize(sgr, name) 26 | @sgr = sgr 27 | @code = CLI::UI::ANSI.sgr(sgr) 28 | @name = name 29 | end 30 | 31 | RED = new('31', :red) 32 | GREEN = new('32', :green) 33 | YELLOW = new('33', :yellow) 34 | # default blue is low-contrast against black in some default terminal color scheme 35 | BLUE = new('94', :blue) # 9x = high-intensity fg color x 36 | MAGENTA = new('35', :magenta) 37 | CYAN = new('36', :cyan) 38 | RESET = new('0', :reset) 39 | BOLD = new('1', :bold) 40 | WHITE = new('97', :white) 41 | 42 | # 240 is very dark gray; 255 is very light gray. 244 is somewhat dark. 43 | GRAY = new('38;5;244', :grey) 44 | 45 | MAP = { 46 | red: RED, 47 | green: GREEN, 48 | yellow: YELLOW, 49 | blue: BLUE, 50 | magenta: MAGENTA, 51 | cyan: CYAN, 52 | reset: RESET, 53 | bold: BOLD, 54 | gray: GRAY, 55 | }.freeze 56 | 57 | class InvalidColorName < ArgumentError 58 | extend T::Sig 59 | 60 | sig { params(name: Symbol).void } 61 | def initialize(name) 62 | super 63 | @name = name 64 | end 65 | 66 | sig { returns(String) } 67 | def message 68 | keys = Color.available.map(&:inspect).join(',') 69 | "invalid color: #{@name.inspect} " \ 70 | "-- must be one of CLI::UI::Color.available (#{keys})" 71 | end 72 | end 73 | 74 | # Looks up a color code by name 75 | # 76 | # ==== Raises 77 | # Raises a InvalidColorName if the color is not available 78 | # You likely need to add it to the +MAP+ or you made a typo 79 | # 80 | # ==== Returns 81 | # Returns a color code 82 | # 83 | sig { params(name: T.any(Symbol, String)).returns(Color) } 84 | def self.lookup(name) 85 | MAP.fetch(name.to_sym) 86 | rescue KeyError 87 | raise InvalidColorName, name 88 | end 89 | 90 | # All available colors by name 91 | # 92 | sig { returns(T::Array[Symbol]) } 93 | def self.available 94 | MAP.keys 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/formatter.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require('cli/ui') 5 | require('strscan') 6 | 7 | module CLI 8 | module UI 9 | class Formatter 10 | extend T::Sig 11 | 12 | # Available mappings of formattings 13 | # To use any of them, you can use {{:}} 14 | # There are presentational (colours and formatters) 15 | # and semantic (error, info, command) formatters available 16 | # 17 | SGR_MAP = { 18 | # presentational 19 | 'red' => '31', 20 | 'green' => '32', 21 | 'yellow' => '33', 22 | # default blue is low-contrast against black in some default terminal color scheme 23 | 'blue' => '94', # 9x = high-intensity fg color x 24 | 'magenta' => '35', 25 | 'cyan' => '36', 26 | 'gray' => '38;5;244', 27 | 'white' => '97', 28 | 'bold' => '1', 29 | 'italic' => '3', 30 | 'underline' => '4', 31 | 'reset' => '0', 32 | 33 | # semantic 34 | 'error' => '31', # red 35 | 'success' => '32', # success 36 | 'warning' => '33', # yellow 37 | 'info' => '94', # bright blue 38 | 'command' => '36', # cyan 39 | }.freeze 40 | 41 | BEGIN_EXPR = '{{' 42 | END_EXPR = '}}' 43 | 44 | SCAN_WIDGET = %r[@widget/(?\w+):(?.*?)}}] 45 | SCAN_FUNCNAME = /\w+:/ 46 | SCAN_GLYPH = /.}}/ 47 | SCAN_BODY = %r{ 48 | .*? 49 | ( 50 | #{BEGIN_EXPR} | 51 | #{END_EXPR} | 52 | \z 53 | ) 54 | }mx 55 | 56 | DISCARD_BRACES = 0..-3 57 | 58 | LITERAL_BRACES = Class.new 59 | 60 | Stack = T.type_alias { T::Array[T.any(String, LITERAL_BRACES)] } 61 | 62 | class FormatError < StandardError 63 | extend T::Sig 64 | 65 | sig { returns(String) } 66 | attr_accessor :input 67 | 68 | sig { returns(Integer) } 69 | attr_accessor :index 70 | 71 | sig { params(message: String, input: String, index: Integer).void } 72 | def initialize(message, input, index) 73 | super(message) 74 | @input = input 75 | @index = index 76 | end 77 | end 78 | 79 | # Initialize a formatter with text. 80 | # 81 | # ===== Attributes 82 | # 83 | # * +text+ - the text to format 84 | # 85 | sig { params(text: String).void } 86 | def initialize(text) 87 | @text = text 88 | @nodes = T.let([], T::Array[[String, Stack]]) 89 | end 90 | 91 | # Format the text using a map. 92 | # 93 | # ===== Attributes 94 | # 95 | # * +sgr_map+ - the mapping of the formattings. Defaults to +SGR_MAP+ 96 | # 97 | # ===== Options 98 | # 99 | # * +:enable_color+ - enable color output? Default is true unless output is redirected 100 | # 101 | sig { params(sgr_map: T::Hash[String, String], enable_color: T::Boolean).returns(String) } 102 | def format(sgr_map = SGR_MAP, enable_color: CLI::UI.enable_color?) 103 | @nodes.replace([]) 104 | stack = parse_body(StringScanner.new(@text)) 105 | prev_fmt = T.let(nil, T.nilable(Stack)) 106 | content = @nodes.each_with_object(+'') do |(text, fmt), str| 107 | if prev_fmt != fmt && enable_color 108 | text = apply_format(text, fmt, sgr_map) 109 | end 110 | str << text 111 | prev_fmt = fmt 112 | end 113 | 114 | stack.reject! { |e| e.is_a?(LITERAL_BRACES) } 115 | 116 | return content unless enable_color 117 | return content if stack == prev_fmt 118 | 119 | unless stack.empty? && (@nodes.size.zero? || T.must(@nodes.last)[1].empty?) 120 | content << apply_format('', stack, sgr_map) 121 | end 122 | content 123 | end 124 | 125 | private 126 | 127 | sig { params(text: String, fmt: Stack, sgr_map: T::Hash[String, String]).returns(String) } 128 | def apply_format(text, fmt, sgr_map) 129 | sgr = fmt.each_with_object(+'0') do |name, str| 130 | next if name.is_a?(LITERAL_BRACES) 131 | 132 | begin 133 | str << ';' << sgr_map.fetch(name) 134 | rescue KeyError 135 | raise FormatError.new( 136 | "invalid format specifier: #{name}", 137 | @text, 138 | -1 139 | ) 140 | end 141 | end 142 | CLI::UI::ANSI.sgr(sgr) + text 143 | end 144 | 145 | sig { params(sc: StringScanner, stack: Stack).returns(Stack) } 146 | def parse_expr(sc, stack) 147 | if (match = sc.scan(SCAN_GLYPH)) 148 | glyph_handle = T.must(match[0]) 149 | begin 150 | glyph = Glyph.lookup(glyph_handle) 151 | emit(glyph.char, [glyph.color.name.to_s]) 152 | rescue Glyph::InvalidGlyphHandle 153 | index = sc.pos - 2 # rewind past '}}' 154 | raise FormatError.new( 155 | "invalid glyph handle at index #{index}: '#{glyph_handle}'", 156 | @text, 157 | index 158 | ) 159 | end 160 | elsif (match = sc.scan(SCAN_WIDGET)) 161 | match_data = T.must(SCAN_WIDGET.match(match)) # Regexp.last_match doesn't work here 162 | widget_handle = T.must(match_data['handle']) 163 | begin 164 | widget = Widgets.lookup(widget_handle) 165 | emit(widget.call(T.must(match_data['args'])), stack) 166 | rescue Widgets::InvalidWidgetHandle 167 | index = sc.pos - 2 # rewind past '}}' 168 | raise(FormatError.new( 169 | "invalid widget handle at index #{index}: '#{widget_handle}'", 170 | @text, index, 171 | )) 172 | end 173 | elsif (match = sc.scan(SCAN_FUNCNAME)) 174 | funcname = match.chop 175 | stack.push(funcname) 176 | else 177 | # We read a {{ but it's not apparently Formatter syntax. 178 | # We could error, but it's nicer to just pass through as text. 179 | # We do kind of assume that the text will probably have balanced 180 | # pairs of {{ }} at least. 181 | emit('{{', stack) 182 | stack.push(LITERAL_BRACES.new) 183 | end 184 | parse_body(sc, stack) 185 | stack 186 | end 187 | 188 | sig { params(sc: StringScanner, stack: Stack).returns(Stack) } 189 | def parse_body(sc, stack = []) 190 | match = sc.scan(SCAN_BODY) 191 | if match&.end_with?(BEGIN_EXPR) 192 | emit(T.must(match[DISCARD_BRACES]), stack) 193 | parse_expr(sc, stack) 194 | elsif match&.end_with?(END_EXPR) 195 | emit(T.must(match[DISCARD_BRACES]), stack) 196 | if stack.pop.is_a?(LITERAL_BRACES) 197 | emit('}}', stack) 198 | end 199 | parse_body(sc, stack) 200 | elsif match 201 | emit(match, stack) 202 | else 203 | emit(sc.rest, stack) 204 | end 205 | stack 206 | end 207 | 208 | sig { params(text: String, stack: Stack).void } 209 | def emit(text, stack) 210 | return if text.empty? 211 | 212 | @nodes << [text, stack.reject { |n| n.is_a?(LITERAL_BRACES) }] 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | module CLI 3 | module UI 4 | module Frame 5 | module FrameStack 6 | COLOR_ENVVAR = 'CLI_FRAME_STACK' 7 | STYLE_ENVVAR = 'CLI_STYLE_STACK' 8 | 9 | class StackItem 10 | extend T::Sig 11 | 12 | sig { returns(CLI::UI::Color) } 13 | attr_reader :color 14 | 15 | sig { returns(CLI::UI::Frame::FrameStyle) } 16 | attr_reader :frame_style 17 | 18 | sig do 19 | params(color_name: CLI::UI::Colorable, style_name: FrameStylable) 20 | .void 21 | end 22 | def initialize(color_name, style_name) 23 | @color = CLI::UI.resolve_color(color_name) 24 | @frame_style = CLI::UI.resolve_style(style_name) 25 | end 26 | end 27 | 28 | class << self 29 | extend T::Sig 30 | 31 | # Fetch all items off the frame stack 32 | sig { returns(T::Array[StackItem]) } 33 | def items 34 | colors = ENV.fetch(COLOR_ENVVAR, '').split(':').map(&:to_sym) 35 | styles = ENV.fetch(STYLE_ENVVAR, '').split(':').map(&:to_sym) 36 | 37 | colors.each_with_index.map do |color, i| 38 | StackItem.new(color, styles[i] || Frame.frame_style) 39 | end 40 | end 41 | 42 | # Push a new item onto the frame stack. 43 | # 44 | # Either an item or a :color/:style pair should be pushed onto the stack. 45 | # 46 | # ==== Attributes 47 | # 48 | # * +item+ a +StackItem+ to push onto the stack. Defaults to nil 49 | # 50 | # ==== Options 51 | # 52 | # * +:color+ the color of the new stack item. Defaults to nil 53 | # * +:style+ the style of the new stack item. Defaults to nil 54 | # 55 | # ==== Raises 56 | # 57 | # If both an item and a color/style pair are given, raises an +ArgumentError+ 58 | # If the given item is not a +StackItem+, raises an +ArgumentError+ 59 | # 60 | sig do 61 | params( 62 | item: T.nilable(StackItem), 63 | color: T.nilable(CLI::UI::Color), 64 | style: T.nilable(CLI::UI::Frame::FrameStyle) 65 | ) 66 | .void 67 | end 68 | def push(item = nil, color: nil, style: nil) 69 | if color.nil? != style.nil? || item.nil? == color.nil? 70 | raise ArgumentError, 'Must give one of item or color: and style:' 71 | end 72 | 73 | item ||= StackItem.new(T.must(color), T.must(style)) 74 | 75 | curr = items 76 | curr << item 77 | 78 | serialize(curr) 79 | end 80 | 81 | # Removes and returns the last stack item off the stack 82 | sig { returns(T.nilable(StackItem)) } 83 | def pop 84 | curr = items 85 | ret = curr.pop 86 | 87 | serialize(curr) 88 | 89 | ret.nil? ? nil : ret 90 | end 91 | 92 | private 93 | 94 | # Serializes the item stack into two ENV variables. 95 | # 96 | # This is done to preserve backward compatibility with earlier versions of cli/ui. 97 | # This ensures that any code that relied upon previous stack behavior should continue 98 | # to work. 99 | sig { params(items: T::Array[StackItem]).void } 100 | def serialize(items) 101 | colors = [] 102 | styles = [] 103 | 104 | items.each do |item| 105 | colors << item.color.name 106 | styles << item.frame_style.style_name 107 | end 108 | 109 | ENV[COLOR_ENVVAR] = colors.join(':') 110 | ENV[STYLE_ENVVAR] = styles.join(':') 111 | end 112 | end 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui/frame' 3 | 4 | module CLI 5 | module UI 6 | module Frame 7 | module FrameStyle 8 | include Kernel 9 | extend T::Sig 10 | extend T::Helpers 11 | abstract! 12 | 13 | autoload(:Box, 'cli/ui/frame/frame_style/box') 14 | autoload(:Bracket, 'cli/ui/frame/frame_style/bracket') 15 | 16 | MAP = { 17 | box: -> { FrameStyle::Box }, 18 | bracket: -> { FrameStyle::Bracket }, 19 | } 20 | 21 | # Lookup a frame style via its name 22 | # 23 | # ==== Attributes 24 | # 25 | # * +symbol+ - frame style name to lookup 26 | sig { params(name: T.any(String, Symbol)).returns(FrameStyle) } 27 | def self.lookup(name) 28 | MAP.fetch(name.to_sym).call 29 | rescue KeyError 30 | raise(InvalidFrameStyleName, name) 31 | end 32 | 33 | sig { abstract.returns(Symbol) } 34 | def style_name; end 35 | 36 | # Returns the character(s) that should be printed at the beginning 37 | # of lines inside this frame 38 | sig { abstract.returns(String) } 39 | def prefix; end 40 | 41 | # Returns the printing width of the prefix 42 | sig { returns(Integer) } 43 | def prefix_width 44 | CLI::UI::ANSI.printing_width(prefix) 45 | end 46 | 47 | # Draws the "Open" line for this frame style 48 | # 49 | # ==== Attributes 50 | # 51 | # * +text+ - (required) the text/title to output in the frame 52 | # 53 | # ==== Options 54 | # 55 | # * +:color+ - (required) The color of the frame. 56 | # 57 | sig { abstract.params(text: String, color: CLI::UI::Color).returns(String) } 58 | def start(text, color:); end 59 | 60 | # Draws the "Close" line for this frame style 61 | # 62 | # ==== Attributes 63 | # 64 | # * +text+ - (required) the text/title to output in the frame 65 | # 66 | # ==== Options 67 | # 68 | # * +:color+ - (required) The color of the frame. 69 | # * +:right_text+ - Text to print at the right of the line. Defaults to nil 70 | # 71 | sig { abstract.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) } 72 | def close(text, color:, right_text: nil); end 73 | 74 | # Draws a "divider" line for the current frame style 75 | # 76 | # ==== Attributes 77 | # 78 | # * +text+ - (required) the text/title to output in the frame 79 | # 80 | # ==== Options 81 | # 82 | # * +:color+ - (required) The color of the frame. 83 | # 84 | sig { abstract.params(text: String, color: CLI::UI::Color).returns(String) } 85 | def divider(text, color:); end 86 | 87 | sig { params(x: Integer, str: String).returns(String) } 88 | def print_at_x(x, str) 89 | CLI::UI::ANSI.cursor_horizontal_absolute(1 + x) + str 90 | end 91 | 92 | class InvalidFrameStyleName < ArgumentError 93 | extend T::Sig 94 | 95 | sig { params(name: T.any(String, Symbol)).void } 96 | def initialize(name) 97 | super 98 | @name = name 99 | end 100 | 101 | sig { returns(String) } 102 | def message 103 | keys = FrameStyle::MAP.keys.map(&:inspect).join(', ') 104 | "invalid frame style: #{@name.inspect}" \ 105 | ' -- must be one of CLI::UI::Frame::FrameStyle::MAP ' \ 106 | "(#{keys})" 107 | end 108 | end 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | module CLI 3 | module UI 4 | module Frame 5 | module FrameStyle 6 | module Box 7 | extend FrameStyle 8 | 9 | VERTICAL = '┃' 10 | HORIZONTAL = '━' 11 | DIVIDER = '┣' 12 | TOP_LEFT = '┏' 13 | BOTTOM_LEFT = '┗' 14 | 15 | class << self 16 | extend T::Sig 17 | 18 | sig { override.returns(Symbol) } 19 | def style_name 20 | :box 21 | end 22 | 23 | sig { override.returns(String) } 24 | def prefix 25 | VERTICAL 26 | end 27 | 28 | # Draws the "Open" line for this frame style 29 | # 30 | # ==== Attributes 31 | # 32 | # * +text+ - (required) the text/title to output in the frame 33 | # 34 | # ==== Options 35 | # 36 | # * +:color+ - (required) The color of the frame. 37 | # 38 | # ==== Output: 39 | # 40 | # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41 | # 42 | sig { override.params(text: String, color: CLI::UI::Color).returns(String) } 43 | def start(text, color:) 44 | edge(text, color: color, first: TOP_LEFT) 45 | end 46 | 47 | # Draws a "divider" line for the current frame style 48 | # 49 | # ==== Attributes 50 | # 51 | # * +text+ - (required) the text/title to output in the frame 52 | # 53 | # ==== Options 54 | # 55 | # * +:color+ - (required) The color of the frame. 56 | # 57 | # ==== Output: 58 | # 59 | # ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 60 | # 61 | sig { override.params(text: String, color: CLI::UI::Color).returns(String) } 62 | def divider(text, color:) 63 | edge(text, color: color, first: DIVIDER) 64 | end 65 | 66 | # Draws the "Close" line for this frame style 67 | # 68 | # ==== Attributes 69 | # 70 | # * +text+ - (required) the text/title to output in the frame 71 | # 72 | # ==== Options 73 | # 74 | # * +:color+ - (required) The color of the frame. 75 | # * +:right_text+ - Text to print at the right of the line. Defaults to nil 76 | # 77 | # ==== Output: 78 | # 79 | # ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 80 | # 81 | sig { override.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) } 82 | def close(text, color:, right_text: nil) 83 | edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT) 84 | end 85 | 86 | private 87 | 88 | sig do 89 | params(text: String, color: CLI::UI::Color, first: String, right_text: T.nilable(String)).returns(String) 90 | end 91 | def edge(text, color:, first:, right_text: nil) 92 | color = CLI::UI.resolve_color(color) 93 | 94 | preamble = +'' 95 | 96 | preamble << color.code << first << (HORIZONTAL * 2) 97 | 98 | unless text.empty? 99 | preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' ' 100 | end 101 | 102 | termwidth = CLI::UI::Terminal.width 103 | 104 | suffix = +'' 105 | 106 | if right_text 107 | suffix << ' ' << right_text << ' ' 108 | end 109 | 110 | preamble_width = CLI::UI::ANSI.printing_width(preamble) 111 | preamble_start = Frame.prefix_width 112 | # If prefix_width is non-zero, we need to subtract the width of 113 | # the final space, since we're going to write over it. 114 | preamble_start -= 1 unless preamble_start.zero? 115 | preamble_end = preamble_start + preamble_width 116 | 117 | suffix_width = CLI::UI::ANSI.printing_width(suffix) 118 | suffix_end = termwidth - 2 119 | suffix_start = suffix_end - suffix_width 120 | 121 | if preamble_end > suffix_start 122 | suffix = '' 123 | # if preamble_end > termwidth 124 | # we *could* truncate it, but let's just let it overflow to the 125 | # next line and call it poor usage of this API. 126 | end 127 | 128 | o = +'' 129 | 130 | # Shopify's CI system supports terminal emulation, but not some of 131 | # the fancier features that we normally use to draw frames 132 | # extra-reliably, so we fall back to a less foolproof strategy. This 133 | # is probably better in general for cases with impoverished terminal 134 | # emulators and no active user. 135 | unless [0, '', nil].include?(ENV['CI']) 136 | linewidth = [0, termwidth - (preamble_end + suffix_width + 1)].max 137 | 138 | o << color.code << preamble 139 | o << color.code << (HORIZONTAL * linewidth) 140 | o << color.code << suffix 141 | o << CLI::UI::Color::RESET.code << "\n" 142 | return o 143 | end 144 | 145 | # Jumping around the line can cause some unwanted flashes 146 | o << CLI::UI::ANSI.hide_cursor 147 | 148 | # reset to column 1 so that things like ^C don't ruin formatting 149 | o << "\r" 150 | 151 | # This code will print out a full line with the given preamble and 152 | # suffix, as exemplified below. 153 | # 154 | # preamble_start suffix_start 155 | # | preamble_end | suffix_end 156 | # | | | | termwidth 157 | # | | | | | 158 | # V V V V V 159 | # --- Preamble text --------------------- suffix text -- 160 | o << color.code 161 | o << print_at_x(preamble_start, HORIZONTAL * (termwidth - preamble_start)) # draw a full line 162 | o << print_at_x(preamble_start, preamble) 163 | o << color.code 164 | o << print_at_x(suffix_start, suffix) 165 | o << CLI::UI::Color::RESET.code 166 | o << CLI::UI::ANSI.show_cursor 167 | o << "\n" 168 | 169 | o 170 | end 171 | end 172 | end 173 | end 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | module CLI 3 | module UI 4 | module Frame 5 | module FrameStyle 6 | module Bracket 7 | extend FrameStyle 8 | 9 | VERTICAL = '┃' 10 | HORIZONTAL = '━' 11 | DIVIDER = '┣' 12 | TOP_LEFT = '┏' 13 | BOTTOM_LEFT = '┗' 14 | 15 | class << self 16 | extend T::Sig 17 | 18 | sig { override.returns(Symbol) } 19 | def style_name 20 | :bracket 21 | end 22 | 23 | sig { override.returns(String) } 24 | def prefix 25 | VERTICAL 26 | end 27 | 28 | # Draws the "Open" line for this frame style 29 | # 30 | # ==== Attributes 31 | # 32 | # * +text+ - (required) the text/title to output in the frame 33 | # 34 | # ==== Options 35 | # 36 | # * +:color+ - (required) The color of the frame. 37 | # 38 | # ==== Output 39 | # 40 | # ┏━━ Open 41 | # 42 | sig { override.params(text: String, color: CLI::UI::Color).returns(String) } 43 | def start(text, color:) 44 | edge(text, color: color, first: TOP_LEFT) 45 | end 46 | 47 | # Draws a "divider" line for the current frame style 48 | # 49 | # ==== Attributes 50 | # 51 | # * +text+ - (required) the text/title to output in the frame 52 | # 53 | # ==== Options 54 | # 55 | # * +:color+ - (required) The color of the frame. 56 | # 57 | # ==== Output: 58 | # 59 | # ┣━━ Divider 60 | # 61 | sig { override.params(text: String, color: CLI::UI::Color).returns(String) } 62 | def divider(text, color:) 63 | edge(text, color: color, first: DIVIDER) 64 | end 65 | 66 | # Draws the "Close" line for this frame style 67 | # 68 | # ==== Attributes 69 | # 70 | # * +text+ - (required) the text/title to output in the frame 71 | # 72 | # ==== Options 73 | # 74 | # * +:color+ - (required) The color of the frame. 75 | # * +:right_text+ - Text to print at the right of the line. Defaults to nil 76 | # 77 | # ==== Output: 78 | # 79 | # ┗━━ Close 80 | # 81 | sig { override.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) } 82 | def close(text, color:, right_text: nil) 83 | edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT) 84 | end 85 | 86 | private 87 | 88 | sig do 89 | params(text: String, color: CLI::UI::Color, first: String, right_text: T.nilable(String)).returns(String) 90 | end 91 | def edge(text, color:, first:, right_text: nil) 92 | color = CLI::UI.resolve_color(color) 93 | 94 | preamble = +'' 95 | 96 | preamble << color.code << first << (HORIZONTAL * 2) 97 | 98 | unless text.empty? 99 | preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' ' 100 | end 101 | 102 | suffix = +'' 103 | 104 | if right_text 105 | suffix << ' ' << right_text << ' ' 106 | end 107 | 108 | o = +'' 109 | 110 | # Shopify's CI system supports terminal emulation, but not some of 111 | # the fancier features that we normally use to draw frames 112 | # extra-reliably, so we fall back to a less foolproof strategy. This 113 | # is probably better in general for cases with impoverished terminal 114 | # emulators and no active user. 115 | unless [0, '', nil].include?(ENV['CI']) 116 | o << color.code << preamble 117 | o << color.code << suffix 118 | o << CLI::UI::Color::RESET.code 119 | o << "\n" 120 | 121 | return o 122 | end 123 | 124 | preamble_start = Frame.prefix_width 125 | 126 | # If prefix_width is non-zero, we need to subtract the width of 127 | # the final space, since we're going to write over it. 128 | preamble_start -= 1 unless preamble_start.zero? 129 | 130 | # Jumping around the line can cause some unwanted flashes 131 | o << CLI::UI::ANSI.hide_cursor 132 | 133 | # reset to column 1 so that things like ^C don't ruin formatting 134 | o << "\r" 135 | 136 | o << color.code 137 | o << print_at_x(preamble_start, preamble + color.code + suffix) 138 | o << CLI::UI::Color::RESET.code 139 | o << CLI::UI::ANSI.show_cursor 140 | o << "\n" 141 | 142 | o 143 | end 144 | end 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/glyph.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui' 3 | 4 | module CLI 5 | module UI 6 | class Glyph 7 | extend T::Sig 8 | 9 | class InvalidGlyphHandle < ArgumentError 10 | extend T::Sig 11 | 12 | sig { params(handle: String).void } 13 | def initialize(handle) 14 | super 15 | @handle = handle 16 | end 17 | 18 | sig { returns(String) } 19 | def message 20 | keys = Glyph.available.join(',') 21 | "invalid glyph handle: #{@handle} " \ 22 | "-- must be one of CLI::UI::Glyph.available (#{keys})" 23 | end 24 | end 25 | 26 | sig { returns(String) } 27 | attr_reader :handle, :to_s, :fmt, :char 28 | 29 | sig { returns(T.any(Integer, T::Array[Integer])) } 30 | attr_reader :codepoint 31 | 32 | sig { returns(Color) } 33 | attr_reader :color 34 | 35 | # Creates a new glyph 36 | # 37 | # ==== Attributes 38 | # 39 | # * +handle+ - The handle in the +MAP+ constant 40 | # * +codepoint+ - The codepoint used to create the glyph (e.g. +0x2717+ for a ballot X) 41 | # * +plain+ - A fallback plain string to be used in case glyphs are disabled 42 | # * +color+ - What color to output the glyph. Check +CLI::UI::Color+ for options. 43 | # 44 | sig { params(handle: String, codepoint: T.any(Integer, T::Array[Integer]), plain: String, color: Color).void } 45 | def initialize(handle, codepoint, plain, color) 46 | @handle = handle 47 | @codepoint = codepoint 48 | @color = color 49 | @char = CLI::UI::OS.current.use_emoji? ? Array(codepoint).pack('U*') : plain 50 | @to_s = color.code + @char + Color::RESET.code 51 | @fmt = "{{#{color.name}:#{@char}}}" 52 | 53 | MAP[handle] = self 54 | end 55 | 56 | # Mapping of glyphs to terminal output 57 | MAP = {} 58 | STAR = new('*', 0x2b51, '*', Color::YELLOW) # YELLOW SMALL STAR (⭑) 59 | INFO = new('i', 0x1d4be, 'i', Color::BLUE) # BLUE MATHEMATICAL SCRIPT SMALL i (𝒾) 60 | QUESTION = new('?', 0x003f, '?', Color::BLUE) # BLUE QUESTION MARK (?) 61 | CHECK = new('v', 0x2713, '√', Color::GREEN) # GREEN CHECK MARK (✓) 62 | X = new('x', 0x2717, 'X', Color::RED) # RED BALLOT X (✗) 63 | BUG = new('b', 0x1f41b, '!', Color::WHITE) # Bug emoji (🐛) 64 | CHEVRON = new('>', 0xbb, '»', Color::YELLOW) # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (») 65 | HOURGLASS = new('H', [0x231b, 0xfe0e], 'H', Color::BLUE) # HOURGLASS + VARIATION SELECTOR 15 (⌛︎) 66 | WARNING = new('!', [0x26a0, 0xfe0f], '!', Color::YELLOW) # WARNING SIGN + VARIATION SELECTOR 16 (⚠️ ) 67 | 68 | # Looks up a glyph by name 69 | # 70 | # ==== Raises 71 | # Raises a InvalidGlyphHandle if the glyph is not available 72 | # You likely need to create it with +.new+ or you made a typo 73 | # 74 | # ==== Returns 75 | # Returns a terminal output-capable string 76 | # 77 | sig { params(name: String).returns(Glyph) } 78 | def self.lookup(name) 79 | MAP.fetch(name.to_s) 80 | rescue KeyError 81 | raise InvalidGlyphHandle, name 82 | end 83 | 84 | # All available glyphs by name 85 | # 86 | sig { returns(T::Array[String]) } 87 | def self.available 88 | MAP.keys 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/os.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'rbconfig' 3 | 4 | module CLI 5 | module UI 6 | class OS 7 | extend T::Sig 8 | 9 | sig { params(emoji: T::Boolean, color_prompt: T::Boolean, arrow_keys: T::Boolean, shift_cursor: T::Boolean).void } 10 | def initialize(emoji: true, color_prompt: true, arrow_keys: true, shift_cursor: false) 11 | @emoji = emoji 12 | @color_prompt = color_prompt 13 | @arrow_keys = arrow_keys 14 | @shift_cursor = shift_cursor 15 | end 16 | 17 | sig { returns(T::Boolean) } 18 | def use_emoji? 19 | @emoji 20 | end 21 | 22 | sig { returns(T::Boolean) } 23 | def use_color_prompt? 24 | @color_prompt 25 | end 26 | 27 | sig { returns(T::Boolean) } 28 | def suggest_arrow_keys? 29 | @arrow_keys 30 | end 31 | 32 | sig { returns(T::Boolean) } 33 | def shift_cursor_back_on_horizontal_absolute? 34 | @shift_cursor 35 | end 36 | 37 | sig { returns(OS) } 38 | def self.current 39 | @current_os ||= case RbConfig::CONFIG['host_os'] 40 | when /darwin/ 41 | MAC 42 | when /linux/ 43 | LINUX 44 | else 45 | if RUBY_PLATFORM !~ /cygwin/ && ENV['OS'] == 'Windows_NT' 46 | WINDOWS 47 | else 48 | raise "Could not determine OS from host_os #{RbConfig::CONFIG["host_os"]}" 49 | end 50 | end 51 | end 52 | 53 | MAC = OS.new 54 | LINUX = OS.new 55 | WINDOWS = OS.new(emoji: false, color_prompt: false, arrow_keys: false, shift_cursor: true) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/printer.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui' 3 | 4 | module CLI 5 | module UI 6 | class Printer 7 | extend T::Sig 8 | 9 | # Print a message to a stream with common utilities. 10 | # Allows overriding the color, encoding, and target stream. 11 | # By default, it formats the string using CLI:UI and rescues common stream errors. 12 | # 13 | # ==== Attributes 14 | # 15 | # * +msg+ - (required) the string to output. Can be frozen. 16 | # 17 | # ==== Options 18 | # 19 | # * +:frame_color+ - Override the frame color. Defaults to nil. 20 | # * +:to+ - Target stream, like $stdout or $stderr. Can be anything with a puts method. Defaults to $stdout. 21 | # * +:encoding+ - Force the output to be in a certain encoding. Defaults to UTF-8. 22 | # * +:format+ - Whether to format the string using CLI::UI.fmt. Defaults to true. 23 | # * +:graceful+ - Whether to gracefully ignore common I/O errors. Defaults to true. 24 | # * +:wrap+ - Whether to wrap text at word boundaries to terminal width. Defaults to true. 25 | # 26 | # ==== Returns 27 | # Returns whether the message was successfully printed, 28 | # which can be useful if +:graceful+ is set to true. 29 | # 30 | # ==== Example 31 | # 32 | # CLI::UI::Printer.puts('{{x}} Ouch', to: $stderr) 33 | # 34 | sig do 35 | params( 36 | msg: String, 37 | frame_color: T.nilable(Colorable), 38 | to: IOLike, 39 | encoding: T.nilable(Encoding), 40 | format: T::Boolean, 41 | graceful: T::Boolean, 42 | wrap: T::Boolean 43 | ).returns(T::Boolean) 44 | end 45 | def self.puts( 46 | msg, 47 | frame_color: nil, 48 | to: $stdout, 49 | encoding: Encoding::UTF_8, 50 | format: true, 51 | graceful: true, 52 | wrap: true 53 | ) 54 | msg = (+msg).force_encoding(encoding) if encoding 55 | msg = CLI::UI.fmt(msg) if format 56 | msg = CLI::UI.wrap(msg) if wrap 57 | 58 | if frame_color 59 | CLI::UI::Frame.with_frame_color_override(frame_color) { to.puts(msg) } 60 | else 61 | to.puts(msg) 62 | end 63 | 64 | true 65 | rescue Errno::EIO, Errno::EPIPE, IOError => e 66 | raise(e) unless graceful 67 | 68 | false 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/progress.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui' 3 | 4 | module CLI 5 | module UI 6 | class Progress 7 | extend T::Sig 8 | 9 | # A Cyan filled block 10 | FILLED_BAR = "\e[46m" 11 | # A bright white block 12 | UNFILLED_BAR = "\e[1;47m" 13 | 14 | # Add a progress bar to the terminal output 15 | # 16 | # https://user-images.githubusercontent.com/3074765/33799794-cc4c940e-dd00-11e7-9bdc-90f77ec9167c.gif 17 | # 18 | # ==== Example Usage: 19 | # 20 | # Set the percent to X 21 | # CLI::UI::Progress.progress do |bar| 22 | # bar.tick(set_percent: percent) 23 | # end 24 | # 25 | # Increase the percent by 1 percent 26 | # CLI::UI::Progress.progress do |bar| 27 | # bar.tick 28 | # end 29 | # 30 | # Increase the percent by X 31 | # CLI::UI::Progress.progress do |bar| 32 | # bar.tick(percent: 0.05) 33 | # end 34 | sig do 35 | type_parameters(:T) 36 | .params(width: Integer, block: T.proc.params(bar: Progress).returns(T.type_parameter(:T))) 37 | .returns(T.type_parameter(:T)) 38 | end 39 | def self.progress(width: Terminal.width, &block) 40 | bar = Progress.new(width: width) 41 | print(CLI::UI::ANSI.hide_cursor) 42 | yield(bar) 43 | ensure 44 | puts bar.to_s 45 | CLI::UI.raw do 46 | print(ANSI.show_cursor) 47 | end 48 | end 49 | 50 | # Initialize a progress bar. Typically used in a +Progress.progress+ block 51 | # 52 | # ==== Options 53 | # One of the follow can be used, but not both together 54 | # 55 | # * +:width+ - The width of the terminal 56 | # 57 | sig { params(width: Integer).void } 58 | def initialize(width: Terminal.width) 59 | @percent_done = T.let(0, Numeric) 60 | @max_width = width 61 | end 62 | 63 | # Set the progress of the bar. Typically used in a +Progress.progress+ block 64 | # 65 | # ==== Options 66 | # One of the follow can be used, but not both together 67 | # 68 | # * +:percent+ - Increment progress by a specific percent amount 69 | # * +:set_percent+ - Set progress to a specific percent 70 | # 71 | # *Note:* The +:percent+ and +:set_percent must be between 0.00 and 1.0 72 | # 73 | sig { params(percent: T.nilable(Numeric), set_percent: T.nilable(Numeric)).void } 74 | def tick(percent: nil, set_percent: nil) 75 | raise ArgumentError, 'percent and set_percent cannot both be specified' if percent && set_percent 76 | 77 | @percent_done += percent || 0.01 78 | @percent_done = set_percent if set_percent 79 | @percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0 80 | 81 | print(to_s) 82 | print(CLI::UI::ANSI.previous_line + "\n") 83 | end 84 | 85 | # Format the progress bar to be printed to terminal 86 | # 87 | sig { returns(String) } 88 | def to_s 89 | suffix = " #{(@percent_done * 100).floor}%".ljust(5) 90 | workable_width = @max_width - Frame.prefix_width - suffix.size 91 | filled = [(@percent_done * workable_width.to_f).ceil, 0].max 92 | unfilled = [workable_width - filled, 0].max 93 | 94 | CLI::UI.resolve_text([ 95 | FILLED_BAR + ' ' * filled, 96 | UNFILLED_BAR + ' ' * unfilled, 97 | CLI::UI::Color::RESET.code + suffix, 98 | ].join) 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | module CLI 3 | module UI 4 | module Prompt 5 | # A class that handles the various options of an InteractivePrompt and their callbacks 6 | class OptionsHandler 7 | extend T::Sig 8 | 9 | sig { void } 10 | def initialize 11 | @options = {} 12 | end 13 | 14 | sig { returns(T::Array[String]) } 15 | def options 16 | @options.keys 17 | end 18 | 19 | sig { params(option: String, handler: T.proc.params(option: String).returns(String)).void } 20 | def option(option, &handler) 21 | @options[option] = handler 22 | end 23 | 24 | sig { params(options: T.any(T::Array[String], String)).returns(String) } 25 | def call(options) 26 | case options 27 | when Array 28 | options.map { |option| @options[option].call(options) } 29 | else 30 | @options[options].call(options) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb: -------------------------------------------------------------------------------- 1 | # typed: ignore 2 | # frozen_string_literal: true 3 | 4 | module T 5 | class << self 6 | def absurd(value); end 7 | def all(type_a, type_b, *types); end 8 | def any(type_a, type_b, *types); end 9 | def attached_class; end 10 | def class_of(klass); end 11 | def enum(values); end 12 | def nilable(type); end 13 | def noreturn; end 14 | def self_type; end 15 | def type_alias(type = nil, &_blk); end 16 | def type_parameter(name); end 17 | def untyped; end 18 | 19 | def assert_type!(value, _type, _checked: true) 20 | value 21 | end 22 | 23 | def cast(value, _type, _checked: true) 24 | value 25 | end 26 | 27 | def let(value, _type, _checked: true) 28 | value 29 | end 30 | 31 | def must(arg, _msg = nil) 32 | arg 33 | end 34 | 35 | def proc 36 | T::Proc.new 37 | end 38 | 39 | def reveal_type(value) 40 | value 41 | end 42 | 43 | def unsafe(value) 44 | value 45 | end 46 | end 47 | 48 | module Sig 49 | def sig(arg0 = nil, &blk); end 50 | end 51 | 52 | module Helpers 53 | def abstract!; end 54 | def interface!; end 55 | def final!; end 56 | def sealed!; end 57 | def mixes_in_class_methods(mod); end 58 | end 59 | 60 | module Generic 61 | include(T::Helpers) 62 | 63 | def type_parameters(*params); end 64 | def type_member(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end 65 | def type_template(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end 66 | 67 | def [](*types) 68 | self 69 | end 70 | end 71 | 72 | module Array 73 | def self.[](type); end 74 | end 75 | 76 | Boolean = Object.new.freeze 77 | 78 | module Configuration 79 | def self.call_validation_error_handler(signature, opts); end 80 | def self.call_validation_error_handler=(value); end 81 | def self.default_checked_level=(default_checked_level); end 82 | def self.enable_checking_for_sigs_marked_checked_tests; end 83 | def self.enable_final_checks_on_hooks; end 84 | def self.enable_legacy_t_enum_migration_mode; end 85 | def self.reset_final_checks_on_hooks; end 86 | def self.hard_assert_handler(str, extra); end 87 | def self.hard_assert_handler=(value); end 88 | def self.inline_type_error_handler(error); end 89 | def self.inline_type_error_handler=(value); end 90 | def self.log_info_handler(str, extra); end 91 | def self.log_info_handler=(value); end 92 | def self.scalar_types; end 93 | def self.scalar_types=(values); end 94 | # rubocop:disable Naming/InclusiveLanguage 95 | def self.sealed_violation_whitelist; end 96 | def self.sealed_violation_whitelist=(sealed_violation_whitelist); end 97 | # rubocop:enable Naming/InclusiveLanguage 98 | def self.sig_builder_error_handler(error, location); end 99 | def self.sig_builder_error_handler=(value); end 100 | def self.sig_validation_error_handler(error, opts); end 101 | def self.sig_validation_error_handler=(value); end 102 | def self.soft_assert_handler(str, extra); end 103 | def self.soft_assert_handler=(value); end 104 | end 105 | 106 | module Enumerable 107 | def self.[](type); end 108 | end 109 | 110 | module Enumerator 111 | def self.[](type); end 112 | end 113 | 114 | module Hash 115 | def self.[](keys, values); end 116 | end 117 | 118 | class Proc 119 | def bind(*_) 120 | self 121 | end 122 | 123 | def params(*_param) 124 | self 125 | end 126 | 127 | def void 128 | self 129 | end 130 | 131 | def returns(_type) 132 | self 133 | end 134 | end 135 | 136 | module Range 137 | def self.[](type); end 138 | end 139 | 140 | module Set 141 | def self.[](type); end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/spinner.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen-string-literal: true 3 | 4 | require 'cli/ui' 5 | 6 | module CLI 7 | module UI 8 | module Spinner 9 | extend T::Sig 10 | 11 | autoload :Async, 'cli/ui/spinner/async' 12 | autoload :SpinGroup, 'cli/ui/spinner/spin_group' 13 | 14 | PERIOD = 0.1 # seconds 15 | TASK_FAILED = :task_failed 16 | 17 | RUNES = if CLI::UI::OS.current.use_emoji? 18 | ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].freeze 19 | else 20 | ['\\', '|', '/', '-', '\\', '|', '/', '-'].freeze 21 | end 22 | 23 | colors = [CLI::UI::Color::CYAN.code] * (RUNES.size / 2).ceil + 24 | [CLI::UI::Color::MAGENTA.code] * (RUNES.size / 2).to_i 25 | GLYPHS = colors.zip(RUNES).map(&:join) 26 | 27 | class << self 28 | extend T::Sig 29 | 30 | sig { returns(T.nilable(Integer)) } 31 | attr_accessor(:index) 32 | 33 | # We use this from CLI::UI::Widgets::Status to render an additional 34 | # spinner next to the "working" element. While this global state looks 35 | # a bit repulsive at first, it's worth realizing that: 36 | # 37 | # * It's managed by the SpinGroup#wait method, not individual tasks; and 38 | # * It would be complete insanity to run two separate but concurrent SpinGroups. 39 | # 40 | # While it would be possible to stitch through some connection between 41 | # the SpinGroup and the Widgets included in its title, this is simpler 42 | # in practice and seems unlikely to cause issues in practice. 43 | sig { returns(String) } 44 | def current_rune 45 | RUNES[index || 0] 46 | end 47 | end 48 | 49 | # Adds a single spinner 50 | # Uses an interactive session to allow the user to pick an answer 51 | # Can use arrows, y/n, numbers (1/2), and vim bindings to control 52 | # 53 | # https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif 54 | # 55 | # ==== Attributes 56 | # 57 | # * +title+ - Title of the spinner to use 58 | # 59 | # ==== Options 60 | # 61 | # * +:auto_debrief+ - Automatically debrief exceptions? Default to true 62 | # 63 | # ==== Block 64 | # 65 | # * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes 66 | # 67 | # ==== Example Usage: 68 | # 69 | # CLI::UI::Spinner.spin('Title') { sleep 1.0 } 70 | # 71 | sig do 72 | params(title: String, auto_debrief: T::Boolean, block: T.proc.params(task: SpinGroup::Task).void) 73 | .returns(T::Boolean) 74 | end 75 | def self.spin(title, auto_debrief: true, &block) 76 | sg = SpinGroup.new(auto_debrief: auto_debrief) 77 | sg.add(title, &block) 78 | sg.wait 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | module CLI 3 | module UI 4 | module Spinner 5 | class Async 6 | extend T::Sig 7 | 8 | # Convenience method for +initialize+ 9 | # 10 | sig { params(title: String).returns(Async) } 11 | def self.start(title) 12 | new(title) 13 | end 14 | 15 | # Initializes a new asynchronous spinner with no specific end. 16 | # Must call +.stop+ to end the spinner 17 | # 18 | # ==== Attributes 19 | # 20 | # * +title+ - Title of the spinner to use 21 | # 22 | # ==== Example Usage: 23 | # 24 | # CLI::UI::Spinner::Async.new('Title') 25 | # 26 | sig { params(title: String).void } 27 | def initialize(title) 28 | require 'thread' 29 | sg = CLI::UI::Spinner::SpinGroup.new 30 | @m = Mutex.new 31 | @cv = ConditionVariable.new 32 | sg.add(title) { @m.synchronize { @cv.wait(@m) } } 33 | @t = Thread.new { sg.wait } 34 | end 35 | 36 | # Stops an asynchronous spinner 37 | # 38 | sig { returns(T::Boolean) } 39 | def stop 40 | @m.synchronize { @cv.signal } 41 | @t.value 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | module CLI 3 | module UI 4 | module Spinner 5 | class SpinGroup 6 | extend T::Sig 7 | 8 | # Initializes a new spin group 9 | # This lets you add +Task+ objects to the group to multi-thread work 10 | # 11 | # ==== Options 12 | # 13 | # * +:auto_debrief+ - Automatically debrief exceptions? Default to true 14 | # 15 | # ==== Example Usage 16 | # 17 | # CLI::UI::SpinGroup.new do |spin_group| 18 | # spin_group.add('Title') { |spinner| sleep 3.0 } 19 | # spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 } 20 | # end 21 | # 22 | # Output: 23 | # 24 | # https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif 25 | # 26 | sig { params(auto_debrief: T::Boolean).void } 27 | def initialize(auto_debrief: true) 28 | @m = Mutex.new 29 | @consumed_lines = 0 30 | @tasks = [] 31 | @auto_debrief = auto_debrief 32 | @start = Time.new 33 | if block_given? 34 | yield self 35 | wait 36 | end 37 | end 38 | 39 | class Task 40 | extend T::Sig 41 | 42 | sig { returns(String) } 43 | attr_reader :title, :stdout, :stderr 44 | 45 | sig { returns(T::Boolean) } 46 | attr_reader :success 47 | 48 | sig { returns(T.nilable(Exception)) } 49 | attr_reader :exception 50 | 51 | # Initializes a new Task 52 | # This is managed entirely internally by +SpinGroup+ 53 | # 54 | # ==== Attributes 55 | # 56 | # * +title+ - Title of the task 57 | # * +block+ - Block for the task, will be provided with an instance of the spinner 58 | # 59 | sig { params(title: String, block: T.proc.params(task: Task).returns(T.untyped)).void } 60 | def initialize(title, &block) 61 | @title = title 62 | @always_full_render = title =~ Formatter::SCAN_WIDGET 63 | @thread = Thread.new do 64 | cap = CLI::UI::StdoutRouter::Capture.new(with_frame_inset: false) { block.call(self) } 65 | begin 66 | cap.run 67 | ensure 68 | @stdout = cap.stdout 69 | @stderr = cap.stderr 70 | end 71 | end 72 | 73 | @m = Mutex.new 74 | @force_full_render = false 75 | @done = false 76 | @exception = nil 77 | @success = false 78 | end 79 | 80 | # Checks if a task is finished 81 | # 82 | sig { returns(T::Boolean) } 83 | def check 84 | return true if @done 85 | return false if @thread.alive? 86 | 87 | @done = true 88 | begin 89 | status = @thread.join.status 90 | @success = (status == false) 91 | @success = false if @thread.value == TASK_FAILED 92 | rescue => exc 93 | @exception = exc 94 | @success = false 95 | end 96 | 97 | @done 98 | end 99 | 100 | # Re-renders the task if required: 101 | # 102 | # We try to be as lazy as possible in re-rendering the full line. The 103 | # spinner rune will change on each render for the most part, but the 104 | # body text will rarely have changed. If the body text *has* changed, 105 | # we set @force_full_render. 106 | # 107 | # Further, if the title string includes any CLI::UI::Widgets, we 108 | # assume that it may change from render to render, since those 109 | # evaluate more dynamically than the rest of our format codes, which 110 | # are just text formatters. This is controlled by @always_full_render. 111 | # 112 | # ==== Attributes 113 | # 114 | # * +index+ - index of the task 115 | # * +force+ - force rerender of the task 116 | # * +width+ - current terminal width to format for 117 | # 118 | sig { params(index: Integer, force: T::Boolean, width: Integer).returns(String) } 119 | def render(index, force = true, width: CLI::UI::Terminal.width) 120 | @m.synchronize do 121 | if force || @always_full_render || @force_full_render 122 | full_render(index, width) 123 | else 124 | partial_render(index) 125 | end 126 | ensure 127 | @force_full_render = false 128 | end 129 | end 130 | 131 | # Update the spinner title 132 | # 133 | # ==== Attributes 134 | # 135 | # * +title+ - title to change the spinner to 136 | # 137 | sig { params(new_title: String).void } 138 | def update_title(new_title) 139 | @m.synchronize do 140 | @always_full_render = new_title =~ Formatter::SCAN_WIDGET 141 | @title = new_title 142 | @force_full_render = true 143 | end 144 | end 145 | 146 | private 147 | 148 | sig { params(index: Integer, terminal_width: Integer).returns(String) } 149 | def full_render(index, terminal_width) 150 | prefix = inset + 151 | glyph(index) + 152 | CLI::UI::Color::RESET.code + 153 | ' ' 154 | 155 | truncation_width = terminal_width - CLI::UI::ANSI.printing_width(prefix) 156 | 157 | prefix + 158 | CLI::UI.resolve_text(title, truncate_to: truncation_width) + 159 | "\e[K" 160 | end 161 | 162 | sig { params(index: Integer).returns(String) } 163 | def partial_render(index) 164 | CLI::UI::ANSI.cursor_forward(inset_width) + glyph(index) + CLI::UI::Color::RESET.code 165 | end 166 | 167 | sig { params(index: Integer).returns(String) } 168 | def glyph(index) 169 | if @done 170 | @success ? CLI::UI::Glyph::CHECK.to_s : CLI::UI::Glyph::X.to_s 171 | else 172 | GLYPHS[index] 173 | end 174 | end 175 | 176 | sig { returns(String) } 177 | def inset 178 | @inset ||= CLI::UI::Frame.prefix 179 | end 180 | 181 | sig { returns(Integer) } 182 | def inset_width 183 | @inset_width ||= CLI::UI::ANSI.printing_width(inset) 184 | end 185 | end 186 | 187 | # Add a new task 188 | # 189 | # ==== Attributes 190 | # 191 | # * +title+ - Title of the task 192 | # * +block+ - Block for the task, will be provided with an instance of the spinner 193 | # 194 | # ==== Example Usage: 195 | # spin_group = CLI::UI::SpinGroup.new 196 | # spin_group.add('Title') { |spinner| sleep 1.0 } 197 | # spin_group.wait 198 | # 199 | sig { params(title: String, block: T.proc.params(task: Task).void).void } 200 | def add(title, &block) 201 | @m.synchronize do 202 | @tasks << Task.new(title, &block) 203 | end 204 | end 205 | 206 | # Tells the group you're done adding tasks and to wait for all of them to finish 207 | # 208 | # ==== Example Usage: 209 | # spin_group = CLI::UI::SpinGroup.new 210 | # spin_group.add('Title') { |spinner| sleep 1.0 } 211 | # spin_group.wait 212 | # 213 | sig { returns(T::Boolean) } 214 | def wait 215 | idx = 0 216 | 217 | loop do 218 | all_done = T.let(true, T::Boolean) 219 | 220 | width = CLI::UI::Terminal.width 221 | 222 | @m.synchronize do 223 | CLI::UI.raw do 224 | @tasks.each.with_index do |task, int_index| 225 | nat_index = int_index + 1 226 | task_done = task.check 227 | all_done = false unless task_done 228 | 229 | if nat_index > @consumed_lines 230 | print(task.render(idx, true, width: width) + "\n") 231 | @consumed_lines += 1 232 | else 233 | offset = @consumed_lines - int_index 234 | move_to = CLI::UI::ANSI.cursor_up(offset) + "\r" 235 | move_from = "\r" + CLI::UI::ANSI.cursor_down(offset) 236 | 237 | print(move_to + task.render(idx, idx.zero?, width: width) + move_from) 238 | end 239 | end 240 | end 241 | end 242 | 243 | break if all_done 244 | 245 | idx = (idx + 1) % GLYPHS.size 246 | Spinner.index = idx 247 | sleep(PERIOD) 248 | end 249 | 250 | if @auto_debrief 251 | debrief 252 | else 253 | @m.synchronize do 254 | @tasks.all?(&:success) 255 | end 256 | end 257 | end 258 | 259 | # Debriefs failed tasks is +auto_debrief+ is true 260 | # 261 | sig { returns(T::Boolean) } 262 | def debrief 263 | @m.synchronize do 264 | @tasks.each do |task| 265 | next if task.success 266 | 267 | e = task.exception 268 | out = task.stdout 269 | err = task.stderr 270 | 271 | CLI::UI::Frame.open('Task Failed: ' + task.title, color: :red, timing: Time.new - @start) do 272 | if e 273 | puts "#{e.class}: #{e.message}" 274 | puts "\tfrom #{e.backtrace.join("\n\tfrom ")}" 275 | end 276 | 277 | CLI::UI::Frame.divider('STDOUT') 278 | out = '(empty)' if out.nil? || out.strip.empty? 279 | puts out 280 | 281 | CLI::UI::Frame.divider('STDERR') 282 | err = '(empty)' if err.nil? || err.strip.empty? 283 | puts err 284 | end 285 | end 286 | @tasks.all?(&:success) 287 | end 288 | end 289 | end 290 | end 291 | end 292 | end 293 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui' 3 | require 'stringio' 4 | 5 | module CLI 6 | module UI 7 | module StdoutRouter 8 | class Writer 9 | extend T::Sig 10 | 11 | sig { params(stream: IOLike, name: Symbol).void } 12 | def initialize(stream, name) 13 | @stream = stream 14 | @name = name 15 | end 16 | 17 | sig { params(args: String).void } 18 | def write(*args) 19 | args = args.map do |str| 20 | if auto_frame_inset? 21 | str = str.dup # unfreeze 22 | str = str.force_encoding(Encoding::UTF_8) 23 | apply_line_prefix(str, CLI::UI::Frame.prefix) 24 | else 25 | @pending_newline = false 26 | str 27 | end 28 | end 29 | 30 | # hook return of false suppresses output. 31 | if (hook = Thread.current[:cliui_output_hook]) 32 | return if hook.call(args.map(&:to_s).join, @name) == false 33 | end 34 | 35 | T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream, args)) 36 | if (dup = StdoutRouter.duplicate_output_to) 37 | T.unsafe(dup).write(*prepend_id(dup, args)) 38 | end 39 | end 40 | 41 | private 42 | 43 | sig { params(stream: IOLike, args: T::Array[String]).returns(T::Array[String]) } 44 | def prepend_id(stream, args) 45 | return args unless prepend_id_for_stream(stream) 46 | 47 | args.map do |a| 48 | next a if a.chomp.empty? # allow new lines to be new lines 49 | 50 | "[#{Thread.current[:cliui_output_id][:id]}] #{a}" 51 | end 52 | end 53 | 54 | sig { params(stream: IOLike).returns(T::Boolean) } 55 | def prepend_id_for_stream(stream) 56 | return false unless Thread.current[:cliui_output_id] 57 | return true if Thread.current[:cliui_output_id][:streams].include?(stream) 58 | 59 | false 60 | end 61 | 62 | sig { returns(T::Boolean) } 63 | def auto_frame_inset? 64 | !Thread.current[:no_cliui_frame_inset] 65 | end 66 | 67 | sig { params(str: String, prefix: String).returns(String) } 68 | def apply_line_prefix(str, prefix) 69 | return '' if str.empty? 70 | 71 | prefixed = +'' 72 | str.force_encoding(Encoding::UTF_8).lines.each do |line| 73 | if @pending_newline 74 | prefixed << line 75 | @pending_newline = false 76 | else 77 | prefixed << prefix << line 78 | end 79 | end 80 | @pending_newline = !str.end_with?("\n") 81 | prefixed 82 | end 83 | end 84 | 85 | class Capture 86 | extend T::Sig 87 | 88 | @m = Mutex.new 89 | @active_captures = 0 90 | @saved_stdin = nil 91 | 92 | sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) } 93 | def self.with_stdin_masked(&block) 94 | @m.synchronize do 95 | if @active_captures.zero? 96 | @saved_stdin = $stdin 97 | $stdin, w = IO.pipe 98 | $stdin.close 99 | w.close 100 | end 101 | @active_captures += 1 102 | end 103 | 104 | yield 105 | ensure 106 | @m.synchronize do 107 | @active_captures -= 1 108 | if @active_captures.zero? 109 | $stdin = @saved_stdin 110 | end 111 | end 112 | end 113 | 114 | sig do 115 | params(with_frame_inset: T::Boolean, block: T.proc.void).void 116 | end 117 | def initialize(with_frame_inset: true, &block) 118 | @with_frame_inset = with_frame_inset 119 | @block = block 120 | @stdout = '' 121 | @stderr = '' 122 | end 123 | 124 | sig { returns(String) } 125 | attr_reader :stdout, :stderr 126 | 127 | sig { returns(T.untyped) } 128 | def run 129 | require 'stringio' 130 | 131 | StdoutRouter.assert_enabled! 132 | 133 | out = StringIO.new 134 | err = StringIO.new 135 | 136 | prev_frame_inset = Thread.current[:no_cliui_frame_inset] 137 | prev_hook = Thread.current[:cliui_output_hook] 138 | 139 | if Thread.current.respond_to?(:report_on_exception) 140 | Thread.current.report_on_exception = false 141 | end 142 | 143 | self.class.with_stdin_masked do 144 | Thread.current[:no_cliui_frame_inset] = !@with_frame_inset 145 | Thread.current[:cliui_output_hook] = ->(data, stream) do 146 | case stream 147 | when :stdout then out.write(data) 148 | when :stderr then err.write(data) 149 | else raise 150 | end 151 | false # suppress writing to terminal 152 | end 153 | 154 | begin 155 | @block.call 156 | ensure 157 | @stdout = out.string 158 | @stderr = err.string 159 | end 160 | end 161 | ensure 162 | Thread.current[:cliui_output_hook] = prev_hook 163 | Thread.current[:no_cliui_frame_inset] = prev_frame_inset 164 | end 165 | end 166 | 167 | class << self 168 | extend T::Sig 169 | 170 | WRITE_WITHOUT_CLI_UI = :write_without_cli_ui 171 | 172 | NotEnabled = Class.new(StandardError) 173 | 174 | sig { returns(T.nilable(IOLike)) } 175 | attr_accessor :duplicate_output_to 176 | 177 | sig do 178 | type_parameters(:T) 179 | .params(on_streams: T::Array[IOLike], block: T.proc.params(id: String).returns(T.type_parameter(:T))) 180 | .returns(T.type_parameter(:T)) 181 | end 182 | def with_id(on_streams:, &block) 183 | require 'securerandom' 184 | id = format('%05d', rand(10**5)) 185 | Thread.current[:cliui_output_id] = { 186 | id: id, 187 | streams: on_streams.map { |stream| T.cast(stream, IOLike) }, 188 | } 189 | yield(id) 190 | ensure 191 | Thread.current[:cliui_output_id] = nil 192 | end 193 | 194 | sig { returns(T.nilable(T::Hash[Symbol, T.any(String, IOLike)])) } 195 | def current_id 196 | Thread.current[:cliui_output_id] 197 | end 198 | 199 | sig { void } 200 | def assert_enabled! 201 | raise NotEnabled unless enabled? 202 | end 203 | 204 | sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) } 205 | def with_enabled(&block) 206 | enable 207 | yield 208 | ensure 209 | disable 210 | end 211 | 212 | # TODO: remove this 213 | sig { void } 214 | def ensure_activated 215 | enable unless enabled? 216 | end 217 | 218 | sig { returns(T::Boolean) } 219 | def enable 220 | return false if enabled?($stdout) || enabled?($stderr) 221 | 222 | activate($stdout, :stdout) 223 | activate($stderr, :stderr) 224 | true 225 | end 226 | 227 | sig { params(stream: IOLike).returns(T::Boolean) } 228 | def enabled?(stream = $stdout) 229 | stream.respond_to?(WRITE_WITHOUT_CLI_UI) 230 | end 231 | 232 | sig { returns(T::Boolean) } 233 | def disable 234 | return false unless enabled?($stdout) && enabled?($stderr) 235 | 236 | deactivate($stdout) 237 | deactivate($stderr) 238 | true 239 | end 240 | 241 | private 242 | 243 | sig { params(stream: IOLike).void } 244 | def deactivate(stream) 245 | sc = stream.singleton_class 246 | sc.send(:remove_method, :write) 247 | sc.send(:alias_method, :write, WRITE_WITHOUT_CLI_UI) 248 | end 249 | 250 | sig { params(stream: IOLike, streamname: Symbol).void } 251 | def activate(stream, streamname) 252 | writer = StdoutRouter::Writer.new(stream, streamname) 253 | 254 | raise if stream.respond_to?(WRITE_WITHOUT_CLI_UI) 255 | 256 | stream.singleton_class.send(:alias_method, WRITE_WITHOUT_CLI_UI, :write) 257 | stream.define_singleton_method(:write) do |*args| 258 | writer.write(*args) 259 | end 260 | end 261 | end 262 | end 263 | end 264 | end 265 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/terminal.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require 'cli/ui' 3 | require 'io/console' 4 | 5 | module CLI 6 | module UI 7 | module Terminal 8 | extend T::Sig 9 | 10 | DEFAULT_WIDTH = 80 11 | DEFAULT_HEIGHT = 24 12 | 13 | # Returns the width of the terminal, if possible 14 | # Otherwise will return DEFAULT_WIDTH 15 | # 16 | sig { returns(Integer) } 17 | def self.width 18 | winsize[1] 19 | end 20 | 21 | # Returns the width of the terminal, if possible 22 | # Otherwise, will return DEFAULT_HEIGHT 23 | # 24 | sig { returns(Integer) } 25 | def self.height 26 | winsize[0] 27 | end 28 | 29 | sig { returns([Integer, Integer]) } 30 | def self.winsize 31 | @winsize ||= begin 32 | winsize = IO.console.winsize 33 | setup_winsize_trap 34 | 35 | if winsize.any?(&:zero?) 36 | [DEFAULT_HEIGHT, DEFAULT_WIDTH] 37 | else 38 | winsize 39 | end 40 | rescue 41 | [DEFAULT_HEIGHT, DEFAULT_WIDTH] 42 | end 43 | end 44 | 45 | sig { void } 46 | def self.setup_winsize_trap 47 | @winsize_trap ||= Signal.trap('WINCH') do 48 | @winsize = nil 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/truncater.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'cli/ui' 5 | 6 | module CLI 7 | module UI 8 | # Truncater truncates a string to a provided printable width. 9 | module Truncater 10 | PARSE_ROOT = :root 11 | PARSE_ANSI = :ansi 12 | PARSE_ESC = :esc 13 | PARSE_ZWJ = :zwj 14 | 15 | ESC = 0x1b 16 | LEFT_SQUARE_BRACKET = 0x5b 17 | ZWJ = 0x200d # emojipedia.org/emoji-zwj-sequences 18 | SEMICOLON = 0x3b 19 | 20 | # EMOJI_RANGE in particular is super inaccurate. This is best-effort. 21 | # If you need this to be more accurate, we'll almost certainly accept a 22 | # PR improving it. 23 | EMOJI_RANGE = 0x1f300..0x1f5ff 24 | NUMERIC_RANGE = 0x30..0x39 25 | LC_ALPHA_RANGE = 0x40..0x5a 26 | UC_ALPHA_RANGE = 0x60..0x71 27 | 28 | TRUNCATED = "\x1b[0m…" 29 | 30 | class << self 31 | extend T::Sig 32 | 33 | sig { params(text: String, printing_width: Integer).returns(String) } 34 | def call(text, printing_width) 35 | return text if text.size <= printing_width 36 | 37 | width = 0 38 | mode = PARSE_ROOT 39 | truncation_index = T.let(nil, T.nilable(Integer)) 40 | 41 | codepoints = text.codepoints 42 | codepoints.each.with_index do |cp, index| 43 | case mode 44 | when PARSE_ROOT 45 | case cp 46 | when ESC # non-printable, followed by some more non-printables. 47 | mode = PARSE_ESC 48 | when ZWJ # non-printable, followed by another non-printable. 49 | mode = PARSE_ZWJ 50 | else 51 | width += width(cp) 52 | if width >= printing_width 53 | truncation_index ||= index 54 | # it looks like we could break here but we still want the 55 | # width calculation for the rest of the characters. 56 | end 57 | end 58 | when PARSE_ESC 59 | mode = case cp 60 | when LEFT_SQUARE_BRACKET 61 | PARSE_ANSI 62 | else 63 | PARSE_ROOT 64 | end 65 | when PARSE_ANSI 66 | # ANSI escape codes preeeetty much have the format of: 67 | # \x1b[0-9;]+[A-Za-z] 68 | case cp 69 | when NUMERIC_RANGE, SEMICOLON 70 | when LC_ALPHA_RANGE, UC_ALPHA_RANGE 71 | mode = PARSE_ROOT 72 | else 73 | # unexpected. let's just go back to the root state I guess? 74 | mode = PARSE_ROOT 75 | end 76 | when PARSE_ZWJ 77 | # consume any character and consider it as having no width 78 | # width(x+ZWJ+y) = width(x). 79 | mode = PARSE_ROOT 80 | end 81 | end 82 | 83 | # Without the `width <= printing_width` check, we truncate 84 | # "foo\x1b[0m" for a width of 3, but it should not be truncated. 85 | # It's specifically for the case where we decided "Yes, this is the 86 | # point at which we'd have to add a truncation!" but it's actually 87 | # the end of the string. 88 | return text if !truncation_index || width <= printing_width 89 | 90 | T.must(codepoints[0...truncation_index]).pack('U*') + TRUNCATED 91 | end 92 | 93 | private 94 | 95 | sig { params(printable_codepoint: Integer).returns(Integer) } 96 | def width(printable_codepoint) 97 | case printable_codepoint 98 | when EMOJI_RANGE 99 | 2 100 | else 101 | 1 102 | end 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/version.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | module CLI 3 | module UI 4 | VERSION = '1.5.1' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/widgets.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require('cli/ui') 3 | 4 | module CLI 5 | module UI 6 | # Widgets are formatter objects with more custom implementations than the 7 | # other features, which all center around formatting text with colours, 8 | # etc. 9 | # 10 | # If you want to extend CLI::UI with your own widgets, you may want to do 11 | # something like this: 12 | # 13 | # require('cli/ui') 14 | # class MyWidget < CLI::UI::Widgets::Base 15 | # # ... 16 | # end 17 | # CLI::UI::Widgets.register('my-widget') { MyWidget } 18 | # puts(CLI::UI.fmt("{{@widget/my-widget:args}}")) 19 | module Widgets 20 | extend T::Sig 21 | 22 | MAP = {} 23 | 24 | autoload(:Base, 'cli/ui/widgets/base') 25 | 26 | sig { params(name: String, cb: T.proc.returns(T.class_of(Widgets::Base))).void } 27 | def self.register(name, &cb) 28 | MAP[name] = cb 29 | end 30 | 31 | autoload(:Status, 'cli/ui/widgets/status') 32 | register('status') { Widgets::Status } 33 | 34 | # Looks up a widget by handle 35 | # 36 | # ==== Raises 37 | # Raises InvalidWidgetHandle if the widget is not available. 38 | # 39 | # ==== Returns 40 | # A callable widget, to be invoked like `.call(argstring)` 41 | # 42 | sig { params(handle: String).returns(T.class_of(Widgets::Base)) } 43 | def self.lookup(handle) 44 | MAP.fetch(handle).call 45 | rescue KeyError, NameError 46 | raise(InvalidWidgetHandle, handle) 47 | end 48 | 49 | # All available widgets by name 50 | # 51 | sig { returns(T::Array[String]) } 52 | def self.available 53 | MAP.keys 54 | end 55 | 56 | class InvalidWidgetHandle < ArgumentError 57 | extend T::Sig 58 | 59 | sig { params(handle: String).void } 60 | def initialize(handle) 61 | super 62 | @handle = handle 63 | end 64 | 65 | sig { returns(String) } 66 | def message 67 | keys = Widgets.available.join(',') 68 | "invalid widget handle: #{@handle} " \ 69 | "-- must be one of CLI::UI::Widgets.available (#{keys})" 70 | end 71 | end 72 | 73 | class InvalidWidgetArguments < ArgumentError 74 | extend T::Sig 75 | 76 | sig { params(argstring: String, pattern: Regexp).void } 77 | def initialize(argstring, pattern) 78 | super 79 | @argstring = argstring 80 | @pattern = pattern 81 | end 82 | 83 | sig { returns(String) } 84 | def message 85 | "invalid widget arguments: #{@argstring} " \ 86 | "-- must match pattern: #{@pattern.inspect}" 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | require('cli/ui') 3 | 4 | module CLI 5 | module UI 6 | module Widgets 7 | class Base 8 | extend T::Sig 9 | extend T::Helpers 10 | abstract! 11 | 12 | sig { params(argstring: String).returns(String) } 13 | def self.call(argstring) 14 | new(argstring).render 15 | end 16 | 17 | sig { params(argstring: String).void } 18 | def initialize(argstring) 19 | pat = self.class.argparse_pattern 20 | unless (@match_data = pat.match(argstring)) 21 | raise(Widgets::InvalidWidgetArguments.new(argstring, pat)) 22 | end 23 | 24 | @match_data.names.each do |name| 25 | instance_variable_set(:"@#{name}", @match_data[name]) 26 | end 27 | end 28 | 29 | sig { abstract.returns(Regexp) } 30 | def self.argparse_pattern; end 31 | 32 | sig { abstract.returns(String) } 33 | def render; end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen-string-literal: true 3 | 4 | require('cli/ui') 5 | 6 | module CLI 7 | module UI 8 | module Widgets 9 | class Status < Widgets::Base 10 | ARGPARSE_PATTERN = %r{ 11 | \A (? \d+) 12 | : (? \d+) 13 | : (? \d+) 14 | : (? \d+) \z 15 | }x # e.g. "1:23:3:404" 16 | OPEN = Color::RESET.code + Color::BOLD.code + '[' + Color::RESET.code 17 | CLOSE = Color::RESET.code + Color::BOLD.code + ']' + Color::RESET.code 18 | ARROW = Color::RESET.code + Color::GRAY.code + '◂' + Color::RESET.code 19 | COMMA = Color::RESET.code + Color::GRAY.code + ',' + Color::RESET.code 20 | 21 | SPINNER_STOPPED = '⠿' 22 | EMPTY_SET = '∅' 23 | 24 | sig { override.returns(Regexp) } 25 | def self.argparse_pattern 26 | ARGPARSE_PATTERN 27 | end 28 | 29 | sig { override.returns(String) } 30 | def render 31 | if zero?(@succeeded) && zero?(@failed) && zero?(@working) && zero?(@pending) 32 | Color::RESET.code + Color::BOLD.code + EMPTY_SET + Color::RESET.code 33 | else 34 | # [ 0✓ , 2✗ ◂ 3⠼ ◂ 4⌛︎ ] 35 | "#{OPEN}#{succeeded_part}#{COMMA}#{failed_part}#{ARROW}#{working_part}#{ARROW}#{pending_part}#{CLOSE}" 36 | end 37 | end 38 | 39 | private 40 | 41 | sig { params(num_str: String).returns(T::Boolean) } 42 | def zero?(num_str) 43 | num_str == '0' 44 | end 45 | 46 | sig { params(num_str: String, rune: String, color: Color).returns(String) } 47 | def colorize_if_nonzero(num_str, rune, color) 48 | color = Color::GRAY if zero?(num_str) 49 | color.code + num_str + rune 50 | end 51 | 52 | sig { returns(String) } 53 | def succeeded_part 54 | colorize_if_nonzero(@succeeded, Glyph::CHECK.char, Color::GREEN) 55 | end 56 | 57 | sig { returns(String) } 58 | def failed_part 59 | colorize_if_nonzero(@failed, Glyph::X.char, Color::RED) 60 | end 61 | 62 | sig { returns(String) } 63 | def working_part 64 | rune = zero?(@working) ? SPINNER_STOPPED : Spinner.current_rune 65 | colorize_if_nonzero(@working, rune, Color::BLUE) 66 | end 67 | 68 | sig { returns(String) } 69 | def pending_part 70 | colorize_if_nonzero(@pending, Glyph::HOURGLASS.char, Color::WHITE) 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /vendor/deps/cli-ui/lib/cli/ui/wrap.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # typed: true 4 | 5 | require 'cli/ui' 6 | require 'cli/ui/frame/frame_stack' 7 | require 'cli/ui/frame/frame_style' 8 | 9 | module CLI 10 | module UI 11 | class Wrap 12 | extend T::Sig 13 | 14 | sig { params(input: String).void } 15 | def initialize(input) 16 | @input = input 17 | end 18 | 19 | sig { returns(String) } 20 | def wrap 21 | max_width = Terminal.width - Frame.prefix_width 22 | width = T.let(0, Integer) 23 | final = [] 24 | # Create an alternation of format codes of parameter lengths 1-20, since + and {1,n} not allowed in lookbehind 25 | format_codes = (1..20).map { |n| /\x1b\[[\d;]{#{n}}m/ }.join('|') 26 | codes = '' 27 | @input.split(/(?=\s|\x1b\[[\d;]+m|\r)|(?<=\s|#{format_codes})/).each do |token| 28 | case token 29 | when '\x1B[0?m' 30 | codes = '' 31 | final << token 32 | when /\x1b\[[\d;]+m/ 33 | codes += token # Track in use format codes so that they are resent after frame coloring 34 | final << token 35 | when "\n" 36 | final << "\n#{codes}" 37 | width = 0 38 | when /\s/ 39 | token_width = ANSI.printing_width(token) 40 | if width + token_width <= max_width 41 | final << token 42 | width += token_width 43 | else 44 | final << "\n#{codes}" 45 | width = 0 46 | end 47 | else 48 | token_width = ANSI.printing_width(token) 49 | if width + token_width <= max_width 50 | final << token 51 | width += token_width 52 | else 53 | final << "\n#{codes}" 54 | final << token 55 | width = token_width 56 | end 57 | end 58 | end 59 | final.join 60 | end 61 | end 62 | end 63 | end 64 | --------------------------------------------------------------------------------