File size: 25,821 Bytes
31b91d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
#!/usr/bin/env bash
# ============================================================
# OpenClaw 工具箱 — 本地开发工具集
#
# 交互式菜单,选择要执行的功能。
# 也支持命令行直接调用:./main.sh <命令> [参数...]
#
# 命令列表:
#   bootstrap       交互式部署 OpenClaw 到 HF Space
#   rebuild-space   推送代码到 Space 并触发重建
#   restart-space   重启 Space
#   pause-space     暂停 Space
#   factory-rebuild Factory 重建 Space(删除重建)
#   cleanup         批量清理 Dataset 中的旧备份
#   find-backup     查找最佳备份(可选清理旧备份)
#   rm-hf           删除 HF 仓库/文件/存储
#   hf-backup       HF Dataset 本地↔远程交互备份(快照/验证/清理/恢复)
#   hf-account      HF 多账号管理(添加/切换/删除账号)
#   storage         HF 存储工具(上传/下载/同步)
# ============================================================

set -euo pipefail

# ---- Fix terminal for interactive input ----
if [[ -t 0 ]]; then
    stty sane 2>/dev/null || true
fi

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"

# 引入 HF 用户 helper
# shellcheck source=/dev/null
source "$SCRIPT_DIR/_hf_user.sh" 2>/dev/null || true

# 获取默认 HF 用户名(用于示例提示)
get_default_hf_user() {
  if declare -f get_hf_username >/dev/null 2>&1; then
    local u
    u="$(get_hf_username 2>/dev/null)" || true
    if [[ -n "$u" ]]; then
      printf '%s' "$u"
      return 0
    fi
  fi
  return 1
}

# 获取默认 Space 或 Dataset 名(基于当前目录名)
get_default_repo_name() {
  basename "$(pwd)" 2>/dev/null || true
}

# ---- HF 账号切换提示 ----
prompt_yes_no() {
    local prompt="$1"
    local default="${2:-}"
    local choice=""
    local answer=""

    while [[ -z "$answer" ]]; do
        if [[ -t 0 ]]; then
            if [[ -n "$default" ]]; then
                read -r -p "$prompt (Y/n) [$default]: " choice || true
            else
                read -r -p "$prompt (y/N) [$default]: " choice || true
            fi
        else
            choice="$default"
        fi
        choice="$(printf '%s' "$choice" | tr -d '\r\n' | tr '[:upper:]' '[:lower:]')"
        if [[ -z "$choice" ]]; then
            choice="$default"
        fi
        case "$choice" in
            y|yes) answer="yes" ;;
            n|no) answer="no" ;;
            *) printf 'Please answer y or n.\n' >&2 ;;
        esac
    done
    printf '%s' "$answer"
}

prompt_secret() {
    local prompt="$1"
    local value=""
    local char=""

    if [[ -t 0 && -t 1 ]]; then
        printf '%s: ' "$prompt" >&2
        while IFS= read -r -s -n 1 char; do
            if [[ -z "$char" ]]; then
                break
            fi
            case "$char" in
                $'\e') break ;;
                '') break ;;
                *) value+="$char"; printf '*' >&2 ;;
            esac
        done
        printf '\n' >&2
    fi
    printf '%s' "$value"
}

