diff --git a/src/scripts/install-wg-ui.sh b/src/scripts/install-wg-ui.sh index 247a0fd..b5ca1ba 100755 --- a/src/scripts/install-wg-ui.sh +++ b/src/scripts/install-wg-ui.sh @@ -122,6 +122,63 @@ setup_lib_dirs() { chmod 755 "$WGUI_LIB_DIR" } +clone_or_update() { + mkdir -p "$(dirname "$INSTALL_DIR")" + if [ -d "$INSTALL_DIR/.git" ]; then + UPDATE=true + log "updating existing install at $INSTALL_DIR ..." + git -C "$INSTALL_DIR" fetch --depth 1 origin "$BRANCH" + local db_backup="$INSTALL_DIR/db/.wgui-pre-update.db" + if [ -f "$INSTALL_DIR/db/wgui" ]; then + cp -a "$INSTALL_DIR/db/wgui" "$db_backup" + fi + git -C "$INSTALL_DIR" reset --hard "origin/$BRANCH" + if [ -f "$db_backup" ]; then + mv -f "$db_backup" "$INSTALL_DIR/db/wgui" + fi + else + log "cloning wg-ui ($REPO_URL, branch $BRANCH) ..." + git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR" + fi +} + +install_dependencies() { + log "installing app dependencies with bun ..." + cd "$INSTALL_DIR" + "$BUN_BIN" install + "$BUN_BIN" bun-sqlite schema +} + +ensure_env_file() { + if [ ! -f "$INSTALL_DIR/.env" ]; then + log "generating $INSTALL_DIR/.env with fresh encryption secrets ..." + { + echo "NODE_ENV=${NODE_ENV:-production}" + echo "ENCRYPTION_KEY=$(openssl rand -base64 32 | tr -d '\n')" + echo "ENCRYPTION_SALT=$(openssl rand -base64 32 | tr -d '\n')" + echo "DATA_DIR=$INSTALL_DIR/.data" + } > "$INSTALL_DIR/.env" + chmod 600 "$INSTALL_DIR/.env" + fi +} + +setup_wireguard() { + local setup_script="$INSTALL_DIR/src/scripts/setup-wireguard.sh" + if [ -x "$setup_script" ] && [ "${SKIP_WIREGUARD_SETUP:-0}" != "1" ]; then + log "running $(basename "$setup_script") to install wireguard on this host ..." + "$setup_script" + fi +} + +install_wg_quick_helper() { + local helper_src="$INSTALL_DIR/src/scripts/wg-quick-manage.sh" + if [ ! -f "$helper_src" ]; then + fail "missing $helper_src — cannot install the wg-quick helper" + fi + log "installing wg-quick helper to $WG_QUICK_HELPER ..." + install -m 755 -o root -g root "$helper_src" "$WG_QUICK_HELPER" +} + install_caddy_stack() { local install_script="$INSTALL_DIR/src/scripts/install-caddy.sh" @@ -190,6 +247,75 @@ print_firewall_instructions() { log " and review all firewall rules under /admin/setup -> Firewall" } +install_systemd_unit() { + local unit="/etc/systemd/system/$SERVICE_NAME.service" + log "writing systemd unit $unit ..." + cat > "$unit" < "$init_script" < { + const defined = new Set(); + + for (const match of SCRIPT.matchAll(/^([A-Za-z_][A-Za-z0-9_]*)\(\) *\{/gm)) { + const name = match[1]; + + if (name) { + defined.add(name); + } + } + + return defined; +} + +/** + * A bare word on its own line is a function call — the installer always + * invokes helpers that way, both at top level and from inside another + * function. Catching the indented ones too is what surfaces a call whose + * definition was deleted. + */ +function grabInvocations(): string[] { + const invocations: string[] = []; + + for (const match of SCRIPT.matchAll(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/gm)) { + const name = match[1]; + + if (!name || BASH_KEYWORDS.has(name)) { + continue; + } + + invocations.push(name); + } + + return invocations; +} + +describe("install-wg-ui.sh", () => { + test("parses as valid bash", () => { + expect(() => + execFileSync("bash", ["-n", SCRIPT_PATH], { stdio: "pipe" }), + ).not.toThrow(); + }); + + test("every invoked function has a definition", () => { + const defined = grabDefinedFunctions(); + const undefined_invocations = grabInvocations().filter( + (name) => !defined.has(name), + ); + + expect(undefined_invocations).toEqual([]); + }); + + test("runs the update flow in the documented order", () => { + const invocations = grabInvocations(); + const expected_order = [ + "install_deps", + "install_bun", + "setup_lib_dirs", + "clone_or_update", + "install_caddy_stack", + "install_dependencies", + "ensure_env_file", + "setup_wireguard", + "install_wg_quick_helper", + ]; + + let cursor = -1; + + for (const name of expected_order) { + const index = invocations.indexOf(name); + + expect(index).toBeGreaterThan(cursor); + cursor = index; + } + }); +});