From ad56a67722d8a2ccd9497200cb794fdd1b3cd704 Mon Sep 17 00:00:00 2001 From: Tony Merlin Date: Wed, 21 Oct 2026 16:34:12 +0200 Subject: [PATCH 1/3] Support `eezo new .` scaffolds in the current directory - Following functionality of other CLIs which scaffold in-place: Django, Phoenix, Rails, and React --- bin/eezo | 66 ++++++++++++++++++++++++++++++++----------- docs/reference/cli.md | 6 +++- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/bin/eezo b/bin/eezo index 875f926..0f7607b 100755 --- a/bin/eezo +++ b/bin/eezo @@ -20,6 +20,7 @@ usage() { eezo — the framework launcher eezo new scaffold a new application in ./ + eezo new . scaffold a new application in the current directory, named after it eezo dev serve, restarting on every save eezo build stage the app for deployment (jars, migrations, Dockerfile) eezo deploy ship it: build, deploy to Fly.io, migrate, wait for health @@ -516,25 +517,59 @@ read_version_file() { } } +# The scaffold's gitignore entries, either written to a new .gitignore file or appended if .gitignore already exists +GITIGNORE_ENTRIES=(target/ .bsp/ .metals/ .bloop/) + +write_gitignore() { # ; sets GITIGNORE_ADDED to the entries it wrote + local file="$1" line + GITIGNORE_ADDED=() + for line in "${GITIGNORE_ENTRIES[@]}"; do + [ -f "$file" ] && grep -qxF "$line" "$file" && continue + GITIGNORE_ADDED+=("$line") + done + [ "${#GITIGNORE_ADDED[@]}" -gt 0 ] || return 0 + if [ -s "$file" ] && [ -n "$(tail -c1 "$file")" ]; then echo >> "$file"; fi + printf '%s\n' "${GITIGNORE_ADDED[@]}" >> "$file" +} + scaffold() { - name="${1:-}" - [ -n "$name" ] || { echo "eezo: new needs a name: eezo new myapp" >&2; exit 1; } - [ ! -e "$name" ] || { echo "eezo: ./$name already exists" >&2; exit 1; } + dir="${1:-}" + [ -n "$dir" ] || { echo "eezo: new needs a name: eezo new myapp, or eezo new . for the current directory ($(basename "$PWD"))" >&2; exit 1; } + # Any reference to the current directory such as: ./, "$PWD", or a symlink to it + if [ -d "$dir" ] && [ "$(cd "$dir" && pwd -P)" = "$(pwd -P)" ]; then dir="."; fi + if [ "$dir" = "." ]; then + # In place: the app is named after the directory + name="$(basename "$PWD")" + local existing=() file + for file in build.sbt build.sc build.mill project src; do + [ ! -e "$file" ] && [ ! -L "$file" ] || existing+=("$file") + done + [ "${#existing[@]}" -eq 0 ] || { + echo "eezo: this directory already has ${existing[*]}; new scaffolds a project only where there is none" >&2 + exit 1 + } + else + name="$dir" + [ ! -e "$dir" ] || { echo "eezo: ./$dir already exists" >&2; exit 1; } + fi read_version_file - mkdir -p "$name/project" "$name/src/main/scala/app" "$name/src/main/scala/models" + local gitignore_existed=0 + [ ! -f "$dir/.gitignore" ] || gitignore_existed=1 - cat > "$name/project/build.properties" < "$dir/project/build.properties" < "$name/project/plugins.sbt" < "$dir/project/plugins.sbt" < "$name/build.sbt" < "$dir/build.sbt" < "$name/src/main/scala/Main.scala" <<'EOF' + cat > "$dir/src/main/scala/Main.scala" <<'EOF' import io.eezo.EezoApp import io.eezo.db.Schema import io.eezo.generated.Routes @@ -574,7 +609,7 @@ object Main extends EezoApp { } EOF - cat > "$name/src/main/scala/app/Index.scala" <<'EOF' + cat > "$dir/src/main/scala/app/Index.scala" <<'EOF' package app import io.eezo.core.html.* @@ -600,17 +635,14 @@ object Index { } EOF - cat > "$name/.gitignore" <<'EOF' -target/ -.bsp/ -.metals/ -.bloop/ -EOF + write_gitignore "$dir/.gitignore" - echo "created ./$name (eezo $EEZO_VERSION, Scala $SCALA_VERSION, sbt $SBT_VERSION)" + echo "created $name (eezo $EEZO_VERSION, Scala $SCALA_VERSION, sbt $SBT_VERSION)" + [ "$gitignore_existed" -eq 0 ] || [ "${#GITIGNORE_ADDED[@]}" -eq 0 ] || + echo "added ${GITIGNORE_ADDED[*]} to the existing .gitignore" echo echo "next:" - echo " cd $name" + [ "$dir" = "." ] || echo " cd $name" echo " eezo dev # serve on :8080, restart on save" echo " eezo routes # what is mounted" } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 5139ca0..8b87384 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -21,7 +21,7 @@ layer can shadow it. No arguments runs the application. | command | layer | needs | what it does | |---|---|---|---| -| `new ` | launcher | `~/.eezo/eezo-version` | scaffolds `./` | +| `new `, `new .` | launcher | `~/.eezo/eezo-version` | scaffolds `./`, or the current directory, named after it | | `dev` | sbt task | | serves with restart on save, the route listing and the reload client; Ctrl-C stops it | | `routes [--json]` | http layer | | the mounted table and boot's warnings | | `db` | database layer | | the eight schema commands, listed; `db help` is the same | @@ -149,6 +149,10 @@ the command's answer and not a build failure: .gitignore ``` +`eezo new .` writes the same files into the current directory. +It refuses if the directory already has a build definition (`build.sbt`, `build.sc`, `build.mill`), a `project/` or a `src/` directory. +A pre-existing `.gitignore` file will be appended. + ## Related [Configuration](configuration.md) is what the commands read from the environment. From fb2142e7e0d08bb18009d50000cd9a95b119e48e Mon Sep 17 00:00:00 2001 From: Tony Merlin Date: Thu, 22 Oct 2026 12:25:57 +0200 Subject: [PATCH 2/3] Address review comments * guard against appending to symlinked .gitignore * fail early if the .gitignore cannot be appended to * silence CD check * improve .gitignore documentation * disambiguate scaffolded path output and update its documentation --- bin/eezo | 40 ++++++++++++++++--------------- docs/reference/cli.md | 3 ++- docs/tutorials/getting-started.md | 2 +- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/bin/eezo b/bin/eezo index 0f7607b..ef7458d 100755 --- a/bin/eezo +++ b/bin/eezo @@ -517,26 +517,26 @@ read_version_file() { } } -# The scaffold's gitignore entries, either written to a new .gitignore file or appended if .gitignore already exists -GITIGNORE_ENTRIES=(target/ .bsp/ .metals/ .bloop/) - -write_gitignore() { # ; sets GITIGNORE_ADDED to the entries it wrote - local file="$1" line - GITIGNORE_ADDED=() - for line in "${GITIGNORE_ENTRIES[@]}"; do - [ -f "$file" ] && grep -qxF "$line" "$file" && continue - GITIGNORE_ADDED+=("$line") +# The scaffold's gitignore entries, either written to a new .gitignore file or appended if .gitignore +# already exists, in which case it names the entries it appended +write_gitignore() { # + local file="$1" existed=0 line added=() + [ ! -f "$file" ] || existed=1 + for line in target/ .bsp/ .metals/ .bloop/; do + [ "$existed" -eq 1 ] && grep -qxF "$line" "$file" && continue + added+=("$line") done - [ "${#GITIGNORE_ADDED[@]}" -gt 0 ] || return 0 + [ "${#added[@]}" -gt 0 ] || return 0 if [ -s "$file" ] && [ -n "$(tail -c1 "$file")" ]; then echo >> "$file"; fi - printf '%s\n' "${GITIGNORE_ADDED[@]}" >> "$file" + printf '%s\n' "${added[@]}" >> "$file" + [ "$existed" -eq 0 ] || echo "added ${added[*]} to the existing .gitignore" } scaffold() { dir="${1:-}" [ -n "$dir" ] || { echo "eezo: new needs a name: eezo new myapp, or eezo new . for the current directory ($(basename "$PWD"))" >&2; exit 1; } # Any reference to the current directory such as: ./, "$PWD", or a symlink to it - if [ -d "$dir" ] && [ "$(cd "$dir" && pwd -P)" = "$(pwd -P)" ]; then dir="."; fi + if [ "$dir" -ef . ]; then dir="."; fi if [ "$dir" = "." ]; then # In place: the app is named after the directory name="$(basename "$PWD")" @@ -548,15 +548,20 @@ scaffold() { echo "eezo: this directory already has ${existing[*]}; new scaffolds a project only where there is none" >&2 exit 1 } + [ ! -L .gitignore ] || { + echo "eezo: .gitignore here is a symlink; new appends its entries only to a plain .gitignore" >&2 + exit 1 + } + [ ! -e .gitignore ] || { [ -f .gitignore ] && [ -w .gitignore ]; } || { + echo "eezo: .gitignore here is not a file new can append to; make it a writable file or remove it" >&2 + exit 1 + } else name="$dir" [ ! -e "$dir" ] || { echo "eezo: ./$dir already exists" >&2; exit 1; } fi read_version_file - local gitignore_existed=0 - [ ! -f "$dir/.gitignore" ] || gitignore_existed=1 - mkdir -p "$dir/project" "$dir/src/main/scala/app" "$dir/src/main/scala/models" cat > "$dir/project/build.properties" </projects/bookshelf (eezo 0.2.0, Scala 3.8.4, sbt 1.12.14) next: cd bookshelf From f16ffbc4c781ffcde2417c6b7a429d6ac047d291 Mon Sep 17 00:00:00 2001 From: Tony Merlin Date: Thu, 22 Oct 2026 16:23:43 +0200 Subject: [PATCH 3/3] CDPATH safety --- bin/eezo | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/eezo b/bin/eezo index ef7458d..45608af 100755 --- a/bin/eezo +++ b/bin/eezo @@ -11,6 +11,7 @@ # https://eezo.io/install writes with the released version. set -euo pipefail +unset CDPATH SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(dirname "$SCRIPT_DIR")" @@ -552,7 +553,7 @@ scaffold() { echo "eezo: .gitignore here is a symlink; new appends its entries only to a plain .gitignore" >&2 exit 1 } - [ ! -e .gitignore ] || { [ -f .gitignore ] && [ -w .gitignore ]; } || { + [ ! -e .gitignore ] || { [ -f .gitignore ] && [ -r .gitignore ] && [ -w .gitignore ]; } || { echo "eezo: .gitignore here is not a file new can append to; make it a writable file or remove it" >&2 exit 1 } @@ -640,7 +641,7 @@ object Index { } EOF - echo "created $name at $(cd "$dir" && pwd) (eezo $EEZO_VERSION, Scala $SCALA_VERSION, sbt $SBT_VERSION)" + echo "created $name at $(cd -- "$dir" 2> /dev/null && pwd) (eezo $EEZO_VERSION, Scala $SCALA_VERSION, sbt $SBT_VERSION)" write_gitignore "$dir/.gitignore" echo echo "next:"