prompt_hf_account_switch() {
    local current_username=""
    local hf_whoami_output

    hf_whoami_output="$(hf auth whoami 2>&1 || true)"
    current_username="$(printf '%s\n' "$hf_whoami_output" | sed -nE 's/^[[:space:]]*user:[[:space:]]*([^[:space:]]+).*/\1/p' | head -n 1)"
    if [[ -z "$current_username" ]]; then
        current_username="$(printf '%s\n' "$hf_whoami_output" | sed -nE 's/.*[Ll]ogged in as[[:space:]]+([^[:space:]]+).*/\1/p' | head -n 1)"
    fi

    if [[ -n "$current_username" ]] && [[ -t 0 ]]; then
        local use_current
        use_current="$(prompt_yes_no "HF CLI is logged in as '$current_username'. Use this user?" "y")"
        if [[ "$use_current" == "yes" ]]; then
            return 0
        fi
    fi

    while true; do
        if [[ -t 0 ]]; then
            local token
            token="$(prompt_secret "Enter HF_TOKEN to switch account")"
            if [[ -z "$token" ]]; then
                printf '[WARN] Empty token, please try again or press Ctrl+C to cancel\n' >&2
                continue
            fi
            local login_output
            login_output="$(hf auth login --token "$token" 2>&1)"
            if [[ $? -eq 0 ]]; then
                local verify_output
                verify_output="$(hf auth whoami 2>&1 || true)"
                local new_username
                new_username="$(printf '%s\n' "$verify_output" | sed -nE 's/^[[:space:]]*user:[[:space:]]*([^[:space:]]+).*/\1/p' | head -n 1)"
                if [[ -z "$new_username" ]]; then
                    new_username="$(printf '%s\n' "$verify_output" | sed -nE 's/.*[Ll]ogged in as[[:space:]]+([^[:space:]]+).*/\1/p' | head -n 1)"
                fi
                if [[ -n "$current_username" ]] && [[ -n "$new_username" ]] && [[ "$current_username" != "$new_username" ]]; then
                    printf '[INFO] HF account switched: %s -> %s\n' "$current_username" "$new_username"
                elif [[ -n "$new_username" ]]; then
                    printf '[INFO] HF login successful as: %s\n' "$new_username"
                else
                    printf '[INFO] HF login successful\n'
                fi
                return 0
            else
                printf '[ERROR] HF login failed: %s\n' "$login_output" >&2
                continue
            fi
        else
            return 1
        fi
    done
}

# ---- 检测 uv 并设置 Python 命令 ----
if command -v uv &>/dev/null && [[ -f "$REPO_ROOT/pyproject.toml" ]]; then
  PYTHON="uv run python3"
else
  PYTHON="python3"
fi

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'

info()  { printf "${GREEN}%s${NC}\n" "$*"; }
warn()  { printf "${YELLOW}%s${NC}\n" "$*" >&2; }
error() { printf "${RED}%s${NC}\n" "$*" >&2; }
title() { printf "\n${CYAN}${BOLD}%s${NC}\n" "$*"; }

# ---- List datasets owned by the current HF user ----
# Echoes dataset IDs (one per line) on stdout. Empty stdout = no datasets
# (or hf CLI not logged in).
list_user_datasets() {
    local user
    user="$(get_default_hf_user 2>/dev/null)" || return 1
    hf datasets list --author "$user" 2>/dev/null \
        | grep -E '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+[[:space:]]' \
        | awk '{print $1}' \
        | sort
}

# ---- Generic numbered picker ----
# Args:
#   $1 = prompt text (shown above the list)
#   $2 = cancel label (e.g. "取消")
#   $3 = "yes" / "no" — whether to add a manual-input option
#   $@ = items to choose from (each becomes one row)
# Behavior:
#   - All UI text goes to stderr; the chosen item is on stdout.
#   - Re-prompts on invalid input; non-numeric / out-of-range → warn.
#   - Returns 0 + item on stdout on success, 1 on cancel, 2 on non-TTY.
pick_from_list() {
    local prompt="$1"
    local cancel_label="$2"
    local allow_manual="$3"
    shift 3
    local -a items=("$@")

    if [[ ! -t 0 ]]; then
        error "Picker requires a TTY"
        return 2
    fi

    if [[ ${#items[@]} -eq 0 ]]; then
        warn "没有可选项"
        return 1
    fi

    echo "$prompt:" >&2
    local i
    for i in "${!items[@]}"; do
        printf "  %d) %s\n" "$((i+1))" "${items[$i]}" >&2
    done
    printf "  c) %s\n" "$cancel_label" >&2
    if [[ "$allow_manual" == "yes" ]]; then
        printf "  m) 手动输入 ID\n" >&2
    fi

    while true; do
        local max="${#items[@]}"
        local prompt_suffix="1-$max, c"
        [[ "$allow_manual" == "yes" ]] && prompt_suffix="$prompt_suffix, m"
        read -r -p "请选择 [$prompt_suffix]: " choice
        case "$choice" in
            c|"") return 1 ;;
            m)
                if [[ "$allow_manual" == "yes" ]]; then
                    read -r -p "请输入 ID: " manual
                    if [[ -n "$manual" ]]; then
                        printf '%s' "$manual"
                        return 0
                    fi
                    warn "输入不能为空"
                else
                    warn "无效选择"
                fi
                ;;
            *)
                if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= max )); then
                    printf '%s' "${items[$((choice-1))]}"
                    return 0
                fi
                warn "无效选择"
                ;;
        esac
    done
}

