Files
summercms/modules/sunrise/new.sh
Jakub Zych 9b3803c098 feat(sunrise): update an application in place with summercms.io/update
- new.sh gains --update, run inside one application directory (go.mod and
  summer.yaml required); it changes only that application
- the framework checkout is read from the app's go.mod replace (single-line
  or block form); module-version replaces, missing targets and foreign or
  dirty checkouts are refused through the shared check_framework_dir
- a detached checkout moves fast-forward only to the newest release (or
  SUMMERCMS_REF) through the shared move_detached; a branch checkout is
  pulled with git pull --no-rebase --ff-only, refusing a missing upstream
  or a diverged branch with the checkout unchanged
- other sm-*-app directories on the same checkout are named on one
  "not rebuilt" line and never built or written
- summer is reinstalled, a missing SUMMER_ADMIN__JWT__SECRET is appended to
  this app's .env (openssl, else head and base64; mode kept, never printed),
  then summer build runs and the script ends with the migrate and restart
  reminder and the shared path_hint
- sunrise.UpdateScript() serves the same script with its last line changed
  to main "$@" --update, so truncated downloads still run nothing
- check-phase14.3.sh --forbidden now allows only the literal
  SUMMER_ADMIN__JWT__SECRET in new.sh and still fails on any other
  SUMMER_ name or on a read or export of one; TestUpdateScript and
  TestUpdateScriptRoute are required tests
2026-10-08 20:15:50 +02:00

