From 72e5756d9eda115e1dedb0e30b613b5dd8c7089f Mon Sep 17 00:00:00 2001 From: Flames Date: Tue, 28 Oct 2025 09:15:09 +0800 Subject: [PATCH 1/9] feat: Enhance copy_env_file to support directory and destination path - Allow copy_env_file to copy directories. - Support specifying a destination path. - Add a message for missing source files. --- bin/lib/runtime.sh | 50 ++++++++++++++++++++++++++++++++++++---------- bin/messages.sh | 10 ++++++++-- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/bin/lib/runtime.sh b/bin/lib/runtime.sh index 19aad18..e518882 100644 --- a/bin/lib/runtime.sh +++ b/bin/lib/runtime.sh @@ -3178,12 +3178,45 @@ git_at_path() { } copy_env_file() { - local file_name="$1" + local entry="$1" local target_dir="$2" - if [ -f "$PROJECT_DIR_ABS/$file_name" ]; then - cp "$PROJECT_DIR_ABS/$file_name" "$target_dir/" - info "$(msg copy_env_file "$file_name")" + local source_rel="$entry" + local dest_rel="" + + if [[ "$entry" == *:* ]]; then + source_rel="${entry%%:*}" + dest_rel="${entry#*:}" + fi + + if [ -z "$dest_rel" ]; then + dest_rel="$source_rel" fi + + local source_path="$PROJECT_DIR_ABS/$source_rel" + local dest_path="$target_dir/$dest_rel" + + if [ -d "$source_path" ]; then + mkdir -p "$dest_path" + if command -v rsync > /dev/null 2>&1; then + rsync -a "$source_path/" "$dest_path/" + else + cp -R "$source_path"/. "$dest_path/" + fi + info "$(msg copy_env_file "$source_rel" "$dest_rel")" + return + fi + + if [ -f "$source_path" ]; then + local dest_dir + dest_dir=$(dirname "$dest_path") + mkdir -p "$dest_dir" + dest_path=${dest_path%/} + cp "$source_path" "$dest_path" + info "$(msg copy_env_file "$source_rel" "$dest_rel")" + return + fi + + info "$(msg copy_env_missing "$source_rel")" } command_exists_for_line() { @@ -4711,16 +4744,13 @@ cmd_add() { if [ "$run_dev" -eq 1 ]; then effective_port=$(port_from_name "$name") - if [ -z "$effective_port" ]; then - if [ -n "$port_candidate" ] && [ "$port_candidate" -lt 1024 ]; then + if [ -z "$effective_port" ] && [ -n "$port_candidate" ]; then + if [ "$port_candidate" -lt 1024 ]; then skip_dev=1 skip_dev_port="$port_candidate" skip_dev_reason="reserved" - elif [ -n "$port_candidate" ] && [ "$port_candidate" -gt 65535 ]; then + elif [ "$port_candidate" -gt 65535 ]; then info "$(msg fallback_default_port)" - else - skip_dev=1 - skip_dev_reason="no_port" fi fi fi diff --git a/bin/messages.sh b/bin/messages.sh index 4cfe390..2d51abd 100644 --- a/bin/messages.sh +++ b/bin/messages.sh @@ -5,7 +5,10 @@ msg_en() { shift || true case "$key" in copy_env_file) - printf '📄 Copy %s' "$1" + printf '📄 Copy %s → %s' "$1" "$2" + ;; + copy_env_missing) + printf '⚠️ Skipping copy; source not found: %s' "$1" ;; command_not_found) printf '⚠️ Command not available: %s; skipping' "$1" @@ -846,7 +849,10 @@ msg_zh() { shift || true case "$key" in copy_env_file) - printf '📄 复制 %s' "$1" + printf '📄 复制 %s → %s' "$1" "$2" + ;; + copy_env_missing) + printf '⚠️ 源文件不存在,已跳过:%s' "$1" ;; command_not_found) printf '⚠️ 未找到命令:%s,已跳过' "$1" From f281a39a375c6cd8e5c0150757262c0fb484155e Mon Sep 17 00:00:00 2001 From: FlamesCN Date: Sun, 22 Mar 2026 22:43:59 +0800 Subject: [PATCH 2/9] fix: guard worktree branch deletion by exact match --- bin/lib/commands.sh | 56 ++++++++++++++------------ bin/lib/runtime.sh | 89 +++++++++++++++++++++++++++++++++++++++++- bin/messages.sh | 18 +++++++++ tests/test_rm_guard.sh | 64 ++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 27 deletions(-) create mode 100755 tests/test_rm_guard.sh diff --git a/bin/lib/commands.sh b/bin/lib/commands.sh index dea3fda..159234b 100644 --- a/bin/lib/commands.sh +++ b/bin/lib/commands.sh @@ -1869,7 +1869,11 @@ remove_worktree_by_name() { fi local branch="" - branch=$(branch_for "$name") + if ! branch=$(worktree_branch_for_path "$target_path" 2> /dev/null); then + branch="" + fi + + validate_removable_worktree_branch "$name" "$branch" "$WORKTREE_BRANCH_PREFIX" || true info "$(msg removing_worktree "$target_path")" local removal_succeeded=0 @@ -1888,9 +1892,11 @@ remove_worktree_by_name() { die "$(msg remove_failed)" fi - if [ -n "$branch" ] && git_project show-ref --verify --quiet "refs/heads/$branch"; then - git_project branch -D "$branch" >&2 || true - info "$(msg removed_branch "$branch")" + if [ -n "$WT_VALIDATED_REMOVE_BRANCH" ] && git_project show-ref --verify --quiet "refs/heads/$WT_VALIDATED_REMOVE_BRANCH"; then + git_project branch -D "$WT_VALIDATED_REMOVE_BRANCH" >&2 || true + info "$(msg removed_branch "$WT_VALIDATED_REMOVE_BRANCH")" + else + log_skipped_branch_removal "$target_path" "$branch" fi info "$(msg worktree_removed "$name")" @@ -2281,19 +2287,14 @@ cmd_remove_global() { fi fi - if [ -z "$branch_name" ]; then - if [ -z "$branch_prefix" ]; then - branch_prefix="$CONFIG_DEFAULT_WORKTREE_ADD_BRANCH_PREFIX" - fi - if [ -n "$branch_prefix" ]; then - branch_name="${branch_prefix}${name}" - fi - fi + validate_removable_worktree_branch "$name" "$branch_name" "$branch_prefix" || true info "$(msg removing_worktree "$target_path")" if git_at_path "$repo_path" worktree remove "$target_path" --force >&2; then - if [ -n "$branch_name" ]; then - git_at_path "$repo_path" branch -D "$branch_name" > /dev/null 2>&1 || true + if [ -n "$WT_VALIDATED_REMOVE_BRANCH" ]; then + git_at_path "$repo_path" branch -D "$WT_VALIDATED_REMOVE_BRANCH" > /dev/null 2>&1 || true + else + log_skipped_branch_removal "$target_path" "$branch_name" fi git_at_path "$repo_path" worktree prune --expire now >&2 || true info "$(msg worktree_removed "$target_path")" @@ -2405,9 +2406,16 @@ cmd_clean() { suffix="${base#"$WORKTREE_NAME_PREFIX"}" if [[ "$suffix" =~ ^[0-9]+$ ]]; then info "$(msg cleaning_worktree "$suffix")" + if ! branch_name=$(worktree_branch_for_path "$worktree_path" 2> /dev/null); then + branch_name="" + fi + validate_removable_worktree_branch "$suffix" "$branch_name" "$WORKTREE_BRANCH_PREFIX" || true git_project worktree remove "$worktree_path" --force >&2 || true - branch_name=$(branch_for "$suffix") - git_project branch -D "$branch_name" > /dev/null 2>&1 || true + if [ -n "$WT_VALIDATED_REMOVE_BRANCH" ]; then + git_project branch -D "$WT_VALIDATED_REMOVE_BRANCH" > /dev/null 2>&1 || true + else + log_skipped_branch_removal "$worktree_path" "$branch_name" + fi removed=$((removed + 1)) if [ "$current_abs" = "$worktree_path" ]; then current_removed=1 @@ -2524,18 +2532,14 @@ cmd_clean_global() { continue fi + validate_removable_worktree_branch "$suffix" "$branch_name" "$branch_prefix" || true + info "$(msg cleaning_worktree "$suffix")" if git_at_path "$repo_path" worktree remove "$wt_path" --force >&2; then - if [ -z "$branch_name" ]; then - if [ -z "$branch_prefix" ]; then - branch_prefix="$CONFIG_DEFAULT_WORKTREE_ADD_BRANCH_PREFIX" - fi - if [ -n "$branch_prefix" ]; then - branch_name="${branch_prefix}${suffix}" - fi - fi - if [ -n "$branch_name" ]; then - git_at_path "$repo_path" branch -D "$branch_name" > /dev/null 2>&1 || true + if [ -n "$WT_VALIDATED_REMOVE_BRANCH" ]; then + git_at_path "$repo_path" branch -D "$WT_VALIDATED_REMOVE_BRANCH" > /dev/null 2>&1 || true + else + log_skipped_branch_removal "$wt_path" "$branch_name" fi git_at_path "$repo_path" worktree prune --expire now >&2 || true removed=$((removed + 1)) diff --git a/bin/lib/runtime.sh b/bin/lib/runtime.sh index e518882..96e981d 100644 --- a/bin/lib/runtime.sh +++ b/bin/lib/runtime.sh @@ -1846,7 +1846,9 @@ project_remove_worktree() { fi if [ -n "$branch" ] && git_at_path "$repo_path" show-ref --verify --quiet "refs/heads/$branch"; then - if git_at_path "$repo_path" branch -D "$branch" > /dev/null 2>&1; then + if protected_worktree_branch "$branch"; then + info "$(msg remove_branch_skip_protected "$path" "$branch" "$branch")" + elif git_at_path "$repo_path" branch -D "$branch" > /dev/null 2>&1; then info "$(msg removed_branch "$branch")" fi fi @@ -3083,6 +3085,91 @@ worktree_branch_for_path() { worktree_branch_for_path_in_repo "$PROJECT_DIR_ABS" "$lookup_path" } +protected_worktree_branch() { + case "${1:-}" in + main | master | develop | dev) + return 0 + ;; + esac + + return 1 +} + +expected_worktree_branch_name() { + if [ $# -lt 1 ]; then + return 1 + fi + + local name="$1" + local branch_prefix="${2:-}" + + if [ -n "$branch_prefix" ]; then + printf '%s%s\n' "$branch_prefix" "$name" + else + printf '%s\n' "$name" + fi +} + +WT_VALIDATED_REMOVE_BRANCH="" +WT_REMOVE_BRANCH_REASON="" +WT_REMOVE_BRANCH_EXPECTED="" + +validate_removable_worktree_branch() { + if [ $# -lt 2 ]; then + return 1 + fi + + local name="$1" + local actual_branch="$2" + local branch_prefix="${3:-}" + + WT_VALIDATED_REMOVE_BRANCH="" + WT_REMOVE_BRANCH_REASON="" + WT_REMOVE_BRANCH_EXPECTED="" + WT_REMOVE_BRANCH_EXPECTED=$(expected_worktree_branch_name "$name" "$branch_prefix") + + if [ -z "$actual_branch" ]; then + WT_REMOVE_BRANCH_REASON='missing' + return 1 + fi + + if protected_worktree_branch "$actual_branch"; then + WT_REMOVE_BRANCH_REASON='protected' + return 1 + fi + + if [ -n "$WT_REMOVE_BRANCH_EXPECTED" ] && [ "$actual_branch" != "$WT_REMOVE_BRANCH_EXPECTED" ]; then + WT_REMOVE_BRANCH_REASON='mismatch' + return 1 + fi + + WT_VALIDATED_REMOVE_BRANCH="$actual_branch" + return 0 +} + +log_skipped_branch_removal() { + if [ $# -lt 1 ]; then + return 1 + fi + + local target_path="$1" + local actual_branch="${2:-}" + local expected_branch="${3:-$WT_REMOVE_BRANCH_EXPECTED}" + local reason="${4:-$WT_REMOVE_BRANCH_REASON}" + + case "$reason" in + protected) + info "$(msg remove_branch_skip_protected "$target_path" "$actual_branch" "$expected_branch")" + ;; + mismatch) + info "$(msg remove_branch_skip_mismatch "$target_path" "$actual_branch" "$expected_branch")" + ;; + missing) + info "$(msg remove_branch_skip_missing "$target_path" "$expected_branch")" + ;; + esac +} + branch_for() { local name="$1" local target_path diff --git a/bin/messages.sh b/bin/messages.sh index 2d51abd..7bac6ae 100644 --- a/bin/messages.sh +++ b/bin/messages.sh @@ -514,6 +514,15 @@ LANG_USAGE_EN remove_failed) printf 'Failed to remove %s' "$1" ;; + remove_branch_skip_protected) + printf '⚠️ Skipping branch deletion for %s: protected branch %s (expected exact match %s)' "$1" "$2" "$3" + ;; + remove_branch_skip_mismatch) + printf '⚠️ Skipping branch deletion for %s: current branch %s does not exactly match expected %s' "$1" "$2" "$3" + ;; + remove_branch_skip_missing) + printf '⚠️ Skipping branch deletion for %s: current branch could not be verified (expected exact match %s)' "$1" "$2" + ;; detach_prompt_worktree) printf 'Remove worktree %s? [Y/n]' "$1" ;; @@ -1361,6 +1370,15 @@ LANG_USAGE_ZH remove_failed) printf '删除 %s 失败' "$1" ;; + remove_branch_skip_protected) + printf '⚠️ 跳过删除分支:%s 当前挂着受保护分支 %s(期望精确匹配 %s)' "$1" "$2" "$3" + ;; + remove_branch_skip_mismatch) + printf '⚠️ 跳过删除分支:%s 当前分支 %s 与期望分支 %s 不完全一致' "$1" "$2" "$3" + ;; + remove_branch_skip_missing) + printf '⚠️ 跳过删除分支:无法确认 %s 当前分支(期望精确匹配 %s)' "$1" "$2" + ;; detach_prompt_worktree) printf '移除工作树 %s?[Y/n]' "$1" ;; diff --git a/tests/test_rm_guard.sh b/tests/test_rm_guard.sh new file mode 100755 index 0000000..101cbc5 --- /dev/null +++ b/tests/test_rm_guard.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../bin" && pwd)" +# shellcheck source=../bin/lib/runtime.sh +source "$SCRIPT_DIR/lib/runtime.sh" + +pass=0 +fail=0 + +assert_equals() { + local expected="$1" + local actual="$2" + local message="$3" + + if [ "$expected" = "$actual" ]; then + printf ' ✓ %s\n' "$message" + pass=$((pass + 1)) + else + printf ' ✗ %s\n 期望: %s\n 实际: %s\n' "$message" "$expected" "$actual" + fail=$((fail + 1)) + fi +} + +assert_empty() { + local actual="$1" + local message="$2" + + if [ -z "$actual" ]; then + printf ' ✓ %s\n' "$message" + pass=$((pass + 1)) + else + printf ' ✗ %s\n 实际: %s\n' "$message" "$actual" + fail=$((fail + 1)) + fi +} + +printf '=== 精确匹配分支删除保护 ===\n' + +validate_removable_worktree_branch "ui9083" "wt-feat/ui9083" "wt-feat/" || true +assert_equals "wt-feat/ui9083" "$WT_VALIDATED_REMOVE_BRANCH" '标准 worktree 分支允许删除' +assert_empty "$WT_REMOVE_BRANCH_REASON" '标准 worktree 分支不记录跳过原因' + +validate_removable_worktree_branch "ui9083" "dev" "wt-feat/" || true +assert_empty "$WT_VALIDATED_REMOVE_BRANCH" '受保护分支不会被加入删除列表' +assert_equals "protected" "$WT_REMOVE_BRANCH_REASON" 'dev 被识别为受保护分支' +assert_equals "wt-feat/ui9083" "$WT_REMOVE_BRANCH_EXPECTED" '受保护分支仍保留期望分支名' + +validate_removable_worktree_branch "ui9083" "feature/ui9083" "wt-feat/" || true +assert_empty "$WT_VALIDATED_REMOVE_BRANCH" '分支名前缀不匹配时不会删除' +assert_equals "mismatch" "$WT_REMOVE_BRANCH_REASON" '前缀不匹配记录 mismatch' + +validate_removable_worktree_branch "ui9083" "" "wt-feat/" || true +assert_empty "$WT_VALIDATED_REMOVE_BRANCH" '无法确认当前分支时不会猜测删除目标' +assert_equals "missing" "$WT_REMOVE_BRANCH_REASON" '缺失分支记录 missing' + +printf '\n' +if [ "$fail" -eq 0 ]; then + printf '✅ rm 分支保护测试全部通过,共 %d 项。\n' "$pass" +else + printf '❌ rm 分支保护测试存在失败:%d 通过,%d 失败。\n' "$pass" "$fail" + exit 1 +fi From 56059a6b3bee17fa43819db73dc11350b0ab1a1b Mon Sep 17 00:00:00 2001 From: FlamesCN Date: Mon, 13 Apr 2026 16:16:49 +0800 Subject: [PATCH 3/9] chore: keep local OMX workspace artifacts out of version control OMX planning files and session state are generated per local run and should not keep showing up as untracked noise or get committed by accident. Recording them in the repository ignore rules keeps the working tree cleaner for anyone using the local agent workflow. Constraint: OMX planning and state files are local workspace artifacts, not shared source files Rejected: Use .git/info/exclude only | would not persist as a repository-level convention Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep local agent scratch files ignored unless a team workflow explicitly versions them Tested: git check-ignore -v .omx findings.md progress.md task_plan.md Tested: git status --short --ignored Not-tested: Fresh clone behavior on another machine --- .gitignore | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 496ee2c..cdf8d34 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -.DS_Store \ No newline at end of file +.DS_Store + +# Local OMX session artifacts +.omx/ +findings.md +progress.md +task_plan.md From 278248de69dc98fcd57ff0b86c1a6f4638bc78d8 Mon Sep 17 00:00:00 2001 From: FlamesCN Date: Wed, 15 Apr 2026 07:14:59 +0800 Subject: [PATCH 4/9] fix: make help requests non-mutating and document add port inference Core subcommands lacked dedicated help text, and `wt add --help` fell through to normal add execution because the dispatcher treated `--help` as the worktree name. This change centralizes subcommand usage output, routes help before scope/name validation, hardens `cmd_add` against option-like names, and documents the trailing-digit port rule such as `wt add ui9083`. Constraint: Help output must be safe even outside configured project scope Rejected: Only special-case `wt add --help` inside cmd_add | other subcommands would still lack targeted help and keep generic fallthroughs Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep subcommand help routing ahead of scope and positional validation when adding new commands Tested: bash -n bin/wt bin/messages.sh bin/lib/runtime.sh bin/lib/commands.sh Tested: git diff --check Tested: ./bin/wt help add; ./bin/wt add --help; ./bin/wt clean --help; ./bin/wt help sync Tested: disposable repo smoke for wt add --help (no new branch/worktree) and wt add ui9083 (feat/ui9083 with PORT=9083) Not-tested: shfmt and shellcheck (not installed in this environment) --- README.md | 11 ++ README.zh-CN.md | 11 ++ bin/lib/commands.sh | 315 ++++++++++++++++++++++++++++------------- bin/lib/runtime.sh | 23 ++- bin/messages.sh | 332 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 589 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 3dee107..0b7b1f6 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ wt add 3000 # - Installs dependencies # - Starts dev server on port 3000 +# Names with trailing digits also set the dev PORT +wt add ui9083 +# - Creates branch feat/ui9083 +# - Starts the configured dev command with PORT=9083 + # 3. Navigate between worktrees wt 3000 # Jump to feature worktree wt main # Return to main repository @@ -97,9 +102,15 @@ wt rm 3000 | `wt lang` | Set CLI language (en/zh) | `wt lang set zh` | | `wt theme` | Switch `wt list` theme (`box`\|`sage`\|`archer`) | `wt theme set box` | | `wt help` | Show command reference | `wt help` | +| `wt help ` | Show subcommand help | `wt help add` | | `wt reinstall` | Update to latest version | `wt reinstall` | | `wt uninstall` | Remove worktree.sh | `wt uninstall` | +## Help and Naming Rules + +- Every core command supports `wt --help`, and `wt help ` is the equivalent long form. +- `wt add ` uses trailing digits as the dev `PORT`. For example, `wt add ui9083` keeps the worktree name `ui9083`, creates branch `feat/ui9083`, and runs the configured dev command with `PORT=9083`. + ## Advanced Features ### Broadcasting Changes Across Worktrees diff --git a/README.zh-CN.md b/README.zh-CN.md index ee3583d..08add07 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -42,6 +42,11 @@ wt add 3000 # - 安装依赖 # - 在端口 3000 上启动开发服务器 +# 名称以数字结尾时,也会把这些数字当作 dev PORT +wt add ui9083 +# - 创建分支 feat/ui9083 +# - 以 PORT=9083 启动配置好的 dev 命令 + # 3. 在 worktree 之间导航 wt 3000 # 跳转到功能 worktree wt main # 返回主仓库 @@ -97,9 +102,15 @@ wt rm 3000 | `wt lang` | 设置 CLI 语言(en/zh) | `wt lang set zh` | | `wt theme` | 切换 `wt list` 主题(`box`\|`sage`\|`archer`) | `wt theme set box` | | `wt help` | 显示命令参考 | `wt help` | +| `wt help ` | 显示子命令帮助 | `wt help add` | | `wt reinstall` | 更新到最新版本 | `wt reinstall` | | `wt uninstall` | 删除 worktree.sh | `wt uninstall` | +## Help 与命名规则 + +- 所有核心命令都支持 `wt --help`,而 `wt help ` 是等价的长写法。 +- `wt add ` 会把名称末尾的数字当作 dev `PORT`。例如 `wt add ui9083` 会保留 worktree 名称 `ui9083`、创建分支 `feat/ui9083`,并以 `PORT=9083` 启动已配置的 dev 命令。 + ## 高级功能 ### 跨 Worktree 广播更改 diff --git a/bin/lib/commands.sh b/bin/lib/commands.sh index 159234b..da3c7a6 100644 --- a/bin/lib/commands.sh +++ b/bin/lib/commands.sh @@ -295,6 +295,198 @@ theme_prompt_interactive() { esac } +is_help_token() { + case "${1:-}" in + -h | --help | help) + return 0 + ;; + *) + return 1 + ;; + esac +} + +shell_hook_usage() { + case "$LANGUAGE" in + zh) + cat << 'HOOK_USAGE_ZH' +wt shell-hook 用法: + wt shell-hook zsh + wt shell-hook bash + +将输出通过 eval/source 加载以安装包装函数,例如: + eval "$(wt shell-hook zsh)" +HOOK_USAGE_ZH + ;; + *) + cat << 'HOOK_USAGE_EN' +wt shell-hook usage: + wt shell-hook zsh + wt shell-hook bash + +Pipe the output into eval/source to install the wrapper, e.g. + eval "$(wt shell-hook zsh)" +HOOK_USAGE_EN + ;; + esac +} + +uninstall_usage() { + case "$LANGUAGE" in + zh) + cat << 'UNINSTALL_USAGE_ZH' +wt uninstall 用法: + wt uninstall [--shell ] [--prefix ] + +选项: + --shell 指定要清理的 shell(zsh|bash|none;默认自动检测) + --prefix wt 安装目录(默认 $HOME/.local/bin) + --help 查看帮助 +UNINSTALL_USAGE_ZH + ;; + *) + cat << 'UNINSTALL_USAGE_EN' +wt uninstall usage: + wt uninstall [--shell ] [--prefix ] + +Options: + --shell Shell to clean hooks for (zsh|bash|none; default: auto-detect) + --prefix Directory where wt is installed (default: $HOME/.local/bin) + --help Show this help +UNINSTALL_USAGE_EN + ;; + esac +} + +reinstall_usage() { + case "$LANGUAGE" in + zh) + cat << 'REINSTALL_USAGE_ZH' +wt reinstall 用法: + wt reinstall [--shell ] [--prefix ] + +说明: + - 先运行 uninstall.sh,再运行 install.sh + - 默认目标目录为 ~/.local/bin(可通过 WT_INSTALL_PREFIX 或 --prefix 覆盖) + - 需在包含 install.sh 与 uninstall.sh 的仓库目录中运行 +REINSTALL_USAGE_ZH + ;; + *) + cat << 'REINSTALL_USAGE_EN' +wt reinstall usage: + wt reinstall [--shell ] [--prefix ] + +Notes: + - Executes uninstall.sh followed by install.sh from this checkout + - Targets ~/.local/bin by default (override via WT_INSTALL_PREFIX or --prefix) + - Must be invoked from a checkout that contains install.sh and uninstall.sh +REINSTALL_USAGE_EN + ;; + esac +} + +init_usage() { + case "$LANGUAGE" in + zh) + cat << 'INIT_USAGE_ZH' +wt init 用法: + wt init + +请在目标仓库运行: +- 创建 ~/.worktree.sh/projects//config.kv(如不存在)。 +- 设置 repo.path 为仓库根目录(根据 git common dir 推导)。 +INIT_USAGE_ZH + ;; + *) + cat << 'INIT_USAGE_EN' +wt init usage: + wt init + +Run inside the target repository: +- Create ~/.worktree.sh/projects//config.kv when missing. +- Set repo.path to the repository root (derived from git common dir). +INIT_USAGE_EN + ;; + esac +} + +command_usage() { + local command="${1:-}" + + case "$command" in + help) + msg help_usage + ;; + list) + msg list_usage + ;; + main) + msg main_usage + ;; + path) + msg path_usage + ;; + add) + msg add_usage + ;; + merge) + msg merge_usage + ;; + sync) + msg sync_usage + ;; + rm | remove) + msg remove_usage + ;; + clean) + msg clean_usage + ;; + detach) + msg detach_usage + ;; + config) + config_usage + ;; + lang) + msg lang_usage + ;; + theme) + msg theme_usage + ;; + shell-hook) + shell_hook_usage + ;; + init) + init_usage + ;; + uninstall) + uninstall_usage + ;; + reinstall) + reinstall_usage + ;; + *) + return 1 + ;; + esac +} + +maybe_print_command_help() { + local command="${1:-}" + shift || true + + if [ $# -eq 0 ]; then + return 1 + fi + + if ! is_help_token "$1"; then + return 1 + fi + + command_usage "$command" || return 1 + return 0 +} + cmd_config() { local scope="$CURRENT_SCOPE" if [ $# -eq 0 ]; then @@ -509,28 +701,7 @@ cmd_shell_hook() { case "$1" in -h | --help | help) - case "$LANGUAGE" in - zh) - cat << 'HOOK_USAGE_ZH' -wt shell-hook 用法: - wt shell-hook zsh - wt shell-hook bash - -将输出通过 eval/source 加载以安装包装函数,例如: - eval "$(wt shell-hook zsh)" -HOOK_USAGE_ZH - ;; - *) - cat << 'HOOK_USAGE_EN' -wt shell-hook usage: - wt shell-hook zsh - wt shell-hook bash - -Pipe the output into eval/source to install the wrapper, e.g. - eval "$(wt shell-hook zsh)" -HOOK_USAGE_EN - ;; - esac + shell_hook_usage return ;; esac @@ -798,30 +969,7 @@ cmd_uninstall() { prefix="$1" ;; -h | --help | help) - case "$LANGUAGE" in - zh) - cat << 'UNINSTALL_USAGE_ZH' -wt uninstall 用法: - wt uninstall [--shell ] [--prefix ] - -选项: - --shell 指定要清理的 shell(zsh|bash|none;默认自动检测) - --prefix wt 安装目录(默认 $HOME/.local/bin) - --help 查看帮助 -UNINSTALL_USAGE_ZH - ;; - *) - cat << 'UNINSTALL_USAGE_EN' -wt uninstall usage: - wt uninstall [--shell ] [--prefix ] - -Options: - --shell Shell to clean hooks for (zsh|bash|none; default: auto-detect) - --prefix Directory where wt is installed (default: $HOME/.local/bin) - --help Show this help -UNINSTALL_USAGE_EN - ;; - esac + uninstall_usage return ;; --) @@ -895,30 +1043,7 @@ cmd_reinstall() { prefix="$1" ;; -h | --help | help) - case "$LANGUAGE" in - zh) - cat << 'REINSTALL_USAGE_ZH' -wt reinstall 用法: - wt reinstall [--shell ] [--prefix ] - -说明: - - 先运行 uninstall.sh,再运行 install.sh - - 默认目标目录为 ~/.local/bin(可通过 WT_INSTALL_PREFIX 或 --prefix 覆盖) - - 需在包含 install.sh 与 uninstall.sh 的仓库目录中运行 -REINSTALL_USAGE_ZH - ;; - *) - cat << 'REINSTALL_USAGE_EN' -wt reinstall usage: - wt reinstall [--shell ] [--prefix ] - -Notes: - - Executes uninstall.sh followed by install.sh from this checkout - - Targets ~/.local/bin by default (override via WT_INSTALL_PREFIX or --prefix) - - Must be invoked from a checkout that contains install.sh and uninstall.sh -REINSTALL_USAGE_EN - ;; - esac + reinstall_usage return ;; --) @@ -1118,28 +1243,7 @@ cmd_init() { assume_yes=1 ;; --help | help) - case "$LANGUAGE" in - zh) - cat << 'INIT_USAGE_ZH' -wt init 用法: - wt init - -请在目标仓库运行: -- 创建 ~/.worktree.sh/projects//config.kv(如不存在)。 -- 设置 repo.path 为仓库根目录(根据 git common dir 推导)。 -INIT_USAGE_ZH - ;; - *) - cat << 'INIT_USAGE_EN' -wt init usage: - wt init - -Run inside the target repository: -- Create ~/.worktree.sh/projects//config.kv when missing. -- Set repo.path to the repository root (derived from git common dir). -INIT_USAGE_EN - ;; - esac + init_usage return ;; -*) @@ -2167,8 +2271,8 @@ cmd_remove() { assume_yes=1 ;; -h | --help | help) - usage - exit 0 + command_usage rm + return ;; -*) die "$(msg remove_unknown_option "$1")" @@ -2226,8 +2330,8 @@ cmd_remove_global() { assume_yes=1 ;; -h | --help | help) - usage - exit 0 + command_usage rm + return ;; -*) die "$(msg remove_unknown_option "$1")" @@ -2323,8 +2427,8 @@ cmd_detach() { force=1 ;; -h | --help | help) - usage - exit 0 + command_usage detach + return ;; --*) die "$(msg detach_unknown_option "$1")" @@ -2597,6 +2701,10 @@ wt_main() { scope=$(current_scope) CURRENT_SCOPE="$scope" + if [ "$command" != "help" ] && maybe_print_command_help "$command" "$@"; then + return + fi + case "$command" in config) cmd_config "$@" @@ -2627,7 +2735,16 @@ wt_main() { return ;; help) - usage + if [ $# -eq 0 ]; then + usage + return + fi + if [ $# -gt 1 ]; then + die "$(msg unexpected_extra_argument "$2")" + fi + if ! command_usage "$1"; then + usage_exit 1 + fi return ;; version) diff --git a/bin/lib/runtime.sh b/bin/lib/runtime.sh index 96e981d..1ab68d8 100644 --- a/bin/lib/runtime.sh +++ b/bin/lib/runtime.sh @@ -2773,7 +2773,7 @@ usage() { 核心命令: init 将当前仓库设为 wt 的默认项目 - add 创建新 worktree,复制环境文件、安装依赖并启动 dev server(可通过 wt config 调整) + add 创建新 worktree;名称为纯数字或以数字结尾时会自动推断 dev 端口 main 跳转到主 worktree merge 将指定 worktree 的分支(feat/)合并到主分支 sync [all|name ...] 将主工作区的暂存改动同步到其他 worktree @@ -2787,6 +2787,10 @@ usage() { reinstall 运行 uninstall.sh + install.sh 重新部署 wt help 显示此帮助 +帮助: + wt help 查看子命令帮助 + wt --help 与上面等价,例如 wt add --help + ${dir_banner_zh} USAGE_ZH @@ -2800,7 +2804,7 @@ Usage: Core commands: init Remember this repository as wt's default project - add Create a new worktree, copy env files, install deps, start dev server (tunable via wt config) + add Create a new worktree; numeric names or trailing digits also infer the dev port main Jump to the main worktree merge Merge the feature branch (feat/) back into the base branch sync [all|name ...] Sync staged changes from the main workspace into other worktrees @@ -2814,6 +2818,10 @@ Core commands: reinstall Run uninstall.sh + install.sh to refresh wt help Show this guide +Help: + wt help Show subcommand help + wt --help Same as above, for example wt add --help + ${dir_banner_en} USAGE_EN @@ -4765,6 +4773,13 @@ cmd_add() { shift || true [ -n "$name" ] || die "$(msg add_requires_name)" + if is_help_token "$name"; then + command_usage add + return + fi + if [[ "$name" == -* ]]; then + die "$(msg add_unknown_option "$name")" + fi if ! validate_worktree_name "$name"; then die "$(msg invalid_worktree_name "$name")" fi @@ -4807,8 +4822,8 @@ cmd_add() { while [ $# -gt 0 ]; do case "$1" in -h | --help | help) - usage - exit 0 + command_usage add + return ;; --) shift diff --git a/bin/messages.sh b/bin/messages.sh index 7bac6ae..364feca 100644 --- a/bin/messages.sh +++ b/bin/messages.sh @@ -235,6 +235,172 @@ Non-interactive: wt lang help Show this help LANG_USAGE_EN ;; + help_usage) + cat << 'HELP_USAGE_EN' +wt help - Show global or command-specific help + +Usage: + wt help + wt help + +Examples: + wt help add + wt help sync + +Tip: + Every core command also accepts --help, for example: + wt add --help +HELP_USAGE_EN + ;; + list_usage) + cat << 'LIST_USAGE_EN' +wt list - Show known worktrees for the current project + +Usage: + wt + wt list + wt list --help + +Notes: + - Inside a configured project, shows that project's main worktree plus siblings. + - Outside a configured project, use wt help for command discovery or wt init in a repo first. +LIST_USAGE_EN + ;; + main_usage) + cat << 'MAIN_USAGE_EN' +wt main - Print the main worktree path + +Usage: + wt main + wt main --help + +Notes: + - With shell-hook installed, calling wt main jumps back to the main repository automatically. + - Without shell-hook, it prints the absolute path so you can cd manually. +MAIN_USAGE_EN + ;; + path_usage) + cat << 'PATH_USAGE_EN' +wt path - Print a worktree path without switching directories + +Usage: + wt path + wt path --help + +Examples: + wt path 3000 + wt path ui9083 +PATH_USAGE_EN + ;; + add_usage) + cat << 'ADD_USAGE_EN' +wt add - Create a new worktree from the current branch + +Usage: + wt add + wt add --help + +What controls: + - Worktree path suffix (for example ../project.) + - Branch suffix (for example feat/) + - Dev PORT when the name is numeric or ends with digits + +Port rules: + - wt add 9083 -> PORT=9083 + - wt add ui9083 -> worktree name ui9083, branch feat/ui9083, PORT=9083 + - Ports below 1024 are treated as reserved and are not passed to the dev command + - Ports above 65535 fall back to the default dev port behavior + +Examples: + wt add 3000 + wt add ui9083 + wt add feat-auth +ADD_USAGE_EN + ;; + merge_usage) + cat << 'MERGE_USAGE_EN' +wt merge - Merge a worktree branch back into the current base branch + +Usage: + wt merge + wt merge --help + +Examples: + wt merge 3000 + wt merge ui9083 + +Notes: + - Run from the main worktree on the branch you want to merge into. + - Both the main worktree and target worktree must be clean. +MERGE_USAGE_EN + ;; + sync_usage) + cat << 'SYNC_USAGE_EN' +wt sync - Apply staged changes from the main worktree into other worktrees + +Usage: + wt sync all + wt sync [name ...] + wt sync --help + +Examples: + wt sync all + wt sync 3000 ui9083 + +Notes: + - Sync reads the staged diff from the current main worktree. + - Target worktrees must be clean before syncing. +SYNC_USAGE_EN + ;; + remove_usage) + cat << 'REMOVE_USAGE_EN' +wt rm - Remove worktrees and their feature branches + +Usage: + wt rm [name ...] + wt rm --help + +Examples: + wt rm 3000 + wt rm ui9083 + wt rm # Remove the current worktree when run inside it + +Notes: + - Protected or mismatched branches are skipped instead of deleted. + - Alias: wt remove +REMOVE_USAGE_EN + ;; + clean_usage) + cat << 'CLEAN_USAGE_EN' +wt clean - Remove numeric worktrees for the current project + +Usage: + wt clean + wt clean --help + +Notes: + - Matches worktrees whose suffix is only digits, such as 3000 or 9083. + - Leaves named worktrees such as ui9083 or feat-auth untouched. +CLEAN_USAGE_EN + ;; + detach_usage) + cat << 'DETACH_USAGE_EN' +wt detach - Remove all tracked worktrees for a project + +Usage: + wt detach [-y|--yes] [project-slug] + wt detach --help + +Examples: + wt detach + wt detach -y + wt detach my-project + +Notes: + - In a project workspace, the current project slug is used by default. + - In global mode, wt can prompt you to choose a project when no slug is provided. +DETACH_USAGE_EN + ;; init_prompt_repo_path) printf 'Repository path for wt to track?' ;; @@ -1088,6 +1254,172 @@ wt lang - 管理 worktree.sh 界面语言 wt lang help 显示本帮助 LANG_USAGE_ZH ;; + help_usage) + cat << 'HELP_USAGE_ZH' +wt help - 查看全局帮助或子命令帮助 + +用法: + wt help + wt help + +示例: + wt help add + wt help sync + +提示: + 核心命令都支持 --help,例如: + wt add --help +HELP_USAGE_ZH + ;; + list_usage) + cat << 'LIST_USAGE_ZH' +wt list - 查看当前项目已登记的 worktree + +用法: + wt + wt list + wt list --help + +说明: + - 在已配置项目中,会显示主 worktree 及其兄弟 worktree。 + - 在未配置目录中,可先用 wt help 看命令,或进入仓库执行 wt init。 +LIST_USAGE_ZH + ;; + main_usage) + cat << 'MAIN_USAGE_ZH' +wt main - 输出主 worktree 路径 + +用法: + wt main + wt main --help + +说明: + - 安装 shell-hook 后,执行 wt main 会自动切回主仓库。 + - 未安装 shell-hook 时,会输出绝对路径,方便手动 cd。 +MAIN_USAGE_ZH + ;; + path_usage) + cat << 'PATH_USAGE_ZH' +wt path - 输出 worktree 路径而不切换目录 + +用法: + wt path + wt path --help + +示例: + wt path 3000 + wt path ui9083 +PATH_USAGE_ZH + ;; + add_usage) + cat << 'ADD_USAGE_ZH' +wt add - 基于当前分支创建新的 worktree + +用法: + wt add + wt add --help + + 会同时决定: + - worktree 路径后缀(例如 ../project.) + - 分支后缀(例如 feat/) + - 当名称为纯数字或以数字结尾时,对应 dev 命令的 PORT + +端口规则: + - wt add 9083 -> PORT=9083 + - wt add ui9083 -> worktree 名称为 ui9083,分支为 feat/ui9083,PORT=9083 + - 小于 1024 的端口视为保留端口,不会传给 dev 命令 + - 大于 65535 的端口会回退到默认 dev 端口行为 + +示例: + wt add 3000 + wt add ui9083 + wt add feat-auth +ADD_USAGE_ZH + ;; + merge_usage) + cat << 'MERGE_USAGE_ZH' +wt merge - 将指定 worktree 分支合并回当前主分支 + +用法: + wt merge + wt merge --help + +示例: + wt merge 3000 + wt merge ui9083 + +说明: + - 请在主 worktree 中、目标主分支上执行。 + - 主 worktree 和目标 worktree 都必须是干净状态。 +MERGE_USAGE_ZH + ;; + sync_usage) + cat << 'SYNC_USAGE_ZH' +wt sync - 将主 worktree 已暂存的改动应用到其他 worktree + +用法: + wt sync all + wt sync [name ...] + wt sync --help + +示例: + wt sync all + wt sync 3000 ui9083 + +说明: + - sync 会读取当前主 worktree 已暂存的 diff。 + - 目标 worktree 在同步前必须保持干净。 +SYNC_USAGE_ZH + ;; + remove_usage) + cat << 'REMOVE_USAGE_ZH' +wt rm - 删除 worktree 及其功能分支 + +用法: + wt rm [name ...] + wt rm --help + +示例: + wt rm 3000 + wt rm ui9083 + wt rm # 在 worktree 目录内执行时删除当前 worktree + +说明: + - 受保护分支或分支名不匹配时,会跳过删分支而不是强删。 + - 别名:wt remove +REMOVE_USAGE_ZH + ;; + clean_usage) + cat << 'CLEAN_USAGE_ZH' +wt clean - 批量删除当前项目中的纯数字 worktree + +用法: + wt clean + wt clean --help + +说明: + - 只匹配后缀为纯数字的 worktree,例如 3000、9083。 + - 不会删除 ui9083、feat-auth 这类带名字的 worktree。 +CLEAN_USAGE_ZH + ;; + detach_usage) + cat << 'DETACH_USAGE_ZH' +wt detach - 删除某个项目登记的全部 worktree + +用法: + wt detach [-y|--yes] [project-slug] + wt detach --help + +示例: + wt detach + wt detach -y + wt detach my-project + +说明: + - 在项目工作区内执行时,默认使用当前项目 slug。 + - 在全局模式下,如果不传 slug,wt 可以交互式让你选择项目。 +DETACH_USAGE_ZH + ;; init_prompt_repo_path) printf 'wt 追踪的主仓库地址?' ;; From 0ea66d8a6e0dc29da14bab4ccc444fa89cd5637d Mon Sep 17 00:00:00 2001 From: FlamesCN Date: Sat, 9 May 2026 00:01:26 +0800 Subject: [PATCH 5/9] feat: add configurable worktree lifecycle hooks Allow projects to register remove.pre-command and add.post-command hooks so repo-specific archive/restore logic can run around wt rm and wt add. Document the new keys in config-example.kv. Manual test steps: - bash -n bin/lib/runtime.sh bin/lib/commands.sh - ./bin/wt help --- bin/lib/commands.sh | 4 +++ bin/lib/runtime.sh | 75 +++++++++++++++++++++++++++++++++++++++++++++ config-example.kv | 2 ++ 3 files changed, 81 insertions(+) diff --git a/bin/lib/commands.sh b/bin/lib/commands.sh index da3c7a6..1e49eb1 100644 --- a/bin/lib/commands.sh +++ b/bin/lib/commands.sh @@ -1979,6 +1979,10 @@ remove_worktree_by_name() { validate_removable_worktree_branch "$name" "$branch" "$WORKTREE_BRANCH_PREFIX" || true + if ! run_configured_hook_command "$REMOVE_PRE_COMMAND" "$PROJECT_DIR_ABS" "remove.pre" "$name" "$target_path" "$branch"; then + die "remove pre-command failed" + fi + info "$(msg removing_worktree "$target_path")" local removal_succeeded=0 if git_project worktree remove "$target_path" --force >&2; then diff --git a/bin/lib/runtime.sh b/bin/lib/runtime.sh index 1ab68d8..6c620d6 100644 --- a/bin/lib/runtime.sh +++ b/bin/lib/runtime.sh @@ -26,6 +26,8 @@ readonly CONFIG_DEFAULT_INSTALL_DEPS_ENABLED=1 readonly CONFIG_DEFAULT_COPY_ENV_ENABLED=1 readonly CONFIG_DEFAULT_INSTALL_DEPS_COMMAND="" readonly CONFIG_DEFAULT_SERVE_DEV_COMMAND="" +readonly CONFIG_DEFAULT_ADD_POST_COMMAND="" +readonly CONFIG_DEFAULT_REMOVE_PRE_COMMAND="" readonly CONFIG_DEFAULT_LANGUAGE="en" readonly CONFIG_DEFAULT_THEME="box" readonly -a CONFIG_DEFAULT_COPY_ENV_FILES=(".env" ".env.local") @@ -40,6 +42,8 @@ INSTALL_DEPS_ENABLED=0 COPY_ENV_ENABLED=0 INSTALL_DEPS_COMMAND="" SERVE_DEV_COMMAND="" +ADD_POST_COMMAND="" +REMOVE_PRE_COMMAND="" LANGUAGE="$CONFIG_DEFAULT_LANGUAGE" LIST_THEME="$CONFIG_DEFAULT_THEME" WORKING_REPO_PATH_CONFIGURED=0 @@ -1814,6 +1818,21 @@ project_remove_worktree() { return 1 fi + local hook_name="" + if [ -n "$branch" ]; then + hook_name="${branch##*/}" + else + hook_name="$(basename "$path")" + if [ -n "$WORKTREE_NAME_PREFIX" ] && [[ "$hook_name" == "$WORKTREE_NAME_PREFIX"* ]]; then + hook_name="${hook_name#"$WORKTREE_NAME_PREFIX"}" + fi + fi + + if ! run_configured_hook_command "$REMOVE_PRE_COMMAND" "$PROJECT_DIR_ABS" "remove.pre" "$hook_name" "$path" "$branch"; then + PROJECT_REMOVE_LAST_ERROR='remove pre-command failed' + return 1 + fi + local removal_failure=0 local removal_output="" @@ -2339,6 +2358,9 @@ config_default_value() { add.install-deps.command) printf '%s\n' "$CONFIG_DEFAULT_INSTALL_DEPS_COMMAND" ;; + add.post-command) + printf '%s\n' "$CONFIG_DEFAULT_ADD_POST_COMMAND" + ;; add.serve-dev.enabled) config_bool_to_string "$CONFIG_DEFAULT_SERVE_DEV_ENABLED" ;; @@ -2348,6 +2370,9 @@ config_default_value() { add.serve-dev.logging-path) printf '%s\n' "$CONFIG_DEFAULT_SERVE_DEV_LOGGING_PATH" ;; + remove.pre-command) + printf '%s\n' "$CONFIG_DEFAULT_REMOVE_PRE_COMMAND" + ;; *) return 1 ;; @@ -2623,6 +2648,10 @@ init_settings_apply_from_sources() { SERVE_DEV_COMMAND="$value" fi + if value=$(config_get "add.post-command" 2> /dev/null); then + ADD_POST_COMMAND="$value" + fi + if value=$(config_get "add.install-deps.enabled" 2> /dev/null); then if bool_val=$(parse_bool "$value" 2> /dev/null); then INSTALL_DEPS_ENABLED="$bool_val" @@ -2662,6 +2691,10 @@ init_settings_apply_from_sources() { fi fi + if value=$(config_get "remove.pre-command" 2> /dev/null); then + REMOVE_PRE_COMMAND="$value" + fi + return 0 } @@ -2675,6 +2708,8 @@ init_settings_defaults() { COPY_ENV_FILE_SELECTION=("${CONFIG_DEFAULT_COPY_ENV_FILES[@]}") INSTALL_DEPS_COMMAND="$CONFIG_DEFAULT_INSTALL_DEPS_COMMAND" SERVE_DEV_COMMAND="$CONFIG_DEFAULT_SERVE_DEV_COMMAND" + ADD_POST_COMMAND="$CONFIG_DEFAULT_ADD_POST_COMMAND" + REMOVE_PRE_COMMAND="$CONFIG_DEFAULT_REMOVE_PRE_COMMAND" WORKING_REPO_PATH_CONFIGURED=0 LANGUAGE="$CONFIG_DEFAULT_LANGUAGE" LIST_THEME="$CONFIG_DEFAULT_THEME" @@ -3408,6 +3443,42 @@ run_install_command() { ) } +run_configured_hook_command() { + local command_line="${1:-}" + local cwd="${2:-}" + local phase="${3:-hook}" + local worktree_name="${4:-}" + local worktree_path="${5:-}" + local worktree_branch="${6:-}" + + if [ -z "$command_line" ]; then + return 0 + fi + + if ! command_exists_for_line "$command_line"; then + info "⚠️ $phase hook command not found: $command_line" + return 1 + fi + + if [ -z "$cwd" ] || [ ! -d "$cwd" ]; then + info "⚠️ $phase hook working directory missing: $cwd" + return 1 + fi + + info "⚙️ Running $phase hook: $command_line" + + ( + cd "$cwd" || exit 1 + env \ + WT_HOOK_PHASE="$phase" \ + WT_PROJECT_ROOT="$PROJECT_DIR_ABS" \ + WT_WORKTREE_NAME="$worktree_name" \ + WT_WORKTREE_PATH="$worktree_path" \ + WT_WORKTREE_BRANCH="$worktree_branch" \ + sh -c "$command_line" + ) +} + command_slug_from_line() { local line="$1" if [ -z "$line" ]; then @@ -4915,6 +4986,10 @@ cmd_add() { run_install_command "$worktree_path" "$install_command" fi + if ! run_configured_hook_command "$ADD_POST_COMMAND" "$worktree_path" "add.post" "$name" "$worktree_path" "$branch"; then + die "add post-command failed" + fi + if [ "$run_dev" -eq 1 ]; then if [ "$skip_dev" -eq 1 ]; then if [ "$skip_dev_reason" = "reserved" ]; then diff --git a/config-example.kv b/config-example.kv index c365db3..7380418 100644 --- a/config-example.kv +++ b/config-example.kv @@ -8,3 +8,5 @@ add.install-deps.command=npm ci add.serve-dev.enabled=true add.serve-dev.command=npm run dev add.serve-dev.logging-path=tmp +add.post-command=bash "$WT_PROJECT_ROOT/scripts/post-add.sh" +remove.pre-command=bash "$WT_PROJECT_ROOT/scripts/pre-remove.sh" From 439c7a0c27885dd44a6a97d781018f5a4f49f97a Mon Sep 17 00:00:00 2001 From: FlamesCN Date: Sat, 9 May 2026 00:29:46 +0800 Subject: [PATCH 6/9] fix: prevent shell hook auto-cd on help flags Keep shell integration from changing directories when users call wt --help, wt -h, or subcommand help. Add a regression test that exercises the generated hook behavior. Manual test steps: - bash tests/test_shell_hook.sh - shellcheck bin/lib/commands.sh tests/test_shell_hook.sh --- bin/lib/commands.sh | 15 +++++- tests/test_shell_hook.sh | 112 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100755 tests/test_shell_hook.sh diff --git a/bin/lib/commands.sh b/bin/lib/commands.sh index 1e49eb1..562563a 100644 --- a/bin/lib/commands.sh +++ b/bin/lib/commands.sh @@ -716,7 +716,7 @@ cmd_shell_hook() { sed "s/__WT_BIN_PATH__/$wt_exec_path_escaped/g" << 'WT_HOOK' # wt shell integration: auto-cd after wt add/path/main/remove/clean wt() { - local __wt_out __wt_status __wt_cmd __wt_should_cd=0 + local __wt_out __wt_status __wt_cmd __wt_should_cd=0 __wt_has_help=0 __wt_arg local __wt_bin="__WT_BIN_PATH__" if [ ! -x "$__wt_bin" ]; then @@ -747,6 +747,15 @@ wt() { __wt_cmd="$1" fi + for __wt_arg in "$@"; do + case "$__wt_arg" in + help | -h | --help) + __wt_has_help=1 + break + ;; + esac + done + case "$__wt_cmd" in add|main|path) __wt_should_cd=1 @@ -765,6 +774,10 @@ wt() { ;; esac + if [ "$__wt_has_help" -eq 1 ]; then + __wt_should_cd=0 + fi + if [ $__wt_should_cd -eq 1 ] && [ -n "$__wt_out" ] && [ -d "$__wt_out" ]; then cd "$__wt_out" || return $? return 0 diff --git a/tests/test_shell_hook.sh b/tests/test_shell_hook.sh new file mode 100755 index 0000000..adeb69b --- /dev/null +++ b/tests/test_shell_hook.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/../bin" && pwd)" +SCRIPT_DIR="$REPO_BIN" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/lib/runtime.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/lib/commands.sh" + +pass=0 +fail=0 +tmpdirs=() + +assert_equals() { + local expected="$1" + local actual="$2" + local message="$3" + + if [ "$expected" = "$actual" ]; then + printf ' ✓ %s\n' "$message" + pass=$((pass + 1)) + else + printf ' ✗ %s\n 期望: %s\n 实际: %s\n' "$message" "$expected" "$actual" + fail=$((fail + 1)) + fi +} + +mktempdir() { + local dir + dir=$(mktemp -d) + tmpdirs+=("$dir") + printf '%s\n' "$dir" +} + +cleanup() { + if [ "${tmpdirs+x}" != x ]; then + return + fi + + local dir + for dir in "${tmpdirs[@]}"; do + rm -rf "$dir" + done +} +trap cleanup EXIT + +printf '=== shell-hook help guard ===\n' + +fake_root=$(mktempdir) +cat > "$fake_root/wt" << 'EOF' +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +case "${1:-}|${2:-}" in + -h\||--help\||help\|) + printf '%s\n' "$script_dir/help-target" + ;; + rm\|--help|add\|--help|path\|--help) + printf '%s\n' "$script_dir/help-target" + ;; + rm\|demo|add\|demo|path\|demo|demo\|) + printf '%s\n' "$script_dir/jump-target" + ;; + *) + printf '%s\n' "$script_dir/default-target" + ;; +esac +EOF +chmod +x "$fake_root/wt" +mkdir -p "$fake_root/help-target" "$fake_root/jump-target" "$fake_root/default-target" + +original_script_dir="$SCRIPT_DIR" +SCRIPT_DIR="$fake_root" +hook="$(cmd_shell_hook bash)" +SCRIPT_DIR="$original_script_dir" + +workspace=$(mktempdir) +mkdir -p "$workspace/start" +cd "$workspace/start" + +eval "$hook" + +before_pwd=$(pwd) +rm_help_output="$(wt rm --help)" +after_rm_help_pwd=$(pwd) +assert_equals "$fake_root/help-target" "$rm_help_output" 'wt rm --help 保留原始输出' +assert_equals "$before_pwd" "$after_rm_help_pwd" 'wt rm --help 不触发 auto-cd' + +root_help_output="$(wt --help)" +after_root_help_pwd=$(pwd) +assert_equals "$fake_root/help-target" "$root_help_output" 'wt --help 保留原始输出' +assert_equals "$before_pwd" "$after_root_help_pwd" 'wt --help 不触发 auto-cd' + +short_help_output="$(wt -h)" +after_short_help_pwd=$(pwd) +assert_equals "$fake_root/help-target" "$short_help_output" 'wt -h 保留原始输出' +assert_equals "$before_pwd" "$after_short_help_pwd" 'wt -h 不触发 auto-cd' + +wt rm demo > /dev/null +assert_equals "$fake_root/jump-target" "$(pwd)" 'wt rm demo 仍会根据路径输出自动切换目录' + +printf '\n' +if [ "$fail" -eq 0 ]; then + printf '✅ shell-hook 测试全部通过,共 %d 项。\n' "$pass" +else + printf '❌ shell-hook 测试存在失败:%d 通过,%d 失败。\n' "$pass" "$fail" + exit 1 +fi From 84859848145166b8e94567b6038d8c1a3f6ebcb9 Mon Sep 17 00:00:00 2001 From: FlamesCN Date: Sat, 9 May 2026 00:29:46 +0800 Subject: [PATCH 7/9] chore: add helper to sync the installed wt binary Add a small helper that reinstalls the current checkout into the local prefix without shell-hook changes, so local testing can refresh ~/.local/bin/wt quickly. Manual test steps: - bash -n scripts/sync-installed-wt.sh - review script path and prefix handling --- scripts/sync-installed-wt.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 scripts/sync-installed-wt.sh diff --git a/scripts/sync-installed-wt.sh b/scripts/sync-installed-wt.sh new file mode 100755 index 0000000..4c68333 --- /dev/null +++ b/scripts/sync-installed-wt.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly SCRIPT_DIR +PREFIX="${WT_SYNC_PREFIX:-$HOME/.local/bin}" +readonly PREFIX + +exec bash "$SCRIPT_DIR/install.sh" --shell none --prefix "$PREFIX" From 1b4b27cd230edb1f898e5362efa287adbd30a910 Mon Sep 17 00:00:00 2001 From: FlamesCN Date: Tue, 26 May 2026 07:30:58 +0800 Subject: [PATCH 8/9] fix: wait for dev port readiness before reporting success --- bin/lib/runtime.sh | 118 ++++++++++++++++++++++++++++++++++++++++++--- bin/messages.sh | 10 +++- 2 files changed, 119 insertions(+), 9 deletions(-) diff --git a/bin/lib/runtime.sh b/bin/lib/runtime.sh index 6c620d6..450a524 100644 --- a/bin/lib/runtime.sh +++ b/bin/lib/runtime.sh @@ -3375,6 +3375,81 @@ command_exists_for_line() { return 1 } +port_is_listening() { + local port="${1:-}" + if [ -z "$port" ]; then + return 1 + fi + + case "$port" in + *[!0-9]*) + return 1 + ;; + esac + + if command -v lsof > /dev/null 2>&1; then + lsof -nP -iTCP:"$port" -sTCP:LISTEN > /dev/null 2>&1 + return $? + fi + + (exec 3<> "/dev/tcp/127.0.0.1/$port") > /dev/null 2>&1 +} + +wait_for_dev_port() { + local port="${1:?port is required}" + local timeout_sec="${2:-60}" + local pid="${3:-}" + local exit_grace_sec="${4:-2}" + local waited=0 + local dead_wait=0 + + while [ "$waited" -lt "$timeout_sec" ]; do + if port_is_listening "$port"; then + return 0 + fi + + if [ -n "$pid" ] && ! kill -0 "$pid" 2> /dev/null; then + dead_wait=$((dead_wait + 1)) + if [ "$dead_wait" -ge "$exit_grace_sec" ]; then + return 1 + fi + else + dead_wait=0 + fi + + sleep 1 + waited=$((waited + 1)) + done + + return 2 +} + +wait_for_dev_settle() { + local port="${1:?port is required}" + local pid="${2:-}" + local timeout_sec="${3:-5}" + local waited=0 + + if [ -z "$pid" ]; then + return 0 + fi + + while [ "$waited" -lt "$timeout_sec" ]; do + if ! kill -0 "$pid" 2> /dev/null; then + return 0 + fi + + if ! port_is_listening "$port"; then + return 1 + fi + + sleep 1 + waited=$((waited + 1)) + done + + return 0 +} + node_lockfile_present() { local worktree_path="${1:?worktree path is required}" if [ -f "$worktree_path/package-lock.json" ] || [ -f "$worktree_path/npm-shrinkwrap.json" ]; then @@ -4248,17 +4323,46 @@ start_dev_server() { pid=$(cat "$pid_file" 2> /dev/null || true) fi - sleep 1 + if [ -n "$port" ]; then + local ready_timeout="${WT_DEV_READY_TIMEOUT:-60}" + local settle_timeout="${WT_DEV_SETTLE_TIMEOUT:-5}" - if [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null; then - if [ -n "$port" ]; then - info "$(msg dev_started_port "$port")" + case "$ready_timeout" in + '' | *[!0-9]*) + ready_timeout=60 + ;; + esac + case "$settle_timeout" in + '' | *[!0-9]*) + settle_timeout=5 + ;; + esac + + if wait_for_dev_port "$port" "$ready_timeout" "$pid"; then + if ! wait_for_dev_settle "$port" "$pid" "$settle_timeout"; then + local pid_display="${pid:-unknown}" + info "$(msg dev_failed "$pid_display" "$log_file")" + else + info "$(msg dev_started_port "$port")" + fi else - info "$(msg dev_started_default)" + local wait_status=$? + local pid_display="${pid:-unknown}" + if [ "$wait_status" -eq 1 ]; then + info "$(msg dev_failed "$pid_display" "$log_file")" + else + info "$(msg dev_timeout "$port" "$ready_timeout" "$log_file")" + fi fi else - local pid_display="${pid:-unknown}" - info "$(msg dev_failed "$pid_display" "$log_file")" + sleep 1 + + if [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null; then + info "$(msg dev_started_default)" + else + local pid_display="${pid:-unknown}" + info "$(msg dev_failed "$pid_display" "$log_file")" + fi fi info "$(msg dev_log_hint "$log_file")" diff --git a/bin/messages.sh b/bin/messages.sh index 364feca..884a516 100644 --- a/bin/messages.sh +++ b/bin/messages.sh @@ -23,7 +23,7 @@ msg_en() { printf '🚀 Starting dev command' ;; dev_started_port) - printf '✅ Dev command running on port %s' "$1" + printf '✅ Dev command is ready; port %s is listening' "$1" ;; dev_started_default) printf '✅ Dev command running' @@ -31,6 +31,9 @@ msg_en() { dev_failed) printf '⚠️ Dev command may not have started correctly (PID: %s); check %s' "$1" "$2" ;; + dev_timeout) + printf '⚠️ Dev command did not become ready on port %s within %s seconds; check %s' "$1" "$2" "$3" + ;; dev_log_hint) printf '📝 Dev command log: tail -f %s' "$1" ;; @@ -1042,7 +1045,7 @@ msg_zh() { printf '🚀 正在启动开发命令' ;; dev_started_port) - printf '✅ 开发命令已在端口 %s 运行' "$1" + printf '✅ 开发命令已就绪,端口 %s 已开始监听' "$1" ;; dev_started_default) printf '✅ 开发命令已启动' @@ -1050,6 +1053,9 @@ msg_zh() { dev_failed) printf '⚠️ 开发命令进程可能未正确启动 (PID: %s),请检查 %s' "$1" "$2" ;; + dev_timeout) + printf '⚠️ 开发命令在 %s 秒内未能在端口 %s 就绪,请检查 %s' "$2" "$1" "$3" + ;; dev_log_hint) printf '📝 开发命令日志:tail -f %s' "$1" ;; From f2b1827dc3836d988e647bc2c272fae835a6240f Mon Sep 17 00:00:00 2001 From: FlamesCN Date: Tue, 26 May 2026 07:59:07 +0800 Subject: [PATCH 9/9] feat: add configurable dev readiness timeouts --- README.md | 9 ++++++ README.zh-CN.md | 9 ++++++ bin/lib/runtime.sh | 70 +++++++++++++++++++++++++++++++++++++++++++--- config-example.kv | 2 ++ 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0b7b1f6..e3212e6 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,12 @@ wt sync 3000 3001 # Disable auto dev server wt config set add.serve-dev.enabled false +# Wait longer for a slow startup script before reporting success +wt config set add.serve-dev.ready-timeout 120 + +# Keep a short post-ready observation window for launcher scripts +wt config set add.serve-dev.settle-timeout 8 + # Change branch prefix wt config set add.branch-prefix "feature/" @@ -142,6 +148,9 @@ wt config list Configuration is stored per-project in `~/.worktree.sh/projects//config.kv`. +- `add.serve-dev.ready-timeout` controls how long `wt add` waits for the inferred port to start listening. +- `add.serve-dev.settle-timeout` keeps watching briefly after readiness so wrapper scripts like `start-smart.sh` can exit without causing a false failure. + ## Use Cases ### Multiple UI Iterations diff --git a/README.zh-CN.md b/README.zh-CN.md index 08add07..842e60c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -133,6 +133,12 @@ wt sync 3000 3001 # 禁用自动开发服务器 wt config set add.serve-dev.enabled false +# 启动脚本较慢时,延长端口就绪等待时间 +wt config set add.serve-dev.ready-timeout 120 + +# 对启动器脚本保留一个短暂的就绪后观察窗口 +wt config set add.serve-dev.settle-timeout 8 + # 更改分支前缀 wt config set add.branch-prefix "feature/" @@ -142,6 +148,9 @@ wt config list 配置按项目存储在 `~/.worktree.sh/projects//config.kv` 中。 +- `add.serve-dev.ready-timeout` 控制 `wt add` 等待目标端口开始监听的最长秒数。 +- `add.serve-dev.settle-timeout` 会在端口就绪后再短暂观察一段时间,避免 `start-smart.sh` 这类包装启动脚本被误判为失败。 + ## 使用场景 ### 多开 UI 抽卡 diff --git a/bin/lib/runtime.sh b/bin/lib/runtime.sh index 450a524..d874b09 100644 --- a/bin/lib/runtime.sh +++ b/bin/lib/runtime.sh @@ -22,6 +22,8 @@ readonly CONFIG_DEFAULT_WORKING_REPO_PATH="${HOME}/Developer/your-project" readonly CONFIG_DEFAULT_WORKTREE_ADD_BRANCH_PREFIX="feat/" readonly CONFIG_DEFAULT_SERVE_DEV_LOGGING_PATH="tmp" readonly CONFIG_DEFAULT_SERVE_DEV_ENABLED=1 +readonly CONFIG_DEFAULT_SERVE_DEV_READY_TIMEOUT=60 +readonly CONFIG_DEFAULT_SERVE_DEV_SETTLE_TIMEOUT=5 readonly CONFIG_DEFAULT_INSTALL_DEPS_ENABLED=1 readonly CONFIG_DEFAULT_COPY_ENV_ENABLED=1 readonly CONFIG_DEFAULT_INSTALL_DEPS_COMMAND="" @@ -38,6 +40,8 @@ WORKTREE_NAME_PREFIX="" WORKTREE_BRANCH_PREFIX="" SERVE_DEV_LOGGING_PATH="" SERVE_DEV_ENABLED=0 +SERVE_DEV_READY_TIMEOUT=0 +SERVE_DEV_SETTLE_TIMEOUT=0 INSTALL_DEPS_ENABLED=0 COPY_ENV_ENABLED=0 INSTALL_DEPS_COMMAND="" @@ -2370,6 +2374,12 @@ config_default_value() { add.serve-dev.logging-path) printf '%s\n' "$CONFIG_DEFAULT_SERVE_DEV_LOGGING_PATH" ;; + add.serve-dev.ready-timeout) + printf '%s\n' "$CONFIG_DEFAULT_SERVE_DEV_READY_TIMEOUT" + ;; + add.serve-dev.settle-timeout) + printf '%s\n' "$CONFIG_DEFAULT_SERVE_DEV_SETTLE_TIMEOUT" + ;; remove.pre-command) printf '%s\n' "$CONFIG_DEFAULT_REMOVE_PRE_COMMAND" ;; @@ -2648,6 +2658,14 @@ init_settings_apply_from_sources() { SERVE_DEV_COMMAND="$value" fi + if value=$(config_get "add.serve-dev.ready-timeout" 2> /dev/null); then + [ -n "$value" ] && SERVE_DEV_READY_TIMEOUT="$value" + fi + + if value=$(config_get "add.serve-dev.settle-timeout" 2> /dev/null); then + [ -n "$value" ] && SERVE_DEV_SETTLE_TIMEOUT="$value" + fi + if value=$(config_get "add.post-command" 2> /dev/null); then ADD_POST_COMMAND="$value" fi @@ -2702,6 +2720,8 @@ init_settings_defaults() { WORKING_REPO_PATH="$CONFIG_DEFAULT_WORKING_REPO_PATH" SERVE_DEV_LOGGING_PATH="$CONFIG_DEFAULT_SERVE_DEV_LOGGING_PATH" SERVE_DEV_ENABLED="$CONFIG_DEFAULT_SERVE_DEV_ENABLED" + SERVE_DEV_READY_TIMEOUT="$CONFIG_DEFAULT_SERVE_DEV_READY_TIMEOUT" + SERVE_DEV_SETTLE_TIMEOUT="$CONFIG_DEFAULT_SERVE_DEV_SETTLE_TIMEOUT" INSTALL_DEPS_ENABLED="$CONFIG_DEFAULT_INSTALL_DEPS_ENABLED" COPY_ENV_ENABLED="$CONFIG_DEFAULT_COPY_ENV_ENABLED" WORKTREE_BRANCH_PREFIX="$CONFIG_DEFAULT_WORKTREE_ADD_BRANCH_PREFIX" @@ -4324,17 +4344,25 @@ start_dev_server() { fi if [ -n "$port" ]; then - local ready_timeout="${WT_DEV_READY_TIMEOUT:-60}" - local settle_timeout="${WT_DEV_SETTLE_TIMEOUT:-5}" + local ready_timeout="$SERVE_DEV_READY_TIMEOUT" + local settle_timeout="$SERVE_DEV_SETTLE_TIMEOUT" case "$ready_timeout" in '' | *[!0-9]*) - ready_timeout=60 + if [ -n "${WT_DEV_READY_TIMEOUT:-}" ] && [[ ! "${WT_DEV_READY_TIMEOUT:-}" =~ [^0-9] ]]; then + ready_timeout="${WT_DEV_READY_TIMEOUT}" + else + ready_timeout="$CONFIG_DEFAULT_SERVE_DEV_READY_TIMEOUT" + fi ;; esac case "$settle_timeout" in '' | *[!0-9]*) - settle_timeout=5 + if [ -n "${WT_DEV_SETTLE_TIMEOUT:-}" ] && [[ ! "${WT_DEV_SETTLE_TIMEOUT:-}" =~ [^0-9] ]]; then + settle_timeout="${WT_DEV_SETTLE_TIMEOUT}" + else + settle_timeout="$CONFIG_DEFAULT_SERVE_DEV_SETTLE_TIMEOUT" + fi ;; esac @@ -5116,6 +5144,8 @@ cmd_add() { config_print_effective() { local has_language_line=0 local has_theme_line=0 + local has_serve_ready_timeout_line=0 + local has_serve_settle_timeout_line=0 if [ -f "$CONFIG_FILE" ]; then if [ -s "$CONFIG_FILE" ]; then @@ -5125,6 +5155,12 @@ config_print_effective() { if grep -q '^theme=' "$CONFIG_FILE" 2> /dev/null; then has_theme_line=1 fi + if grep -q '^add\.serve-dev\.ready-timeout=' "$CONFIG_FILE" 2> /dev/null; then + has_serve_ready_timeout_line=1 + fi + if grep -q '^add\.serve-dev\.settle-timeout=' "$CONFIG_FILE" 2> /dev/null; then + has_serve_settle_timeout_line=1 + fi cat "$CONFIG_FILE" else info "$(msg config_list_empty "$CONFIG_FILE")" @@ -5158,6 +5194,28 @@ config_print_effective() { printf 'theme=%s\n' "$effective_theme" fi + local effective_ready_timeout + if effective_ready_timeout=$(config_get_or_default "add.serve-dev.ready-timeout" 2> /dev/null); then + : + else + effective_ready_timeout="$CONFIG_DEFAULT_SERVE_DEV_READY_TIMEOUT" + fi + + if [ "$has_serve_ready_timeout_line" -eq 0 ]; then + printf 'add.serve-dev.ready-timeout=%s\n' "$effective_ready_timeout" + fi + + local effective_settle_timeout + if effective_settle_timeout=$(config_get_or_default "add.serve-dev.settle-timeout" 2> /dev/null); then + : + else + effective_settle_timeout="$CONFIG_DEFAULT_SERVE_DEV_SETTLE_TIMEOUT" + fi + + if [ "$has_serve_settle_timeout_line" -eq 0 ]; then + printf 'add.serve-dev.settle-timeout=%s\n' "$effective_settle_timeout" + fi + return 0 } @@ -5189,6 +5247,8 @@ wt config - 查看或更新 worktree.sh 配置 add.serve-dev.enabled 是否在 wt add 后启动开发服务 add.serve-dev.command 启动开发服务的命令(留空则自动推断) add.serve-dev.logging-path Dev 服务日志所在子目录(默认: tmp) + add.serve-dev.ready-timeout 等待端口开始监听的秒数(默认: 60) + add.serve-dev.settle-timeout 端口就绪后继续观察的秒数(默认: 5,用于兼容短命启动脚本) theme wt list 的显示主题(box 或 sage,默认 box,可用 "wt theme" 切换) 语言与主题: @@ -5221,6 +5281,8 @@ Supported keys: add.serve-dev.enabled Whether wt add starts the dev service add.serve-dev.command Command used to start the dev service (empty = auto-detect) add.serve-dev.logging-path Subdirectory for dev logs (default: tmp) + add.serve-dev.ready-timeout Seconds to wait for the dev port to start listening (default: 60) + add.serve-dev.settle-timeout Extra seconds to observe the port after readiness (default: 5; useful for short-lived launcher scripts) theme Theme for wt list output (box or sage; default box). Use "wt theme" to change. Language & theme: diff --git a/config-example.kv b/config-example.kv index 7380418..5f1de8b 100644 --- a/config-example.kv +++ b/config-example.kv @@ -8,5 +8,7 @@ add.install-deps.command=npm ci add.serve-dev.enabled=true add.serve-dev.command=npm run dev add.serve-dev.logging-path=tmp +add.serve-dev.ready-timeout=60 +add.serve-dev.settle-timeout=5 add.post-command=bash "$WT_PROJECT_ROOT/scripts/post-add.sh" remove.pre-command=bash "$WT_PROJECT_ROOT/scripts/pre-remove.sh"