# ---- Pick a dataset (current user's datasets, with manual fallback) ----
# Falls back to manual entry if the user has no datasets (or hf CLI
# isn't logged in).
pick_dataset() {
    local prompt="${1:-选择 dataset}"
    local -a list
    mapfile -t list < <(list_user_datasets)

    if [[ ${#list[@]} -eq 0 || -z "${list[0]:-}" ]]; then
        warn "账号下没有 dataset(或 hf CLI 未登录),请手动输入"
        read -r -p "Dataset ID (user/name): " manual
        if [[ -n "$manual" ]]; then
            printf '%s' "$manual"
            return 0
        fi
        return 1
    fi

    pick_from_list "$prompt" "取消" "yes" "${list[@]}"
}

# ============================================================
# 命令实现
# ============================================================
cmd_bootstrap() {
  info "▶ 启动交互式部署引导..."
  exec "$SCRIPT_DIR/bootstrap-hf.sh"
}

cmd_rebuild() {
  local default_user default_repo
  default_user="$(get_default_hf_user)" || default_user="<your-hf-username>"
  default_repo="$(get_default_repo_name)" || default_repo="<space-name>"

  if [ $# -gt 0 ]; then
    exec "$SCRIPT_DIR/rebuild-space.sh" "$@"
  fi
  read -r -p "Space ID (例如 ${default_user}/${default_repo}): " repo
  read -r -p "HF_TOKEN (留空从文件读取): " token
  exec "$SCRIPT_DIR/rebuild-space.sh" "${repo}" ${token:+"$token"}
}

cmd_cleanup() {
  local default_user default_repo
  default_user="$(get_default_hf_user)" || default_user="<your-hf-username>"
  default_repo="$(get_default_repo_name)" || default_repo="<dataset-name>"

  if [ $# -gt 1 ]; then
    exec "$SCRIPT_DIR/delete-backups.sh" "$@"
  fi
  local repo
  repo=$(pick_dataset "选择 dataset (提示示例: ${default_user}/${default_repo}-backup)") || return 1
  read -r -p "删除此日期及更早的备份 (YYYYMMDD): " date
  read -r -p "HF_TOKEN (留空从文件读取): " token
  exec "$SCRIPT_DIR/delete-backups.sh" "${repo}" "${date}" ${token:+"$token"}
}

cmd_find_backup() {
  local default_user default_repo
  default_user="$(get_default_hf_user)" || default_user="<your-hf-username>"
  default_repo="$(get_default_repo_name)" || default_repo="<dataset-name>"

  if [ $# -gt 0 ]; then
    exec $PYTHON "$SCRIPT_DIR/find-largest-backup.py" "$@"
  fi
  local repo
  repo=$(pick_dataset "选择 dataset (提示示例: ${default_user}/${default_repo}-backup)") || return 1
  shift_args=()
  read -r -p "详细模式? (y/n, 默认 n): " verbose
  [[ "$verbose" == "y" ]] && shift_args+=("--verbose")
  read -r -p "找到最佳备份后清理旧备份? (y/n, 默认 n): " del
  [[ "$del" == "y" ]] && shift_args+=("--delete-before")
  if [[ "${del}" == "y" ]]; then
    read -r -p "清理后跳过超级压缩(真正释放存储空间)? (y/n, 默认 n): " nosquash
    [[ "$nosquash" == "y" ]] && shift_args+=("--no-super-squash")
  fi
  exec $PYTHON "$SCRIPT_DIR/find-largest-backup.py" "$repo" "${shift_args[@]}"
}

cmd_delete_hf() {
  while true; do
    echo "删除类型:"
    echo "  1) 删除整个仓库 (Space/Dataset/Model)"
    echo "  2) 按通配符批量删除文件"
    echo "  3) 删除 Space 持久存储"
    echo "  9) 返回主菜单"
    read -r -p "请选择 [1-3, 9]: " t
    echo ""
    case "$t" in
      1)
        read -r -p "Repo ID: " rid
        read -r -p "类型 (space/dataset/model): " rtype
        "$SCRIPT_DIR/delete-hf.py" repo "$rid" --type "$rtype"
        echo ""; echo "按回车继续..."; read -r ;;
      2)
        read -r -p "Repo ID: " rid
        read -r -p "通配符模式: " pat
        "$SCRIPT_DIR/delete-hf.py" files "$rid" --pattern "$pat"
        echo ""; echo "按回车继续..."; read -r ;;
      3)
        read -r -p "Space ID: " sid
        "$SCRIPT_DIR/delete-hf.py" storage "$sid"
        echo ""; echo "按回车继续..."; read -r ;;
      9) break ;;
      *) error "无效选择" ;;
    esac
  done
}

cmd_hf_backup() {
  local default_user
  default_user="$(get_default_hf_user)" || default_user="<your-hf-username>"
  while true; do
    echo ""
    echo "HF Dataset 备份工具"
    echo "===================="
    echo "  1) backup   创建增量备份"
    echo "  2) restore  从本地恢复到 HF"
    echo "  3) verify   验证备份完整性"
    echo "  4) prune    清理旧快照(释放空间)"
    echo "  5) snapshots 查看快照列表"
    echo "  6) info     查看 HF Dataset 信息"
    echo "  9) 返回主菜单"
    read -r -p "请选择 [1-6, 9]: " op
    echo ""
    case "$op" in
      1)
        rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/<dataset>-backup)") || continue
        "$SCRIPT_DIR/hf-dataset-mgr.sh" backup "$rid"
        echo ""; echo "按回车继续..."; read -r ;;
      2)
        rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/<dataset>-backup)") || continue
        read -r -p "指定快照名? (留空使用最新): " snap
        if [[ -n "$snap" ]]; then
          "$SCRIPT_DIR/hf-dataset-mgr.sh" restore "$rid" --snap "$snap"
        else
          "$SCRIPT_DIR/hf-dataset-mgr.sh" restore "$rid"
        fi
        echo ""; echo "按回车继续..."; read -r ;;
      3)
        rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/<dataset>-backup)") || continue
        "$SCRIPT_DIR/hf-dataset-mgr.sh" verify "$rid"
        echo ""; echo "按回车继续..."; read -r ;;
      4)
        rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/<dataset>-backup)") || continue
        read -r -p "保留快照数 (默认 5): " keep
        read -r -p "清理后执行 Super-Squash? (y/n, 默认 y): " squash
        [[ "$squash" != "n" ]] && SQUASH="--squash" || SQUASH=""
        "$SCRIPT_DIR/hf-dataset-mgr.sh" prune "$rid" --keep "${keep:-5}" $SQUASH
        echo ""; echo "按回车继续..."; read -r ;;
      5)
        rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/<dataset>-backup)") || continue
        "$SCRIPT_DIR/hf-dataset-mgr.sh" snapshots "$rid"
        echo ""; echo "按回车继续..."; read -r ;;
      6)
        rid=$(pick_dataset "选择 dataset (提示示例: ${default_user}/<dataset>-backup)") || continue
        "$SCRIPT_DIR/hf-dataset-mgr.sh" info "$rid"
        echo ""; echo "按回车继续..."; read -r ;;
      9) break ;;
      *) error "无效选择" ;;
    esac
  done
}