619 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
# SummerCMS bootstrap: create a new SummerCMS application in one command, or
# update an existing one.
#
# curl -fsSL https://summercms.io/new | bash -s -- <name>
# cd sm-<name>-app && curl -fsSL https://summercms.io/update | bash
# cd sm-<name>-app && curl -fsSL https://summercms.io/new | bash -s -- --update
#
# The script checks for Go 1.27 or newer, git and (optionally) Docker, clones
# the framework into ./summercms at its newest release tag (or moves an
# existing release checkout forward to it), installs the summer CLI with
# `go install` and runs `summer new <name>` next to the checkout, which
# writes ./sm-<name>-app.
#
# Update mode (--update, which /update presets) runs inside one application
# and changes only that application. It reads the framework checkout from the
# application's go.mod replace and moves it forward: a detached checkout fast-
# forward only to the newest release (or SUMMERCMS_REF), a branch checkout
# with git pull --ff-only. It then reinstalls summer, adds a missing admin
# token secret to this application's .env, runs summer build here, and names
# other applications that share the checkout without rebuilding them.
#
# Environment overrides:
# SUMMERCMS_REPO framework repository to clone
# (default https://git.golem15.com/golem15/summercms);
# used only when cloning
# SUMMERCMS_DIR framework checkout directory (default ./summercms); the
# application is created next to it. Not used in update
# mode, where the checkout comes from go.mod
# SUMMERCMS_REF tag or commit to check out instead of the newest release
# (detached checkouts only)
#
# The login shell named by SHELL only picks the form of the PATH hint printed
# at the end (fish, zsh, bash, or a generic export line).
#
# Everything below is a function definition and the last line calls main, so
# a download cut short runs nothing. Only bash builtins and the git, go and
# docker commands are used, plus, in update mode, openssl (or head and
# base64) to make the admin token secret and the installed summer.
say() {
printf '==> %s\n' "$*" >&2
}
warn() {
printf 'warning: %s\n' "$*" >&2
}
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
usage() {
printf '%s\n' \
'Usage: curl -fsSL https://summercms.io/new | bash -s -- <name>' \
' cd sm-<name>-app && curl -fsSL https://summercms.io/update | bash' \
' curl -fsSL https://summercms.io/new | bash -s -- --update' \
' bash new.sh <name>' \
'' \
'Creates sm-<name>-app next to a SummerCMS framework checkout. <name> is' \
'lower-case letters and digits, starting with a letter.' \
'' \
'Steps: check Go 1.27+, git and Docker; clone the framework (or move an' \
'existing release checkout forward) at its newest release tag; install' \
'the summer CLI with go install; run summer new <name>.' \
'' \
'--update takes no other argument and runs inside one application (a' \
'directory with go.mod and summer.yaml). It reads the framework checkout' \
'from the go.mod replace and moves it forward (detached: fast-forward only' \
'to the newest release; branch: git pull --ff-only), reinstalls summer,' \
'adds a missing admin token secret to the .env and runs summer build there.' \
'Other applications sharing the checkout are named, not rebuilt.' \
'' \
'Environment:' \
' SUMMERCMS_REPO repository to clone (default https://git.golem15.com/golem15/summercms)' \
' SUMMERCMS_DIR framework checkout directory (default ./summercms); not used by --update' \
' SUMMERCMS_REF tag or commit to check out instead of the newest release'
}
# framework_module prints the module path of the SummerCMS framework.
framework_module() {
printf '%s\n' git.golem15.com/golem15/summercms
}
# trim prints $1 without leading and trailing whitespace.
trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s\n' "$s"
}
# abspath prints $1 as an absolute path against $PWD with ".", ".." and
# repeated slashes removed lexically.
abspath() {
local p="$1" out="" seg
local -a parts
[[ $p == /* ]] || p="$PWD/$p"
IFS=/ read -r -a parts <<<"$p"
for seg in "${parts[@]}"; do
case "$seg" in
'' | .) ;;
..) out="${out%/*}" ;;
*) out="$out/$seg" ;;
esac
done
printf '%s\n' "${out:-/}"
}
# check_go requires go 1.27 or newer.
check_go() {
command -v go >/dev/null 2>&1 ||
die "Go is not installed. SummerCMS needs Go 1.27 or newer: https://go.dev/dl/"
local v
# Read the version from / so a go.mod in the current directory cannot
# make the go command switch toolchains for the check.
v="$(cd / && go env GOVERSION </dev/null 2>/dev/null)" || v=""
if [[ $v =~ ^go([0-9]+)\.([0-9]+) ]]; then
local major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}"
if ((major < 1 || (major == 1 && minor < 27))); then
die "SummerCMS needs Go 1.27 or newer, found $v. Install a newer Go from https://go.dev/dl/"
fi
else
warn "could not read the Go version (go env GOVERSION printed '${v}'); SummerCMS needs Go 1.27 or newer"
fi
}
# check_docker only warns: the application builds without Docker, the
# generated compose.yaml just needs it for the development database.
check_docker() {
if ! command -v docker >/dev/null 2>&1; then
warn "Docker is not installed; the generated compose.yaml needs it to run PostgreSQL (https://docs.docker.com/engine/install/)"
elif ! docker compose version </dev/null >/dev/null 2>&1; then
warn "the Docker compose plugin is missing; the generated compose.yaml needs 'docker compose'"
fi
}
# latest_tag prints the newest vX.Y.Z tag of the checkout in $1.
latest_tag() {
local dir="$1" tags tag
# Capture the whole list first: piping into head under pipefail can
# fail with SIGPIPE.
tags="$(git -C "$dir" tag -l 'v*' --sort=-v:refname </dev/null)" || return 1
while IFS= read -r tag; do
if [[ $tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
printf '%s\n' "$tag"
return 0
fi
done <<<"$tags"
return 1
}
# target_commit prints the commit to check out in $1: SUMMERCMS_REF ($2)
# when set, otherwise the newest release tag.
target_commit() {
local dir="$1" ref="$2" label commit
if [[ -n $ref ]]; then
[[ $ref != -* ]] || die "SUMMERCMS_REF must be a tag or commit, not '$ref'"
label="$ref"
else
label="$(latest_tag "$dir")" ||
die "no release tag (vX.Y.Z) found in $dir; set SUMMERCMS_REF to a tag or commit"
ref="refs/tags/$label"
fi
commit="$(git -C "$dir" rev-parse --verify --quiet "${ref}^{commit}" </dev/null)" ||
die "$label is not a tag or commit in $dir"
say "framework target: $label (${commit:0:12})"
printf '%s\n' "$commit"
}
# check_framework_dir refuses an existing directory $1 that is not a clean
# SummerCMS framework checkout at the root of a git repository; $2 is the
# hint appended to every refusal. It never changes the directory.
check_framework_dir() {
local dir="$1" hint="$2" top real line module="" status fw
fw="$(framework_module)"
[[ -d $dir ]] || die "$dir exists and is not a directory; $hint"
[[ -f $dir/go.mod ]] || die "$dir is not a SummerCMS framework checkout (no go.mod); $hint"
while IFS= read -r line || [[ -n $line ]]; do
if [[ $line =~ ^[[:space:]]*module[[:space:]]+\"?([^[:space:]\"]+) ]]; then
module="${BASH_REMATCH[1]}"
break
fi
done <"$dir/go.mod"
[[ $module == "$fw" ]] ||
die "$dir is not a SummerCMS framework checkout (go.mod module is '${module}'); $hint"
top="$(git -C "$dir" rev-parse --show-toplevel </dev/null 2>/dev/null)" ||
die "$dir is not a git repository; $hint"
real="$(cd "$dir" && pwd -P)"
[[ $top == "$real" ]] || die "$dir is not the root of a git repository; $hint"
status="$(git -C "$dir" status --porcelain --untracked-files=no </dev/null)" ||
die "git status failed in $dir"
[[ -z $status ]] || die "$dir has uncommitted changes to tracked files; commit or stash them, or $hint"
}
# check_checkout refuses an existing directory that is not a clean,
# detached SummerCMS framework checkout. It never changes the directory.
check_checkout() {
local dir="$1" hint branch
hint="use SUMMERCMS_DIR for a separate clone, or run 'summer new --framework <checkout>' yourself"
check_framework_dir "$dir" "$hint"
if branch="$(git -C "$dir" symbolic-ref -q --short HEAD </dev/null)"; then
die "$dir is on branch $branch; this installer only moves a detached release checkout. To keep your branch, $hint"
fi
}
# checkout_framework clones the framework into $2 or moves an existing
# detached checkout forward, fast-forward only.
checkout_framework() {
local repo="$1" dir="$2" ref="$3" target
if [[ ! -e $dir && ! -L $dir ]]; then
say "cloning $repo into $dir"
git clone --quiet -- "$repo" "$dir" </dev/null || die "git clone of $repo failed"
target="$(target_commit "$dir" "$ref")" ||
die "the new clone in $dir has no usable target; remove $dir and run again"
git -C "$dir" -c advice.detachedHead=false checkout --quiet --detach "$target" </dev/null
return 0
fi
check_checkout "$dir"
move_detached "$dir" "$ref"
}
# move_detached fetches tags into the detached checkout $1 and moves it to
# SUMMERCMS_REF ($2) or the newest release tag, fast-forward only.
move_detached() {
local dir="$1" ref="$2" target head
say "updating the framework checkout in $dir"
# No --force: a tag that moved upstream is never overwritten locally,
# git refuses and the script stops.
git -C "$dir" fetch --quiet --tags origin </dev/null ||
die "git fetch in $dir failed (a release tag may have moved upstream); the checkout was not changed"
target="$(target_commit "$dir" "$ref")" || exit 1
head="$(git -C "$dir" rev-parse HEAD </dev/null)"
if [[ $head == "$target" ]]; then
say "framework checkout is already at the target"
elif git -C "$dir" merge-base --is-ancestor "$head" "$target" </dev/null; then
git -C "$dir" -c advice.detachedHead=false checkout --quiet --detach "$target" </dev/null
else
die "moving $dir from ${head:0:12} to ${target:0:12} is not a fast-forward; the checkout was not changed. $dir must be an ancestor of the target; use SUMMERCMS_DIR for a fresh clone"
fi
}
# go_bin_dir prints the directory go install writes to: GOBIN, else the
# first GOPATH entry plus /bin.
go_bin_dir() {
local gobin gopath
gobin="$(cd / && go env GOBIN </dev/null)" || return 1
if [[ -z $gobin ]]; then
gopath="$(cd / && go env GOPATH </dev/null)" || return 1
gopath="${gopath%%:*}"
[[ -n $gopath ]] || return 1
gobin="$gopath/bin"
fi
printf '%s\n' "$gobin"
}
# dq_escape prints $1 escaped for the double quotes of the shell named by $2:
# backslash, double quote and dollar always; for shells other than fish also
# backtick, and ! moved into single quotes against history expansion.
dq_escape() {
local s="$1" sh="$2" out="" c i
for ((i = 0; i < ${#s}; i++)); do
c="${s:i:1}"
case "$c" in
\\ | \" | \$) out+="\\$c" ;;
\`) if [[ $sh == fish ]]; then out+="$c"; else out+="\\$c"; fi ;;
!) if [[ $sh == fish ]]; then out+="$c"; else out+="\"'!'\""; fi ;;
*) out+="$c" ;;
esac
done
printf '%s\n' "$out"
}
# path_hint prints how to put the Go bin directory $1 on PATH, in the form
# of the login shell named by SHELL, when it is not on PATH yet.
path_hint() {
local gobin="$1"
if [[ :${PATH:-}: != *":$gobin:"* ]]; then
# Under curl | bash this is always bash; SHELL still names the
# user's login shell, which picks the hint form.
local login="${SHELL:-}" note
login="${login##*/}"
case "$login" in
fish) note='fish_add_path also keeps it for new fish sessions' ;;
zsh) note='and add it to ~/.zshrc' ;;
bash) note='and add it to ~/.bashrc' ;;
*) note='and add it to your shell profile' ;;
esac
printf '\n==> summer is installed in %s, which is not on your PATH. Run this (%s):\n' "$gobin" "$note"
if [[ $login == fish ]]; then
printf ' fish_add_path "%s"\n' "$(dq_escape "$gobin" fish)"
else
# $PATH stays literal: the user's shell expands it.
# shellcheck disable=SC2016
printf ' export PATH="%s:$PATH"\n' "$(dq_escape "$gobin" "$login")"
fi
fi
}
# is_local_path tells whether the replace target $1 is a directory path
# rather than a module path, by Go's rule: ".", ".." or a leading "./",
# "../" or "/".
is_local_path() {
case "$1" in
. | .. | ./* | ../* | /*) return 0 ;;
*) return 1 ;;
esac
}
# framework_replace reads the go.mod file $1 the way summer build does
# (// comments cut, single-line and block replace directives, the first
# field of each side, the left side matched exactly against the framework
# module, a version may follow it) and prints the replacement: its path or
# module on the first line and its version, if any, on a second line. It
# returns 1 when go.mod has no replace for the framework.
framework_replace() {
local file="$1" fw line rest lhs rhs first ver="" inblock=0
local block_re='^replace[[:space:]]*[(]$' single_re='^replace[[:space:]]+(.+)$'
local quoted_re='^"([^"]*)"(.*)$'
fw="$(framework_module)"
while IFS= read -r line || [[ -n $line ]]; do
line="$(trim "${line%%//*}")"
[[ -n $line ]] || continue
if ((inblock)); then
if [[ $line == ')' ]]; then
inblock=0
continue
fi
rest="$line"
elif [[ $line =~ $block_re ]]; then
inblock=1
continue
elif [[ $line =~ $single_re ]]; then
rest="${BASH_REMATCH[1]}"
else
continue
fi
[[ $rest == *'=>'* ]] || continue
read -r lhs _ <<<"${rest%%=>*}"
[[ ${lhs:-} == "$fw" ]] || continue
rhs="$(trim "${rest#*=>}")"
if [[ $rhs =~ $quoted_re ]]; then
first="${BASH_REMATCH[1]}"
read -r ver _ <<<"${BASH_REMATCH[2]}"
else
read -r first ver _ <<<"$rhs"
fi
[[ -n ${first:-} ]] || continue
printf '%s\n' "$first"
if [[ -n ${ver:-} ]]; then
printf '%s\n' "$ver"
fi
return 0
done <"$file"
return 1
}
# pull_branch fast-forwards the branch $2 checked out in $1 from its
# upstream. SUMMERCMS_REF ($3) does not apply to a branch. A missing
# upstream or a diverged branch stops the script with the checkout
# unchanged; check_framework_dir has already refused a dirty tree.
pull_branch() {
local dir="$1" branch="$2" ref="$3" upstream before after
if [[ -n $ref ]]; then
warn "SUMMERCMS_REF is ignored for a branch checkout; $branch is pulled from its upstream"
fi
upstream="$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' </dev/null 2>/dev/null)" ||
die "$dir is on branch $branch, which has no upstream; the checkout was not changed. Set one with git branch --set-upstream-to, or pull it yourself"
before="$(git -C "$dir" rev-parse HEAD </dev/null)"
say "pulling $branch from $upstream (fast-forward only)"
# --no-rebase: a pull.rebase setting must not turn this into a rebase.
if ! git -C "$dir" pull --quiet --no-rebase --ff-only </dev/null; then
die "git pull --ff-only in $dir failed: $branch has diverged from $upstream (not a fast-forward) or the fetch failed; the checkout was not changed"
fi
after="$(git -C "$dir" rev-parse HEAD </dev/null)"
if [[ $before == "$after" ]]; then
say "framework checkout is already up to date"
else
say "framework checkout moved from ${before:0:12} to ${after:0:12}"
fi
}
# note_shared_apps names the other sm-*-app directories, next to the
# application $2 and next to the checkout $1, whose go.mod replace resolves
# to the same checkout. It only reads their go.mod; they are not rebuilt.
note_shared_apps() {
local dir="$1" app="$2" real self parent fwparent scan last="" cand target list=""
real="$(cd "$dir" && pwd -P)"
self="$(cd "$app" && pwd -P)"
parent="${self%/*}"
fwparent="${real%/*}"
for scan in "${parent:-/}" "${fwparent:-/}"; do
[[ $scan != "$last" ]] || continue
last="$scan"
for cand in "${scan%/}"/sm-*-app; do
[[ -d $cand && -f $cand/go.mod && -r $cand/go.mod ]] || continue
[[ "$(cd "$cand" 2>/dev/null && pwd -P)" != "$self" ]] || continue
target="$(framework_replace "$cand/go.mod")" || continue
target="${target%%$'\n'*}"
is_local_path "$target" || continue
[[ $target == /* ]] || target="$cand/$target"
[[ -d $target ]] || continue
[[ "$(cd "$target" 2>/dev/null && pwd -P)" == "$real" ]] || continue
list+=" $cand"
done
done
if [[ -n $list ]]; then
say "not rebuilt, also using $dir:$list; each picks up the new framework on its next summer build"
fi
}
# env_line_state tells whether the .env line $2 sets the key $1, read the
# way the framework's .env loader reads it (trimmed, # comments skipped, an
# optional "export " prefix, the key trimmed before the first =, matching
# surrounding quotes removed from the value): 0 with a value, 2 with an
# empty value, 1 when the line does not set the key.
env_line_state() {
local key="$1" line k v
line="$(trim "$2")"
[[ -n $line && $line != '#'* ]] || return 1
if [[ $line == 'export '* ]]; then
line="$(trim "${line#export }")"
fi
[[ $line == *=* ]] || return 1
k="$(trim "${line%%=*}")"
[[ $k == "$key" ]] || return 1
v="$(trim "${line#*=}")"
if ((${#v} >= 2)) && [[ $v == \"*\" || $v == \'*\' ]]; then
v="${v:1:${#v}-2}"
fi
[[ -n $v ]] || return 2
return 0
}
# generate_secret prints 32 random bytes as standard base64 (44 characters),
# from openssl when it is installed, else from head and base64 over
# /dev/urandom. It returns 1 when neither produces a valid value. The value
# only travels through command substitution and printf.
generate_secret() {
local s="" re='^[A-Za-z0-9+/]{43}=$'
if command -v openssl >/dev/null 2>&1; then
s="$(openssl rand -base64 32 </dev/null 2>/dev/null)" || s=""
s="${s//$'\n'/}"
s="${s//$'\r'/}"
fi
if [[ ! $s =~ $re ]] && command -v head >/dev/null 2>&1 &&
command -v base64 >/dev/null 2>&1 && [[ -r /dev/urandom ]]; then
s="$(head -c 32 /dev/urandom | base64 2>/dev/null)" || s=""
s="${s//$'\n'/}"
s="${s//$'\r'/}"
fi
[[ $s =~ $re ]] || return 1
printf '%s\n' "$s"
}
# ensure_admin_secret appends the admin token secret to the .env of the
# application $1 when no active line sets it (applications created before
# the admin was wired in lack it). The append keeps the file mode, adds a
# newline first when the file lacks a final one, and never prints the value.
ensure_admin_secret() {
local app="$1" key=SUMMER_ADMIN__JWT__SECRET env line state found=0 empty=0 newline="" secret
env="$app/.env"
if [[ ! -f $env ]]; then
say "no .env in $app; skipped the admin token secret"
return 0
fi
[[ -r $env && -w $env ]] || die "$env is not readable and writable; add a $key line with any long random value, then run the update again"
while IFS= read -r line; do
state=0
env_line_state "$key" "$line" || state=$?
if ((state != 1)); then
found=1
empty=$((state == 2 ? 1 : 0))
fi
done <"$env"
# A non-empty $line here is a final line without a newline.
if [[ -n $line ]]; then
newline=$'\n'
state=0
env_line_state "$key" "$line" || state=$?
if ((state != 1)); then
found=1
empty=$((state == 2 ? 1 : 0))
fi
fi
if ((found)); then
if ((empty)); then
warn "$key in $env is empty; the admin refuses to start until it has a value"
fi
return 0
fi
secret="$(generate_secret)" ||
die "could not generate $key for $env (needs openssl, or head and base64); add a $key line with any long random value, then run the update again"
if ! printf '%s%s=%s\n' "$newline" "$key" "$secret" >>"$env"; then
die "could not append $key to $env"
fi
say "added $key to $env"
}
# update_app updates the application in the current directory: the
# framework checkout its go.mod replace names, summer, the admin token
# secret in its .env and its build. Nothing else is built or written.
update_app() {
local app="$PWD" fw rhs target dir ref="${SUMMERCMS_REF:-}" branch gobin
fw="$(framework_module)"
[[ -f $app/go.mod ]] || die "run this from inside an sm-<name>-app directory: $app has no go.mod"
[[ -f $app/summer.yaml ]] || die "run this from inside an sm-<name>-app directory: $app has no summer.yaml"
rhs="$(framework_replace "$app/go.mod")" ||
die "$app/go.mod has no replace for $fw; --update works on applications whose go.mod points the framework at a local checkout, as summer new writes it"
target="${rhs%%$'\n'*}"
is_local_path "$target" ||
die "$app/go.mod replaces $fw with ${rhs//$'\n'/ }, not a local checkout; update that requirement with go get instead"
if [[ $target == /* ]]; then
dir="$(abspath "$target")"
else
dir="$(abspath "$app/$target")"
fi
[[ $dir != / ]] || die "the framework replace in $app/go.mod must not point at /"
[[ -e $dir || -L $dir ]] || die "the framework replace in $app/go.mod points at $dir, which does not exist"
if [[ -n ${SUMMERCMS_DIR:-} ]]; then
warn "SUMMERCMS_DIR is ignored by --update; the framework checkout comes from go.mod ($dir)"
fi
command -v git >/dev/null 2>&1 || die "git is not installed; install git and run again"
check_go
check_framework_dir "$dir" "point the framework replace in $app/go.mod at a SummerCMS framework checkout"
if branch="$(git -C "$dir" symbolic-ref -q --short HEAD </dev/null)"; then
pull_branch "$dir" "$branch" "$ref"
else
move_detached "$dir" "$ref"
fi
note_shared_apps "$dir" "$app"
gobin="$(go_bin_dir)" || die "could not determine the Go install directory (go env GOBIN / GOPATH)"
say "installing summer into $gobin"
GOWORK=off go -C "$dir" install ./cmd/summer </dev/null
ensure_admin_secret "$app"
say "building $app"
"$gobin/summer" build </dev/null || die "summer build failed in $app; fix the error above and run the update again"
printf '\n==> Done. Run summer migrate, then restart summer serve.\n'
path_hint "$gobin"
}
main() {
set -euo pipefail
export GIT_TERMINAL_PROMPT=0
if (($# == 1)) && [[ $1 == -h || $1 == --help ]]; then
usage
exit 0
fi
local arg update=0 others=0
if (($#)); then
for arg in "$@"; do
if [[ $arg == --update ]]; then
update=1
else
others=$((others + 1))
fi
done
fi
if ((update)); then
if ((others)); then
printf 'error: --update takes no other argument\n\n' >&2
usage >&2
exit 2
fi
update_app
return 0
fi
if (($# != 1)); then
usage >&2
exit 2
fi
local name="$1"
if [[ ! $name =~ ^[a-z][a-z0-9]*$ ]]; then
printf 'error: invalid application name %q: use lower-case letters and digits, starting with a letter\n\n' "$name" >&2
usage >&2
exit 2
fi
local repo="${SUMMERCMS_REPO:-https://git.golem15.com/golem15/summercms}"
local ref="${SUMMERCMS_REF:-}"
local dir parent app
dir="$(abspath "${SUMMERCMS_DIR:-summercms}")"
[[ $dir != / ]] || die "SUMMERCMS_DIR must not be /"
parent="${dir%/*}"
parent="${parent:-/}"
app="${parent%/}/sm-$name-app"
[[ -d $parent ]] || die "$parent does not exist"
if [[ -e $app || -L $app ]]; then
die "$app already exists; choose another name or remove it"
fi
command -v git >/dev/null 2>&1 || die "git is not installed; install git and run again"
check_go
check_docker
checkout_framework "$repo" "$dir" "$ref"
local gobin
gobin="$(go_bin_dir)" || die "could not determine the Go install directory (go env GOBIN / GOPATH)"
say "installing summer into $gobin"
GOWORK=off go -C "$dir" install ./cmd/summer </dev/null
say "creating $app"
(cd "$parent" && "$gobin/summer" new "$name" --framework "$dir" </dev/null)
printf '\n==> Done. Start with:\n cd %s\n' "$app"
path_hint "$gobin"
}
main "$@"