From e54850a3aa127435cf6b9a3655115bcda2eed42c Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 29 Mar 2016 13:37:19 +0200 Subject: [PATCH] Add Frey support Squashed commit of the following: commit 9071377570a97d599f7fc769f4d26b45d03020cb Author: Kevin van Zonneveld Date: Tue Mar 29 13:36:57 2016 +0200 Remove useless env code commit 529aed3535d0159dd7291532ebb6cfcc9ae401c6 Author: Kevin van Zonneveld Date: Tue Mar 29 13:31:38 2016 +0200 Update state, add launch target commit fde533f9f6cc896a7e2abd7e68320d806d90ba27 Author: Kevin van Zonneveld Date: Tue Mar 29 11:07:35 2016 +0200 First swing at moving Frey to tusd --- .gitignore | 5 + .infra/Frey-state-terraform.tfstate | 119 +++++++++++++++ .infra/Frey-state-terraform.tfstate.backup | 119 +++++++++++++++ .infra/Freyfile.toml | 159 +++++++++++++++++++++ .infra/README.md | 3 + .infra/env.example.sh | 10 ++ .infra/env.infra.example.sh | 5 + .infra/ssh/frey-infra-tusd.pem.cast5 | Bin 0 -> 1696 bytes .infra/ssh/frey-infra-tusd.pub | 0 .infra/templates/upstart-tusd.conf.j2 | 25 ++++ .travis.yml | 5 +- Makefile | 19 +++ 12 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 .infra/Frey-state-terraform.tfstate create mode 100644 .infra/Frey-state-terraform.tfstate.backup create mode 100644 .infra/Freyfile.toml create mode 100644 .infra/README.md create mode 100644 .infra/env.example.sh create mode 100644 .infra/env.infra.example.sh create mode 100644 .infra/ssh/frey-infra-tusd.pem.cast5 create mode 100644 .infra/ssh/frey-infra-tusd.pub create mode 100644 .infra/templates/upstart-tusd.conf.j2 create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index 63d6663..13506b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ tusd/data cover.out data/ +.infra/env.sh +.infra/env.infra.sh +.infra/ssh/infra-tusd.pem +.infra/Frey-residu* +.infra/ssh/frey-infra-tusd.pem diff --git a/.infra/Frey-state-terraform.tfstate b/.infra/Frey-state-terraform.tfstate new file mode 100644 index 0000000..a711876 --- /dev/null +++ b/.infra/Frey-state-terraform.tfstate @@ -0,0 +1,119 @@ +{ + "version": 1, + "serial": 20, + "modules": [ + { + "path": [ + "root" + ], + "outputs": { + "endpoint": "http://master.tus.io:80/", + "public_address": "ec2-54-161-247-36.compute-1.amazonaws.com", + "public_addresses": "ec2-54-161-247-36.compute-1.amazonaws.com" + }, + "resources": { + "aws_instance.infra-tusd-server": { + "type": "aws_instance", + "primary": { + "id": "i-d59cff7e", + "attributes": { + "ami": "ami-9bce7af0", + "availability_zone": "us-east-1e", + "ebs_block_device.#": "0", + "ebs_optimized": "false", + "ephemeral_block_device.#": "0", + "iam_instance_profile": "", + "id": "i-d59cff7e", + "instance_state": "running", + "instance_type": "c3.large", + "key_name": "infra-tusd", + "monitoring": "false", + "private_dns": "ip-10-167-178-105.ec2.internal", + "private_ip": "10.167.178.105", + "public_dns": "ec2-54-161-247-36.compute-1.amazonaws.com", + "public_ip": "54.161.247.36", + "root_block_device.#": "1", + "root_block_device.0.delete_on_termination": "true", + "root_block_device.0.iops": "24", + "root_block_device.0.volume_size": "8", + "root_block_device.0.volume_type": "gp2", + "security_groups.#": "1", + "security_groups.1246499019": "fw-infra-tusd-main", + "source_dest_check": "true", + "subnet_id": "", + "tags.#": "1", + "tags.Name": "master.tus.io", + "tenancy": "default", + "vpc_security_group_ids.#": "0" + }, + "meta": { + "schema_version": "1" + } + } + }, + "aws_route53_record.www": { + "type": "aws_route53_record", + "depends_on": [ + "aws_instance.infra-tusd-server" + ], + "primary": { + "id": "Z3IT8X6U91XY1P_master.tus.io_CNAME", + "attributes": { + "failover": "", + "fqdn": "master.tus.io", + "health_check_id": "", + "id": "Z3IT8X6U91XY1P_master.tus.io_CNAME", + "name": "master.tus.io", + "records.#": "1", + "records.1116730303": "ec2-54-161-247-36.compute-1.amazonaws.com", + "set_identifier": "", + "ttl": "300", + "type": "CNAME", + "weight": "-1", + "zone_id": "Z3IT8X6U91XY1P" + } + } + }, + "aws_security_group.fw-infra-tusd-main": { + "type": "aws_security_group", + "primary": { + "id": "sg-2ff78c42", + "attributes": { + "description": "Infra tusd", + "egress.#": "0", + "id": "sg-2ff78c42", + "ingress.#": "3", + "ingress.2214680975.cidr_blocks.#": "1", + "ingress.2214680975.cidr_blocks.0": "0.0.0.0/0", + "ingress.2214680975.from_port": "80", + "ingress.2214680975.protocol": "tcp", + "ingress.2214680975.security_groups.#": "0", + "ingress.2214680975.self": "false", + "ingress.2214680975.to_port": "80", + "ingress.516175195.cidr_blocks.#": "1", + "ingress.516175195.cidr_blocks.0": "0.0.0.0/0", + "ingress.516175195.from_port": "8080", + "ingress.516175195.protocol": "tcp", + "ingress.516175195.security_groups.#": "0", + "ingress.516175195.self": "false", + "ingress.516175195.to_port": "8080", + "ingress.614077637.cidr_blocks.#": "3", + "ingress.614077637.cidr_blocks.0": "62.163.187.106/32", + "ingress.614077637.cidr_blocks.1": "84.146.0.0/16", + "ingress.614077637.cidr_blocks.2": "24.134.75.132/32", + "ingress.614077637.from_port": "22", + "ingress.614077637.protocol": "tcp", + "ingress.614077637.security_groups.#": "0", + "ingress.614077637.self": "false", + "ingress.614077637.to_port": "22", + "name": "fw-infra-tusd-main", + "owner_id": "402421253186", + "tags.#": "0", + "vpc_id": "" + } + } + } + } + } + ] +} diff --git a/.infra/Frey-state-terraform.tfstate.backup b/.infra/Frey-state-terraform.tfstate.backup new file mode 100644 index 0000000..a711876 --- /dev/null +++ b/.infra/Frey-state-terraform.tfstate.backup @@ -0,0 +1,119 @@ +{ + "version": 1, + "serial": 20, + "modules": [ + { + "path": [ + "root" + ], + "outputs": { + "endpoint": "http://master.tus.io:80/", + "public_address": "ec2-54-161-247-36.compute-1.amazonaws.com", + "public_addresses": "ec2-54-161-247-36.compute-1.amazonaws.com" + }, + "resources": { + "aws_instance.infra-tusd-server": { + "type": "aws_instance", + "primary": { + "id": "i-d59cff7e", + "attributes": { + "ami": "ami-9bce7af0", + "availability_zone": "us-east-1e", + "ebs_block_device.#": "0", + "ebs_optimized": "false", + "ephemeral_block_device.#": "0", + "iam_instance_profile": "", + "id": "i-d59cff7e", + "instance_state": "running", + "instance_type": "c3.large", + "key_name": "infra-tusd", + "monitoring": "false", + "private_dns": "ip-10-167-178-105.ec2.internal", + "private_ip": "10.167.178.105", + "public_dns": "ec2-54-161-247-36.compute-1.amazonaws.com", + "public_ip": "54.161.247.36", + "root_block_device.#": "1", + "root_block_device.0.delete_on_termination": "true", + "root_block_device.0.iops": "24", + "root_block_device.0.volume_size": "8", + "root_block_device.0.volume_type": "gp2", + "security_groups.#": "1", + "security_groups.1246499019": "fw-infra-tusd-main", + "source_dest_check": "true", + "subnet_id": "", + "tags.#": "1", + "tags.Name": "master.tus.io", + "tenancy": "default", + "vpc_security_group_ids.#": "0" + }, + "meta": { + "schema_version": "1" + } + } + }, + "aws_route53_record.www": { + "type": "aws_route53_record", + "depends_on": [ + "aws_instance.infra-tusd-server" + ], + "primary": { + "id": "Z3IT8X6U91XY1P_master.tus.io_CNAME", + "attributes": { + "failover": "", + "fqdn": "master.tus.io", + "health_check_id": "", + "id": "Z3IT8X6U91XY1P_master.tus.io_CNAME", + "name": "master.tus.io", + "records.#": "1", + "records.1116730303": "ec2-54-161-247-36.compute-1.amazonaws.com", + "set_identifier": "", + "ttl": "300", + "type": "CNAME", + "weight": "-1", + "zone_id": "Z3IT8X6U91XY1P" + } + } + }, + "aws_security_group.fw-infra-tusd-main": { + "type": "aws_security_group", + "primary": { + "id": "sg-2ff78c42", + "attributes": { + "description": "Infra tusd", + "egress.#": "0", + "id": "sg-2ff78c42", + "ingress.#": "3", + "ingress.2214680975.cidr_blocks.#": "1", + "ingress.2214680975.cidr_blocks.0": "0.0.0.0/0", + "ingress.2214680975.from_port": "80", + "ingress.2214680975.protocol": "tcp", + "ingress.2214680975.security_groups.#": "0", + "ingress.2214680975.self": "false", + "ingress.2214680975.to_port": "80", + "ingress.516175195.cidr_blocks.#": "1", + "ingress.516175195.cidr_blocks.0": "0.0.0.0/0", + "ingress.516175195.from_port": "8080", + "ingress.516175195.protocol": "tcp", + "ingress.516175195.security_groups.#": "0", + "ingress.516175195.self": "false", + "ingress.516175195.to_port": "8080", + "ingress.614077637.cidr_blocks.#": "3", + "ingress.614077637.cidr_blocks.0": "62.163.187.106/32", + "ingress.614077637.cidr_blocks.1": "84.146.0.0/16", + "ingress.614077637.cidr_blocks.2": "24.134.75.132/32", + "ingress.614077637.from_port": "22", + "ingress.614077637.protocol": "tcp", + "ingress.614077637.security_groups.#": "0", + "ingress.614077637.self": "false", + "ingress.614077637.to_port": "22", + "name": "fw-infra-tusd-main", + "owner_id": "402421253186", + "tags.#": "0", + "vpc_id": "" + } + } + } + } + } + ] +} diff --git a/.infra/Freyfile.toml b/.infra/Freyfile.toml new file mode 100644 index 0000000..ef7f385 --- /dev/null +++ b/.infra/Freyfile.toml @@ -0,0 +1,159 @@ +[global] + appname = "infra-tusd" + +[global.ssh] + key_dir = "./ssh" + +[global.ansiblecfg.privilege_escalation] + become = true + +[global.ansiblecfg.defaults] + host_key_checking = "False" + +[infra.provider.aws] + access_key = "${var.FREY_AWS_ACCESS_KEY}" + region = "us-east-1" + secret_key = "${var.FREY_AWS_SECRET_KEY}" + +[infra.variable.ami.default] + us-east-1 = "ami-9bce7af0" +[infra.variable.ip_all] + default = "0.0.0.0/0" +[infra.variable.ip_kevin] + default = "62.163.187.106/32" +[infra.variable.ip_marius] + default = "84.146.0.0/16" +[infra.variable.ip_tim] + default = "24.134.75.132/32" +[infra.variable.region] + default = "us-east-1" + +[infra.output.public_address] + value = "${aws_instance.infra-tusd-server.0.public_dns}" +[infra.output.public_addresses] + value = "${join(\"\n\", aws_instance.infra-tusd-server.*.public_dns)}" +[infra.output.endpoint] + value = "http://${aws_route53_record.www.name}:80/" + +[infra.resource.aws_instance.infra-tusd-server] + ami = "${lookup(var.ami, var.region)}" + instance_type = "c3.large" + key_name = "infra-tusd" + security_groups = ["fw-infra-tusd-main"] + [infra.resource.aws_instance.infra-tusd-server.connection] + key_file = "{{{config.global.ssh.privatekey_file}}}" + user = "{{{config.global.ssh.user}}}" + [infra.resource.aws_instance.infra-tusd-server.tags] + Name = "${var.FREY_DOMAIN}" + +[infra.resource.aws_route53_record.www] + name = "${var.FREY_DOMAIN}" + records = ["${aws_instance.infra-tusd-server.public_dns}"] + ttl = "300" + type = "CNAME" + zone_id = "${var.FREY_AWS_ZONE_ID}" + +[infra.resource.aws_security_group.fw-infra-tusd-main] + description = "Infra tusd" + name = "fw-infra-tusd-main" + [[infra.resource.aws_security_group.fw-infra-tusd-main.ingress]] + cidr_blocks = ["${var.ip_all}"] + from_port = 8080 + protocol = "tcp" + to_port = 8080 + [[infra.resource.aws_security_group.fw-infra-tusd-main.ingress]] + cidr_blocks = ["${var.ip_all}"] + from_port = 80 + protocol = "tcp" + to_port = 80 + [[infra.resource.aws_security_group.fw-infra-tusd-main.ingress]] + cidr_blocks = [ + "${var.ip_kevin}", + "${var.ip_marius}", + "${var.ip_tim}", + ] + from_port = 22 + protocol = "tcp" + to_port = 22 + +[[install.playbooks]] + hosts = "infra-tusd-server" + name = "Install infra-tusd-server" + roles = [ + "{{{init.paths.roles_dir}}}/apt/v1.0.0", + ] + [install.playbooks.vars] + apt_packages = [ + "apg", + "build-essential", + "curl", + "git-core", + "htop", + "iotop", + "libpcre3", + "logtail", + "mlocate", + "mtr", + "psmisc", + "telnet", + "vim", + "wget", + ] + + [[install.playbooks.tasks]] + action = "lineinfile dest=/home/ubuntu/.bashrc line=\"alias wtf='sudo tail -f /var/log/*{log,err} /var/log/{dmesg,messages,*{,/*}{log,err}}'\"" + name = "Common | Add convenience shortcut wtf" + +[[setup.playbooks]] + hosts = "infra-tusd-server" + name = "Setup infra-tusd" + + [[setup.playbooks.tasks]] + hostname = "name={{lookup('env', 'FREY_DOMAIN')}}" + name = "infra-tusd | Set hostname" + + [[setup.playbooks.tasks]] + file = "path=/srv/tusd/shared/logs state=directory owner=www-data group=www-data mode=0755 recurse=yes" + name = "infra-tusd | Create shared log dir" + + [[setup.playbooks.tasks]] + file = "path=/mnt/tusd-data state=directory owner=www-data group=www-data mode=0755 recurse=yes" + name = "infra-tusd | Create tusd data dir" + + [[setup.playbooks.tasks]] + action = "template src=templates/upstart-tusd.conf.j2 dest=/etc/init/tusd.conf" + name = "infra-tusd | Install upstart file" + +[[deploy.playbooks]] + hosts = "infra-tusd-server" + name = "Deploy infra-tusd" + roles = [ + "{{{init.paths.roles_dir}}}/deploy/v1.4.0", + ] + [deploy.playbooks.vars] + ansistrano_get_url = "https://github.com/tus/tusd/releases/download/0.1.2/tusd_linux_amd64.tar.gz" + ansistrano_deploy_to = "/srv/tusd" + ansistrano_deploy_via = "download_unarchive" + ansistrano_npm = no + ansistrano_owner = "www-data" + ansistrano_group = "www-data" + + [[deploy.playbooks.tasks]] + copy = "src=../env.sh dest=/srv/tusd/current/env.sh mode=0600 owner=root group=root" + name = "infra-tusd | Upload environment" + + [[deploy.playbooks.tasks]] + name = "tusd | Set file attributes" + file = "path=/srv/tusd/current/tusd_linux_amd64/tusd mode=0755 owner=www-data group=www-data" + +[[restart.playbooks]] + hosts = "infra-tusd-server" + name = "Restart infra-tusd" + + [[restart.playbooks.tasks]] + shell = "iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080" + name = "infra-tusd | Redirect HTTP traffic to tusd" + + [[restart.playbooks.tasks]] + action = "service name=tusd state=restarted" + name = "infra-tusd | Restart" diff --git a/.infra/README.md b/.infra/README.md new file mode 100644 index 0000000..eac03a4 --- /dev/null +++ b/.infra/README.md @@ -0,0 +1,3 @@ +This folder is charge of launching master.tus.io via [Frey](https://github.com/kvz/frey). +You could re-use bits of this, but you could also run tusd any other way you want. +It's not a requirement for running a tusd server. diff --git a/.infra/env.example.sh b/.infra/env.example.sh new file mode 100644 index 0000000..0fe3534 --- /dev/null +++ b/.infra/env.example.sh @@ -0,0 +1,10 @@ +# Rename this file to env.sh, it will be kept out of Git. +# So suitable for adding secret keys and such + +# export DEBUG="frey:*" +# export FREY_DOMAIN="master.tus.io" +# export FREY_ENCRYPTION_SECRET="***" + +# source env.sh +# travis encrypt --add env.global "FREY_DOMAIN=${FREY_DOMAIN}" +# travis encrypt --add env.global "FREY_ENCRYPTION_SECRET=${FREY_ENCRYPTION_SECRET}" diff --git a/.infra/env.infra.example.sh b/.infra/env.infra.example.sh new file mode 100644 index 0000000..e961ab3 --- /dev/null +++ b/.infra/env.infra.example.sh @@ -0,0 +1,5 @@ +source env.sh + +# export FREY_AWS_ACCESS_KEY="***" +# export FREY_AWS_SECRET_KEY="***" +# export FREY_AWS_ZONE_ID="***" diff --git a/.infra/ssh/frey-infra-tusd.pem.cast5 b/.infra/ssh/frey-infra-tusd.pem.cast5 new file mode 100644 index 0000000000000000000000000000000000000000..215171ffe7f8352fe577c2ad0ff7d012dd7a14ca GIT binary patch literal 1696 zcmV;R24DHNEgP#BlHP0|6U5}`zeb9;6Jf4Q4dYZJ@^X*$Y5%dWBec(tl3~fXPf0?S zFsJ16MS=VccQQ_dWYmA^x^iNB#3@=|TwCP_fha{QbM$x*Ys-)t zYWHL!4zh&M0?P*BiBQPPsh(%QU@3)tO7Z@q#{UmwL7wZ{`>hp*<}x|zeDBTv(kf)5 z+dR(P2%04mRDH(;uKnOv^W%H6(g2yE4O(ZOCA?ET!u>y5&e1?4c}V^;I?WIJhg2N@ zS$D9fA#w_)U~xg{QG7J}9<(v>)7W9Fc;)g3x;Ld>FVG>;U=)HBGdB*W8s??MtD^TR zvW$lkeBGZVgtI5EjoRzteIIB2V=L$*(#l?|_P6qlFxzxur1iK?JQtUnv_vti{3BJE zU&J#iEOU!E$eicY-owR@FSETKR6_$XQpvVO4@X59W z@S(>A-_kX`Pi#ioL*S6S!4V}9Z^*8~!LjY4;#fFPl&h7#lq6Pxm*QXY|57KareE9V z`3=c%3AQUs-n}@Iksm>eYPTmuaw?JttWz)gdBofiE3HvLHsl%=dA4(*sN%Y;7d!Fr zCTFY(9V$i-?~7sZPjhdcZxJptv6&hmHTpD~+ijeY#d}r1l^tq|$|L1jf`W+}QuNU{ zOoMibAs?mLyGLd^S14WRjV)SRBn~9|#B}Pk0&^KnGfRv|xy)KM2Wk8-O`nHQoWUuh zsofbgUfkBR-q?HUc^Z0}XN^L?r?fLLBX{e62R%SO(40+fjo)`$ObZq_8rL`-oV@O# z>&${SyIyu$NjKddCuK7d8HzsC6Ml@iQl=A!NnSmFt+BRH5?Hr*H!2(eIhL!^2=1$GFUeEueW_0j2B{J#!i^s<_Ss zew?coc~_=cGi<9L6$q34l>(b#J&O3uu~!@v9MHl1)g#NqSGJOdS_chJw6&@P%rDPS z!6GJ?1A(UlRzW($po(*>HP!K}QGa`f{gg?Gzt zyM=c1&&By{GBXIn^!!40vKF+ag*2tf zy=U0G?{)%l_2c?HmN6Gu;ABgkzAc)*85zE`l8a3J;m0B|()a zsK)YWJW$aNU~`Ba>#=T<5?o4Kx9;8q-K@SR0XKVWOp(s$yJiI7}90q#+zBpTD7{PiivJa6Ik$rJ6C(88v?rY|y1$NDr%# z-=diVG?bz@-dfWa4xwA|-(Nla3R_fvmzGfiMwNEw#WpG_W!C^|h`$lS6=D8FwNI9H zu3Okp!>Bq)&uJjZ^OALCLA|WY7~h#%VKl*{A9ud0PQ#DQz@9)`pFL~%93>xb{NHJ7 zH`R;}pqM`R{sCP3leu!b1{H>hnE`865|3BZ1$K;X!qh}29-j7)Hx`vcoK9!3)G2Zv q;Md8^J1Zc(u%b7kGm?rZMRK-qi1dvvc&}$drcD(n4)V|b<5&CIwOKg; literal 0 HcmV?d00001 diff --git a/.infra/ssh/frey-infra-tusd.pub b/.infra/ssh/frey-infra-tusd.pub new file mode 100644 index 0000000..e69de29 diff --git a/.infra/templates/upstart-tusd.conf.j2 b/.infra/templates/upstart-tusd.conf.j2 new file mode 100644 index 0000000..5a74f75 --- /dev/null +++ b/.infra/templates/upstart-tusd.conf.j2 @@ -0,0 +1,25 @@ +stop on runlevel [016] +start on (started networking) + +# The respawn limits function as follows: If the process is respawned +# more than count times within an interval of timeout seconds, +# the process will be stopped automatically, and not restarted. +# Unless set explicitly, the limit defaults to 10 times within 5 seconds. +# http://upstart.ubuntu.com/wiki/Stanzas#respawn_limit +respawn +respawn limit 10 5 + +limit nofile 32768 32768 + +pre-stop exec status tusd | grep -q "stop/waiting" && initctl emit --no-wait stopped JOB=tusd || true + +script + set -e + set -x + mkfifo /tmp/tusd-log-fifo + ( logger -t tusd /tmp/tusd-log-fifo + rm /tmp/tusd-log-fifo + exec bash -c "cd /srv/tusd/current/tusd_linux_amd64/ \ + && exec sudo -EHu www-data ./tusd -port=8080 -dir=/mnt/tusd-data -store-size=10737418240" +end script diff --git a/.travis.yml b/.travis.yml index 13f0727..d10ea33 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ matrix: - go: tip install: -- export PACKAGES=$(find ./ -maxdepth 1 -type d -not \( -name ".git" -or -name "cmd" -or -name "vendor" -or -name "data" -or -name ".hooks" \)) +- export PACKAGES=$(find ./ -maxdepth 1 -type d -not \( -name ".git" -or -name "cmd" -or -name ".infra" -or -name "vendor" -or -name "data" -or -name ".hooks" \)) - rsync -r ./vendor/ $GOPATH/src script: @@ -50,3 +50,6 @@ deploy: tags: true go: 1.5 repo: tus/tusd + +after_deploy: + - if [ "${TRAVIS_PULL_REQUEST}" == "false" ] && [ "${TRAVIS_BRANCH}" == "master" ]; then (make frey && frey setup --projectDir .infra) else echo "Skipping deploy for non-master/PRs"; fi diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7e5c818 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +PHONY: frey +frey: + @npm install --global frey@0.3.12 + +PHONY: deploy +deploy: + @cd .infra && source env.sh && frey setup + +PHONY: launch +launch: + @cd .infra && source env.infra.sh && frey infra + +PHONY: console +console: + @cd .infra && source env.sh && frey remote + +PHONY: deploy-localfrey +deploy-localfrey: + @cd .infra && source env.sh && babel-node ${HOME}/code/frey/src/cli.js setup