cmd_hf_account() {
  while true; do
    echo ""
    echo "HF 账号管理工具"
    echo "================"
    echo "  1) add     添加/更新账号"
    echo "  2) list    列出所有账号"
    echo "  3) use     切换默认账号"
    echo "  4) current 显示当前账号"
    echo "  5) remove  删除账号"
    echo "  6) get-token 获取账号 token"
    echo "  9) 返回主菜单"
    read -r -p "请选择 [1-6, 9]: " op
    echo ""
    case "$op" in
      1)
        read -r -p "用户名: " name
        [[ -z "$name" ]] && { error "用户名不能为空"; continue; }
        read -r -p "Token (留空交互式输入): " token
        if [[ -z "$token" ]]; then
          "$SCRIPT_DIR/hf-account.sh" add "$name"
        else
          "$SCRIPT_DIR/hf-account.sh" add "$name" "$token"
        fi
        echo ""; echo "按回车继续..."; read -r ;;
      2) "$SCRIPT_DIR/hf-account.sh" list; echo ""; echo "按回车继续..."; read -r ;;
      3)
        "$SCRIPT_DIR/hf-account.sh" use
        echo ""; echo "按回车继续..."; read -r ;;
      4) "$SCRIPT_DIR/hf-account.sh" current; echo ""; echo "按回车继续..."; read -r ;;
      5)
        read -r -p "用户名: " name
        [[ -z "$name" ]] && { error "用户名不能为空"; continue; }
        "$SCRIPT_DIR/hf-account.sh" remove "$name"
        echo ""; echo "按回车继续..."; read -r ;;
      6)
        read -r -p "用户名 (留空当前账号): " name
        if [[ -z "$name" ]]; then
          "$SCRIPT_DIR/hf-account.sh" get-token
        else
          "$SCRIPT_DIR/hf-account.sh" get-token "$name"
        fi
        echo ""; echo "按回车继续..."; read -r ;;
      9) break ;;
      *) error "无效选择" ;;
    esac
  done
}

