@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.
@@ -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 $?