From 50b5f99cbdb63693ac437d8d103bb5e718bbbc17 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:37:02 +0900 Subject: [PATCH 01/11] feat: import server properties with per-server ownership --- libexec/mcserver-kit/home-tui.sh | 24 +++- libexec/mcserver-kit/server-config.py | 99 +++++++++++++++- libexec/mcserver-kit/server-manager.sh | 63 +++++++++++ libexec/mcserver-kit/server-properties-tui.sh | 31 ++++- libexec/mcserver-kit/windows-dialog.ps1 | 14 ++- share/mcserver-kit/locales/en.json | 17 ++- share/mcserver-kit/locales/ja.json | 17 ++- tests/run-tests.sh | 106 ++++++++++++++++++ 8 files changed, 362 insertions(+), 9 deletions(-) diff --git a/libexec/mcserver-kit/home-tui.sh b/libexec/mcserver-kit/home-tui.sh index d7f397e..a1cfe6b 100755 --- a/libexec/mcserver-kit/home-tui.sh +++ b/libexec/mcserver-kit/home-tui.sh @@ -131,15 +131,18 @@ run_and_show() { } server_action_menu() { - local id="$1" directory="$2" choice + local id="$1" directory="$2" choice output while true; do - choice="$(whiptail --title "$id" --menu "$(tr home.server_status "$(server_status "$directory")")" 21 78 11 \ + choice="$(whiptail --title "$id" --menu "$(tr home.server_status "$(server_status "$directory")")" 23 78 13 \ start "$(tr home.start)" \ stop "$(tr home.stop)" \ restart "$(tr home.restart)" \ status "$(tr home.status)" \ logs "$(tr home.logs)" \ properties "$(tr home.properties)" \ + import-properties "$(tr home.import_properties)" \ + open-data "$(tr home.open_data)" \ + open-server "$(tr home.open_server)" \ down "$(tr home.down)" \ back "$(tr tui.back)" \ 3>&1 1>&2 2>&3)" || return @@ -155,6 +158,23 @@ server_action_menu() { properties) "${SCRIPT_DIR}/server-manager.sh" server "$id" properties || true ;; + import-properties) + if whiptail --yesno "$(tr home.import_confirm "$id")" 12 76; then + clear + "${SCRIPT_DIR}/server-manager.sh" server "$id" import-properties || true + pause_for_enter + fi + ;; + open-data | open-server) + new_temp_file output + if [[ "$choice" == open-data ]]; then + "${SCRIPT_DIR}/server-manager.sh" server "$id" open data >"$output" 2>&1 || + whiptail --title "$(tr common.error)" --textbox "$output" 14 78 + else + "${SCRIPT_DIR}/server-manager.sh" server "$id" open server >"$output" 2>&1 || + whiptail --title "$(tr common.error)" --textbox "$output" 14 78 + fi + ;; down) if whiptail --yesno "$(tr home.down_confirm "$id")" 10 72; then run_and_show "$id" "${SCRIPT_DIR}/server-manager.sh" server "$id" down diff --git a/libexec/mcserver-kit/server-config.py b/libexec/mcserver-kit/server-config.py index fa6c696..c41ab14 100755 --- a/libexec/mcserver-kit/server-config.py +++ b/libexec/mcserver-kit/server-config.py @@ -9,6 +9,7 @@ import shutil import sys import tempfile +from datetime import datetime from pathlib import Path MANAGED_DEFAULTS = { @@ -116,6 +117,68 @@ def read_properties(path: Path) -> dict[str, str]: return values +def set_property(path: Path, env_key: str, value: str) -> None: + property_key = PROPERTY_KEYS[env_key] + if "\n" in value or "\r" in value: + raise ValueError("Property values cannot contain newlines") + lines = path.read_text(encoding="utf-8").splitlines(keepends=True) + replacement = f"{property_key}={value}\n" + indices = [ + index for index, line in enumerate(lines) + if line.partition("=")[0].strip() == property_key + ] + if indices: + lines[indices[-1]] = replacement + else: + if lines and not lines[-1].endswith("\n"): + lines[-1] += "\n" + lines.append(replacement) + atomic_write(path, "".join(lines)) + + +def validate_properties_source(source: Path) -> None: + if not source.is_file(): + raise ValueError(f"Not a file: {source}") + if source.name != "server.properties": + raise ValueError("Select a file named server.properties") + if not read_properties(source): + raise ValueError("The selected file has no property entries") + + +def import_properties(server_dir: Path, source: Path) -> Path | None: + validate_properties_source(source) + + destination = server_dir / "data" / "server.properties" + destination.parent.mkdir(parents=True, exist_ok=True) + if source.resolve() == destination.resolve(): + raise ValueError("The source is already this server's server.properties") + backup = None + if destination.exists(): + stamp = datetime.now().strftime("%Y%m%d-%H%M%S") + backup = destination.with_name(f"server.properties.mcserver-kit.{stamp}.bak") + suffix = 1 + while backup.exists(): + backup = destination.with_name(f"server.properties.mcserver-kit.{stamp}.{suffix}.bak") + suffix += 1 + shutil.copy2(destination, backup) + + descriptor, temporary_name = tempfile.mkstemp(prefix="server.properties.", dir=destination.parent) + try: + with os.fdopen(descriptor, "wb") as output, source.open("rb") as input_file: + shutil.copyfileobj(input_file, output) + os.chmod(temporary_name, 0o644) + env_path = server_dir / "server.env" + values = read_env(env_path) + values["OVERRIDE_SERVER_PROPERTIES"] = "false" + write_env(env_path, values) + os.replace(temporary_name, destination) + finally: + if os.path.exists(temporary_name): + os.unlink(temporary_name) + + return backup + + def compose_environment(path: Path, dotenv: dict[str, str]) -> dict[str, str]: values: dict[str, str] = {} if not path.is_file(): @@ -195,7 +258,7 @@ def migrate(server_dir: Path) -> None: backup = server_dir / "compose.yaml.mcserver-kit.bak" if not target.exists() and not backup.exists(): shutil.copy2(compose, backup) - values: dict[str, str] = {} + values: dict[str, str] = dict(existing) for env_key, default in MANAGED_DEFAULTS.items(): property_key = PROPERTY_KEYS.get(env_key, "") values[env_key] = existing.get( @@ -214,13 +277,45 @@ def migrate(server_dir: Path) -> None: def main() -> int: if len(sys.argv) < 3: - print("usage: server-config.py get|set|migrate TARGET [KEY] [VALUE]", file=sys.stderr) + print("usage: server-config.py get|set|migrate|property-get|property-set|import-properties TARGET [KEY] [VALUE]", file=sys.stderr) return 2 operation = sys.argv[1] target = Path(sys.argv[2]) if operation == "migrate" and len(sys.argv) == 3: migrate(target) return 0 + if operation == "validate-properties" and len(sys.argv) == 3: + try: + validate_properties_source(target) + except (OSError, ValueError, UnicodeError) as error: + print(f"Invalid server.properties: {error}", file=sys.stderr) + return 1 + return 0 + if operation == "import-properties" and len(sys.argv) == 4: + try: + backup = import_properties(target, Path(sys.argv[3])) + except (OSError, ValueError, UnicodeError) as error: + print(f"Import failed: {error}", file=sys.stderr) + return 1 + if backup: + print(backup) + return 0 + if operation == "property-get" and len(sys.argv) in (4, 5): + key = PROPERTY_KEYS.get(sys.argv[3]) + if key is None: + return 2 + default = sys.argv[4] if len(sys.argv) == 5 else "" + print(read_properties(target).get(key, default)) + return 0 + if operation == "property-set" and len(sys.argv) == 5: + if sys.argv[3] not in PROPERTY_KEYS: + return 2 + try: + set_property(target, sys.argv[3], sys.argv[4]) + except (OSError, ValueError, UnicodeError) as error: + print(f"Could not edit server.properties: {error}", file=sys.stderr) + return 1 + return 0 if operation == "get" and len(sys.argv) in (4, 5): values = read_env(target) default = sys.argv[4] if len(sys.argv) == 5 else "" diff --git a/libexec/mcserver-kit/server-manager.sh b/libexec/mcserver-kit/server-manager.sh index a52321f..cb61a85 100755 --- a/libexec/mcserver-kit/server-manager.sh +++ b/libexec/mcserver-kit/server-manager.sh @@ -6,6 +6,8 @@ set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${MCSERVER_KIT_CONFIG:-${HOME}/.config/mcserver-compose-kit/config.yml}" CONFIG_VALUE="${SCRIPT_DIR}/config-value.py" +CONFIG_TOOL="${SCRIPT_DIR}/server-config.py" +WINDOWS_DIALOG="${SCRIPT_DIR}/windows-dialog.ps1" # shellcheck source=libexec/mcserver-kit/i18n.sh source "${SCRIPT_DIR}/i18n.sh" @@ -82,6 +84,59 @@ compose_in() { ) } +require_stopped() { + local directory="$1" running + running="$(compose_in "$directory" ps --status running --services)" || die "$(tr server.status_failed)" + [[ -z "$running" ]] || die "$(tr server.stop_before_import)" +} + +choose_properties_file() { + local enabled dialog_path encoded selected + enabled="$(python3 "$CONFIG_VALUE" get "$CONFIG_FILE" ui windows_dialogs 2>/dev/null || printf true)" + if [[ "$enabled" == true ]] && command -v wslpath >/dev/null 2>&1 && + [[ -x /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe && -f "$WINDOWS_DIALOG" ]]; then + dialog_path="$(wslpath -w "$WINDOWS_DIALOG")" + encoded="$(/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe \ + -NoProfile -ExecutionPolicy Bypass -File "$dialog_path" -Mode SelectProperties 2>/dev/null)" || return 1 + encoded="${encoded//$'\r'/}" + selected="$(printf '%s' "$encoded" | base64 --decode)" || return 1 + wslpath -u "$selected" + return + fi + [[ -t 0 ]] || die "$(tr server.import_usage)" + read -r -p "$(tr server.import_path_prompt)" selected + [[ -n "$selected" ]] || return 1 + printf '%s' "$selected" +} + +import_properties() { + local directory="$1" source="${2-}" backup + require_stopped "$directory" + if [[ -z "$source" ]]; then + source="$(choose_properties_file)" || return 0 + fi + [[ -f "$source" ]] || die "$(tr server.import_source_missing "$source")" + python3 "$CONFIG_TOOL" validate-properties "$source" || die "$(tr server.import_failed)" + python3 "$CONFIG_TOOL" migrate "$directory" || die "$(tr server.import_failed)" + backup="$(python3 "$CONFIG_TOOL" import-properties "$directory" "$source")" || die "$(tr server.import_failed)" + compose_in "$directory" config --quiet || die "$(tr properties.compose_invalid)" + [[ -z "$backup" ]] || printf '%s\n' "$(tr server.import_backup "$backup")" + printf '%s\n' "$(tr server.import_done "${directory}/data/server.properties")" +} + +open_folder() { + local directory="$1" part="${2:-data}" target windows_path + case "$part" in + data) target="${directory}/data" ;; + server) target="$directory" ;; + *) die "$(tr server.open_usage)" ;; + esac + [[ -d "$target" ]] || die "$(tr server.open_missing "$target")" + command -v wslpath >/dev/null 2>&1 && command -v explorer.exe >/dev/null 2>&1 || die "$(tr server.explorer_unavailable)" + windows_path="$(wslpath -w "$target")" || die "$(tr server.explorer_unavailable)" + explorer.exe "$windows_path" +} + manage_server() { local id="${1-}" local action="${2-}" @@ -124,6 +179,14 @@ manage_server() { properties) exec "${SCRIPT_DIR}/server-properties-tui.sh" "$id" "$directory" ;; + import-properties) + [[ $# -le 1 ]] || die "$(tr server.import_usage)" + import_properties "$directory" "${1-}" + ;; + open) + [[ $# -le 1 ]] || die "$(tr server.open_usage)" + open_folder "$directory" "${1:-data}" + ;; *) die "$(tr server.unknown_action "$action")" ;; diff --git a/libexec/mcserver-kit/server-properties-tui.sh b/libexec/mcserver-kit/server-properties-tui.sh index 46dd053..9d67445 100755 --- a/libexec/mcserver-kit/server-properties-tui.sh +++ b/libexec/mcserver-kit/server-properties-tui.sh @@ -7,8 +7,10 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SERVER_ID="${1-}" SERVER_DIR="${2-}" SERVER_ENV="${SERVER_DIR}/server.env" +SERVER_PROPERTIES="${SERVER_DIR}/data/server.properties" CONFIG_TOOL="${SCRIPT_DIR}/server-config.py" changed=false +manual_properties=false # shellcheck source=libexec/mcserver-kit/i18n.sh source "${SCRIPT_DIR}/i18n.sh" @@ -40,11 +42,27 @@ die() { } setting_get() { - python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2" + if [[ "$manual_properties" == true ]]; then + case "$1" in + WHITELIST | EXISTING_WHITELIST_FILE | OPS | EXISTING_OPS_FILE) + python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2" ;; + *) python3 "$CONFIG_TOOL" property-get "$SERVER_PROPERTIES" "$1" "$2" ;; + esac + else + python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2" + fi } setting_set() { - python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2" + if [[ "$manual_properties" == true ]]; then + case "$1" in + WHITELIST | EXISTING_WHITELIST_FILE | OPS | EXISTING_OPS_FILE) + python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2" ;; + *) python3 "$CONFIG_TOOL" property-set "$SERVER_PROPERTIES" "$1" "$2" ;; + esac + else + python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2" + fi changed=true } @@ -212,6 +230,15 @@ main() { whiptail --yesno "$(tr properties.migration_prompt)" 11 76 || return fi python3 "$CONFIG_TOOL" migrate "$SERVER_DIR" + if [[ "$(python3 "$CONFIG_TOOL" get "$SERVER_ENV" OVERRIDE_SERVER_PROPERTIES true)" == false ]]; then + [[ -f "$SERVER_PROPERTIES" ]] || die "$(tr properties.file_missing)" + local running + running="$(cd "$SERVER_DIR" && docker compose ps --status running --services)" || die "$(tr server.status_failed)" + if [[ -n "$running" ]]; then + die "$(tr properties.stop_first "$SERVER_ID")" + fi + manual_properties=true + fi (cd "$SERVER_DIR" && docker compose config --quiet) || die "$(tr properties.compose_invalid)" while true; do diff --git a/libexec/mcserver-kit/windows-dialog.ps1 b/libexec/mcserver-kit/windows-dialog.ps1 index 5899e1f..d4db1ff 100644 --- a/libexec/mcserver-kit/windows-dialog.ps1 +++ b/libexec/mcserver-kit/windows-dialog.ps1 @@ -1,6 +1,6 @@ param( [Parameter(Mandatory = $true)] - [ValidateSet('SelectWorld', 'InputMotd', 'Check')] + [ValidateSet('SelectWorld', 'SelectProperties', 'InputMotd', 'Check')] [string]$Mode, [string]$DefaultValue = '' @@ -58,6 +58,18 @@ if ($Mode -eq 'SelectWorld') { exit 0 } +if ($Mode -eq 'SelectProperties') { + $dialog = New-Object System.Windows.Forms.OpenFileDialog + $dialog.Title = 'Select server.properties / server.propertiesを選択' + $dialog.Filter = 'server.properties|server.properties|All files (*.*)|*.*' + $dialog.CheckFileExists = $true + if ($dialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { + exit 1 + } + Write-Utf8Base64 $dialog.FileName + exit 0 +} + if ($Mode -eq 'InputMotd') { $form = New-Object System.Windows.Forms.Form $form.Text = 'Minecraft Server Kit' diff --git a/share/mcserver-kit/locales/en.json b/share/mcserver-kit/locales/en.json index cc69484..ccc1a89 100644 --- a/share/mcserver-kit/locales/en.json +++ b/share/mcserver-kit/locales/en.json @@ -40,6 +40,10 @@ "home.status": "Show status", "home.logs": "Follow logs", "home.properties": "Server settings", + "home.import_properties": "Import server.properties", + "home.import_confirm": "Import server.properties for %s? Stop the server first. The current file will be backed up, and this server will use data/server.properties for property settings.", + "home.open_data": "Open persistent data in Explorer", + "home.open_server": "Open server folder in Explorer", "home.down": "Remove containers", "home.down_confirm": "Remove containers and networks for %s? World data is preserved.", "home.diagnostics_title": "Environment diagnostics", @@ -130,7 +134,18 @@ "server.running": "running", "server.stopped": "stopped", "server.none": "(no servers)", - "server.usage": "Usage: mcserver-kit server SERVER_ID start|stop|restart|status|logs|down|properties", + "server.usage": "Usage: mcserver-kit server SERVER_ID start|stop|restart|status|logs|down|properties|import-properties|open", + "server.status_failed": "Could not check whether the server is running. Check Docker and try again.", + "server.stop_before_import": "Stop the server before importing server.properties.", + "server.import_usage": "Usage: mcserver-kit server SERVER_ID import-properties [PATH]", + "server.import_path_prompt": "Path to server.properties: ", + "server.import_source_missing": "File not found: %s", + "server.import_failed": "Could not import server.properties.", + "server.import_backup": "Previous server.properties backup: %s", + "server.import_done": "Imported to %s. This server now uses data/server.properties as the source of truth for property settings.", + "server.open_usage": "Usage: mcserver-kit server SERVER_ID open [data|server]", + "server.open_missing": "Folder not found: %s", + "server.explorer_unavailable": "Windows Explorer is unavailable. This action requires WSL interop and wslpath.", "server.validating": "Validating Docker Compose configuration for %s...", "server.starting": "Starting %s...", "server.stopping": "Stopping %s...", diff --git a/share/mcserver-kit/locales/ja.json b/share/mcserver-kit/locales/ja.json index 3280b12..a322c55 100644 --- a/share/mcserver-kit/locales/ja.json +++ b/share/mcserver-kit/locales/ja.json @@ -40,6 +40,10 @@ "home.status": "状態を表示", "home.logs": "ログを表示", "home.properties": "サーバー設定", + "home.import_properties": "server.propertiesをインポート", + "home.import_confirm": "%sにserver.propertiesをインポートしますか?先にサーバーを停止してください。既存ファイルをバックアップし、このサーバーのプロパティ設定はdata/server.propertiesを正本に切り替えます。", + "home.open_data": "永続データをExplorerで開く", + "home.open_server": "サーバーフォルダをExplorerで開く", "home.down": "コンテナを削除", "home.down_confirm": "%sのコンテナとネットワークを削除しますか?ワールドデータは保持されます。", "home.diagnostics_title": "動作環境の診断", @@ -130,7 +134,18 @@ "server.running": "起動中", "server.stopped": "停止中", "server.none": "(サーバーがありません)", - "server.usage": "使い方: mcserver-kit server サーバーID start|stop|restart|status|logs|down|properties", + "server.usage": "使い方: mcserver-kit server サーバーID start|stop|restart|status|logs|down|properties|import-properties|open", + "server.status_failed": "サーバーが起動中か確認できませんでした。Dockerを確認して再試行してください。", + "server.stop_before_import": "server.propertiesのインポート前にサーバーを停止してください。", + "server.import_usage": "使い方: mcserver-kit server サーバーID import-properties [パス]", + "server.import_path_prompt": "server.propertiesのパス: ", + "server.import_source_missing": "ファイルが見つかりません: %s", + "server.import_failed": "server.propertiesをインポートできませんでした。", + "server.import_backup": "元のserver.propertiesのバックアップ: %s", + "server.import_done": "%sにインポートしました。このサーバーのプロパティ設定はdata/server.propertiesが正本になります。", + "server.open_usage": "使い方: mcserver-kit server サーバーID open [data|server]", + "server.open_missing": "フォルダが見つかりません: %s", + "server.explorer_unavailable": "Windows Explorerを利用できません。WSL連携とwslpathが必要です。", "server.validating": "%sのDocker Compose設定を検証しています...", "server.starting": "%sを起動しています...", "server.stopping": "%sを停止しています...", diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 8d97bee..a132e0d 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -674,6 +674,111 @@ WHIPTAIL assert_equal 'Unified MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "$server_env" MOTD)" 'the properties TUI updates unified settings' } +test_property_import_and_explorer() { + local temp_dir="$1" root + root="${temp_dir}/property-import/servers" + local config_file="${temp_dir}/property-import/config.yml" + local fake_bin="${temp_dir}/property-import/bin" + local source_dir="${temp_dir}/property-import/distribution" + local server_dir="${root}/alpha" output + mkdir -p "${server_dir}/data" "$fake_bin" "$source_dir" + cat >"$config_file" <"${server_dir}/compose.yaml" <<'COMPOSE' +services: + minecraft: + image: itzg/minecraft-server:java21 + env_file: + - server.env + volumes: + - ./data:/data +COMPOSE + cat >"${server_dir}/server.env" <<'ENV' +MOTD="Old MOTD" +DIFFICULTY="easy" +ENV + cat >"${server_dir}/data/server.properties" <<'PROPERTIES' +# original +motd=Old MOTD +PROPERTIES + cat >"${source_dir}/server.properties" <<'PROPERTIES' +# distributed settings +motd=Distributed MOTD +difficulty=hard +custom-setting=preserve-me +PROPERTIES + cat >"${fake_bin}/docker" <<'DOCKER' +#!/usr/bin/env bash +if [[ "$*" == 'compose ps --status running --services' && "${MCSERVER_KIT_TEST_RUNNING:-false}" == true ]]; then + printf 'minecraft\n' +fi +DOCKER + cat >"${fake_bin}/wslpath" <<'WSLPATH' +#!/usr/bin/env bash +printf 'WIN:%s\n' "$2" +WSLPATH + cat >"${fake_bin}/explorer.exe" <<'EXPLORER' +#!/usr/bin/env bash +printf '%s\n' "$1" >"$MCSERVER_KIT_TEST_EXPLORER_LOG" +EXPLORER + chmod +x "${fake_bin}/docker" "${fake_bin}/wslpath" "${fake_bin}/explorer.exe" + + : >"${source_dir}/invalid.properties" + assert_fails 'invalid property input is rejected before migration' \ + env PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/invalid.properties" + assert_equal 'absent' "$(! grep -q '^MAX_PLAYERS=' "${server_dir}/server.env" && printf absent)" 'invalid import does not migrate server settings' + + assert_fails 'import refuses to replace properties while the server is running' \ + env PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + MCSERVER_KIT_TEST_RUNNING=true bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/server.properties" + assert_equal 'Old MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'running server keeps the original properties' + + output="$(PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/server.properties")" + assert_equal 'Distributed MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'import copies the supplied server.properties' + assert_equal 'false' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'import disables property overrides for this server' + assert_equal 'present' "$(grep -q 'custom-setting=preserve-me' "${server_dir}/data/server.properties" && printf present)" 'import keeps custom settings' + assert_equal 'present' "$(grep -q 'motd=Old MOTD' "${server_dir}"/data/server.properties.mcserver-kit.*.bak && printf present)" 'import backs up the previous properties' + assert_equal 'present' "$(grep -q 'Previous server.properties backup' <<<"$output" && printf present)" 'import reports the backup location' + + python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" migrate "$server_dir" + assert_equal 'false' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'later migration preserves manual property mode' + python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-set "${server_dir}/data/server.properties" MOTD 'Edited MOTD' + assert_equal 'Edited MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'manual property editor writes the data file' + assert_equal 'present' "$(grep -q 'custom-setting=preserve-me' "${server_dir}/data/server.properties" && printf present)" 'manual property editor preserves unrelated entries' + + cat >"${fake_bin}/whiptail" <<'WHIPTAIL' +#!/usr/bin/env bash +case " $* " in + *' --inputbox '*) printf 'TUI MOTD' >&2 ;; + *' --yesno '*) exit 1 ;; + *' --menu '*) + if [[ -f "$MCSERVER_KIT_TEST_WHIPTAIL_STATE" ]]; then + printf '__exit' >&2 + else + : >"$MCSERVER_KIT_TEST_WHIPTAIL_STATE" + printf 'MOTD' >&2 + fi + ;; + *) exit 0 ;; +esac +WHIPTAIL + chmod +x "${fake_bin}/whiptail" + PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en \ + MCSERVER_KIT_TEST_WHIPTAIL_STATE="${temp_dir}/property-import/whiptail-state" \ + bash "${REPO_ROOT}/libexec/mcserver-kit/server-properties-tui.sh" alpha "$server_dir" >/dev/null + assert_equal 'TUI MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'the TUI edits imported properties directly' + assert_equal 'Old MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'manual property edits do not rewrite the old environment value' + + PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + MCSERVER_KIT_TEST_EXPLORER_LOG="${temp_dir}/property-import/explorer.log" \ + bash "${REPO_ROOT}/mcserver-kit" server alpha open data + assert_equal "WIN:${server_dir}/data" "$(<"${temp_dir}/property-import/explorer.log")" 'open data passes the persistent folder to Explorer' +} + main() { TEST_TEMP_DIR="$(mktemp -d)" trap cleanup EXIT @@ -695,6 +800,7 @@ main() { test_config_value_editor "$TEST_TEMP_DIR" test_server_management "$TEST_TEMP_DIR" test_server_property_editor "$TEST_TEMP_DIR" + test_property_import_and_explorer "$TEST_TEMP_DIR" test_home_dashboard "$TEST_TEMP_DIR" printf 'PASS: %d specification tests, %d skipped\n' "$tests_run" "$tests_skipped" From 760473d440e3c8bc3d39a4e2205352d4fcd3aae9 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:37:12 +0900 Subject: [PATCH 02/11] docs: explain properties import and Explorer access --- README-JA.md | 9 ++++++++- README.md | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README-JA.md b/README-JA.md index c9374f7..3d66dc9 100644 --- a/README-JA.md +++ b/README-JA.md @@ -170,6 +170,9 @@ mcserver-kit server logs mcserver-kit server logs --no-follow mcserver-kit server down mcserver-kit server properties +mcserver-kit server import-properties /path/to/server.properties +mcserver-kit server open data +mcserver-kit server open server ``` `stop`と`shutdown`はコンテナを削除せず停止します。`down`はコンテナとネットワークを削除します。いずれもサーバーの`data/`は削除しません。 @@ -184,7 +187,11 @@ mcserver-kit server properties MOTD、難易度、ゲームモード、最大人数、オンラインモード、ホワイトリスト、OP、飛行、コマンドブロック、PvP、描画・シミュレーション距離、スポーン保護、ネザー、Mob/NPC生成、リソースパックなどを編集できます。 -ツールが管理する設定の正本は各サーバーの`server.env`です。Docker Composeが値を`itzg/minecraft-server`へ渡し、コンテナ起動時に`server.properties`へ反映します。 +通常、ツールが管理する設定の正本は各サーバーの`server.env`です。Docker Composeが値を`itzg/minecraft-server`へ渡し、コンテナ起動時に`server.properties`へ反映します。 + +配布された`server.properties`を使う場合は、サーバーを停止してホーム画面の「server.propertiesをインポート」を選ぶか、`mcserver-kit server import-properties /path/to/server.properties`を実行します。Windowsダイアログを有効にしているWSL環境では、ホーム画面からWindowsのファイル選択画面を開けます。既存ファイルは置換前にバックアップされます。インポートしたサーバーだけ、プロパティ設定の正本が`data/server.properties`に切り替わり、設定画面もそのファイルを読み書きします。ホワイトリストとOPのメンバー一覧は引き続き`server.env`で管理します。起動中にファイルを直接編集しないでください。 + +ホーム画面からサーバーフォルダ、または永続データの`data/`をWindowsのExplorerで開けます。コマンドでは`mcserver-kit server open server`または`open data`です。WSLとExplorerの連携が必要です。 古い形式のサーバーを初めて開く場合は、移行前に確認画面を表示します。元のComposeは`compose.yaml.mcserver-kit.bak`として保存します。 diff --git a/README.md b/README.md index 6984bb6..6f7b3a2 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,9 @@ mcserver-kit server logs mcserver-kit server logs --no-follow mcserver-kit server down mcserver-kit server properties +mcserver-kit server import-properties /path/to/server.properties +mcserver-kit server open data +mcserver-kit server open server ``` `stop` and `shutdown` stop the container without removing it. `down` removes the container and network. These commands do not delete the server's `data/` directory. @@ -183,7 +186,11 @@ mcserver-kit server properties The editor covers MOTD, difficulty, game mode, player limit, online mode, whitelist, operators, flight, command blocks, PvP, view and simulation distance, spawn protection, Nether and entity spawning, and resource packs. -`server.env` is the source of truth for settings managed by the toolkit. Docker Compose passes these values to `itzg/minecraft-server`, which applies them to `server.properties` when the container starts. +Normally, `server.env` is the source of truth for settings managed by the toolkit. Docker Compose passes these values to `itzg/minecraft-server`, which applies them to `server.properties` when the container starts. + +For a distributed `server.properties`, stop the server and choose **Import server.properties** in the dashboard, or run `mcserver-kit server import-properties /path/to/server.properties`. On WSL, the dashboard offers a Windows file picker when Windows dialogs are enabled. The existing file is backed up before replacement. Only that server switches to using `data/server.properties` as the source of truth for property settings; the editor then reads and writes that file. Whitelist and OP member lists remain in `server.env`. Do not edit the file while the server is running. + +The dashboard can also open the server folder or its persistent `data/` folder in Windows Explorer. From a terminal, use `mcserver-kit server open server` or `open data`. This requires WSL interop with Explorer. When an older server is opened for the first time, the editor asks before migrating it. The original Compose file is saved as `compose.yaml.mcserver-kit.bak`. From ef63c7cacc585834bd86f00b5a712329174b8686 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:38:39 +0900 Subject: [PATCH 03/11] chore: prepare v1.1.2 release branch --- .github/workflows/shell-tests.yml | 1 + VERSION | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/shell-tests.yml b/.github/workflows/shell-tests.yml index cf00f0e..ef28ab6 100644 --- a/.github/workflows/shell-tests.yml +++ b/.github/workflows/shell-tests.yml @@ -9,6 +9,7 @@ on: branches: - main - develop + - 'release/**' permissions: contents: read diff --git a/VERSION b/VERSION index 524cb55..45a1b3f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.1 +1.1.2 From e517dc59f5cdf561568152e1a7e693442fefe49c Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:58:56 +0900 Subject: [PATCH 04/11] fix: satisfy ShellCheck for Explorer availability --- libexec/mcserver-kit/server-manager.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libexec/mcserver-kit/server-manager.sh b/libexec/mcserver-kit/server-manager.sh index cb61a85..94ed1d1 100755 --- a/libexec/mcserver-kit/server-manager.sh +++ b/libexec/mcserver-kit/server-manager.sh @@ -132,7 +132,9 @@ open_folder() { *) die "$(tr server.open_usage)" ;; esac [[ -d "$target" ]] || die "$(tr server.open_missing "$target")" - command -v wslpath >/dev/null 2>&1 && command -v explorer.exe >/dev/null 2>&1 || die "$(tr server.explorer_unavailable)" + if ! command -v wslpath >/dev/null 2>&1 || ! command -v explorer.exe >/dev/null 2>&1; then + die "$(tr server.explorer_unavailable)" + fi windows_path="$(wslpath -w "$target")" || die "$(tr server.explorer_unavailable)" explorer.exe "$windows_path" } From 0edecc811c0a646f550608100744ac248e7c8ad4 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:01:26 +0900 Subject: [PATCH 05/11] test: derive installed version expectations from VERSION --- tests/run-tests.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/run-tests.sh b/tests/run-tests.sh index a132e0d..cf8b78d 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -379,7 +379,9 @@ test_local_installation() { local install_log="${temp_dir}/install/install.log" local shell_rc="${temp_dir}/install/bashrc" local update_cache="${temp_dir}/install/cache" - local installed_help + local installed_help repo_version + + repo_version="$(head -n 1 "${REPO_ROOT}/VERSION")" mkdir -p "$fake_bin" ln -s /usr/bin/true "${fake_bin}/unzip" @@ -394,7 +396,7 @@ test_local_installation() { assert_equal 'present' "$([[ -x "${bin_dir}/mcserver-kit" ]] && printf present)" 'the installer creates the launcher' installed_help="$("${bin_dir}/mcserver-kit" --help)" assert_equal 'present' "$(grep -q 'mcserver-kit setup' <<<"$installed_help" && printf present)" 'the installed launcher exposes subcommand help' - assert_equal '1.1.1' "$("${bin_dir}/mcserver-kit" --version | awk '{print $2}')" 'the installed launcher reports its version' + assert_equal "$repo_version" "$("${bin_dir}/mcserver-kit" --version | awk '{print $2}')" 'the installed launcher reports its version' assert_equal 'present' "$([[ -f "${config_dir}/config.yml" ]] && printf present)" 'the installer creates the initial config' assert_equal 'present' "$([[ -f "${install_dir}/libexec/mcserver-kit/windows-dialog.ps1" ]] && printf present)" 'the installer includes the Windows dialog helper' assert_equal 'present' "$([[ -x "${install_dir}/libexec/mcserver-kit/lang.sh" ]] && printf present)" 'the installer includes the language command' @@ -408,7 +410,7 @@ test_local_installation() { assert_equal 'en' "$(cat "${config_dir}/language")" 'the installer defaults to English' assert_equal '1' "$(grep -Fxc '# >>> mcserver-kit PATH >>>' "$shell_rc")" 'the installer registers one managed PATH block' assert_equal 'present' "$(grep -q 'mcserver-kit setup' "$install_log" && printf present)" 'the installer instructs the user to run setup' - assert_equal 'present' "$(grep -q 'mcserver-kit 1.1.1' "$install_log" && printf present)" 'the installer shows the installed version' + assert_equal 'present' "$(grep -Fq "mcserver-kit ${repo_version}" "$install_log" && printf present)" 'the installer shows the installed version' assert_equal 'absent' "$(! grep -q '初回セットアップを開始' "$install_log" && printf absent)" 'the installer does not start setup automatically' printf '\n# preserve-on-update\n' >>"${config_dir}/config.yml" From 5d70375e80e6c50ab75d8a9c51888e956c0909ec Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:12:38 +0900 Subject: [PATCH 06/11] fix: clear stale terminal output between TUI screens --- libexec/mcserver-kit/home-tui.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libexec/mcserver-kit/home-tui.sh b/libexec/mcserver-kit/home-tui.sh index a1cfe6b..a47c063 100755 --- a/libexec/mcserver-kit/home-tui.sh +++ b/libexec/mcserver-kit/home-tui.sh @@ -116,6 +116,9 @@ dashboard_text() { pause_for_enter() { printf '\n%s' "$(tr home.press_enter)" read -r _ || true + # whiptail restores the underlying terminal screen when a dialog closes. + # Do not leave output from the previous CLI action beneath the next menu. + clear } run_and_show() { @@ -240,7 +243,7 @@ diagnostics() { } main() { - local root choice + local root choice installed_version command -v whiptail >/dev/null 2>&1 || { tr tui.missing >&2 exit 1 @@ -276,7 +279,11 @@ main() { update) clear if "${SCRIPT_DIR}/update.sh"; then - exec "${ROOT_DIR}/mcserver-kit" home + installed_version="$(head -n 1 "${ROOT_DIR}/VERSION" 2>/dev/null || printf unknown)" + if [[ "$installed_version" != "$VERSION" ]]; then + clear + exec "${ROOT_DIR}/mcserver-kit" home + fi fi pause_for_enter ;; From cbbb743b50ca92025a70945385f35ab84faf1688 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:13:07 +0900 Subject: [PATCH 07/11] test: cover up-to-date TUI screen transition --- tests/run-tests.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/run-tests.sh b/tests/run-tests.sh index cf8b78d..44ec56b 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -476,6 +476,51 @@ CURL assert_equal 'present' "$(grep -q 'Update available: v1.1.1' "$whiptail_log" && printf present)" 'the dashboard announces a newer cached release' } +test_home_screen_transition_after_current_update() { + local temp_dir="$1" repo_version + local config_file="${temp_dir}/home-transition/config.yml" + local fake_bin="${temp_dir}/home-transition/bin" + local event_log="${temp_dir}/home-transition/events.log" + local menu_count="${temp_dir}/home-transition/menu-count" + local output + repo_version="$(head -n 1 "${REPO_ROOT}/VERSION")" + mkdir -p "$fake_bin" + cat >"$config_file" <"${fake_bin}/whiptail" <<'WHIPTAIL' +#!/usr/bin/env bash +printf 'menu\n' >>"$MCSERVER_KIT_TEST_EVENTS" +if [[ ! -f "$MCSERVER_KIT_TEST_MENU_COUNT" ]]; then + : >"$MCSERVER_KIT_TEST_MENU_COUNT" + printf 'update' >&2 +else + printf 'exit' >&2 +fi +WHIPTAIL + cat >"${fake_bin}/clear" <<'CLEAR' +#!/usr/bin/env bash +printf 'clear\n' >>"$MCSERVER_KIT_TEST_EVENTS" +CLEAR + cat >"${fake_bin}/curl" <<'CURL' +#!/usr/bin/env bash +printf '%s' "$MCSERVER_KIT_TEST_LATEST_URL" +CURL + chmod +x "${fake_bin}/whiptail" "${fake_bin}/clear" "${fake_bin}/curl" + + output="$(printf '\n' | PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en \ + MCSERVER_KIT_CONFIG="$config_file" MCSERVER_KIT_TUI_TEST=true \ + MCSERVER_KIT_CURRENT_VERSION="$repo_version" \ + MCSERVER_KIT_TEST_LATEST_URL="https://github.com/cotore-game/mcserver-compose-kit/releases/tag/v${repo_version}" \ + MCSERVER_KIT_UPDATE_CACHE_DIR="${temp_dir}/home-transition/cache" \ + MCSERVER_KIT_TEST_EVENTS="$event_log" MCSERVER_KIT_TEST_MENU_COUNT="$menu_count" \ + bash "${REPO_ROOT}/mcserver-kit" home)" + assert_equal 'present' "$(grep -Fq "v${repo_version} is up to date." <<<"$output" && printf present)" 'up-to-date status is shown before returning to the home menu' + assert_equal 'present' "$(grep -Fq 'Press Enter to return' <<<"$output" && printf present)" 'up-to-date status waits for Enter instead of restarting the TUI' + assert_equal $'menu\nclear\nclear\nmenu' "$(<"$event_log")" 'terminal output is cleared before the next TUI menu' +} + test_setup_command() { local temp_dir="$1" local config_file="${temp_dir}/setup/config.yml" @@ -804,6 +849,7 @@ main() { test_server_property_editor "$TEST_TEMP_DIR" test_property_import_and_explorer "$TEST_TEMP_DIR" test_home_dashboard "$TEST_TEMP_DIR" + test_home_screen_transition_after_current_update "$TEST_TEMP_DIR" printf 'PASS: %d specification tests, %d skipped\n' "$tests_run" "$tests_skipped" } From b8a8586616201a3b2b4cb3b28509c5beea2b5ae4 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:09:17 +0900 Subject: [PATCH 08/11] fix: keep imported server properties in server.env --- libexec/mcserver-kit/server-config.py | 38 +++++++++++++++++++++++--- libexec/mcserver-kit/server-manager.sh | 2 +- tests/run-tests.sh | 25 ++++++++++++----- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/libexec/mcserver-kit/server-config.py b/libexec/mcserver-kit/server-config.py index c41ab14..f52d542 100755 --- a/libexec/mcserver-kit/server-config.py +++ b/libexec/mcserver-kit/server-config.py @@ -141,12 +141,45 @@ def validate_properties_source(source: Path) -> None: raise ValueError(f"Not a file: {source}") if source.name != "server.properties": raise ValueError("Select a file named server.properties") - if not read_properties(source): + if not parse_import_properties(source): raise ValueError("The selected file has no property entries") +def parse_import_properties(source: Path) -> dict[str, str]: + """Read the simple key=value format emitted by Minecraft without dropping entries.""" + values: dict[str, str] = {} + for number, line in enumerate(source.read_text(encoding="utf-8-sig").splitlines(), 1): + stripped = line.strip() + if not stripped or stripped.startswith(("#", "!")): + continue + if "=" not in line: + raise ValueError(f"Line {number}: expected key=value") + key, value = line.split("=", 1) + key = key.strip() + if not re.fullmatch(r"[A-Za-z0-9_.-]+", key): + raise ValueError(f"Line {number}: unsupported property key: {key}") + if "\\" in key or "\\" in value: + raise ValueError(f"Line {number}: escaped properties are not supported") + values[key] = value + if values.get("level-name", "world") != "world": + raise ValueError("level-name must be world for this server layout") + if values.get("server-port", "25565") != "25565": + raise ValueError("server-port must be 25565 for this server layout") + return values + + def import_properties(server_dir: Path, source: Path) -> Path | None: validate_properties_source(source) + imported = parse_import_properties(source) + env_path = server_dir / "server.env" + values = read_env(env_path) + for env_key, property_key in PROPERTY_KEYS.items(): + if property_key in imported: + values[env_key] = imported[property_key] + known = set(PROPERTY_KEYS.values()) | {"level-name", "server-port"} + extras = [f"{key}={value}" for key, value in imported.items() if key not in known] + values["CUSTOM_SERVER_PROPERTIES"] = "\n".join(extras) + values["OVERRIDE_SERVER_PROPERTIES"] = "true" destination = server_dir / "data" / "server.properties" destination.parent.mkdir(parents=True, exist_ok=True) @@ -167,9 +200,6 @@ def import_properties(server_dir: Path, source: Path) -> Path | None: with os.fdopen(descriptor, "wb") as output, source.open("rb") as input_file: shutil.copyfileobj(input_file, output) os.chmod(temporary_name, 0o644) - env_path = server_dir / "server.env" - values = read_env(env_path) - values["OVERRIDE_SERVER_PROPERTIES"] = "false" write_env(env_path, values) os.replace(temporary_name, destination) finally: diff --git a/libexec/mcserver-kit/server-manager.sh b/libexec/mcserver-kit/server-manager.sh index 94ed1d1..0e2a1c4 100755 --- a/libexec/mcserver-kit/server-manager.sh +++ b/libexec/mcserver-kit/server-manager.sh @@ -121,7 +121,7 @@ import_properties() { backup="$(python3 "$CONFIG_TOOL" import-properties "$directory" "$source")" || die "$(tr server.import_failed)" compose_in "$directory" config --quiet || die "$(tr properties.compose_invalid)" [[ -z "$backup" ]] || printf '%s\n' "$(tr server.import_backup "$backup")" - printf '%s\n' "$(tr server.import_done "${directory}/data/server.properties")" + printf '%s\n' "$(tr server.import_done "${directory}/server.env")" } open_folder() { diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 44ec56b..49a6d7b 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -755,6 +755,7 @@ PROPERTIES motd=Distributed MOTD difficulty=hard custom-setting=preserve-me +plugin.option=enabled PROPERTIES cat >"${fake_bin}/docker" <<'DOCKER' #!/usr/bin/env bash @@ -778,6 +779,16 @@ EXPLORER bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/invalid.properties" assert_equal 'absent' "$(! grep -q '^MAX_PLAYERS=' "${server_dir}/server.env" && printf absent)" 'invalid import does not migrate server settings' + mkdir -p "${source_dir}/incompatible" + cat >"${source_dir}/incompatible/server.properties" <<'PROPERTIES' +motd=Should not be imported +server-port=25566 +PROPERTIES + assert_fails 'an incompatible server port is rejected before migration' \ + env PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/incompatible/server.properties" + assert_equal 'Old MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'rejected import leaves the environment unchanged' + assert_fails 'import refuses to replace properties while the server is running' \ env PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ MCSERVER_KIT_TEST_RUNNING=true bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/server.properties" @@ -786,16 +797,16 @@ EXPLORER output="$(PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/server.properties")" assert_equal 'Distributed MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'import copies the supplied server.properties' - assert_equal 'false' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'import disables property overrides for this server' + assert_equal 'true' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'import keeps server.env authoritative' + assert_equal 'Distributed MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'import converts managed properties to environment settings' + assert_equal 'hard' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" DIFFICULTY)" 'import updates the difficulty' + assert_equal $'custom-setting=preserve-me\nplugin.option=enabled' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" CUSTOM_SERVER_PROPERTIES)" 'import converts extra properties to CUSTOM_SERVER_PROPERTIES' assert_equal 'present' "$(grep -q 'custom-setting=preserve-me' "${server_dir}/data/server.properties" && printf present)" 'import keeps custom settings' assert_equal 'present' "$(grep -q 'motd=Old MOTD' "${server_dir}"/data/server.properties.mcserver-kit.*.bak && printf present)" 'import backs up the previous properties' assert_equal 'present' "$(grep -q 'Previous server.properties backup' <<<"$output" && printf present)" 'import reports the backup location' python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" migrate "$server_dir" - assert_equal 'false' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'later migration preserves manual property mode' - python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-set "${server_dir}/data/server.properties" MOTD 'Edited MOTD' - assert_equal 'Edited MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'manual property editor writes the data file' - assert_equal 'present' "$(grep -q 'custom-setting=preserve-me' "${server_dir}/data/server.properties" && printf present)" 'manual property editor preserves unrelated entries' + assert_equal 'true' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'later migration preserves environment property mode' cat >"${fake_bin}/whiptail" <<'WHIPTAIL' #!/usr/bin/env bash @@ -817,8 +828,8 @@ WHIPTAIL PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en \ MCSERVER_KIT_TEST_WHIPTAIL_STATE="${temp_dir}/property-import/whiptail-state" \ bash "${REPO_ROOT}/libexec/mcserver-kit/server-properties-tui.sh" alpha "$server_dir" >/dev/null - assert_equal 'TUI MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'the TUI edits imported properties directly' - assert_equal 'Old MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'manual property edits do not rewrite the old environment value' + assert_equal 'TUI MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'the TUI edits the authoritative environment value' + assert_equal 'Distributed MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'the data file changes only when the container applies server.env' PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ MCSERVER_KIT_TEST_EXPLORER_LOG="${temp_dir}/property-import/explorer.log" \ From b1df1c97a633beed080677790f51d3481689db7c Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:09:17 +0900 Subject: [PATCH 09/11] ui: move property import into server settings --- README-JA.md | 2 +- README.md | 2 +- libexec/mcserver-kit/home-tui.sh | 8 ------- libexec/mcserver-kit/server-properties-tui.sh | 21 +++++++++++++++++++ share/mcserver-kit/locales/en.json | 5 +++-- share/mcserver-kit/locales/ja.json | 5 +++-- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/README-JA.md b/README-JA.md index 3d66dc9..4fea211 100644 --- a/README-JA.md +++ b/README-JA.md @@ -189,7 +189,7 @@ MOTD、難易度、ゲームモード、最大人数、オンラインモード 通常、ツールが管理する設定の正本は各サーバーの`server.env`です。Docker Composeが値を`itzg/minecraft-server`へ渡し、コンテナ起動時に`server.properties`へ反映します。 -配布された`server.properties`を使う場合は、サーバーを停止してホーム画面の「server.propertiesをインポート」を選ぶか、`mcserver-kit server import-properties /path/to/server.properties`を実行します。Windowsダイアログを有効にしているWSL環境では、ホーム画面からWindowsのファイル選択画面を開けます。既存ファイルは置換前にバックアップされます。インポートしたサーバーだけ、プロパティ設定の正本が`data/server.properties`に切り替わり、設定画面もそのファイルを読み書きします。ホワイトリストとOPのメンバー一覧は引き続き`server.env`で管理します。起動中にファイルを直接編集しないでください。 +配布された`server.properties`を使う場合は、サーバーを停止して「サーバー設定 → server.propertiesをインポート」を選ぶか、`mcserver-kit server import-properties /path/to/server.properties`を実行します。Windowsダイアログを有効にしているWSL環境では、設定画面からWindowsのファイル選択画面を開けます。既存のデータファイルは置換前にバックアップします。対応する項目を`server.env`へ取り込み、その他のキーは`CUSTOM_SERVER_PROPERTIES`へ保存します。設定の正本は引き続き`server.env`で、元ファイルも`data/server.properties`へコピーします。ワールド名やポートがこのサーバー構成と異なる場合、または未対応の書式の場合は黙って変更せずエラーにします。サーバー起動中にファイルを直接編集しないでください。 ホーム画面からサーバーフォルダ、または永続データの`data/`をWindowsのExplorerで開けます。コマンドでは`mcserver-kit server open server`または`open data`です。WSLとExplorerの連携が必要です。 diff --git a/README.md b/README.md index 6f7b3a2..da3563c 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ The editor covers MOTD, difficulty, game mode, player limit, online mode, whitel Normally, `server.env` is the source of truth for settings managed by the toolkit. Docker Compose passes these values to `itzg/minecraft-server`, which applies them to `server.properties` when the container starts. -For a distributed `server.properties`, stop the server and choose **Import server.properties** in the dashboard, or run `mcserver-kit server import-properties /path/to/server.properties`. On WSL, the dashboard offers a Windows file picker when Windows dialogs are enabled. The existing file is backed up before replacement. Only that server switches to using `data/server.properties` as the source of truth for property settings; the editor then reads and writes that file. Whitelist and OP member lists remain in `server.env`. Do not edit the file while the server is running. +For a distributed `server.properties`, stop the server and choose **Server settings → Import server.properties**, or run `mcserver-kit server import-properties /path/to/server.properties`. On WSL, the settings screen offers a Windows file picker when Windows dialogs are enabled. The existing data file is backed up before replacement. Supported properties are imported into `server.env`; additional keys are stored in `CUSTOM_SERVER_PROPERTIES`. `server.env` remains the source of truth, and the imported file is also copied to `data/server.properties`. Imports with a nonstandard world name, port, or unsupported property syntax are rejected rather than silently changing the server layout. Do not edit the file while the server is running. The dashboard can also open the server folder or its persistent `data/` folder in Windows Explorer. From a terminal, use `mcserver-kit server open server` or `open data`. This requires WSL interop with Explorer. diff --git a/libexec/mcserver-kit/home-tui.sh b/libexec/mcserver-kit/home-tui.sh index a47c063..8988f0e 100755 --- a/libexec/mcserver-kit/home-tui.sh +++ b/libexec/mcserver-kit/home-tui.sh @@ -143,7 +143,6 @@ server_action_menu() { status "$(tr home.status)" \ logs "$(tr home.logs)" \ properties "$(tr home.properties)" \ - import-properties "$(tr home.import_properties)" \ open-data "$(tr home.open_data)" \ open-server "$(tr home.open_server)" \ down "$(tr home.down)" \ @@ -161,13 +160,6 @@ server_action_menu() { properties) "${SCRIPT_DIR}/server-manager.sh" server "$id" properties || true ;; - import-properties) - if whiptail --yesno "$(tr home.import_confirm "$id")" 12 76; then - clear - "${SCRIPT_DIR}/server-manager.sh" server "$id" import-properties || true - pause_for_enter - fi - ;; open-data | open-server) new_temp_file output if [[ "$choice" == open-data ]]; then diff --git a/libexec/mcserver-kit/server-properties-tui.sh b/libexec/mcserver-kit/server-properties-tui.sh index 9d67445..2622eab 100755 --- a/libexec/mcserver-kit/server-properties-tui.sh +++ b/libexec/mcserver-kit/server-properties-tui.sh @@ -220,6 +220,25 @@ finish() { fi } +import_properties() { + local output result + whiptail --yesno "$(tr home.import_confirm "$SERVER_ID")" 12 76 || return + output="$(mktemp)" + if "${SCRIPT_DIR}/server-manager.sh" server "$SERVER_ID" import-properties >"$output" 2>&1; then + if [[ -s "$output" ]]; then + whiptail --title "$(tr properties.import)" --textbox "$output" 16 82 + manual_properties=false + changed=true + fi + else + result=$? + whiptail --title "$(tr common.error)" --textbox "$output" 16 82 + rm -f -- "$output" + return "$result" + fi + rm -f -- "$output" +} + main() { local selected local items @@ -260,11 +279,13 @@ main() { SIMULATION_DISTANCE "$(menu_item "$(tr properties.simulation_distance)" "$(setting_get SIMULATION_DISTANCE 10)")" __more "$(tr properties.more_settings)" __resource "$(tr properties.resource_pack_settings)" + __import "$(tr properties.import)" __exit "$(tr properties.exit)" ) selected="$(whiptail --title "${SERVER_ID}" --menu "$(tr properties.choose)" 25 94 18 "${items[@]}" 3>&1 1>&2 2>&3)" || break case "$selected" in __exit) break ;; + __import) import_properties || true ;; __more) selected="$(whiptail --title "${SERVER_ID}" --menu "$(tr properties.more_settings)" 24 90 15 \ FORCE_GAMEMODE "$(menu_item "$(tr properties.force_gamemode)" "$(setting_get FORCE_GAMEMODE false)")" \ diff --git a/share/mcserver-kit/locales/en.json b/share/mcserver-kit/locales/en.json index ccc1a89..4618302 100644 --- a/share/mcserver-kit/locales/en.json +++ b/share/mcserver-kit/locales/en.json @@ -41,7 +41,7 @@ "home.logs": "Follow logs", "home.properties": "Server settings", "home.import_properties": "Import server.properties", - "home.import_confirm": "Import server.properties for %s? Stop the server first. The current file will be backed up, and this server will use data/server.properties for property settings.", + "home.import_confirm": "Import server.properties for %s? Stop the server first. The current file will be backed up. Imported settings will be managed in server.env.", "home.open_data": "Open persistent data in Explorer", "home.open_server": "Open server folder in Explorer", "home.down": "Remove containers", @@ -142,7 +142,7 @@ "server.import_source_missing": "File not found: %s", "server.import_failed": "Could not import server.properties.", "server.import_backup": "Previous server.properties backup: %s", - "server.import_done": "Imported to %s. This server now uses data/server.properties as the source of truth for property settings.", + "server.import_done": "Imported successfully. Property settings are managed in %s; the original file was copied to data/server.properties.", "server.open_usage": "Usage: mcserver-kit server SERVER_ID open [data|server]", "server.open_missing": "Folder not found: %s", "server.explorer_unavailable": "Windows Explorer is unavailable. This action requires WSL interop and wslpath.", @@ -177,6 +177,7 @@ "properties.current": "%s: %s", "properties.exit": "Save and exit", "properties.choose": "Choose a setting to edit", + "properties.import": "Import server.properties", "properties.selected": "(current)", "properties.motd": "Message of the day (MOTD)", "properties.max_players": "Maximum players", diff --git a/share/mcserver-kit/locales/ja.json b/share/mcserver-kit/locales/ja.json index a322c55..94fdf86 100644 --- a/share/mcserver-kit/locales/ja.json +++ b/share/mcserver-kit/locales/ja.json @@ -41,7 +41,7 @@ "home.logs": "ログを表示", "home.properties": "サーバー設定", "home.import_properties": "server.propertiesをインポート", - "home.import_confirm": "%sにserver.propertiesをインポートしますか?先にサーバーを停止してください。既存ファイルをバックアップし、このサーバーのプロパティ設定はdata/server.propertiesを正本に切り替えます。", + "home.import_confirm": "%sにserver.propertiesをインポートしますか?先にサーバーを停止してください。既存ファイルをバックアップし、インポートした設定はserver.envで管理します。", "home.open_data": "永続データをExplorerで開く", "home.open_server": "サーバーフォルダをExplorerで開く", "home.down": "コンテナを削除", @@ -142,7 +142,7 @@ "server.import_source_missing": "ファイルが見つかりません: %s", "server.import_failed": "server.propertiesをインポートできませんでした。", "server.import_backup": "元のserver.propertiesのバックアップ: %s", - "server.import_done": "%sにインポートしました。このサーバーのプロパティ設定はdata/server.propertiesが正本になります。", + "server.import_done": "インポートしました。プロパティ設定は%sで管理し、元ファイルはdata/server.propertiesにコピーしました。", "server.open_usage": "使い方: mcserver-kit server サーバーID open [data|server]", "server.open_missing": "フォルダが見つかりません: %s", "server.explorer_unavailable": "Windows Explorerを利用できません。WSL連携とwslpathが必要です。", @@ -177,6 +177,7 @@ "properties.current": "%s: %s", "properties.exit": "保存して終了", "properties.choose": "編集する設定を選択してください", + "properties.import": "server.propertiesをインポート", "properties.selected": "(現在値)", "properties.motd": "サーバー説明(MOTD)", "properties.max_players": "最大プレイヤー数", From b006ef523aa3d8306d6a60510f71242af2e84e3f Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:15:31 +0900 Subject: [PATCH 10/11] fix: keep server operation errors visible in the TUI --- libexec/mcserver-kit/server-manager.sh | 13 +++++- libexec/mcserver-kit/server-properties-tui.sh | 39 ++++++++++++---- tests/run-tests.sh | 46 +++++++++++++++++++ 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/libexec/mcserver-kit/server-manager.sh b/libexec/mcserver-kit/server-manager.sh index 0e2a1c4..0c24749 100755 --- a/libexec/mcserver-kit/server-manager.sh +++ b/libexec/mcserver-kit/server-manager.sh @@ -125,7 +125,7 @@ import_properties() { } open_folder() { - local directory="$1" part="${2:-data}" target windows_path + local directory="$1" part="${2:-data}" target windows_path output status case "$part" in data) target="${directory}/data" ;; server) target="$directory" ;; @@ -136,7 +136,16 @@ open_folder() { die "$(tr server.explorer_unavailable)" fi windows_path="$(wslpath -w "$target")" || die "$(tr server.explorer_unavailable)" - explorer.exe "$windows_path" + # Explorer can return 1 after handing the folder to an existing window. + # Only accept that status when it supplied no error diagnostics. + if output="$(explorer.exe "$windows_path" 2>&1)"; then + return 0 + else + status=$? + fi + [[ "$status" == 1 && -z "$output" ]] && return 0 + [[ -z "$output" ]] || printf '%s\n' "$output" >&2 + die "$(tr server.explorer_unavailable)" } manage_server() { diff --git a/libexec/mcserver-kit/server-properties-tui.sh b/libexec/mcserver-kit/server-properties-tui.sh index 2622eab..79052b2 100755 --- a/libexec/mcserver-kit/server-properties-tui.sh +++ b/libexec/mcserver-kit/server-properties-tui.sh @@ -37,10 +37,30 @@ actcheckbox=white,blue fi die() { - printf '%s: %s\n' "$(tr common.error)" "$*" >&2 + if command -v whiptail >/dev/null 2>&1; then + whiptail --title "$(tr common.error)" --msgbox "$*" 14 82 || true + else + printf '%s: %s\n' "$(tr common.error)" "$*" >&2 + fi exit 1 } +run_checked() { + local output status + output="$(mktemp)" + if "$@" >"$output" 2>&1; then + [[ ! -s "$output" ]] || whiptail --title "$SERVER_ID" --textbox "$output" 22 84 || true + rm -f -- "$output" + else + status=$? + # Preserve Docker's actual diagnostic until the user dismisses the dialog. + [[ -s "$output" ]] || printf '%s (exit %s)\n' "$1" "$status" >"$output" + whiptail --title "$(tr common.error)" --textbox "$output" 22 84 || true + rm -f -- "$output" + exit "$status" + fi +} + setting_get() { if [[ "$manual_properties" == true ]]; then case "$1" in @@ -210,13 +230,13 @@ menu_item() { } finish() { - [[ "$changed" == true ]] || return - (cd "$SERVER_DIR" && docker compose config --quiet) || die "$(tr properties.compose_invalid)" + [[ "$changed" == true ]] || return 0 + cd -- "$SERVER_DIR" + run_checked docker compose config --quiet if whiptail --yesno "$(tr properties.restart_prompt "$SERVER_ID")" 10 72; then - printf '%s\n' "$(tr properties.applying "$SERVER_ID")" - (cd "$SERVER_DIR" && docker compose up -d --force-recreate) + run_checked docker compose up -d --force-recreate else - printf '%s\n' "$(tr properties.saved_restart_later)" + whiptail --msgbox "$(tr properties.saved_restart_later)" 10 76 || true fi } @@ -246,9 +266,9 @@ main() { [[ -f "${SERVER_DIR}/compose.yaml" ]] || die "$(tr server.compose_missing "$SERVER_ID")" if [[ ! -f "$SERVER_ENV" ]]; then - whiptail --yesno "$(tr properties.migration_prompt)" 11 76 || return + whiptail --yesno "$(tr properties.migration_prompt)" 11 76 || return 0 fi - python3 "$CONFIG_TOOL" migrate "$SERVER_DIR" + run_checked python3 "$CONFIG_TOOL" migrate "$SERVER_DIR" if [[ "$(python3 "$CONFIG_TOOL" get "$SERVER_ENV" OVERRIDE_SERVER_PROPERTIES true)" == false ]]; then [[ -f "$SERVER_PROPERTIES" ]] || die "$(tr properties.file_missing)" local running @@ -258,7 +278,8 @@ main() { fi manual_properties=true fi - (cd "$SERVER_DIR" && docker compose config --quiet) || die "$(tr properties.compose_invalid)" + cd -- "$SERVER_DIR" + run_checked docker compose config --quiet while true; do items=( diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 49a6d7b..18cf47d 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -762,6 +762,10 @@ PROPERTIES if [[ "$*" == 'compose ps --status running --services' && "${MCSERVER_KIT_TEST_RUNNING:-false}" == true ]]; then printf 'minecraft\n' fi +if [[ "${MCSERVER_KIT_TEST_DOCKER_MISSING:-false}" == true ]]; then + printf "The command 'docker' could not be found in this WSL 2 distro.\n" >&2 + exit 127 +fi DOCKER cat >"${fake_bin}/wslpath" <<'WSLPATH' #!/usr/bin/env bash @@ -770,6 +774,8 @@ WSLPATH cat >"${fake_bin}/explorer.exe" <<'EXPLORER' #!/usr/bin/env bash printf '%s\n' "$1" >"$MCSERVER_KIT_TEST_EXPLORER_LOG" +[[ -z "${MCSERVER_KIT_TEST_EXPLORER_ERROR:-}" ]] || printf '%s\n' "$MCSERVER_KIT_TEST_EXPLORER_ERROR" >&2 +exit "${MCSERVER_KIT_TEST_EXPLORER_STATUS:-0}" EXPLORER chmod +x "${fake_bin}/docker" "${fake_bin}/wslpath" "${fake_bin}/explorer.exe" @@ -835,6 +841,46 @@ WHIPTAIL MCSERVER_KIT_TEST_EXPLORER_LOG="${temp_dir}/property-import/explorer.log" \ bash "${REPO_ROOT}/mcserver-kit" server alpha open data assert_equal "WIN:${server_dir}/data" "$(<"${temp_dir}/property-import/explorer.log")" 'open data passes the persistent folder to Explorer' + + for part in data server; do + output="$(PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + MCSERVER_KIT_TEST_EXPLORER_LOG="${temp_dir}/property-import/explorer.log" \ + MCSERVER_KIT_TEST_EXPLORER_STATUS=1 \ + bash "${REPO_ROOT}/mcserver-kit" server alpha open "$part")" + assert_equal '' "$output" "Explorer silent status 1 succeeds for $part without an error dialog" + done + assert_fails 'Explorer failure with diagnostics remains an error' \ + env PATH="${fake_bin}:$PATH" MCSERVER_KIT_CONFIG="$config_file" \ + MCSERVER_KIT_TEST_EXPLORER_LOG="${temp_dir}/property-import/explorer.log" \ + MCSERVER_KIT_TEST_EXPLORER_STATUS=1 MCSERVER_KIT_TEST_EXPLORER_ERROR='Access denied' \ + bash "${REPO_ROOT}/mcserver-kit" server alpha open data + assert_fails 'Explorer execution failure remains an error' \ + env PATH="${fake_bin}:$PATH" MCSERVER_KIT_CONFIG="$config_file" \ + MCSERVER_KIT_TEST_EXPLORER_LOG="${temp_dir}/property-import/explorer.log" \ + MCSERVER_KIT_TEST_EXPLORER_STATUS=126 \ + bash "${REPO_ROOT}/mcserver-kit" server alpha open server + + cat >"${fake_bin}/whiptail" <<'WHIPTAIL' +#!/usr/bin/env bash +while (($#)); do + case "$1" in + --textbox) cat "$2" >"$MCSERVER_KIT_TEST_DIALOG_LOG"; exit 0 ;; + --menu) printf '__exit' >&2; exit 0 ;; + esac + shift +done +WHIPTAIL + local result=0 + output="$(PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en \ + MCSERVER_KIT_TEST_DOCKER_MISSING=true \ + MCSERVER_KIT_TEST_DIALOG_LOG="${temp_dir}/property-import/dialog.log" \ + bash "${REPO_ROOT}/libexec/mcserver-kit/server-properties-tui.sh" alpha "$server_dir" 2>&1)" || result=$? + assert_equal 127 "$result" 'properties preserves the Docker failure exit code' + assert_equal '' "$output" 'Docker errors do not flash on the underlying terminal' + assert_equal "The command 'docker' could not be found in this WSL 2 distro." "$(<"${temp_dir}/property-import/dialog.log")" 'Docker diagnostic is presented in a persistent TUI textbox' + PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en \ + bash "${REPO_ROOT}/libexec/mcserver-kit/server-properties-tui.sh" alpha "$server_dir" + assert_equal 0 "$?" 'closing properties without changes succeeds' } main() { From 326b912cd4885a0a1b2830937841514a37b40392 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:57:05 +0900 Subject: [PATCH 11/11] chore: keep text files LF across platforms --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..26edebd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# This WSL-first project keeps text files in LF format on every platform. +* text=auto eol=lf