cmd_storage() {
  while true; do
    echo ""
    echo "存储操作"
    echo "========="
    echo "  1) 上传文件"
    echo "  2) 上传目录"
    echo "  3) 下载文件"
    echo "  4) 列出文件"
    echo "  5) 同步目录"
    echo "  9) 返回主菜单"
    read -r -p "请选择 [1-5, 9]: " op
    echo ""
    case "$op" in
      1)
        read -r -p "文件路径: " f
        read -r -p "目标路径 (留空同上): " d
        "$SCRIPT_DIR/hf-storage.sh" upload "$f" ${d:+"$d"}
        echo ""; echo "按回车继续..."; read -r ;;
      2)
        read -r -p "目录路径: " d
        read -r -p "前缀 (留空无前缀): " p
        "$SCRIPT_DIR/hf-storage.sh" upload-dir "$d" ${p:+"$p"}
        echo ""; echo "按回车继续..."; read -r ;;
      3)
        read -r -p "远程文件: " f
        read -r -p "本地路径 (留空当前目录): " l
        "$SCRIPT_DIR/hf-storage.sh" download "$f" ${l:+"$l"}
        echo ""; echo "按回车继续..."; read -r ;;
      4) "$SCRIPT_DIR/hf-storage.sh" list; echo ""; echo "按回车继续..."; read -r ;;
      5)
        read -r -p "本地目录: " l
        read -r -p "远程前缀 (留空无): " r
        "$SCRIPT_DIR/hf-storage.sh" sync "$l" ${r:+"$r"}
        echo ""; echo "按回车继续..."; read -r ;;
      9) break ;;
      *) error "无效选择" ;;
    esac
  done
}

cmd_restart_space() {
  local default_user default_repo
  default_user="$(get_default_hf_user)" || default_user="<your-hf-username>"
  default_repo="$(get_default_repo_name)" || default_repo="<space-name>"

  local repo="$1"
  if [ -z "$repo" ]; then
    read -r -p "Space ID (例如 ${default_user}/${default_repo}): " repo
  fi
  info "▶ 重启 Space: $repo"
  if hf spaces restart "$repo" 2>&1; then
    info "✓ 重启请求已发送"
  else
    error "✗ 重启失败"
    return 1
  fi
}

