diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..bb8d3f3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,92 @@ +# Contributor Covenant 3.0 Code of Conduct + +## Our Pledge + +We pledge to make our community welcoming, safe, and equitable for all. + +We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. + + +## Encouraged Behaviors + +While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. + +With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: + +1. Respecting the **purpose of our community**, our activities, and our ways of gathering. +2. Engaging **kindly and honestly** with others. +3. Respecting **different viewpoints** and experiences. +4. **Taking responsibility** for our actions and contributions. +5. Gracefully giving and accepting **constructive feedback**. +6. Committing to **repairing harm** when it occurs. +7. Behaving in other ways that promote and sustain the **well-being of our community**. + + +## Restricted Behaviors + +We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. + +1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. +2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. +3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits. +4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. +5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. +6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. +7. Behaving in other ways that **threaten the well-being** of our community. + +### Other Restrictions + +1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. +2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. +3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. +4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. + + +## Reporting an Issue + +Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. + +When an incident does occur, it is important to report it promptly. To report a possible violation, please [open an issue](https://github.com/Silentely/esim-tools/issues) in this repository or contact the maintainers privately via email at Abner@cosr.eu.org. + +Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. + + +## Addressing and Repairing Harm + +If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. + +1) **Warning** + - **Event**: A violation involving a single incident or series of incidents. + - **Consequence**: A private, written warning from the Community Moderators. + - **Repair**: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. + +2) **Temporarily Limited Activities** + - **Event**: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. + - **Consequence**: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. + - **Repair**: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. + +3) **Temporary Suspension** + - **Event**: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. + - **Consequence**: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. + - **Repair**: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. + +4) **Permanent Ban** + - **Event**: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. + - **Consequence**: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. + - **Repair**: There is no possible repair in cases of this severity. + +This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. + + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). + +Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) + +For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). \ No newline at end of file diff --git a/README.md b/README.md index 0f4502b..2688b09 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,10 @@ open tests/test_simyo_esim.html ![](https://starchart.cc/Silentely/eSIM-Tools.svg) +## 贡献者公约 + +本项目遵循[贡献者公约](CODE_OF_CONDUCT.md)行为准则。 + ## 📄 许可证 - 本项目的所有代码除另有说明外,均按照 [MIT License](LICENSE) 发布。 diff --git a/README_EN.md b/README_EN.md index 7921a4c..006b959 100644 --- a/README_EN.md +++ b/README_EN.md @@ -317,6 +317,10 @@ If you encounter issues or have suggestions: ![](https://starchart.cc/Silentely/eSIM-Tools.svg) +## Code of Conduct + +This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). + ## 📄 License - All code in this project, unless otherwise stated, is released under the [MIT License](LICENSE). diff --git a/runtime-common.sh b/runtime-common.sh deleted file mode 100644 index 564d1d0..0000000 --- a/runtime-common.sh +++ /dev/null @@ -1,808 +0,0 @@ -#!/bin/bash -# -# Runtime common functions for fuckits -# Shared between main.sh (EN) and zh_main.sh (ZH) -# This file is sourced at runtime, not during build -# - -_fuck_json_escape() { - local input="$1" - if command -v python3 >/dev/null 2>&1; then - printf '%s' "$input" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read())[1:-1], end='')" - return - fi - if command -v node >/dev/null 2>&1; then - printf '%s' "$input" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d).slice(1,-1)))" - return - fi - # Minimal fallback: escape backslashes and double quotes only - printf '%s' "$input" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' -} - -_fuck_truthy() { - local value="${1:-}" - local normalized - normalized=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]') - case "$normalized" in - 1|true|yes|y|on|是|开|真) - return 0 - ;; - *) - return 1 - ;; - esac -} - -_fuck_security_level_value() { - case "$1" in - block) printf '3\n' ;; - challenge) printf '2\n' ;; - warn) printf '1\n' ;; - *) printf '0\n' ;; - esac -} - -_fuck_security_match_rule() { - local command="$1" - local table="$2" - local -a rules=() - - # SECURITY NOTE: This eval is SAFE because: - # - $table only receives hardcoded internal array names from callers within this script - # - Valid values: _FUCK_SECURITY_BLOCK_RULES, _FUCK_SECURITY_CHALLENGE_RULES, _FUCK_SECURITY_WARN_RULES - # - No user input can reach this variable; it's purely for dynamic array name resolution - # - This pattern is a standard Bash idiom for indirect array access (Bash 3.x compatible) - eval "rules=(\"\${${table}[@]}\")" - - local rule pattern reason - for rule in "${rules[@]}"; do - pattern=${rule%%|||*} - reason=${rule#*|||} - [ -z "$pattern" ] && continue - - if printf '%s' "$command" | grep -Eiq -- "$pattern"; then - printf '%s\n' "$reason" - return 0 - fi - done - - return 1 -} - -_fuck_security_is_whitelisted() { - local command="$1" - local whitelist="${FUCK_SECURITY_WHITELIST:-}" - - if [ -z "$whitelist" ]; then - return 1 - fi - - local normalized entry - normalized=$(printf '%s' "$whitelist" | tr ',' '\n') - - while IFS= read -r entry; do - entry=$(printf '%s' "$entry" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - [ -z "$entry" ] && continue - - # 前缀匹配:命令必须以白名单条目开头(或完全相等) - # 例如白名单 "ls" 匹配 "ls -la" 但不匹配 "vls" - if [[ "$command" == "$entry" ]] || [[ "$command" == "$entry "* ]]; then - return 0 - fi - done <<< "$normalized" - - return 1 -} - -_fuck_security_mode() { - local mode="${FUCK_SECURITY_MODE:-balanced}" - mode=$(printf '%s' "$mode" | tr '[:upper:]' '[:lower:]') - - case "$mode" in - strict) printf 'strict\n' ;; - off|disabled|none) printf 'off\n' ;; - balanced|default|"") printf 'balanced\n' ;; - *) printf 'balanced\n' ;; - esac -} - -_fuck_mark_static_cache_dirty() { - _FUCK_STATIC_CACHE_DIRTY=1 -} - -_fuck_load_static_cache() { - # Return early if cache is already loaded - if [ "${_FUCK_STATIC_CACHE_LOADED:-0}" -eq 1 ]; then - return 0 - fi - - _FUCK_STATIC_CACHE_LOADED=1 - - # Source cache file if it exists - if [ -f "$FUCK_SYSINFO_CACHE_FILE" ]; then - # shellcheck disable=SC1090 - source "$FUCK_SYSINFO_CACHE_FILE" || true - fi -} - -_fuck_persist_static_cache() { - # Only persist if cache is dirty - if [ "${_FUCK_STATIC_CACHE_DIRTY:-0}" -ne 1 ]; then - return 0 - fi - - # Ensure cache directory exists - local cache_dir - cache_dir=$(dirname "$FUCK_SYSINFO_CACHE_FILE") - if ! mkdir -p "$cache_dir" 2>/dev/null; then - return 1 - fi - - # Create temporary file for atomic write - local tmp_file - tmp_file=$(mktemp) || return 1 - - # Write cached variables to temporary file - { - printf '_FUCK_CACHED_DISTRO=%q\\n' "${_FUCK_CACHED_DISTRO:-}" - printf '_FUCK_CACHED_KERNEL=%q\\n' "${_FUCK_CACHED_KERNEL:-}" - printf '_FUCK_CACHED_ARCH=%q\\n' "${_FUCK_CACHED_ARCH:-}" - printf '_FUCK_CACHED_PKG_MANAGER=%q\\n' "${_FUCK_CACHED_PKG_MANAGER:-}" - } > "$tmp_file" - - # Atomic move to final location - if command mv -f -- "$tmp_file" "$FUCK_SYSINFO_CACHE_FILE" 2>/dev/null; then - _FUCK_STATIC_CACHE_DIRTY=0 - return 0 - else - # Clean up temporary file on failure - rm -f "$tmp_file" - return 1 - fi -} - -_fuck_audit_log() { - # Check if audit logging is enabled - if [ "${FUCK_AUDIT_LOG:-false}" != "true" ]; then - return 0 - fi - - local event="$1" - local command="$2" - local exit_code="${3:--}" - local timestamp - timestamp=$(date -u '+%Y-%m-%d %H:%M:%S UTC' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S') - local log_file="${FUCK_AUDIT_LOG_FILE:-$INSTALL_DIR/.audit.log}" - - # Ensure log directory exists - mkdir -p "$(dirname "$log_file")" 2>/dev/null || true - - # Sanitize command for logging (normalize newlines, escape delimiter, limit length) - local sanitized_cmd - local raw_len=${#command} - sanitized_cmd=$(printf '%s' "$command" | tr '\r\n' ' ' | sed 's/|/\\|/g' | head -c 200) - if [ "$raw_len" -gt 200 ]; then - sanitized_cmd="${sanitized_cmd}..." - fi - - # Write to log file (format: timestamp|user|event|exit_code|command) - printf '%s|%s|%s|%s|%s\n' "${timestamp}" "${USER:-unknown}" "${event}" "${exit_code}" "${sanitized_cmd}" >> "$log_file" 2>/dev/null || true - - # Secure the log file - chmod 600 "$log_file" 2>/dev/null || true -} - -_fuck_detect_distro() { - _fuck_load_static_cache - - # Return cached value if available - if [ -n "${_FUCK_CACHED_DISTRO:-}" ]; then - printf '%s\n' "$_FUCK_CACHED_DISTRO" - return 0 - fi - - local kernel_name distro id version pretty family - kernel_name=$(uname -s 2>/dev/null || printf 'unknown') - distro="unknown" - - # macOS detection - if [ "$kernel_name" = "Darwin" ]; then - local product version - product=$(sw_vers -productName 2>/dev/null || printf 'macOS') - product=$(printf '%s' "$product" | tr -d '\r\n') - version=$(sw_vers -productVersion 2>/dev/null || printf 'unknown') - version=$(printf '%s' "$version" | tr -d '\r\n') - distro="$product $version" - # Linux detection using /etc/os-release - elif [ -r /etc/os-release ]; then - id=$(grep -E '^ID=' /etc/os-release | head -n1 | cut -d= -f2 | tr -d '"' | tr '[:upper:]' '[:lower:]') - version=$(grep -E '^VERSION_ID=' /etc/os-release | head -n1 | cut -d= -f2 | tr -d '"') - pretty=$(grep -E '^PRETTY_NAME=' /etc/os-release | head -n1 | cut -d= -f2- | tr -d '"') - - # Determine OS family for better categorization - family="" - case "$id" in - ubuntu|debian) - family="Debian-based" - ;; - centos|rhel|rocky|almalinux|fedora) - family="RHEL-based" - ;; - arch|manjaro|endeavouros) - family="Arch-based" - ;; - esac - - # Format distribution string with family and version - if [ -n "$family" ]; then - distro="$family ${version:-}" - if [ -n "$pretty" ]; then - distro="$distro (${pretty})" - fi - else - distro="${pretty:-Linux $version}" - fi - else - distro="$kernel_name" - fi - - # Cache and return result - _FUCK_CACHED_DISTRO="$distro" - _fuck_mark_static_cache_dirty - printf '%s\n' "$distro" -} - -_fuck_detect_pkg_manager() { - _fuck_load_static_cache - - # Return cached value if available - if [ -n "${_FUCK_CACHED_PKG_MANAGER:-}" ]; then - printf '%s\n' "$_FUCK_CACHED_PKG_MANAGER" - return 0 - fi - - local manager="unknown" - - # Detect package manager in order of preference - if command -v apt-get &> /dev/null; then - manager="apt" - elif command -v yum &> /dev/null; then - manager="yum" - elif command -v dnf &> /dev/null; then - manager="dnf" - elif command -v pacman &> /dev/null; then - manager="pacman" - elif command -v zypper &> /dev/null; then - manager="zypper" - elif command -v brew &> /dev/null; then - manager="brew" - fi - - # Cache and return result - _FUCK_CACHED_PKG_MANAGER="$manager" - _fuck_mark_static_cache_dirty - printf '%s\n' "$manager" -} - -_fuck_get_architecture() { - _fuck_load_static_cache - - # Return cached value if available - if [ -n "${_FUCK_CACHED_ARCH:-}" ]; then - printf '%s\n' "$_FUCK_CACHED_ARCH" - return 0 - fi - - local arch - arch=$(uname -m 2>/dev/null || printf 'unknown') - arch=$(printf '%s' "$arch" | tr -d '\r\n') - - # Cache and return result - _FUCK_CACHED_ARCH="$arch" - _fuck_mark_static_cache_dirty - printf '%s\n' "$arch" -} - -_fuck_get_kernel_version() { - _fuck_load_static_cache - - # Return cached value if available - if [ -n "${_FUCK_CACHED_KERNEL:-}" ]; then - printf '%s\n' "$_FUCK_CACHED_KERNEL" - return 0 - fi - - local kernel - kernel=$(uname -sr 2>/dev/null || uname -s 2>/dev/null || printf 'unknown') - kernel=$(printf '%s' "$kernel" | tr -d '\r\n') - - # Cache and return result - _FUCK_CACHED_KERNEL="$kernel" - _fuck_mark_static_cache_dirty - printf '%s\n' "$kernel" -} - -_fuck_collect_tool_versions() { - local tools tool version result - tools="git docker npm kubectl curl wget" - result="" - - for tool in $tools; do - version="not-installed" - - if command -v "$tool" >/dev/null 2>&1; then - case "$tool" in - git|docker|curl|wget) - version=$("$tool" --version 2>/dev/null | head -n1) - ;; - npm) - version=$("$tool" --version 2>/dev/null | head -n1) - [ -n "$version" ] && version="npm $version" - ;; - kubectl) - version=$("$tool" version --client --short 2>/dev/null | head -n1) - ;; - esac - fi - - # Clean up version string - version=$(printf '%s' "${version:-unknown}" | tr '\r\n' ' ' | tr -s ' ' | sed -e 's/^ *//' -e 's/ *$//') - [ -z "$version" ] && version="unknown" - - result="$result$tool:$version; " - done - - # Remove trailing semicolon and space - result="${result%; }" - printf '%s' "$result" -} - -_fuck_append_config_hint() { - local key="$1" - local comment="$2" - local sample="$3" - local quoted="${4:-1}" - [ -f "$CONFIG_FILE" ] || return - if grep -Eq "^\\s*#?\\s*export\\s+$key" "$CONFIG_FILE"; then - return - fi - - local assignment - if [ "$quoted" = "1" ]; then - assignment="# export $key=\"$sample\"" - else - assignment="# export $key=$sample" - fi - - { - printf '\n' - printf '# %s\n' "$comment" - printf '%s\n' "$assignment" - } >> "$CONFIG_FILE" -} - -_fuck_define_aliases() { - local default_alias="fuck" - - if ! _fuck_truthy "${FUCK_DISABLE_DEFAULT_ALIAS:-0}"; then - alias "$default_alias"='_fuck_execute_prompt' - fi - - if [ -n "${FUCK_ALIAS:-}" ] && [ "$FUCK_ALIAS" != "$default_alias" ]; then - alias "$FUCK_ALIAS"='_fuck_execute_prompt' - fi -} - -_fuck_detect_dangerous_command() { - _fuck_security_evaluate_command "$1" -} - -_fuck_local_system_prompt() { - local sysinfo="$1" - if [ "$FUCKITS_LOCALE" = "zh" ]; then - printf '你是一个专业的 shell 命令生成器。用户会用自然语言描述他们想要完成的任务。你的任务是生成直接可执行的 shell 命令来完成用户的目标。 - -重要规则: -1. 用户输入是自然语言描述意图,不是命令参数。例如"列出目录"意思是执行 ls 命令,而不是 ls "列出目录" -2. 生成直接可执行的命令,不要生成带参数判断的脚本模板(如 if [ $# -eq 0 ]) -3. 对于简单任务直接返回单条命令,复杂任务可以是多行脚本 -4. 不要提供任何解释、注释、markdown 格式(比如 ```bash)或 shebang(例如 #!/bin/bash) - -示例: -- 用户说"列出目录" → 输出: ls -- 用户说"显示详细文件列表" → 输出: ls -la -- 用户说"查找大于10MB的文件" → 输出: find . -type f -size +10M - -用户的系统信息是:%s' "$sysinfo" - else - printf 'You are an expert shell command generator. Users describe tasks in natural language. Your task is to generate directly executable shell commands to accomplish their goals. - -Important rules: -1. User input is natural language intent, NOT command arguments. For example, "list directory" means run ls, not ls "list directory" -2. Generate directly executable commands, not script templates with parameter handling (like if [ $# -eq 0 ]) -3. For simple tasks return single commands, complex tasks can be multi-line scripts -4. Do not provide any explanation, comments, markdown formatting (like ```bash), or a shebang (e.g., #!/bin/bash) - -Examples: -- User says "list directory" → Output: ls -- User says "show detailed file list" → Output: ls -la -- User says "find files larger than 10MB" → Output: find . -type f -size +10M - -The user'"'"'s system info is: %s' "$sysinfo" - fi -} - -_fuck_secure_config_file() { - if [ -f "$CONFIG_FILE" ]; then - chmod 600 "$CONFIG_FILE" 2>/dev/null || true - fi -} - -_fuck_security_apply_mode() { - local mode="$1" - local severity="$2" - - case "$mode" in - strict) - case "$severity" in - warn) severity="challenge" ;; - challenge) severity="block" ;; - esac - ;; - esac - - printf '%s\n' "$severity" -} - -_fuck_security_prompt_phrase() { - local phrase="$1" - local input="" - - printf "%b> %b" "$C_BOLD" "$C_RESET" >&2 - - if [ -r /dev/tty ]; then - if ! IFS= read -r input < /dev/tty; then - printf "\n" >&2 - return 1 - fi - else - if ! IFS= read -r input; then - printf "\n" >&2 - return 1 - fi - fi - - printf "\n" >&2 - [ "$input" = "$phrase" ] -} - -_fuck_security_promote() { - local current="$1" - local candidate="$2" - local current_val candidate_val - - current_val=$(_fuck_security_level_value "$current") - candidate_val=$(_fuck_security_level_value "$candidate") - - if [ "$candidate_val" -gt "$current_val" ]; then - printf '%s\n' "$candidate" - else - printf '%s\n' "$current" - fi -} - -_fuck_should_use_local_api() { - if [ -n "${FUCK_OPENAI_API_KEY:-}" ]; then - return 0 - fi - return 1 -} - -# --- 以下为共享工具函数(从 main.sh/zh_main.sh 提取) --- - -# 安全验证配置文件,防止代码注入 -# 参数:$1 - 要验证的文件路径 -# 返回:0 如果安全,1 如果不安全或出错 -_fuck_validate_config_file() { - local file="$1" - - # 文件必须存在且可读 - if [ ! -f "$file" ] || [ ! -r "$file" ]; then - return 1 - fi - - # 检查文件权限 - 必须由当前用户拥有 - if [ "$(stat -c '%u' "$file" 2>/dev/null || stat -f '%u' "$file" 2>/dev/null)" != "$(id -u)" ]; then - echo -e "${C_RED}Config file not owned by current user, refusing to source.${C_RESET}" >&2 - return 1 - fi - - local line_num=0 - local line - - # 预定义正则表达式变量(zsh 兼容性:变量形式避免特殊字符解析问题) - local re_cmd_sub='\$\(' - local re_backtick='`' - local re_arith='\$\(\(' - local re_semi=';' - local re_and='&&' - local re_or='\|\|' - local re_pipe='\|' - local re_gt='>' - local re_lt='<' - local re_amp='&' - local re_esc='\\\$' - - while IFS= read -r line || [ -n "$line" ]; do - line_num=$((line_num + 1)) - - # 跳过空行 - if [ -z "$line" ] || [[ "$line" =~ ^[[:space:]]*$ ]]; then - continue - fi - - # 跳过注释行(以 # 开头,可有前导空白) - if [[ "$line" =~ ^[[:space:]]*# ]]; then - continue - fi - - # 检查危险的 shell 元字符和命令替换 - # 拒绝:$(), ``, $((), ;, &&, ||, | (管道), >, <, &, 换行转义 - if [[ "$line" =~ $re_cmd_sub ]] || \ - [[ "$line" =~ $re_backtick ]] || \ - [[ "$line" =~ $re_arith ]] || \ - [[ "$line" =~ $re_semi ]] || \ - [[ "$line" =~ $re_and ]] || \ - [[ "$line" =~ $re_or ]] || \ - [[ "$line" =~ $re_pipe ]] || \ - [[ "$line" =~ $re_gt ]] || \ - [[ "$line" =~ $re_lt ]] || \ - [[ "$line" =~ $re_amp ]] || \ - [[ "$line" =~ $re_esc ]]; then - _fuck_debug "Config validation failed at line $line_num: dangerous metacharacter detected" - echo -e "${C_RED}Unsafe config file: dangerous shell metacharacter at line $line_num${C_RESET}" >&2 - return 1 - fi - - # 只允许:export FUCK_*=... 或 FUCK_*=...(可有前导空白) - # 也允许:export FUCKITS_LOCALE=... - if [[ "$line" =~ ^[[:space:]]*(export[[:space:]]+)?(FUCK_[A-Z_]+|FUCKITS_LOCALE)= ]]; then - continue - fi - - # 拒绝其他任何内容 - _fuck_debug "Config validation failed at line $line_num: unrecognized pattern" - echo -e "${C_RED}Unsafe config file: unrecognized pattern at line $line_num${C_RESET}" >&2 - return 1 - done < "$file" - - return 0 -} - -# 验证后安全地加载配置文件 -# 参数:$1 - 要加载的文件路径 -# 返回:0 如果成功加载,1 如果验证失败或文件不存在 -_fuck_safe_source_config() { - local file="$1" - - if [ ! -f "$file" ]; then - return 0 # 没有文件也没关系 - fi - - if _fuck_validate_config_file "$file"; then - # shellcheck disable=SC1090 - source "$file" - return $? - else - echo -e "${C_YELLOW}Config file validation failed, skipping: $file${C_RESET}" >&2 - return 1 - fi -} - -# 找用户 shell 配置文件的辅助函数 -_installer_detect_profile() { - if [ -n "${SHELL:-}" ] && echo "$SHELL" | grep -q "zsh"; then - echo "$HOME/.zshrc" - elif [ -n "${SHELL:-}" ] && echo "$SHELL" | grep -q "bash"; then - echo "$HOME/.bashrc" - elif [ -f "$HOME/.profile" ]; then - # 兼容 sh, ksh 等 - echo "$HOME/.profile" - elif [ -f "$HOME/.zshrc" ]; then - # SHELL 变量没设置时的备用方案 - echo "$HOME/.zshrc" - elif [ -f "$HOME/.bashrc" ]; then - # SHELL 变量没设置时的备用方案 - echo "$HOME/.bashrc" - else - echo "unknown_profile" - fi -} - -# 收集用户信息包括权限级别 -# 输出:用户信息字符串(如:"User=john uid=1000 level=sudoer Groups=john adm sudo") -_fuck_collect_user_info() { - local current_user uid groups level - current_user="${USER:-}" - - # 如果 USER 未设置则使用备用方法 - if [ -z "$current_user" ]; then - current_user=$(whoami 2>/dev/null || printf 'unknown') - fi - - # 如果 id 命令可用则获取 UID 和用户组 - uid="unknown" - groups="unknown" - if command -v id >/dev/null 2>&1; then - uid=$(id -u "$current_user" 2>/dev/null || id -u 2>/dev/null || printf 'unknown') - groups=$(id -Gn "$current_user" 2>/dev/null || id -Gn 2>/dev/null || printf 'unknown') - fi - - # 确定权限级别 - level="user" - if [ "$uid" = "0" ]; then - level="root" - elif printf '%s' "$groups" | grep -Eq '(^|[[:space:]])(sudo|wheel|admin)([[:space:]]|$)'; then - level="sudoer" - fi - - printf 'User=%s uid=%s level=%s Groups=%s' "$current_user" "$uid" "$level" "$groups" -} - -# 从 JSON 文件中提取命令内容 -# 参数:$1 - JSON 文件路径 -# 返回:命令内容到 stdout -_fuck_extract_command_from_json() { - local json_file="$1" - if command -v python3 >/dev/null 2>&1; then - python3 - "$json_file" <<'PY' -import json, sys -path = sys.argv[1] -with open(path, 'r', encoding='utf-8') as f: - data = json.load(f) -choices = data.get('choices') or [] -if not choices: - sys.exit(1) -message = choices[0].get('message') or {} -content = (message.get('content') or '').strip() -if not content: - sys.exit(1) -print(content) -PY - return - fi - - if command -v node >/dev/null 2>&1; then - node - "$json_file" <<'JS' -const fs = require('fs'); -const path = process.argv[1]; -const data = JSON.parse(fs.readFileSync(path, 'utf8')); -const choice = (data.choices && data.choices[0]) || {}; -const message = choice.message || {}; -const content = (message.content || '').trim(); -if (!content) { - process.exit(1); -} -console.log(content); -JS - return - fi - - echo -e "${C_RED}Cannot parse AI response: neither python3 nor node is available.${C_RESET}" >&2 - return 1 -} - -# Spinner 加载动画 -# 参数:$1 - 进程 PID,$2 - 可选的前缀文本 -_fuck_spinner() { - local pid=$1 - local prefix="${2:-}" - local delay=0.1 - local -a frames=("|" "/" "-" "\\") - local frame_count=${#frames[@]} - local frame_idx=0 - local has_prefix=0 - if [ -n "$prefix" ]; then - has_prefix=1 - fi - - # 仅在 stderr 是终端时操作光标 - if [ -t 2 ]; then - tput civis 2>/dev/null || printf "\033[?25l" >&2 - fi - - while kill -0 "$pid" 2>/dev/null; do - if [ "$has_prefix" -eq 1 ]; then - printf "\r%s%s" "$prefix" "${frames[$frame_idx]}" - else - printf " %s " "${frames[$frame_idx]}" - printf "\b\b\b" - fi - frame_idx=$(( (frame_idx + 1) % frame_count )) - sleep "$delay" - done - - if [ "$has_prefix" -eq 1 ]; then - printf "\r%s" "$prefix" - tput el 2>/dev/null || printf "\033[K" - else - printf " \b\b\b" - fi - - if [ -t 2 ]; then - tput cnorm 2>/dev/null || printf "\033[?25h" >&2 - fi -} - -# 初始化历史文件 -# 参数:$1 - 历史文件路径(可选,默认 $INSTALL_DIR/history.json) -_fuck_init_history_file() { - local history_file="${1:-$INSTALL_DIR/history.json}" - - if [ ! -f "$history_file" ]; then - cat > "$history_file" <<'HISTORY_EOF' -{ - "version": "1.0.0", - "commands": [], - "favorites": [] -} -HISTORY_EOF - chmod 600 "$history_file" - fi -} - -# 记录命令执行到历史 -# 参数:$1 - 提示词,$2 - 命令,$3 - 退出码(可选),$4 - 耗时(可选) -_fuck_log_history() { - local prompt="$1" - local command="$2" - local exit_code="${3:-0}" - local duration="${4:-0}" - - local history_file="$INSTALL_DIR/history.json" - - # 检查 jq 是否可用 - if ! command -v jq &> /dev/null; then - return 1 - fi - - # 初始化历史文件 - _fuck_init_history_file "$history_file" - - # 生成唯一 ID - local cmd_id="cmd_$(date +%s)_$$" - local timestamp - timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%S" 2>/dev/null || echo "unknown") - - # 创建条目 - local entry - entry=$(jq -n \ - --arg id "$cmd_id" \ - --arg timestamp "$timestamp" \ - --arg prompt "$prompt" \ - --arg command "$command" \ - --argjson exit_code "$exit_code" \ - --argjson duration "$duration" \ - '{ - id: $id, - timestamp: $timestamp, - prompt: $prompt, - command: $command, - exitCode: $exit_code, - duration: $duration - }' 2>/dev/null) - - if [ -z "$entry" ]; then - return 1 - fi - - # 追加到历史并限制为 1000 条(避免竞态条件) - local temp_file="${history_file}.tmp" - if jq ".commands += [$entry] | .commands |= .[-1000:]" "$history_file" > "$temp_file" 2>/dev/null; then - command mv -f -- "$temp_file" "$history_file" - chmod 600 "$history_file" - else - rm -f "$temp_file" - fi -} - diff --git a/src/js/modules/footer.js b/src/js/modules/footer.js index 5547461..c5a3e35 100644 --- a/src/js/modules/footer.js +++ b/src/js/modules/footer.js @@ -43,6 +43,38 @@ function bindContainerAria(container) { onLocaleChange(apply); } +function createNetlifyBadge() { + const link = document.createElement('a'); + link.href = 'https://www.netlify.com'; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.setAttribute('aria-label', 'Deploys by Netlify'); + link.style.display = 'block'; + link.style.textAlign = 'center'; + link.style.marginTop = '16px'; + + const img = document.createElement('img'); + img.src = 'https://www.netlify.com/assets/badges/netlify-badge-light.svg'; + img.alt = 'Deploys by Netlify'; + img.style.height = '26px'; + img.style.opacity = '0.7'; + img.style.transition = 'opacity 0.2s ease'; + img.style.filter = 'grayscale(20%)'; + + link.addEventListener('mouseenter', () => { + img.style.opacity = '0.9'; + img.style.filter = 'grayscale(0%)'; + }); + + link.addEventListener('mouseleave', () => { + img.style.opacity = '0.7'; + img.style.filter = 'grayscale(20%)'; + }); + + link.appendChild(img); + return link; +} + function bindCopyright(span, options = {}) { const custom = typeof options.text === 'string' ? options.text.trim() : ''; if (custom) { @@ -69,12 +101,16 @@ export function injectFooter(options = {}) { // 3) 最后退回到 body(避免因 body 为 flex row 导致横排:因此仅最后兜底) const createCopyrightNode = () => { - const div = document.createElement('div'); - div.style.display = 'flex'; - div.style.alignItems = 'center'; - div.style.justifyContent = 'center'; - div.style.gap = '12px'; - div.style.marginTop = '8px'; + const wrapper = document.createElement('div'); + wrapper.style.textAlign = 'center'; + wrapper.style.marginTop = '8px'; + + // 版权信息行 + const copyrightRow = document.createElement('div'); + copyrightRow.style.display = 'flex'; + copyrightRow.style.alignItems = 'center'; + copyrightRow.style.justifyContent = 'center'; + copyrightRow.style.gap = '12px'; const span = document.createElement('span'); bindCopyright(span, options); @@ -104,10 +140,16 @@ export function injectFooter(options = {}) { githubLink.appendChild(githubIcon); bindGithubLink(githubLink); - div.appendChild(span); - div.appendChild(githubLink); + copyrightRow.appendChild(span); + copyrightRow.appendChild(githubLink); - return div; + // Netlify 徽章行(放在版权信息下方) + const netlifyBadge = createNetlifyBadge(); + + wrapper.appendChild(copyrightRow); + wrapper.appendChild(netlifyBadge); + + return wrapper; }; // 情形 1:已有站内 footer(如首页免责声明区)