#!/usr/bin/env ruby class DevHelper # Support dashes in command names COMMAND_TO_METHOD = { "ts-node" => :ts_node, "check-types" => :check_types, "bash-completions" => :bash_completions, "plantuml-to-png" => :plantuml_to_png, } METHOD_TO_COMMAND = COMMAND_TO_METHOD.invert REPLACE_PROCESS = "replace_process" WAIT_FOR_PROCESS = "wait_for_process" # External Interface def self.call(*args) new.call(*args) end # Core logic def call(*args, **kwargs) command = args[0] method = COMMAND_TO_METHOD.fetch(command, command) if args.length.positive? && respond_to?(method) public_send(method, *args.drop(1), **kwargs) else compose(*args, **kwargs) end end def compose(*args, **kwargs) command = compose_command(*args, **kwargs) puts "Running: #{command}" unless kwargs[:slient] case kwargs[:execution_mode] when WAIT_FOR_PROCESS wait_for_process_with_logging(command) else exec(command) end end # Primary command wrappers def build(*args, **kwargs) compose(%w[build], *args, **kwargs) end def compile(*args, **kwargs) run(*%w[api npm run build], execution_mode: WAIT_FOR_PROCESS) exit($?.exitstatus) unless $?.success? run(*%w[web npm run build]) end def up(*args, **kwargs) compose(*%w[up --remove-orphans], *args, **kwargs) end def down(*args, **kwargs) compose(*%w[down --remove-orphans], *args, **kwargs) end def logs(*args, **kwargs) compose(*%w[logs -f], *args, **kwargs) end def run(*args, **kwargs) compose(*%w[run --rm], *args, **kwargs) end def ps(*args, **kwargs) compose(*%w[ps], *args, **kwargs) end # Custom helpers def api(*args, **kwargs) run(*%w[api], *args, **kwargs) end def web(*args, **kwargs) run(*%w[web], *args, **kwargs) end def check_types(*args, **kwargs) run(*%w[api npm run check-types], *args, **kwargs) end def test(*args, **kwargs) service = args[0] if service == "api" test_api(*args.drop(1), **kwargs) elsif service == "web" test_web(*args.drop(1), **kwargs) else test_api(*args, **kwargs) end end def test_api(*args, **kwargs) reformat_project_relative_path_filter_for_vitest!(args, "api/") run(*%w[test_api npm run test], *args, **kwargs) end def test_web(*args, **kwargs) reformat_project_relative_path_filter_for_vitest!(args, "web/") run(*%w[test_web npm run test], *args, **kwargs) end def sqlcmd(*args, **kwargs) db_host = ENV.fetch('DB_HOST', 'localhost') db_user = ENV.fetch('DB_USER', 'sa') db_pass = ENV.fetch('DB_PASS', '1m5ecure!') db_name = ENV.fetch('DB_NAME', 'YHSI') compose( *%w[exec db /opt/mssql-tools/bin/sqlcmd], *%W[-U #{db_user}], *%W[-P #{db_pass}], *%W[-H #{db_host}], *%W[-d #{db_name}], '-I', # enable quoted identifiers, e.g. "table"."column" *args, **kwargs ) end def db(*args, **kwargs) compose(*%w[exec db], *args, **kwargs) end def debug api_container_id = container_id("api") puts "Waiting for breakpoint to trigger..." puts "'ctrl-c' to exit." command = "docker attach --detach-keys ctrl-c #{api_container_id}" puts "Running: #{command}" exec(command) exit 0 end def npm(*args, **kwargs) run(*%w[api npm], *args, **kwargs) end def ts_node(*args, **kwargs) run(*%w[api npm run ts-node], *args, **kwargs) end def knex(*args, **kwargs) if RUBY_PLATFORM =~ /linux/ run(*%w[api npm run knex], *args, execution_mode: WAIT_FOR_PROCESS, **kwargs) file_or_directory = "#{project_root}/api/src/db/migrations" exit(0) unless take_over_needed?(file_or_directory) ownit file_or_directory else run(*%w[api npm run knex], *args, **kwargs) end end def migrate(*args, **kwargs) action = args[0] knex("migrate:#{action}", *args.drop(1), **kwargs) end def seed(*args, **kwargs) action = args[0] knex("seed:#{action}", *args.drop(1), **kwargs) end def ownit(*args, **kwargs) file_or_directory = args[0] raise ScriptError, "Must provide a file or directory path." if file_or_directory.nil? if RUBY_PLATFORM =~ /linux/ puts "Take ownership of the file or directory? #{file_or_directory}" exec("sudo chown -R #{user_id}:#{group_id} #{file_or_directory}") else raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" end end def plantuml_to_png(*args, **kwargs) file_path = args.pop raise ScriptError, "Must provide a file path." if file_path.nil? png_path = file_path.gsub(/\.(wsd|pu|puml|plantuml|uml)$/, ".png") command = <<~BASH curl #{args.join(" ")} \ --data-binary @'#{file_path}' \ http://localhost:9999/png > '#{png_path}' BASH puts "Running: #{command}" exec(command) end def bash_completions completions = public_methods(false) .reject { |word| %i[call].include?(word) } .map { |word| METHOD_TO_COMMAND.fetch(word, word) } puts completions end private def wait_for_process_with_logging(command) IO.popen("#{command} 2>&1") do |io| until io.eof? line = io.gets puts line end end end def container_id(container_name, *args, **kwargs) command = compose_command(*%w[ps -q], container_name, *args, **kwargs) puts "Running: #{command}" id_of_container = `#{command}`.chomp puts "Container id is: #{id_of_container}" id_of_container end def service_running?(container_name) ps(*%w[-q --status=running], execution_mode: WAIT_FOR_PROCESS, slient: true) != "" end def compose_command(*args, **kwargs) environment = kwargs.fetch(:environment, "development") "cd #{project_root} && docker compose -f docker-compose.#{environment}.yml #{args.join(" ")}" end def project_root @project_root ||= File.absolute_path("#{__dir__}/..") end def take_over_needed?(file_or_directory) files_owned_by_others = system("find #{file_or_directory} -not -user #{user_id} -print -quit | grep -q .") files_owned_by_others end def user_id unless RUBY_PLATFORM =~ /linux/ raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" end `id -u`.strip end def group_id unless RUBY_PLATFORM =~ /linux/ raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" end `id -g`.strip end def reformat_project_relative_path_filter_for_vitest!(args, prefix) if args.length.positive? && args[0].start_with?(prefix) src_path_prefix = "#{prefix}src/" test_path_regex = Regexp.escape(prefix) src_path_regex = Regexp.escape(src_path_prefix) if args[0].start_with?(src_path_prefix) # TODO: handle other file types args[0] = args[0].gsub(/^#{src_path_regex}/, "tests/").gsub(/\.ts$/, ".test.ts") else args[0] = args[0].gsub(/^#{test_path_regex}/, "") end puts "Reformatted path filter from project relative to service relative for vitest." end end end # Only execute main function when file is executed DevHelper.call(*ARGV) if $PROGRAM_NAME == __FILE__ ## Dev completions # https://iridakos.com/programming/2018/03/01/bash-programmable-completion-tutorial # _dev_completions () { # local dev_command_path="$(which dev)" # local dev_function_names # dev_function_names="$(ruby "$dev_command_path" bash_completions)" # # COMP_WORDS: an array of all the words typed after the name of the program the compspec belongs to # # COMP_CWORD: an index of the COMP_WORDS array pointing to the word the current cursor is at - in other words, the index of the word the cursor was when the tab key was pressed # # COMP_LINE: the current command line # COMPREPLY=($(compgen -W "$dev_function_names" "${COMP_WORDS[$COMP_CWORD]}")) # } # complete -F _dev_completions dev # complete -W "allow" direnv