cmd_pause_space() {
  local default_user default_repo
  default_user="$(get_default_hf_user)" || default_user="<your-hf-username>"
  default_repo="$(get_default_repo_name)" || default_repo="<space-name>"

  local repo="$1"
  if [ -z "$repo" ]; then
    read -r -p "Space ID (例如 ${default_user}/${default_repo}): " repo
  fi
  warn "▶ 暂停 Space: $repo"
  warn "警告: Space 暂停后无法通过网络访问,需手动重启恢复"
  read -r -p "确认暂停? (y/n): " confirm
  [[ "$confirm" != "y" ]] && info "已取消" && return 0
  if hf spaces pause "$repo" 2>&1; then
    info "✓ Space 已暂停"
  else
    error "✗ 暂停失败"
    return 1
  fi
}

cmd_factory_rebuild() {
  local default_user default_repo
  default_user="$(get_default_hf_user)" || default_user="<your-hf-username>"
  default_repo="$(get_default_repo_name)" || default_repo="<space-name>"

  local repo="$1"
  if [ -z "$repo" ]; then
    read -r -p "Space ID (例如 ${default_user}/${default_repo}): " repo
  fi
  warn "▶ Factory 重建 Space: $repo"
  warn "警告: 这将删除 Space 后重新创建!所有持久存储数据将被清除!"
  read -r -p "确认重建? (输入 Space 名称确认): " confirm
  [[ "$confirm" != "${repo#*/}" ]] && error "名称不匹配,已取消" && return 1

  info "1/3 删除 Space..."
  $PYTHON -c "
from huggingface_hub import HfApi
import os
api = HfApi()
try:
    api.delete_repo('${repo}', repo_type='space')
    print('✓ 已删除')
except Exception as e:
    print(f'删除失败(可能不存在): {e}')
"
  sleep 3
  info "2/3 重新创建 Space..."
  hf repos create "$repo" --repo-type space --space-sdk docker --private
  info "3/3 上传代码..."
  exec "$SCRIPT_DIR/rebuild-space.sh" "$repo"
}

show_menu() {
  clear 2>/dev/null || true
  title "╔══════════════════════════════════════╗"
  title "║       OpenClaw 工具箱                ║"
  title "║       本地开发工具集                  ║"
  title "╚══════════════════════════════════════╝"
  echo ""
  info "  1) 🚀  部署到 HF Space"
  echo "      交互式全流程部署 OpenClaw 到 Hugging Face Space"
  echo ""
  info "  2) 📤  推送代码到 Space"
  echo "      将本地最新代码强制推送到 Space 并触发重建"
  echo ""
  info "  3) 🗑️  清理备份"
  echo "      删除 Dataset 中指定日期及更早的所有备份文件"
  echo ""
  info "  4) 🔍  查找最佳备份"
  echo "      综合评分找出最佳备份,可选清理旧备份"
  echo ""
  info "  5) 💾  HF Dataset 备份"
  echo "      本地↔远程交互备份,支持快照/验证/清理/恢复"
  echo ""
  info "  6) 👤  HF 账号管理"
  echo "      多账号切换,添加/删除/切换 HF 用户"
  echo ""
  info "  7) ❌  删除 HF 资源"
  echo "      删除仓库、批量删除文件、或删除 Space 持久存储"
  echo ""
  info "  8) 📦  存储管理"
  echo "      HuggingFace 存储工具:上传/下载/同步文件"
  echo ""
  info "  ── Space 运维 ──"
  echo ""
  info "  9) 🔄  重启 Space"
  echo "      发送重启请求到 HuggingFace Space"
  echo ""
  info " 10) ⏸️  暂停 Space"
  echo "      暂停 Space,暂停后无法网络访问"
  echo ""
  info " 11) 🏭  Factory 重建"
  echo "      删除 Space 后重新创建并上传代码(清除所有数据和持久存储)"
  echo ""
  info "  0) ❎  退出"
  echo ""
}

