@tencent-ai/codebuddy-code 2.106.0 → 2.106.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/codebuddy-headless.js +244 -29
- package/dist/codebuddy.js +267 -52
- package/dist/web-ui/assets/index-1KNobXKH.js +859 -0
- package/dist/web-ui/index.html +1 -1
- package/dist/web-ui/sw.js +1 -1
- package/package.json +1 -1
- package/product.cloudhosted.json +2 -2
- package/product.internal.json +2 -2
- package/product.ioa.json +2 -2
- package/product.json +2 -2
- package/product.selfhosted.json +2 -2
- package/vendor/genie-trash/darwin-arm64 +0 -0
- package/vendor/genie-trash/darwin-x64 +0 -0
- package/vendor/genie-trash/linux-arm64 +0 -0
- package/vendor/genie-trash/linux-x64 +0 -0
- package/vendor/genie-trash/win32-x64.exe +0 -0
- package/vendor/shim/genie-safe-delete.cjs +589 -0
- package/vendor/shim/safe-bin/rm +18 -0
- package/vendor/shim/safe-bin/rmdir +18 -0
- package/vendor/shim/safe-bin/safe-delete-bash-env.sh +8 -0
- package/vendor/shim/safe-bin/safe-delete-common.sh +433 -0
- package/vendor/shim/safe-bin/unlink +18 -0
- package/vendor/shim/sitecustomize.py +471 -0
- package/dist/web-ui/assets/index-_HamWecR.js +0 -860
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# safe-delete rm entrypoint.
|
|
3
|
+
# Shared implementation lives in safe-delete-common.sh.
|
|
4
|
+
|
|
5
|
+
set -o pipefail
|
|
6
|
+
|
|
7
|
+
REAL_RM="${SAFE_DELETE_REAL_RM:-/bin/rm}"
|
|
8
|
+
[ -x "$REAL_RM" ] || REAL_RM="/usr/bin/rm"
|
|
9
|
+
|
|
10
|
+
# Zero-overhead: no sandbox session → pass through immediately
|
|
11
|
+
[ -z "$CODEBUDDY_SESSION_ID" ] && [ -z "$CLAUDE_SESSION_ID" ] && exec "$REAL_RM" "$@"
|
|
12
|
+
|
|
13
|
+
SELF_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
|
14
|
+
# shellcheck source=/dev/null
|
|
15
|
+
. "${SELF_DIR}/safe-delete-common.sh"
|
|
16
|
+
|
|
17
|
+
safe_delete_main rm "$@"
|
|
18
|
+
exit $?
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# safe-delete rmdir entrypoint.
|
|
3
|
+
# Shared implementation lives in safe-delete-common.sh.
|
|
4
|
+
|
|
5
|
+
set -o pipefail
|
|
6
|
+
|
|
7
|
+
REAL_RMDIR="${SAFE_DELETE_REAL_RMDIR:-/bin/rmdir}"
|
|
8
|
+
[ -x "$REAL_RMDIR" ] || REAL_RMDIR="/usr/bin/rmdir"
|
|
9
|
+
|
|
10
|
+
# Zero-overhead: no sandbox session → pass through immediately
|
|
11
|
+
[ -z "$CODEBUDDY_SESSION_ID" ] && [ -z "$CLAUDE_SESSION_ID" ] && exec "$REAL_RMDIR" "$@"
|
|
12
|
+
|
|
13
|
+
SELF_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
|
14
|
+
# shellcheck source=/dev/null
|
|
15
|
+
. "${SELF_DIR}/safe-delete-common.sh"
|
|
16
|
+
|
|
17
|
+
safe_delete_main rmdir "$@"
|
|
18
|
+
exit $?
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# genie-safe-delete bash environment — sourced via BASH_ENV for every bash subprocess.
|
|
2
|
+
# Defines rm/unlink/rmdir wrappers to intercept shell delete commands and move to OS trash.
|
|
3
|
+
if [ -n "${CODEBUDDY_SAFE_DELETE_BIN_DIR:-}" ]; then
|
|
4
|
+
rm() { "${CODEBUDDY_SAFE_DELETE_BIN_DIR}/rm" "$@"; }
|
|
5
|
+
unlink() { "${CODEBUDDY_SAFE_DELETE_BIN_DIR}/unlink" "$@"; }
|
|
6
|
+
rmdir() { "${CODEBUDDY_SAFE_DELETE_BIN_DIR}/rmdir" "$@"; }
|
|
7
|
+
export -f rm unlink rmdir
|
|
8
|
+
fi
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared safe-delete implementation for rm / unlink / rmdir wrappers.
|
|
3
|
+
|
|
4
|
+
set -o pipefail
|
|
5
|
+
|
|
6
|
+
REAL_RM="${SAFE_DELETE_REAL_RM:-/bin/rm}"
|
|
7
|
+
[ -x "$REAL_RM" ] || REAL_RM="/usr/bin/rm"
|
|
8
|
+
REAL_UNLINK="${SAFE_DELETE_REAL_UNLINK:-/usr/bin/unlink}"
|
|
9
|
+
[ -x "$REAL_UNLINK" ] || REAL_UNLINK="/bin/unlink"
|
|
10
|
+
REAL_RMDIR="${SAFE_DELETE_REAL_RMDIR:-/bin/rmdir}"
|
|
11
|
+
[ -x "$REAL_RMDIR" ] || REAL_RMDIR="/usr/bin/rmdir"
|
|
12
|
+
|
|
13
|
+
OS="$(uname -s)"
|
|
14
|
+
NOW="$(date -u +%Y-%m-%dT%H:%M:%S)"
|
|
15
|
+
SAFE_DELETE_BIN_DIR="$(CDPATH= cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
|
|
17
|
+
_safe_delete_one_line() {
|
|
18
|
+
printf '%s' "$1" | tr '\n\r' ' '
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_safe_delete_diag() {
|
|
22
|
+
echo "[safe-delete][diag] $*" >&2
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
is_dir_empty() {
|
|
26
|
+
local p="$1"
|
|
27
|
+
[ -z "$(ls -A "$p" 2>/dev/null)" ]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# ---- platform-specific trash helpers -----------------------------------------
|
|
31
|
+
|
|
32
|
+
trash_darwin() {
|
|
33
|
+
local p="$1"
|
|
34
|
+
|
|
35
|
+
# macOS fallback: use CoreServices API via python ctypes.
|
|
36
|
+
# This avoids AppleScript/Finder automation permissions in sandbox.
|
|
37
|
+
local py_out
|
|
38
|
+
py_out="$(python3 - "$p" 2>&1 <<'PY'
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
from ctypes import Structure, byref, c_char, c_char_p, cdll
|
|
42
|
+
from ctypes.util import find_library
|
|
43
|
+
|
|
44
|
+
path = sys.argv[1]
|
|
45
|
+
foundation_path = find_library("Foundation")
|
|
46
|
+
coreservices_path = find_library("CoreServices")
|
|
47
|
+
if not foundation_path or not coreservices_path:
|
|
48
|
+
print("Foundation/CoreServices unavailable", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
Foundation = cdll.LoadLibrary(foundation_path)
|
|
52
|
+
CoreServices = cdll.LoadLibrary(coreservices_path)
|
|
53
|
+
GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
|
|
54
|
+
GetMacOSStatusCommentString.restype = c_char_p
|
|
55
|
+
FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions
|
|
56
|
+
FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync
|
|
57
|
+
|
|
58
|
+
class FSRef(Structure):
|
|
59
|
+
_fields_ = [("hidden", c_char * 80)]
|
|
60
|
+
|
|
61
|
+
def status_to_message(code: int) -> str:
|
|
62
|
+
raw = GetMacOSStatusCommentString(code)
|
|
63
|
+
if not raw:
|
|
64
|
+
return f"OSStatus {code}"
|
|
65
|
+
return raw.decode("utf-8", errors="replace")
|
|
66
|
+
|
|
67
|
+
path_b = os.fsencode(path)
|
|
68
|
+
fp = FSRef()
|
|
69
|
+
rc = FSPathMakeRefWithOptions(path_b, 0x01, byref(fp), None)
|
|
70
|
+
if rc:
|
|
71
|
+
print(status_to_message(rc), file=sys.stderr)
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
rc = FSMoveObjectToTrashSync(byref(fp), None, 0)
|
|
75
|
+
if rc:
|
|
76
|
+
print(status_to_message(rc), file=sys.stderr)
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
PY
|
|
79
|
+
)"
|
|
80
|
+
local py_rc=$?
|
|
81
|
+
if [ $py_rc -ne 0 ]; then
|
|
82
|
+
_safe_delete_diag "python ctypes fallback failed: exit=$py_rc path=$p stderr=$(_safe_delete_one_line "$py_out")"
|
|
83
|
+
return 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# API 调用成功后追加存在性校验:路径仍在原处 = 失败,拒绝删除(fail-closed)
|
|
87
|
+
if [ -e "$p" ] || [ -L "$p" ]; then
|
|
88
|
+
_safe_delete_diag "python ctypes fallback reported success but file still exists: path=$p"
|
|
89
|
+
return 1
|
|
90
|
+
fi
|
|
91
|
+
_safe_delete_diag "python ctypes fallback succeeded: path=$p"
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
trash_linux() {
|
|
96
|
+
local p="$1"
|
|
97
|
+
local d="${XDG_DATA_HOME:-$HOME/.local/share}/Trash"
|
|
98
|
+
mkdir -p "$d/files" "$d/info" 2>/dev/null || return 1
|
|
99
|
+
|
|
100
|
+
local b="${p##*/}"
|
|
101
|
+
local tf="$d/files/$b" ti="$d/info/$b.trashinfo"
|
|
102
|
+
local n=1
|
|
103
|
+
while [ -e "$tf" ] || [ -e "$ti" ]; do
|
|
104
|
+
n=$((n + 1))
|
|
105
|
+
[ $n -gt 1000 ] && return 1
|
|
106
|
+
tf="$d/files/$b.$n"
|
|
107
|
+
ti="$d/info/$b.$n.trashinfo"
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
# freedesktop spec 要求 Path 字段百分比编码非 ASCII 字符。
|
|
111
|
+
# 已知限制:此处不编码,含中文/Unicode 文件名与 Node.js/Python 版本的
|
|
112
|
+
# trashinfo 格式不一致。bash 下无法可靠实现 UTF-8 字节级编码,暂且接受。
|
|
113
|
+
printf '[Trash Info]\nPath=%s\nDeletionDate=%s\n' "$p" "$NOW" > "$ti" || return 1
|
|
114
|
+
chmod 600 "$ti" 2>/dev/null
|
|
115
|
+
|
|
116
|
+
if mv "$p" "$tf" 2>/dev/null; then
|
|
117
|
+
return 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# EXDEV: cross-device — fall back to copy + delete
|
|
121
|
+
# 使用 $REAL_RM 而非裸 rm:PATH 中 safe-bin/ 已前置,裸 rm 会递归回自身
|
|
122
|
+
if [ -d "$p" ] && [ ! -L "$p" ]; then
|
|
123
|
+
cp -R "$p" "$tf" 2>/dev/null || { "$REAL_RM" -f "$ti" 2>/dev/null; return 1; }
|
|
124
|
+
"$REAL_RM" -rf "$p" 2>/dev/null || { "$REAL_RM" -f "$ti" 2>/dev/null; "$REAL_RM" -rf "$tf" 2>/dev/null; return 1; }
|
|
125
|
+
else
|
|
126
|
+
cp "$p" "$tf" 2>/dev/null || { "$REAL_RM" -f "$ti" 2>/dev/null; return 1; }
|
|
127
|
+
"$REAL_RM" -f "$p" 2>/dev/null || { "$REAL_RM" -f "$ti" 2>/dev/null; "$REAL_RM" -f "$tf" 2>/dev/null; return 1; }
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# ---- 解析 genie-trash binary 路径 ----
|
|
132
|
+
|
|
133
|
+
_gt_arch() {
|
|
134
|
+
case "$(uname -m)" in
|
|
135
|
+
arm64|aarch64) echo arm64 ;;
|
|
136
|
+
x86_64|amd64) echo x64 ;;
|
|
137
|
+
*) echo "$(uname -m)" ;;
|
|
138
|
+
esac
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# 将 Git Bash / MSYS2 / Cygwin 的 uname -s 输出映射为 win32,
|
|
142
|
+
# 使其与 genie-trash binary 文件名(win32-x64.exe)和 process.platform 一致。
|
|
143
|
+
_gt_platform() {
|
|
144
|
+
case "$(uname -s)" in
|
|
145
|
+
MINGW*|MSYS*|CYGWIN*) echo "win32" ;;
|
|
146
|
+
*) echo "$(uname -s | tr 'A-Z' 'a-z')" ;;
|
|
147
|
+
esac
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
GT_PLATFORM="$(_gt_platform)"
|
|
151
|
+
GT_ARCH="$(_gt_arch)"
|
|
152
|
+
GT_EXT=""
|
|
153
|
+
[ "$GT_PLATFORM" = "win32" ] && GT_EXT=".exe"
|
|
154
|
+
|
|
155
|
+
if [ -n "${GENIE_TRASH_DIR:-}" ]; then
|
|
156
|
+
TRASH_BIN="${GENIE_TRASH_DIR}/${GT_PLATFORM}-${GT_ARCH}${GT_EXT}"
|
|
157
|
+
else
|
|
158
|
+
TRASH_BIN="${SAFE_DELETE_BIN_DIR}/../../genie-trash/${GT_PLATFORM}-${GT_ARCH}${GT_EXT}"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
trash_windows() {
|
|
162
|
+
local p="$1"
|
|
163
|
+
# 转为 Windows 风格路径(powershell 不接受 / 分隔的路径)
|
|
164
|
+
local wp
|
|
165
|
+
wp="$(cygpath -w "$p" 2>/dev/null || echo "$p" | sed 's|/|\\|g')"
|
|
166
|
+
# 转义路径中的单引号:' → ''(PowerShell 单引号字符串内的转义方式)
|
|
167
|
+
local wp_escaped="${wp//\'/\'\'}"
|
|
168
|
+
|
|
169
|
+
# 用 powershell COM API 移入回收站,与 Node.js shim 的 trashOnWindows 一致
|
|
170
|
+
local ps_out
|
|
171
|
+
ps_out="$(powershell -NoProfile -NonInteractive -Command "
|
|
172
|
+
try {
|
|
173
|
+
\$targetPath = '${wp_escaped}'
|
|
174
|
+
if (Test-Path -LiteralPath \$targetPath -PathType Container) {
|
|
175
|
+
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
176
|
+
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(\$targetPath,
|
|
177
|
+
[Microsoft.VisualBasic.FileIO.UIOption]::OnlyErrorDialogs,
|
|
178
|
+
[Microsoft.VisualBasic.FileIO.RecycleOption]::SendToRecycleBin)
|
|
179
|
+
} else {
|
|
180
|
+
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
181
|
+
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile(\$targetPath,
|
|
182
|
+
[Microsoft.VisualBasic.FileIO.UIOption]::OnlyErrorDialogs,
|
|
183
|
+
[Microsoft.VisualBasic.FileIO.RecycleOption]::SendToRecycleBin)
|
|
184
|
+
}
|
|
185
|
+
Write-Output 'OK'
|
|
186
|
+
} catch {
|
|
187
|
+
Write-Error \$_.Exception.Message
|
|
188
|
+
}
|
|
189
|
+
" 2>&1)"
|
|
190
|
+
local ps_rc=$?
|
|
191
|
+
|
|
192
|
+
if [ $ps_rc -eq 0 ] && echo "$ps_out" | grep -q '^OK$'; then
|
|
193
|
+
# API 调用成功后追加存在性校验(fail-closed)
|
|
194
|
+
if [ -e "$p" ]; then
|
|
195
|
+
_safe_delete_diag "powershell COM reported success but file still exists: path=$p"
|
|
196
|
+
return 1
|
|
197
|
+
fi
|
|
198
|
+
_safe_delete_diag "powershell COM succeeded: path=$p"
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
_safe_delete_diag "powershell COM fallback failed: exit=$ps_rc path=$p stderr=$(_safe_delete_one_line "$ps_out")"
|
|
202
|
+
return 1
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
trash_one() {
|
|
206
|
+
local p="$1"
|
|
207
|
+
# 转为绝对路径:genie-trash binary 拒绝相对路径
|
|
208
|
+
# / 开头(Unix)或盘符开头(C:/... Windows)都视为绝对路径
|
|
209
|
+
case "$p" in
|
|
210
|
+
/*|[A-Za-z]:/*) ;;
|
|
211
|
+
*) p="$PWD/$p" ;;
|
|
212
|
+
esac
|
|
213
|
+
# 优先 native binary
|
|
214
|
+
if [ -x "$TRASH_BIN" ]; then
|
|
215
|
+
local native_path="$p"
|
|
216
|
+
if [ "$GT_PLATFORM" = "win32" ]; then
|
|
217
|
+
native_path="$(cygpath -w "$p" 2>/dev/null || echo "$p" | sed 's|/|\\|g')"
|
|
218
|
+
fi
|
|
219
|
+
local native_out
|
|
220
|
+
native_out="$("$TRASH_BIN" "$native_path" 2>&1)"
|
|
221
|
+
local native_rc=$?
|
|
222
|
+
if [ $native_rc -eq 0 ]; then
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
_safe_delete_diag "genie-trash failed: exit=$native_rc bin=$TRASH_BIN path=$p stderr=$(_safe_delete_one_line "$native_out")"
|
|
226
|
+
else
|
|
227
|
+
_safe_delete_diag "genie-trash unavailable: bin-not-executable path=$TRASH_BIN"
|
|
228
|
+
fi
|
|
229
|
+
# ---- 降级:当前平台实现 ----
|
|
230
|
+
case "$OS" in
|
|
231
|
+
Darwin) trash_darwin "$p" ;;
|
|
232
|
+
MINGW*|MSYS*|CYGWIN*) trash_windows "$p" ;;
|
|
233
|
+
Linux) trash_linux "$p" ;;
|
|
234
|
+
*) return 1 ;;
|
|
235
|
+
esac
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try_trash() {
|
|
239
|
+
local p="$1"
|
|
240
|
+
if trash_one "$p"; then
|
|
241
|
+
return 0
|
|
242
|
+
fi
|
|
243
|
+
echo "[safe-delete][SAFE_DELETE_FAIL_CLOSED] target=$p msg=trash-failed TRASH_BIN=${TRASH_BIN:-unset} GENIE_TRASH_DIR=${GENIE_TRASH_DIR:-unset}" >&2
|
|
244
|
+
return 1
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# ---- command mode dispatch ----------------------------------------------------
|
|
248
|
+
|
|
249
|
+
safe_delete_rm() {
|
|
250
|
+
local targets=()
|
|
251
|
+
local force=false
|
|
252
|
+
local recursive=false
|
|
253
|
+
local allow_dir=false
|
|
254
|
+
local interactive=false
|
|
255
|
+
local after_double_dash=false
|
|
256
|
+
local a
|
|
257
|
+
|
|
258
|
+
for a in "$@"; do
|
|
259
|
+
if $after_double_dash; then
|
|
260
|
+
targets+=("$a")
|
|
261
|
+
continue
|
|
262
|
+
fi
|
|
263
|
+
if [ "$a" = "--" ]; then
|
|
264
|
+
after_double_dash=true
|
|
265
|
+
continue
|
|
266
|
+
fi
|
|
267
|
+
case "$a" in
|
|
268
|
+
-f|--force) force=true ;;
|
|
269
|
+
-r|-R|--recursive) recursive=true ;;
|
|
270
|
+
-d|--dir|--directory) allow_dir=true ;;
|
|
271
|
+
-i|-I|--interactive|--interactive=*) interactive=true ;;
|
|
272
|
+
-[!-]*)
|
|
273
|
+
# 仅解析短选项组合(如 -rf/-fr/-di);不把长选项
|
|
274
|
+
# --preserve-root 误判为包含 -r。
|
|
275
|
+
[[ "$a" == *f* ]] && force=true
|
|
276
|
+
[[ "$a" == *r* || "$a" == *R* ]] && recursive=true
|
|
277
|
+
[[ "$a" == *d* ]] && allow_dir=true
|
|
278
|
+
[[ "$a" == *i* || "$a" == *I* ]] && interactive=true
|
|
279
|
+
;;
|
|
280
|
+
esac
|
|
281
|
+
case "$a" in
|
|
282
|
+
-*) : ;;
|
|
283
|
+
*) targets+=("$a") ;;
|
|
284
|
+
esac
|
|
285
|
+
done
|
|
286
|
+
|
|
287
|
+
# No targets (e.g. "rm --help") → let real rm handle
|
|
288
|
+
if [ ${#targets[@]} -eq 0 ]; then
|
|
289
|
+
exec "$REAL_RM" "$@"
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
# rm -i / --interactive 有 prompt 语义;不能回退真删,只能 fail-closed。
|
|
293
|
+
if $interactive; then
|
|
294
|
+
echo "rm: interactive mode is not supported by safe-delete" >&2
|
|
295
|
+
return 1
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
local _exit=0
|
|
299
|
+
local p
|
|
300
|
+
for p in "${targets[@]}"; do
|
|
301
|
+
if [ -e "$p" ] || [ -L "$p" ]; then
|
|
302
|
+
if [ -d "$p" ] && [ ! -L "$p" ]; then
|
|
303
|
+
if ! $recursive && ! $allow_dir; then
|
|
304
|
+
echo "rm: $p: is a directory" >&2
|
|
305
|
+
_exit=1
|
|
306
|
+
continue
|
|
307
|
+
fi
|
|
308
|
+
if ! $recursive && $allow_dir && ! is_dir_empty "$p"; then
|
|
309
|
+
echo "rm: $p: Directory not empty" >&2
|
|
310
|
+
_exit=1
|
|
311
|
+
continue
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
if ! try_trash "$p"; then
|
|
315
|
+
_exit=1
|
|
316
|
+
fi
|
|
317
|
+
elif $force; then
|
|
318
|
+
# -f: silently skip non-existent files (match real rm behavior)
|
|
319
|
+
:
|
|
320
|
+
else
|
|
321
|
+
# No -f: report error for non-existent files (match real rm behavior)
|
|
322
|
+
echo "rm: $p: No such file or directory" >&2
|
|
323
|
+
_exit=1
|
|
324
|
+
fi
|
|
325
|
+
done
|
|
326
|
+
return $_exit
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
safe_delete_unlink() {
|
|
330
|
+
local target=""
|
|
331
|
+
|
|
332
|
+
if [ "$#" -eq 0 ]; then
|
|
333
|
+
exec "$REAL_UNLINK" "$@"
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
if [ "$1" = "--" ]; then
|
|
337
|
+
if [ "$#" -ne 2 ]; then
|
|
338
|
+
exec "$REAL_UNLINK" "$@"
|
|
339
|
+
fi
|
|
340
|
+
target="$2"
|
|
341
|
+
else
|
|
342
|
+
case "$1" in
|
|
343
|
+
-*) exec "$REAL_UNLINK" "$@" ;;
|
|
344
|
+
esac
|
|
345
|
+
if [ "$#" -ne 1 ]; then
|
|
346
|
+
exec "$REAL_UNLINK" "$@"
|
|
347
|
+
fi
|
|
348
|
+
target="$1"
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
if [ ! -e "$target" ] && [ ! -L "$target" ]; then
|
|
352
|
+
echo "unlink: $target: No such file or directory" >&2
|
|
353
|
+
return 1
|
|
354
|
+
fi
|
|
355
|
+
if [ -d "$target" ] && [ ! -L "$target" ]; then
|
|
356
|
+
echo "unlink: $target: Is a directory" >&2
|
|
357
|
+
return 1
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
try_trash "$target"
|
|
361
|
+
return $?
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
safe_delete_rmdir() {
|
|
365
|
+
local targets=()
|
|
366
|
+
local after_double_dash=false
|
|
367
|
+
local has_option=false
|
|
368
|
+
local a
|
|
369
|
+
|
|
370
|
+
for a in "$@"; do
|
|
371
|
+
if $after_double_dash; then
|
|
372
|
+
targets+=("$a")
|
|
373
|
+
continue
|
|
374
|
+
fi
|
|
375
|
+
if [ "$a" = "--" ]; then
|
|
376
|
+
after_double_dash=true
|
|
377
|
+
continue
|
|
378
|
+
fi
|
|
379
|
+
case "$a" in
|
|
380
|
+
-*) has_option=true ;;
|
|
381
|
+
*) targets+=("$a") ;;
|
|
382
|
+
esac
|
|
383
|
+
done
|
|
384
|
+
|
|
385
|
+
# No targets (e.g. "rmdir --help") → let real rmdir handle
|
|
386
|
+
if [ ${#targets[@]} -eq 0 ]; then
|
|
387
|
+
exec "$REAL_RMDIR" "$@"
|
|
388
|
+
fi
|
|
389
|
+
# rmdir 的选项(-p/--ignore-fail-on-non-empty 等)会改变删除语义;
|
|
390
|
+
# 不能回退真删,只能 fail-closed。
|
|
391
|
+
if $has_option; then
|
|
392
|
+
echo "rmdir: options are not supported by safe-delete" >&2
|
|
393
|
+
return 1
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
local _exit=0
|
|
397
|
+
local p
|
|
398
|
+
for p in "${targets[@]}"; do
|
|
399
|
+
if [ ! -e "$p" ] && [ ! -L "$p" ]; then
|
|
400
|
+
echo "rmdir: $p: No such file or directory" >&2
|
|
401
|
+
_exit=1
|
|
402
|
+
continue
|
|
403
|
+
fi
|
|
404
|
+
if [ -L "$p" ] || [ ! -d "$p" ]; then
|
|
405
|
+
echo "rmdir: $p: Not a directory" >&2
|
|
406
|
+
_exit=1
|
|
407
|
+
continue
|
|
408
|
+
fi
|
|
409
|
+
if ! is_dir_empty "$p"; then
|
|
410
|
+
echo "rmdir: $p: Directory not empty" >&2
|
|
411
|
+
_exit=1
|
|
412
|
+
continue
|
|
413
|
+
fi
|
|
414
|
+
if ! try_trash "$p"; then
|
|
415
|
+
_exit=1
|
|
416
|
+
fi
|
|
417
|
+
done
|
|
418
|
+
return $_exit
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
safe_delete_main() {
|
|
422
|
+
local mode="$1"
|
|
423
|
+
shift
|
|
424
|
+
case "$mode" in
|
|
425
|
+
rm) safe_delete_rm "$@"; return $? ;;
|
|
426
|
+
unlink) safe_delete_unlink "$@"; return $? ;;
|
|
427
|
+
rmdir) safe_delete_rmdir "$@"; return $? ;;
|
|
428
|
+
*)
|
|
429
|
+
echo "[safe-delete] unknown mode: $mode" >&2
|
|
430
|
+
return 2
|
|
431
|
+
;;
|
|
432
|
+
esac
|
|
433
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# safe-delete unlink entrypoint.
|
|
3
|
+
# Shared implementation lives in safe-delete-common.sh.
|
|
4
|
+
|
|
5
|
+
set -o pipefail
|
|
6
|
+
|
|
7
|
+
REAL_UNLINK="${SAFE_DELETE_REAL_UNLINK:-/usr/bin/unlink}"
|
|
8
|
+
[ -x "$REAL_UNLINK" ] || REAL_UNLINK="/bin/unlink"
|
|
9
|
+
|
|
10
|
+
# Zero-overhead: no sandbox session → pass through immediately
|
|
11
|
+
[ -z "$CODEBUDDY_SESSION_ID" ] && [ -z "$CLAUDE_SESSION_ID" ] && exec "$REAL_UNLINK" "$@"
|
|
12
|
+
|
|
13
|
+
SELF_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
|
14
|
+
# shellcheck source=/dev/null
|
|
15
|
+
. "${SELF_DIR}/safe-delete-common.sh"
|
|
16
|
+
|
|
17
|
+
safe_delete_main unlink "$@"
|
|
18
|
+
exit $?
|