Release 1.3.2

Interactive installer: prompt before replacing an existing install or
overwriting an existing config, reading answers from /dev/tty so it works
under 'curl ... | sh'. Falls back to the prior non-interactive contract
when no terminal is available. MOVE_FORCE=1 skips all prompts; new
MOVE_RESEED_CONFIG=1 reseeds the config unattended (old file kept as .bak).

Fix: install.sh no longer wipes a working install before downloading. The
tree is built in a staging dir and swapped into place only once complete,
so a failed download/extract/build leaves the existing install intact.
This commit is contained in:
2026-08-17 09:28:13 -05:00
parent 68e64eeaef
commit b9669269bc
4 changed files with 267 additions and 49 deletions
+30
View File
@@ -5,6 +5,35 @@ All notable changes to `move` are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.3.2] - 2026-08-17
### Added
- `install.sh` is now interactive. When it finds an existing install and a
controlling terminal is available, it reports what's there and asks before
replacing it, instead of leaving `MOVE_FORCE` as the only control. If a
config file already exists it asks separately whether to reseed it from the
shipped defaults. Both questions are asked before anything is downloaded or
deleted, so declining changes nothing.
- `MOVE_RESEED_CONFIG=1` overwrites the user config with the shipped defaults
without prompting, for unattended use. The previous file is kept as
`config.json.bak`; the same backup is written when reseeding is confirmed
at the prompt.
### Changed
- `MOVE_FORCE=1` now means "skip every prompt and reinstall unconditionally".
It deliberately does not touch the user config, so automation that
reinstalls the CLI can't take customizations down with it.
- Existing-install detection looks for the install tree and the wrapper, not
just the `.installed-version` marker, so a half-finished or hand-moved
install is caught rather than silently overwritten.
### Fixed
- Installing a *different* version over an existing one used to wipe and
replace it with no warning; only an exact version match was ever reported.
That case now prompts. With no terminal (CI, cron, container builds) the
previous non-interactive behavior is preserved exactly: an identical
version is a no-op, a different version is replaced.
## [1.3.1] - 2026-08-14 ## [1.3.1] - 2026-08-14
### Added ### Added
@@ -126,6 +155,7 @@ Initial release.
- Source split into `src/{move,cli,config,keeper}.ts`. - Source split into `src/{move,cli,config,keeper}.ts`.
- `bin` entry + shebang so `bun link` registers `move` globally. - `bin` entry + shebang so `bun link` registers `move` globally.
[1.3.2]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.3.1...v1.3.2
[1.3.1]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.3.0...v1.3.1 [1.3.1]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.3.0...v1.3.1
[1.3.0]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.2.0...v1.3.0 [1.3.0]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.2.0...v1.3.0
[1.2.0]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.1.1...v1.2.0 [1.2.0]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.1.1...v1.2.0
+33 -6
View File
@@ -33,14 +33,38 @@ The installer respects the XDG Base Directory Specification:
`XDG_BIN_HOME` is the widely-recognized de facto convention; XDG itself `XDG_BIN_HOME` is the widely-recognized de facto convention; XDG itself
doesn't standardize a user bin dir. doesn't standardize a user bin dir.
### Reinstalling over an existing install
The installer never replaces an existing install silently. When it finds
one and it can reach a terminal, it tells you what's there and asks:
```
==> Found an existing move install (v1.2.0) at /home/you/.local/share/move
Replace it with master? [Y/n]
```
If a config file already exists, it asks separately whether to overwrite
it with the shipped defaults (default: no). Both questions come *before*
anything is downloaded or deleted, so declining costs you nothing.
This works under `curl ... | sh` too: the prompts read from `/dev/tty`
rather than stdin, which the piped script itself occupies.
With no terminal available — CI, cron, a container build — there's nobody
to ask, so the installer falls back to its long-standing behavior: an
identical version is a no-op, a different version is replaced, and your
config is left alone. Use the env vars below to drive it explicitly.
Env vars (all optional): Env vars (all optional):
| Var | Default | Purpose | | Var | Default | Purpose |
| --- | ------- | ------- | | --- | ------- | ------- |
| `MOVE_VERSION` | `master` | Branch or tag to install. Pin with e.g. `v1.0.0`. | | `MOVE_VERSION` | `master` | Branch or tag to install. Pin with e.g. `v1.0.0`. |
| `MOVE_FORCE` | unset | Set to `1` to reinstall when the same version is already present. | | `MOVE_FORCE` | unset | Set to `1` to skip every prompt and reinstall unconditionally. Never touches your config. |
| `MOVE_RESEED_CONFIG` | unset | Set to `1` to overwrite your config with the shipped defaults without asking. The old file is kept as `config.json.bak`. |
| `XDG_DATA_HOME` | `$HOME/.local/share` | Where the source tree is installed (under `move/`). | | `XDG_DATA_HOME` | `$HOME/.local/share` | Where the source tree is installed (under `move/`). |
| `XDG_BIN_HOME` | `$HOME/.local/bin` | Where the `move` wrapper is placed. | | `XDG_BIN_HOME` | `$HOME/.local/bin` | Where the `move` wrapper is placed. |
| `XDG_CONFIG_HOME` | `$HOME/.config` | Where the config file lives (under `move/`). |
Bun must already be installed; the installer fails with a clear pointer Bun must already be installed; the installer fails with a clear pointer
to <https://bun.sh> if it isn't. to <https://bun.sh> if it isn't.
@@ -123,11 +147,14 @@ ${XDG_CONFIG_HOME:-$HOME/.config}/move/config.json
``` ```
The installer seeds this file with the default values on a fresh install, The installer seeds this file with the default values on a fresh install,
**only if no file already exists at that path**. Existing configs yours **only if no file already exists at that path**. An existing config —
or from a previous install — are never overwritten. If you remove the yours or from a previous install — is never overwritten silently: the
file later, `move` still works: missing defaults fall back to the values installer asks first, and replaces it only if you say yes (or if you set
baked into the binary (which match what was seeded, since both come from `MOVE_RESEED_CONFIG=1`), keeping the old file as `config.json.bak` either
`scripts/config.default.json`). way. `MOVE_FORCE=1` reinstalls the software but leaves your config alone.
If you remove the file later, `move` still works: missing defaults fall
back to the values baked into the binary (which match what was seeded,
since both come from `scripts/config.default.json`).
Pass `-C` / `--config <path>` to point at a different file; in that mode Pass `-C` / `--config <path>` to point at a different file; in that mode
the file must exist. the file must exist.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "move", "name": "move",
"version": "1.3.1", "version": "1.3.2",
"private": true, "private": true,
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"type": "module", "type": "module",
+194 -33
View File
@@ -10,25 +10,41 @@
# 1. Detect platform; bail on anything @nut-tree-fork/nut-js doesn't ship. # 1. Detect platform; bail on anything @nut-tree-fork/nut-js doesn't ship.
# 2. Require Bun; fail with a clear hint if missing (no auto-install). # 2. Require Bun; fail with a clear hint if missing (no auto-install).
# 3. Resolve XDG-compliant install paths. # 3. Resolve XDG-compliant install paths.
# 4. Idempotence check via a version marker file. # 4. Detect an existing install and, on a terminal, ask before replacing it.
# 5. Download the source tarball from Gitea, extract under the install dir. # 5. Download and build in a temp staging dir; swap it over the install
# dir only once it's complete, so a failed run can't destroy a working
# install.
# 6. `bun install --production` (skips devDependencies). # 6. `bun install --production` (skips devDependencies).
# 7. Drop a small wrapper script as `move` on the user's bin dir. # 7. Drop a small wrapper script as `move` on the user's bin dir.
# 8. Seed the user's config file with defaults, only if one doesn't already # 8. Seed the user's config file with defaults if one doesn't already exist
# exist at $XDG_CONFIG_HOME/move/config.json. # at $XDG_CONFIG_HOME/move/config.json; if one does, offer to reseed it.
# 9. Verify PATH, surface macOS Accessibility hint, print final status. # 9. Verify PATH, surface macOS Accessibility hint, print final status.
# #
# Interactivity:
# When a controlling terminal is available, an existing install is never
# replaced without asking, and an existing config is never overwritten
# without asking. Both questions are put up front, before anything is
# downloaded or deleted, so declining costs nothing. With no terminal
# (CI, cron, container build) the script falls back to its historical
# non-interactive contract: an identical version is a no-op, a different
# version is replaced, and the config is left alone.
#
# Env vars (all optional): # Env vars (all optional):
# MOVE_VERSION Branch or tag to install. Default: master. # MOVE_VERSION Branch or tag to install. Default: master.
# MOVE_FORCE Set to 1 to reinstall even if the version marker matches. # MOVE_FORCE Set to 1 to skip every prompt and reinstall
# unconditionally. Does not touch the user config.
# MOVE_RESEED_CONFIG Set to 1 to overwrite the user config with the
# shipped defaults without asking. The previous file
# is saved alongside it as config.json.bak.
# XDG_DATA_HOME Source install root (default $HOME/.local/share). # XDG_DATA_HOME Source install root (default $HOME/.local/share).
# Final source location is $XDG_DATA_HOME/move. # Final source location is $XDG_DATA_HOME/move.
# XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin). # XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin).
# Final binary location is $XDG_BIN_HOME/move. # Final binary location is $XDG_BIN_HOME/move.
# XDG_CONFIG_HOME User config root (default $HOME/.config). # XDG_CONFIG_HOME User config root (default $HOME/.config).
# Default config file path is $XDG_CONFIG_HOME/move/config.json. # Default config file is $XDG_CONFIG_HOME/move/config.json.
# #
# POSIX sh; no bashisms. # POSIX sh; no bashisms. Note the absence of `local`: helper functions use
# `_`-prefixed globals, which POSIX sh leaves us with.
set -eu set -eu
@@ -38,11 +54,13 @@ GITEA_HOST="gitea.cahlen.com"
MOVE_VERSION="${MOVE_VERSION:-master}" MOVE_VERSION="${MOVE_VERSION:-master}"
MOVE_FORCE="${MOVE_FORCE:-0}" MOVE_FORCE="${MOVE_FORCE:-0}"
MOVE_RESEED_CONFIG="${MOVE_RESEED_CONFIG:-0}"
INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move" INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move"
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}" BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/move" CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/move"
CONFIG_FILE="$CONFIG_DIR/config.json" CONFIG_FILE="$CONFIG_DIR/config.json"
WRAPPER="$BIN_DIR/move"
die() { die() {
printf 'Error: %s\n' "$1" >&2 printf 'Error: %s\n' "$1" >&2
@@ -62,6 +80,56 @@ assert_safe_dir() {
esac esac
} }
# --- Interactive prompt support ----------------------------------------------
#
# The documented entry point is `curl -fsSL ... | sh`, which means stdin is
# the *script source itself*. Reading a prompt answer from stdin would
# consume the rest of the program and truncate execution mid-run, so every
# prompt reads from /dev/tty directly.
#
# Detecting whether that's possible needs a real open(2) attempt. A `[ -r
# /dev/tty ]` test is not enough: the device node exists and is mode 0666
# even in contexts with no controlling terminal (cron, CI, container
# builds), where opening it fails with ENXIO. The probe runs in a subshell
# because a redirection failure on `exec` -- a special built-in -- exits a
# non-interactive shell outright under POSIX.
if (: >/dev/tty) 2>/dev/null; then
INTERACTIVE=1
else
INTERACTIVE=0
fi
# confirm PROMPT DEFAULT -> 0 for yes, 1 for no.
#
# DEFAULT is 'y' or 'n' and is taken on a bare Enter or on EOF (^D), so the
# loop can't spin forever against a closed terminal. Prompts are written to
# /dev/tty rather than stdout so they stay visible when the caller redirects
# our output.
confirm() {
_prompt="$1"
_default="$2"
case "$_default" in
y) _hint='[Y/n]' ;;
*) _hint='[y/N]' ;;
esac
while :; do
printf '%s %s ' "$_prompt" "$_hint" > /dev/tty
if ! IFS= read -r _reply < /dev/tty; then
printf '\n' > /dev/tty
_reply=''
fi
if [ -z "$_reply" ]; then
_reply="$_default"
fi
case "$_reply" in
[yY] | [yY][eE][sS]) return 0 ;;
[nN] | [nN][oO]) return 1 ;;
*) printf "Please answer 'y' or 'n'.\n" > /dev/tty ;;
esac
done
}
# --- Prerequisite tools ------------------------------------------------------ # --- Prerequisite tools ------------------------------------------------------
for tool in curl tar mktemp; do for tool in curl tar mktemp; do
@@ -100,53 +168,143 @@ fi
BUN_VERSION=$(bun --version) BUN_VERSION=$(bun --version)
printf '==> Using bun %s\n' "$BUN_VERSION" printf '==> Using bun %s\n' "$BUN_VERSION"
# --- Idempotence check ------------------------------------------------------- # --- Existing install check --------------------------------------------------
#
# Both questions this script can ask are asked here, before anything is
# downloaded, deleted, or written. Declining therefore costs the user
# nothing, and no prompt appears minutes into a `bun install`.
assert_safe_dir "$INSTALL_DIR" assert_safe_dir "$INSTALL_DIR"
assert_safe_dir "$CONFIG_DIR"
if [ "$MOVE_FORCE" != "1" ] && [ -f "$INSTALL_DIR/.installed-version" ]; then INSTALLED_VERSION=''
CURRENT=$(cat "$INSTALL_DIR/.installed-version" 2>/dev/null || printf '') if [ -f "$INSTALL_DIR/.installed-version" ]; then
if [ "$CURRENT" = "$MOVE_VERSION" ]; then INSTALLED_VERSION=$(cat "$INSTALL_DIR/.installed-version" 2>/dev/null || printf '')
printf 'move %s is already installed at %s/move.\n' "$MOVE_VERSION" "$BIN_DIR" fi
# Look wider than the version marker: a half-finished or hand-edited install
# can leave a tree or a wrapper behind without one, and steamrolling that
# silently is precisely what this check exists to prevent.
FOUND_EXISTING=0
if [ -d "$INSTALL_DIR" ] || [ -e "$WRAPPER" ] || [ -L "$WRAPPER" ]; then
FOUND_EXISTING=1
fi
# Decided here, applied at the end -- the seed file it copies from only
# exists once the tarball has been extracted.
RESEED_CONFIG="$MOVE_RESEED_CONFIG"
if [ "$FOUND_EXISTING" = "1" ] && [ "$MOVE_FORCE" != "1" ]; then
if [ -n "$INSTALLED_VERSION" ]; then
printf '==> Found an existing move install (%s) at %s\n' \
"$INSTALLED_VERSION" "$INSTALL_DIR"
else
printf '==> Found an existing move install at %s (version unknown)\n' \
"$INSTALL_DIR"
fi
if [ "$INTERACTIVE" = "1" ]; then
# The defaults below are chosen so that a bare Enter reproduces
# exactly what this script did before it learned to ask: skip when
# the version is identical, replace when it differs.
if [ "$INSTALLED_VERSION" = "$MOVE_VERSION" ]; then
REPLACE_PROMPT="Reinstall move $MOVE_VERSION over it?"
REPLACE_DEFAULT=n
else
REPLACE_PROMPT="Replace it with $MOVE_VERSION?"
REPLACE_DEFAULT=y
fi
if ! confirm "$REPLACE_PROMPT" "$REPLACE_DEFAULT"; then
printf 'Leaving the existing install alone. Nothing was changed.\n'
exit 0
fi
else
# Nowhere to ask, so fall back to the historical contract.
if [ "$INSTALLED_VERSION" = "$MOVE_VERSION" ]; then
printf 'move %s is already installed at %s.\n' "$MOVE_VERSION" "$WRAPPER"
printf 'Set MOVE_FORCE=1 to reinstall, or set MOVE_VERSION to a different ref.\n' printf 'Set MOVE_FORCE=1 to reinstall, or set MOVE_VERSION to a different ref.\n'
exit 0 exit 0
fi fi
printf '==> No terminal available; replacing %s with %s\n' \
"${INSTALLED_VERSION:-unknown}" "$MOVE_VERSION"
fi
fi fi
# --- Clean install dir ------------------------------------------------------- if [ -e "$CONFIG_FILE" ] && [ "$RESEED_CONFIG" != "1" ] &&
[ "$INTERACTIVE" = "1" ] && [ "$MOVE_FORCE" != "1" ]; then
printf '==> A config file already exists at %s\n' "$CONFIG_FILE"
if confirm 'Overwrite it with the shipped defaults?' n; then
RESEED_CONFIG=1
fi
fi
# --- Stage, download, build --------------------------------------------------
#
# Everything is assembled in a temp staging dir first; the existing install
# is removed only once the staged tree is fully built and ready to swap in.
# A failed download, extract, or `bun install` therefore leaves a working
# install untouched -- unlike the old flow, which wiped INSTALL_DIR before
# the download even started and left nothing behind on any failure.
mkdir -p "$BIN_DIR" mkdir -p "$BIN_DIR"
rm -rf "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# --- Download source --------------------------------------------------------- DATA_ROOT=$(dirname "$INSTALL_DIR")
mkdir -p "$DATA_ROOT"
TARBALL_URL="https://$GITEA_HOST/$REPO_OWNER/$REPO_NAME/archive/$MOVE_VERSION.tar.gz"
TARBALL_TMP=$(mktemp) || die "could not create temp file" TARBALL_TMP=$(mktemp) || die "could not create temp file"
# Stage on the same filesystem as INSTALL_DIR so the final swap is a rename,
# not a cross-device copy.
STAGE_DIR=$(mktemp -d "$DATA_ROOT/.move-stage.XXXXXX") ||
{ rm -f "$TARBALL_TMP"; die "could not create staging dir under $DATA_ROOT"; }
# On any exit, clean up the tarball and any leftover staging dir. After a
# successful swap STAGE_DIR has been renamed away, so the rm -rf is a no-op.
cleanup() { cleanup() {
rm -f "$TARBALL_TMP" rm -f "$TARBALL_TMP"
rm -rf "$STAGE_DIR"
} }
trap cleanup EXIT INT TERM trap cleanup EXIT INT TERM
TARBALL_URL="https://$GITEA_HOST/$REPO_OWNER/$REPO_NAME/archive/$MOVE_VERSION.tar.gz"
printf '==> Downloading %s\n' "$TARBALL_URL" printf '==> Downloading %s\n' "$TARBALL_URL"
if ! curl -fsSL "$TARBALL_URL" -o "$TARBALL_TMP"; then if ! curl -fsSL "$TARBALL_URL" -o "$TARBALL_TMP"; then
die "could not download $TARBALL_URL (check MOVE_VERSION='$MOVE_VERSION' and network)" die "could not download $TARBALL_URL (check MOVE_VERSION='$MOVE_VERSION' and network)"
fi fi
printf '==> Extracting source to %s\n' "$INSTALL_DIR" printf '==> Extracting source\n'
if ! tar -xzf "$TARBALL_TMP" -C "$INSTALL_DIR" --strip-components=1; then if ! tar -xzf "$TARBALL_TMP" -C "$STAGE_DIR" --strip-components=1; then
die "could not extract tarball from $TARBALL_URL" die "could not extract tarball from $TARBALL_URL"
fi fi
# --- Install runtime deps ----------------------------------------------------
printf '==> Installing runtime dependencies (bun install --production)\n' printf '==> Installing runtime dependencies (bun install --production)\n'
(cd "$INSTALL_DIR" && bun install --production) (cd "$STAGE_DIR" && bun install --production)
# Sanity-check the staged tree before we disturb the existing install: a
# truncated or wrong tarball that's missing the config seed should fail here,
# while the old install is still intact and swappable-out.
if [ ! -f "$STAGE_DIR/scripts/config.default.json" ]; then
die "downloaded tree is missing scripts/config.default.json (bad MOVE_VERSION='$MOVE_VERSION'?)"
fi
# Record the version inside the staged tree so the install is self-consistent
# the instant it lands.
printf '%s\n' "$MOVE_VERSION" > "$STAGE_DIR/.installed-version"
# --- Swap staged tree into place ---------------------------------------------
#
# The only destructive step, kept as late as possible: the window where
# INSTALL_DIR is absent is just this rm + rename, not the whole build.
assert_safe_dir "$INSTALL_DIR"
printf '==> Installing to %s\n' "$INSTALL_DIR"
rm -rf "$INSTALL_DIR"
if ! mv "$STAGE_DIR" "$INSTALL_DIR"; then
die "could not move staged install into place at $INSTALL_DIR"
fi
# --- Drop the wrapper -------------------------------------------------------- # --- Drop the wrapper --------------------------------------------------------
WRAPPER="$BIN_DIR/move"
printf '==> Writing wrapper to %s\n' "$WRAPPER" printf '==> Writing wrapper to %s\n' "$WRAPPER"
cat > "$WRAPPER" <<EOF cat > "$WRAPPER" <<EOF
#!/usr/bin/env sh #!/usr/bin/env sh
@@ -154,23 +312,21 @@ exec bun "$INSTALL_DIR/src/move.ts" "\$@"
EOF EOF
chmod +x "$WRAPPER" chmod +x "$WRAPPER"
# --- Write version marker ---------------------------------------------------- # --- Seed user config file ---------------------------------------------------
printf '%s\n' "$MOVE_VERSION" > "$INSTALL_DIR/.installed-version"
# --- Seed user config file (only if absent) ----------------------------------
# #
# The defaults file shipped with the source tree (scripts/config.default.json) # The defaults file shipped with the source tree (scripts/config.default.json)
# is also the single source of truth for the runtime defaults loaded by # is also the single source of truth for the runtime defaults loaded by
# src/config.ts, so seeding a fresh user file from the same place keeps the # src/config.ts, so seeding a fresh user file from the same place keeps the
# CLI behavior and the user-visible config in sync. # CLI behavior and the user-visible config in sync.
# #
# Strict policy: never overwrite an existing user config. The uninstaller # Policy: an existing user config is never overwritten *silently*. It is
# follows the matching policy of never removing it; together that # replaced only on an explicit answer to the prompt above or an explicit
# preserves user customizations unconditionally across (re)installs and # MOVE_RESEED_CONFIG=1, and even then the previous file is kept as a .bak
# uninstalls. # rather than destroyed. Everything else -- MOVE_FORCE=1, a non-interactive
# run -- leaves it untouched, so automation that reinstalls the software
# can't take a user's customizations down with it. The uninstaller follows
# the matching policy of never removing the config at all.
assert_safe_dir "$CONFIG_DIR"
SEED_SRC="$INSTALL_DIR/scripts/config.default.json" SEED_SRC="$INSTALL_DIR/scripts/config.default.json"
if [ ! -f "$SEED_SRC" ]; then if [ ! -f "$SEED_SRC" ]; then
@@ -181,6 +337,11 @@ mkdir -p "$CONFIG_DIR"
if [ ! -e "$CONFIG_FILE" ]; then if [ ! -e "$CONFIG_FILE" ]; then
cp "$SEED_SRC" "$CONFIG_FILE" cp "$SEED_SRC" "$CONFIG_FILE"
printf '==> Wrote default config to %s\n' "$CONFIG_FILE" printf '==> Wrote default config to %s\n' "$CONFIG_FILE"
elif [ "$RESEED_CONFIG" = "1" ]; then
cp "$CONFIG_FILE" "$CONFIG_FILE.bak"
cp "$SEED_SRC" "$CONFIG_FILE"
printf '==> Reseeded %s (previous file saved as %s)\n' \
"$CONFIG_FILE" "$CONFIG_FILE.bak"
else else
printf '==> Config already exists at %s; leaving it alone\n' "$CONFIG_FILE" printf '==> Config already exists at %s; leaving it alone\n' "$CONFIG_FILE"
fi fi