# ============================================================
# 交互菜单
# ============================================================
interactive_mode() {
  while true; do
    show_menu
    read -r -p "请选择 [0-11]: " choice
    echo ""
    case "$choice" in
      1) cmd_bootstrap ;;
      2) cmd_rebuild ;;
      3) cmd_cleanup ;;
      4) cmd_find_backup ;;
      5) cmd_hf_backup ;;
      6) cmd_hf_account ;;
      7) cmd_delete_hf ;;
      8) cmd_storage ;;
      9) cmd_restart_space ;;
      10) cmd_pause_space ;;
      11) cmd_factory_rebuild ;;
      0) info "再见!"; exit 0 ;;
      *) warn "无效选择,请重新输入"; sleep 1 ;;
    esac
  done
}

show_usage() {
  title "OpenClaw 工具箱 — 本地开发工具集"
  echo ""
  echo "用法: $0 [命令] [参数...]"
  echo ""
  echo "不带参数时启动交互式菜单。"
  echo ""
  info "命令模式:"
  echo "  $0 bootstrap                      交互式部署到 HF Space"
  echo "  $0 rebuild-space <ID> [TOKEN]     推送代码到 Space"
  echo "  $0 restart-space <ID>             重启 Space"
  echo "  $0 pause-space <ID>               暂停 Space"
  echo "  $0 factory-rebuild <ID>           Factory 重建 Space(删除重建)"
  echo "  $0 cleanup <ID> <日期> [TOKEN]    清理旧备份"
  echo "  $0 find-backup <ID> [选项]         查找最佳备份"
  echo "  $0 hf-backup <命令> [选项]         HF Dataset 本地↔远程交互备份"
  echo "  $0 hf-account <命令> [选项]        HF 多账号管理"
  echo "  $0 rm-hf <子命令> [参数]           删除 HF 资源"
  echo "  $0 storage <子命令> [参数]         存储管理"
  echo ""
  info "hf-backup 子命令:"
  echo "  $0 hf-backup backup <dataset>     创建增量备份"
  echo "  $0 hf-backup restore <dataset>     从本地恢复到 HF"
  echo "  $0 hf-backup verify <dataset>      验证备份完整性"
  echo "  $0 hf-backup prune <dataset>       清理旧快照"
  echo "  $0 hf-backup snapshots <dataset>  查看快照列表"
  echo "  $0 hf-backup info <dataset>       查看 Dataset 信息"
  echo ""
  info "hf-account 子命令:"
  echo "  $0 hf-account add <user> [token]   添加/更新账号"
  echo "  $0 hf-account list                   列出所有账号"
  echo "  $0 hf-account use [user]            切换默认账号(无参:交互式选择器)"
  echo "  $0 hf-account current                显示当前账号"
  echo "  $0 hf-account remove <user>         删除账号"
  echo "  $0 hf-account get-token [user]     获取 token"
  echo ""
  info "示例:"
  echo "  $0                                   # 交互菜单"
  echo "  $0 rebuild-space <user>/<space>   # 推送代码"
  echo "  $0 hf-backup backup <user>/<dataset>-backup  # 备份 dataset"
  echo "  $0 hf-account add <user> <token>  # 添加 HF 账号"
}

# ============================================================
# 主入口
# ============================================================
main() {
  # 检查并获取 HF token(支持账号切换)
  if [[ -t 0 ]]; then
    prompt_hf_account_switch || true
  fi

  if [ $# -eq 0 ]; then
    interactive_mode
    exit 0
  fi

  local cmd="$1"
  shift

  case "$cmd" in
    -h|--help|help)        show_usage ;;
    bootstrap)             cmd_bootstrap "$@" ;;
    rebuild-space|rebuild)  cmd_rebuild "$@" ;;
    restart-space)         cmd_restart_space "$@" ;;
    pause-space)           cmd_pause_space "$@" ;;
    factory-rebuild)        cmd_factory_rebuild "$@" ;;
    cleanup)                cmd_cleanup "$@" ;;
    find-backup|find)       cmd_find_backup "$@" ;;
    hf-backup|hfb)          cmd_hf_backup "$@" ;;
    hf-account|hfa)         cmd_hf_account "$@" ;;
    rm-hf|delete-hf)        cmd_delete_hf "$@" ;;
    storage)                cmd_storage "$@" ;;
    *)
      error "未知命令: $cmd"
      echo "使用 '$0 --help' 查看可用命令"
      exit 1
      ;;
  esac
}

main "$@"