first commit

This commit is contained in:
2026-06-19 23:55:45 -07:00
commit f2e4730549
297 changed files with 30726 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
# `dev` command documentation
## `dev` Command
The `dev` command is a helper for using docker compose.
For example, you can run a sql script via
```bash
dev sqlcmd -i ./data/funding_submission_lines.sql
```
assuming the file is located at `/db/data/funding_submission_lines.sql`
Note that the `dev` command uses the `db` service, and so only has access to folders under the top-level `db` directory.
## Set up `dev` command
The `dev` command vastly simplifies development using docker compose. It only requires `ruby`; however, `direnv` and `asdf` will make it easier to use.
It's simply a wrapper around docker compose with the ability to quickly add custom helpers.
All commands are just strings joined together, so it's easy to add new commmands. `dev` prints out each command that it runs, so that you can run the command manually to debug it, or just so you learn some docker compose syntax as you go.
1. (optional) Install `asdf` as seen in <https://asdf-vm.com/guide/getting-started.html>.
e.g. for Linux
```bash
apt install curl git
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.12.0
echo '
# asdf
. "$HOME/.asdf/asdf.sh"
. "$HOME/.asdf/completions/asdf.bash"
' >> ~/.bashrc
```
2. Install `ruby` via `asdf` as seen here <https://github.com/asdf-vm/asdf-ruby>, or using whatever custom Ruby install method works for your platform.
e.g. for Linux
```bash
asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git
# install version from .tool-versions file
asdf install ruby
asdf reshim ruby
```
You will now be able to run the `./bin/dev` command.
3. (optional) Install [direnv](https://direnv.net/) and create an `.envrc` with
```bash
#!/usr/bin/env bash
PATH_add bin
```
and then run `direnv allow`.
You will now be able to do `dev xxx` instead ov `./bin/dev xxx`.
## Extras
If you want to take over a directory or file in Linux you can use `dev ownit <path-to-directory-or-file>`.
If you are on Windows or Mac, and you want that to work, you should implement it in the `bin/dev` file. You might never actually need to take ownership of anything, so this might not be relevant to you.
Executable
+295
View File
@@ -0,0 +1,295 @@
#!/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