[
  {
    "path": ".dockerignore",
    "content": ".git\n.gitignore\n\n**/target\n**/node_modules\n\n**/*.log\n**/*.md\n**/tmp\n\nDockerfile\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n# Use 4 spaces for Python, Rust and Bash files\n[*.{py,rs,sh}]\nindent_size = 4\n\n# Makefiles always use tabs for indentation\n[Makefile]\nindent_style = tab\n\n[*.bat]\nindent_size = 2\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.ts]\nquote_type= \"single\"\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Exclude all non-source directories from language detection\nbin/**/*                  linguist-vendored\ndist/**/*                 linguist-vendored\nscripts/**/*              linguist-vendored\ntests/**/*                linguist-vendored\ndocs/**/*                 linguist-vendored\n.github/**/*              linguist-vendored\nnode_modules/**/*         linguist-vendored\n\n# Exclude build artifacts and config files\n/cli.js                   linguist-vendored\n/rollup.config.js         linguist-vendored\n/icns2png.py              linguist-vendored\n*.json                    linguist-vendored\n\n# Exclude Tauri generated and vendor code\nsrc-tauri/target/**/*     linguist-vendored\nsrc-tauri/gen/**/*        linguist-vendored\nsrc-tauri/capabilities/** linguist-vendored\nsrc-tauri/icons/**/*      linguist-vendored\nsrc-tauri/assets/**/*     linguist-vendored\nsrc-tauri/png/**/*        linguist-vendored\nsrc-tauri/.pake/**/*      linguist-vendored\nsrc-tauri/.cargo/**/*     linguist-vendored\n\n# Exclude injection system (since it's mostly JS/CSS)\nsrc-tauri/src/inject/**/* linguist-vendored\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [\"tw93\"]\ncustom: [\"https://miaoyan.app/cats.html?name=Pake\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug report\ndescription: Problems with the software\ntitle: \"[Bug] \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you very much for your feedback!\n\n        For suggestions or help, please consider using [Github Discussion](https://github.com/tw93/Pake/discussions) instead.\n  - type: checkboxes\n    attributes:\n      label: Search before asking\n      description: >\n        🙊 Check out [Issues](https://github.com/tw93/Pake/issues?q=) before reporting. Please provide your system version, screencasts, screenshots, way to reproduce, and the expected result – helpful for me to understand and fix up this issue! Besides, for suggestions or something else, head to [Pake's Discussions Platform](https://github.com/tw93/Pake/discussions).\n      options:\n        - label: >\n            I searched in the [issues](https://github.com/tw93/Pake/issues?q=) and found nothing similar.\n          required: true\n  - type: textarea\n    attributes:\n      label: Pake version\n      description: >\n        Please provide the version of Pake you are using (e.g., `pake --version`). If you are using the main/dev branch, please provide the commit id.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Rust version\n      description: >\n        Please provide the Rust version (e.g., `rustc --version`). This is critical for build issues.\n    validations:\n      required: true\n  - type: dropdown\n    attributes:\n      label: Package Manager\n      description: Which package manager are you using?\n      options:\n        - npm\n        - pnpm\n        - yarn\n        - bun\n        - other\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: System version\n      description: >\n        Please provide the version of System you are using (e.g., macOS 14.2, Windows 11, Ubuntu 24.04).\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Node.js version\n      description: >\n        Please provide the Node.js version.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Build Command\n      description: >\n        Please provide the exact command you used to build the app (e.g., `pake https://github.com --name GitHub`).\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Minimal reproduce step\n      description: Please try to give reproducing steps to facilitate quick location of the problem.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: What did you expect to see?\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: What did you see instead?\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Anything else?\n  - type: checkboxes\n    attributes:\n      label: Are you willing to submit a PR?\n      description: >\n        We look forward to the community of developers or users helping solve Pake problems together. If you are willing to submit a PR to fix this problem, please check the box.\n      options:\n        - label: I'm willing to submit a PR!\n  - type: markdown\n    attributes:\n      value: \"Thanks for completing our form!\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a question or get support\n    url: https://github.com/tw93/Pake/discussions/categories/q-a\n    about: Ask a question or request support for Pake\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "name: Feature\ndescription: Add new feature, improve code, and more\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you very much for your feature proposal!\n  - type: checkboxes\n    attributes:\n      label: Search before asking\n      description: >\n        Please search [issues](https://github.com/tw93/Pake/issues?q=) to check if your issue has already been reported.\n      options:\n        - label: >\n            I searched in the [issues](https://github.com/tw93/Pake/issues?q=) and found nothing similar.\n          required: true\n  - type: textarea\n    attributes:\n      label: Motivation\n      description: Describe the motivations for this feature, like how it fixes the problem you meet.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Solution\n      description: Describe the proposed solution and add related materials like links if any.\n  - type: textarea\n    attributes:\n      label: Alternatives\n      description: Describe other alternative solutions or features you considered, but rejected.\n  - type: textarea\n    attributes:\n      label: Anything else?\n  - type: checkboxes\n    attributes:\n      label: Are you willing to submit a PR?\n      description: >\n        We look forward to the community of developers or users helping develop Pake features together. If you are willing to submit a PR to implement the feature, please check the box.\n      options:\n        - label: I'm willing to submit a PR!\n  - type: markdown\n    attributes:\n      value: \"Thanks for completing our form!\"\n"
  },
  {
    "path": ".github/actions/setup-env/action.yml",
    "content": "name: Setup Development Environment\ndescription: Unified environment setup with Node.js, Rust, and system dependencies\n\ninputs:\n  mode:\n    description: |\n      Setup mode:\n      - build: Complete environment (Node + Rust + System deps + Cache)\n      - node: Node.js only (pnpm + Node 22)\n      - rust: Rust only (toolchain + targets)\n    required: false\n    default: \"build\"\n\noutputs:\n  setup-complete:\n    description: Setup completion status\n    value: \"true\"\n\nruns:\n  using: composite\n  steps:\n    # Parse mode and set environment flags\n    - name: Setup environment flags\n      shell: bash\n      run: |\n        MODE=\"${{ inputs.mode }}\"\n\n        # Validate and set flags in one pass\n        case \"$MODE\" in\n          build|full)\n            echo \"SETUP_NODE=true\" >> $GITHUB_ENV\n            echo \"SETUP_RUST=true\" >> $GITHUB_ENV\n            echo \"SETUP_SYSTEM=true\" >> $GITHUB_ENV\n            ;;\n          node|node-only)\n            echo \"SETUP_NODE=true\" >> $GITHUB_ENV\n            echo \"SETUP_RUST=false\" >> $GITHUB_ENV\n            echo \"SETUP_SYSTEM=false\" >> $GITHUB_ENV\n            ;;\n          rust|rust-only)\n            echo \"SETUP_NODE=false\" >> $GITHUB_ENV\n            echo \"SETUP_RUST=true\" >> $GITHUB_ENV\n            echo \"SETUP_SYSTEM=false\" >> $GITHUB_ENV\n            ;;\n          *)\n            echo \"❌ Invalid mode: '$MODE'. Valid modes: build, node, rust\"\n            exit 1\n            ;;\n        esac\n\n    # Node.js Environment Setup\n    - name: Install pnpm\n      if: env.SETUP_NODE == 'true'\n      uses: pnpm/action-setup@v4\n      with:\n        version: \"10.26.2\"\n        run_install: false\n\n    - name: Setup Node.js\n      if: env.SETUP_NODE == 'true'\n      uses: actions/setup-node@v4\n      with:\n        node-version: \"22\"\n        cache: pnpm\n\n    - name: Install dependencies\n      if: env.SETUP_NODE == 'true'\n      shell: bash\n      run: pnpm install --frozen-lockfile\n\n    # Rust Environment Setup\n    - name: Setup Rust for Linux\n      if: env.SETUP_RUST == 'true' && runner.os == 'Linux'\n      uses: dtolnay/rust-toolchain@stable\n      with:\n        toolchain: stable\n        target: x86_64-unknown-linux-gnu\n\n    - name: Setup Rust for Windows\n      if: env.SETUP_RUST == 'true' && runner.os == 'Windows'\n      uses: dtolnay/rust-toolchain@stable\n      with:\n        toolchain: stable-x86_64-msvc\n        target: x86_64-pc-windows-msvc\n\n    - name: Setup Rust for macOS\n      if: env.SETUP_RUST == 'true' && runner.os == 'macOS'\n      uses: dtolnay/rust-toolchain@stable\n      with:\n        toolchain: stable\n\n    - name: Add macOS universal targets\n      if: env.SETUP_RUST == 'true' && runner.os == 'macOS'\n      shell: bash\n      run: |\n        rustup target add x86_64-apple-darwin\n        rustup target add aarch64-apple-darwin\n\n    # System Dependencies\n    - name: Install Ubuntu dependencies\n      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Linux'\n      uses: awalsh128/cache-apt-pkgs-action@v1.4.3\n      with:\n        packages: >\n          libdbus-1-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev\n          build-essential curl wget file libxdo-dev libssl-dev libgtk-3-dev\n          libayatana-appindicator3-dev librsvg2-dev gnome-video-effects\n          libglib2.0-dev libgirepository1.0-dev\n          pkg-config\n        version: 1.1\n\n    - name: Set PKG_CONFIG_PATH for Linux\n      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Linux'\n      shell: bash\n      run: |\n        echo \"PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig\" >> $GITHUB_ENV\n\n    - name: Cache WIX Toolset\n      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Windows'\n      uses: actions/cache@v4\n      id: wix-cache\n      with:\n        path: C:\\Program Files (x86)\\WiX Toolset v3.11\n        key: windows-wix-3.11.2\n\n    - name: Install WIX Toolset\n      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Windows' && steps.wix-cache.outputs.cache-hit != 'true'\n      shell: powershell\n      run: |\n        try {\n          # Download and install WIX Toolset v3.11\n          Invoke-WebRequest -Uri \"https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311.exe\" -OutFile \"wix311.exe\"\n          Start-Process -FilePath \"wix311.exe\" -ArgumentList \"/quiet\" -Wait\n          Write-Host \"✅ WIX Toolset installed successfully\"\n        } catch {\n          Write-Error \"Failed to install WIX Toolset: $($_.Exception.Message)\"\n          exit 1\n        }\n\n    - name: Add WIX to PATH\n      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Windows'\n      shell: powershell\n      run: |\n        $wixPath = \"${env:ProgramFiles(x86)}\\WiX Toolset v3.11\\bin\"\n        if (Test-Path $wixPath) {\n          echo $wixPath >> $env:GITHUB_PATH\n        }\n\n    # Build optimizations (caching)\n    - name: Setup sccache\n      if: inputs.mode == 'build'\n      uses: mozilla-actions/sccache-action@v0.0.9\n\n    - name: Enable sccache\n      if: inputs.mode == 'build'\n      shell: bash\n      run: |\n        echo \"SCCACHE_GHA_ENABLED=true\" >> $GITHUB_ENV\n        echo \"RUSTC_WRAPPER=sccache\" >> $GITHUB_ENV\n\n    - name: Setup Rust cache\n      if: inputs.mode == 'build'\n      uses: swatinem/rust-cache@v2\n      with:\n        workspaces: \"src-tauri -> target\"\n        shared-key: \"pake-${{ runner.os }}\"\n"
  },
  {
    "path": ".github/workflows/pake-cli.yaml",
    "content": "name: Build App With Pake CLI\n\nenv:\n  NODE_VERSION: \"22\"\n  PNPM_VERSION: \"10.26.2\"\n\non:\n  workflow_dispatch:\n    inputs:\n      platform:\n        description: \"Platform\"\n        required: true\n        default: \"macos-latest\"\n        type: choice\n        options:\n          - \"windows-latest\"\n          - \"macos-latest\"\n          - \"ubuntu-24.04\"\n      url:\n        description: \"Website URL\"\n        required: true\n      name:\n        description: \"App name (lowercase for Linux)\"\n        required: true\n      icon:\n        description: \"Icon URL, auto-fetch if empty\"\n        required: false\n      width:\n        description: \"Window width (px)\"\n        required: false\n        default: \"1200\"\n      height:\n        description: \"Window height (px)\"\n        required: false\n        default: \"780\"\n      fullscreen:\n        description: \"Start in fullscreen mode\"\n        required: false\n        type: boolean\n        default: false\n      hide_title_bar:\n        description: \"Hide title bar (macOS only)\"\n        required: false\n        type: boolean\n        default: false\n      multi_arch:\n        description: \"Universal binary (macOS only)\"\n        required: false\n        type: boolean\n        default: false\n      targets:\n        description: \"Package formats (comma-separated: deb,appimage,rpm)\"\n        required: false\n        default: \"deb\"\n\njobs:\n  build:\n    name: ${{ inputs.platform }}\n    runs-on: ${{ inputs.platform }}\n    strategy:\n      fail-fast: false\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n\n      - name: Setup Node.js Environment\n        uses: ./.github/actions/setup-env\n        with:\n          mode: build\n\n      - name: Build CLI\n        run: pnpm run cli:build\n\n      - name: Setup mold linker\n        if: runner.os == 'Linux'\n        uses: rui314/setup-mold@v1\n\n      - name: Rust cache restore\n        uses: actions/cache/restore@v4.2.0\n        id: cache_store\n        with:\n          path: |\n            ~/.cargo/bin/\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            src-tauri/target/\n          key: ${{ runner.os }}-cargo-pake-${{ hashFiles('**/Cargo.lock') }}\n\n      - name: Build App (Linux/macOS)\n        if: runner.os != 'Windows'\n        timeout-minutes: 25\n        shell: bash\n        run: |\n          ARGS=(\"${{ inputs.url }}\" \"--name\" \"${{ inputs.name }}\")\n\n          if [ -n \"${{ inputs.icon }}\" ]; then\n            ARGS+=(\"--icon\" \"${{ inputs.icon }}\")\n          fi\n\n          if [ -n \"${{ inputs.width }}\" ]; then\n            ARGS+=(\"--width\" \"${{ inputs.width }}\")\n          fi\n\n          if [ -n \"${{ inputs.height }}\" ]; then\n            ARGS+=(\"--height\" \"${{ inputs.height }}\")\n          fi\n\n          if [ \"${{ inputs.fullscreen }}\" == \"true\" ]; then\n            ARGS+=(\"--fullscreen\")\n          fi\n\n          if [ \"${{ inputs.hide_title_bar }}\" == \"true\" ]; then\n            ARGS+=(\"--hide-title-bar\")\n          fi\n\n          if [ \"${{ inputs.multi_arch }}\" == \"true\" ]; then\n            ARGS+=(\"--multi-arch\")\n          fi\n\n          if [ -n \"${{ inputs.targets }}\" ] && [ \"${{ runner.os }}\" == \"Linux\" ]; then\n            ARGS+=(\"--targets\" \"${{ inputs.targets }}\")\n          fi\n\n          echo \"Running: node dist/cli.js ${ARGS[@]}\"\n          node dist/cli.js \"${ARGS[@]}\"\n\n      - name: Build App (Windows)\n        if: runner.os == 'Windows'\n        timeout-minutes: 25\n        shell: pwsh\n        run: |\n          $args = \"${{ inputs.url }}\", \"--name\", \"${{ inputs.name }}\"\n\n          if (\"${{ inputs.icon }}\" -ne \"\") {\n            $args += \"--icon\", \"${{ inputs.icon }}\"\n          }\n\n          if (\"${{ inputs.width }}\" -ne \"\") {\n            $args += \"--width\", \"${{ inputs.width }}\"\n          }\n\n          if (\"${{ inputs.height }}\" -ne \"\") {\n            $args += \"--height\", \"${{ inputs.height }}\"\n          }\n\n          if (\"${{ inputs.fullscreen }}\" -eq \"true\") {\n            $args += \"--fullscreen\"\n          }\n\n          if (\"${{ inputs.hide_title_bar }}\" -eq \"true\") {\n            $args += \"--hide-title-bar\"\n          }\n\n          Write-Host \"Running: node dist/cli.js $($args -join ' ')\"\n          node dist/cli.js $args\n\n          git checkout -- src-tauri/Cargo.lock\n\n      - name: Upload DMG (macOS)\n        if: runner.os == 'macOS'\n        uses: actions/upload-artifact@v6\n        with:\n          name: ${{ inputs.name }}-macOS\n          path: ${{ inputs.name }}.dmg\n          retention-days: 3\n\n      - name: Upload DEB (Linux)\n        if: runner.os == 'Linux'\n        uses: actions/upload-artifact@v6\n        with:\n          name: ${{ inputs.name }}-Linux-deb\n          path: ${{ inputs.name }}.deb\n          retention-days: 3\n          if-no-files-found: ignore\n\n      - name: Upload AppImage (Linux)\n        if: runner.os == 'Linux'\n        uses: actions/upload-artifact@v6\n        with:\n          name: ${{ inputs.name }}-Linux-AppImage\n          path: ${{ inputs.name }}.AppImage\n          retention-days: 3\n          if-no-files-found: ignore\n\n      - name: Upload MSI (Windows)\n        if: runner.os == 'Windows'\n        uses: actions/upload-artifact@v6\n        with:\n          name: ${{ inputs.name }}-Windows\n          path: ${{ inputs.name }}.msi\n          retention-days: 3\n\n      - name: Rust cache store\n        uses: actions/cache/save@v4.2.0\n        if: steps.cache_store.outputs.cache-hit != 'true'\n        with:\n          path: |\n            ~/.cargo/bin/\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            src-tauri/target/\n          key: ${{ runner.os }}-cargo-pake-${{ hashFiles('**/Cargo.lock') }}\n"
  },
  {
    "path": ".github/workflows/quality-and-test.yml",
    "content": "name: Quality & Testing\n\non:\n  push:\n    branches: [main, dev]\n  pull_request:\n    branches: [main, dev]\n  workflow_dispatch:\n\nenv:\n  NODE_VERSION: \"22\"\n  PNPM_VERSION: \"10.26.2\"\n\npermissions:\n  actions: write\n  contents: read\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  auto-format:\n    name: Auto-fix Formatting\n    runs-on: ubuntu-latest\n    if: github.event_name == 'push'\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Setup Development Environment\n        uses: ./.github/actions/setup-env\n        with:\n          mode: node\n\n      - name: Auto-fix Prettier formatting\n        run: npx prettier --write . --ignore-unknown --cache\n\n      - name: Auto-fix Rust formatting\n        run: cargo fmt --all --manifest-path src-tauri/Cargo.toml\n\n      - name: Commit formatting fixes\n        run: |\n          git config --local user.email \"action@github.com\"\n          git config --local user.name \"GitHub Action\"\n          git add .\n          if ! git diff --staged --quiet; then\n            git commit -m \"Auto-fix formatting issues\"\n            git push\n          else\n            echo \"No formatting changes to commit\"\n          fi\n\n\n  rust-quality:\n    name: Rust Code Quality\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n\n    defaults:\n      run:\n        shell: bash\n        working-directory: src-tauri\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Rust Environment\n        uses: ./.github/actions/setup-env\n        with:\n          mode: build\n\n      - name: Install Rust components\n        shell: bash\n        run: rustup component add rustfmt clippy\n\n      - uses: rui314/setup-mold@v1\n\n      - name: Cache cargo-hack\n        uses: actions/cache@v5\n        id: cargo-hack-cache\n        with:\n          path: ~/.cargo/bin/cargo-hack\n          key: ${{ runner.os }}-cargo-hack-${{ hashFiles('~/.cargo/bin/cargo-hack') }}\n          restore-keys: |\n            ${{ runner.os }}-cargo-hack-\n\n      - name: Install cargo-hack\n        if: steps.cargo-hack-cache.outputs.cache-hit != 'true'\n        run: cargo install cargo-hack --force\n\n      - name: Check Rust formatting\n        run: cargo fmt --all -- --color=always --check\n\n      - name: Run Clippy lints\n        run: cargo hack --feature-powerset --exclude-features cli-build --no-dev-deps clippy # cspell:disable-line\n\n  validation:\n    name: CLI & Build Validation (${{ matrix.os }})\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n      fail-fast: false\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Build Environment\n        uses: ./.github/actions/setup-env\n        with:\n          mode: build\n\n      - name: Build CLI\n        run: pnpm run cli:build\n\n      - name: Run CI Test Suite\n        run: pnpm test\n        timeout-minutes: 30\n        env:\n          CI: true\n          NODE_ENV: test\n\n      - name: Test CLI Integration\n        shell: bash\n        run: |\n          echo \"Testing CLI integration...\"\n          if [[ \"$RUNNER_OS\" == \"Windows\" ]]; then\n            timeout 60s PAKE_CREATE_APP=1 node dist/cli.js https://weekly.tw93.fun --name \"CITest\" --debug --iterative-build || true\n          else\n            timeout 30s PAKE_CREATE_APP=1 node dist/cli.js https://weekly.tw93.fun --name \"CITest\" --debug --iterative-build || true\n          fi\n\n  summary:\n    name: Quality Summary\n    runs-on: ubuntu-latest\n    needs: [auto-format, rust-quality, validation]\n    if: always()\n    steps:\n      - name: Generate Summary\n        run: |\n          {\n            echo \"# Quality & Testing Summary\"\n            echo \"\"\n            echo \"| Check | Status |\"\n            echo \"|-------|--------|\"\n            echo \"| Auto Formatting | ${{ needs.auto-format.result == 'success' && 'PASSED' || needs.auto-format.result == 'skipped' && 'SKIPPED' || 'FAILED' }} |\"\n            echo \"| Rust Quality | ${{ needs.rust-quality.result == 'success' && 'PASSED' || 'FAILED' }} |\"\n            echo \"| CLI & Build Validation | ${{ needs.validation.result == 'success' && 'PASSED' || 'FAILED' }} |\"\n          } >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release & Publish\n\non:\n  push:\n    tags:\n      - \"V*\"\n  workflow_dispatch:\n    inputs:\n      release_apps:\n        description: \"Build popular apps\"\n        type: boolean\n        default: false\n      publish_docker:\n        description: \"Publish Docker image\"\n        type: boolean\n        default: false\n\nenv:\n  NODE_VERSION: \"22\"\n  PNPM_VERSION: \"10.26.2\"\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  # Build and release popular apps\n  release-apps:\n    if: |\n      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||\n      (github.event_name == 'workflow_dispatch' && inputs.release_apps)\n    runs-on: ubuntu-latest\n    outputs:\n      apps_name: ${{ steps.read-apps-config.outputs.apps_name }}\n      apps_config: ${{ steps.read-apps-config.outputs.apps_config }}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Get Apps Config\n        id: read-apps-config\n        run: |\n          echo \"apps_name=$(jq -c '[.[] | .name]' default_app_list.json)\" >> $GITHUB_OUTPUT\n          echo \"apps_config=$(jq -c '.' default_app_list.json)\" >> $GITHUB_OUTPUT\n\n  create-release:\n    name: Create GitHub Release\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs/tags/')\n    permissions:\n      contents: write\n    steps:\n      - name: Create release placeholder\n        uses: ncipollo/release-action@v1\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          skipIfReleaseExists: true\n\n  build-cli:\n    name: Build CLI\n    needs: release-apps\n    if: needs.release-apps.result == 'success'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Node.js Environment\n        uses: ./.github/actions/setup-env\n        with:\n          mode: 'node'\n\n      - name: Build CLI\n        run: pnpm run cli:build\n\n      - name: Upload CLI Artifact\n        uses: actions/upload-artifact@v6\n        with:\n          name: pake-cli-dist\n          path: dist/\n          retention-days: 1\n\n  build-popular-apps:\n    name: ${{ matrix.config.title }}\n    needs: [release-apps, build-cli, create-release]\n    if: |\n      needs.release-apps.result == 'success' &&\n      needs.build-cli.result == 'success' &&\n      (needs.create-release.result == 'success' || needs.create-release.result == 'skipped')\n    strategy:\n      matrix:\n        config: ${{ fromJSON(needs.release-apps.outputs.apps_config) }}\n    uses: ./.github/workflows/single-app.yaml\n    secrets: inherit\n    with:\n      name: ${{ matrix.config.name }}\n      title: ${{ matrix.config.title }}\n      name_zh: ${{ matrix.config.name_zh }}\n      url: ${{ matrix.config.url }}\n      icon: ${{ matrix.config.icon }}\n      new_window: ${{ matrix.config.new_window || false }}\n\n  # Publish Docker image (runs in parallel with app builds)\n  publish-docker:\n    if: |\n      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||\n      (github.event_name == 'workflow_dispatch' && inputs.publish_docker)\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to Container registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=raw,value=latest,enable={{is_default_branch}}\n            type=ref,event=tag\n            type=sha\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/single-app.yaml",
    "content": "name: Build Single Popular App\n\nenv:\n  NODE_VERSION: \"22\"\n  PNPM_VERSION: \"10.26.2\"\n\non:\n  workflow_dispatch:\n    inputs:\n      name:\n        description: \"App Name\"\n        required: true\n        default: \"twitter\"\n      title:\n        description: \"App Title\"\n        required: true\n        default: \"Twitter\"\n      name_zh:\n        description: \"App Name in Chinese\"\n        required: true\n        default: \"推特\"\n      url:\n        description: \"App URL\"\n        required: true\n        default: \"https://twitter.com/\"\n      icon:\n        description: \"App Icon\"\n        required: false\n      new_window:\n        description: \"Allow sites to open new windows\"\n        required: false\n        default: false\n  workflow_call:\n    inputs:\n      name:\n        description: \"App Name\"\n        type: string\n        required: true\n        default: \"twitter\"\n      title:\n        description: \"App Title\"\n        required: true\n        type: string\n        default: \"Twitter\"\n      name_zh:\n        description: \"App Name in Chinese\"\n        required: true\n        type: string\n        default: \"推特\"\n      url:\n        description: \"App URL\"\n        required: true\n        type: string\n        default: \"https://twitter.com/\"\n      icon:\n        description: \"App Icon\"\n        type: string\n        required: false\n      new_window:\n        description: \"Allow sites to open new windows\"\n        type: boolean\n        required: false\n        default: false\n    secrets:\n      PAKE_SIGNING_IDENTITY:\n        required: false\n      PAKE_CERTIFICATE_P12:\n        required: false\n      PAKE_CERTIFICATE_PASSWORD:\n        required: false\n      PAKE_NOTARIZE_APPLE_ID:\n        required: false\n      PAKE_NOTARIZE_TEAM_ID:\n        required: false\n      PAKE_NOTARIZE_PASSWORD:\n        required: false\n\njobs:\n  build:\n    name: ${{ inputs.title }} (${{ matrix.build }})\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        build: [linux, macos, windows]\n        include:\n          - build: linux\n            os: ubuntu-latest\n            rust: stable\n          - build: windows\n            os: windows-latest\n            rust: stable-x86_64-msvc\n          - build: macos\n            os: macos-latest\n            rust: stable\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Install Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ matrix.rust }}\n\n      - name: Setup Node.js Environment\n        uses: ./.github/actions/setup-env\n        with:\n          mode: build\n\n      - name: Download CLI Artifact\n        if: github.event_name != 'workflow_dispatch'\n        uses: actions/download-artifact@v7\n        with:\n          name: pake-cli-dist\n          path: dist\n\n      - name: Build CLI (workflow_dispatch)\n        if: github.event_name == 'workflow_dispatch'\n        run: pnpm run cli:build\n\n      - name: Setup mold linker\n        if: matrix.os == 'ubuntu-latest'\n        uses: rui314/setup-mold@v1\n\n      - name: Prepare macOS signing\n        if: matrix.os == 'macos-latest'\n        env:\n          PAKE_SIGNING_IDENTITY: ${{ secrets.PAKE_SIGNING_IDENTITY }}\n          PAKE_CERTIFICATE_P12: ${{ secrets.PAKE_CERTIFICATE_P12 }}\n          PAKE_CERTIFICATE_PASSWORD: ${{ secrets.PAKE_CERTIFICATE_PASSWORD }}\n        run: |\n          set -euo pipefail\n\n          SIGNING_IDENTITY=\"${PAKE_SIGNING_IDENTITY:-}\"\n\n          if [ -n \"${PAKE_CERTIFICATE_P12:-}\" ] && [ -n \"${PAKE_CERTIFICATE_PASSWORD:-}\" ]; then\n            echo \"Importing macOS signing certificate...\"\n            KEYCHAIN_PATH=\"$RUNNER_TEMP/app-signing.keychain-db\"\n            KEYCHAIN_PASSWORD=\"$(openssl rand -base64 16)\"\n\n            security create-keychain -p \"$KEYCHAIN_PASSWORD\" \"$KEYCHAIN_PATH\"\n            security set-keychain-settings -lut 21600 \"$KEYCHAIN_PATH\"\n            security unlock-keychain -p \"$KEYCHAIN_PASSWORD\" \"$KEYCHAIN_PATH\"\n\n            echo \"$PAKE_CERTIFICATE_P12\" | base64 --decode > \"$RUNNER_TEMP/certificate.p12\"\n            security import \"$RUNNER_TEMP/certificate.p12\" -P \"$PAKE_CERTIFICATE_PASSWORD\" -A -t cert -f pkcs12 -k \"$KEYCHAIN_PATH\"\n            security set-key-partition-list -S apple-tool:,apple: -s -k \"$KEYCHAIN_PASSWORD\" \"$KEYCHAIN_PATH\"\n            security list-keychain -d user -s \"$KEYCHAIN_PATH\"\n\n            if [ -z \"$SIGNING_IDENTITY\" ]; then\n              SIGNING_IDENTITY=\"$(security find-identity -v -p codesigning \"$KEYCHAIN_PATH\" | sed -n 's/.*\"\\(Developer ID Application:.*\\)\".*/\\1/p' | head -n 1)\"\n            fi\n          else\n            echo \"No certificate secret configured, fallback to ad-hoc signing.\"\n          fi\n\n          if [ -z \"$SIGNING_IDENTITY\" ]; then\n            SIGNING_IDENTITY=\"-\"\n          fi\n\n          SIGNING_IDENTITY=\"$SIGNING_IDENTITY\" node <<'NODE'\n          const fs = require('fs');\n          const file = 'src-tauri/tauri.macos.conf.json';\n          const config = JSON.parse(fs.readFileSync(file, 'utf8'));\n          config.bundle ??= {};\n          config.bundle.macOS ??= {};\n          config.bundle.macOS.signingIdentity = process.env.SIGNING_IDENTITY || '-';\n          fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\\n`);\n          NODE\n\n          echo \"Using signing identity: $SIGNING_IDENTITY\"\n\n      - name: Build for Linux\n        if: matrix.os == 'ubuntu-latest'\n        timeout-minutes: 20\n        run: |\n          ARGS=(\"${{ inputs.url }}\" \"--name\" \"${{ inputs.name }}\")\n\n          # Auto-detect local icon or use provided icon\n          if [ -n \"${{ inputs.icon }}\" ]; then\n            ARGS+=(\"--icon\" \"${{ inputs.icon }}\")\n          elif [ -f \"src-tauri/png/${{ inputs.name }}_512.png\" ]; then\n            ARGS+=(\"--icon\" \"src-tauri/png/${{ inputs.name }}_512.png\")\n          fi\n\n          if [ \"${{ inputs.new_window }}\" = \"true\" ]; then\n            ARGS+=(\"--new-window\")\n          fi\n\n          # Build once with multiple targets (faster than separate builds)\n          node dist/cli.js \"${ARGS[@]}\" --targets deb,appimage\n\n          mkdir -p output/linux\n\n          # The CLI copies files to project root and removes them from bundle directory\n          # Check project root first, then fallback to bundle directory\n          if [ -f \"${{ inputs.name }}.deb\" ]; then\n            mv \"${{ inputs.name }}.deb\" output/linux/${{inputs.title}}_`arch`.deb\n          elif [ -n \"$(ls src-tauri/target/release/bundle/deb/*.deb 2>/dev/null)\" ]; then\n            mv src-tauri/target/release/bundle/deb/*.deb output/linux/${{inputs.title}}_`arch`.deb\n          else\n            echo \"Error: No DEB file found\"\n            find . -name \"*.deb\" -type f\n            exit 1\n          fi\n\n          if [ -f \"${{ inputs.name }}.AppImage\" ]; then\n            mv \"${{ inputs.name }}.AppImage\" output/linux/\"${{inputs.title}}\"_`arch`.AppImage\n          elif [ -n \"$(ls src-tauri/target/release/bundle/appimage/*.AppImage 2>/dev/null)\" ]; then\n            mv src-tauri/target/release/bundle/appimage/*.AppImage output/linux/\"${{inputs.title}}\"_`arch`.AppImage\n          else\n            echo \"Error: No AppImage file found\"\n            find . -name \"*.AppImage\" -type f\n            exit 1\n          fi\n\n      - name: Build for macOS\n        if: matrix.os == 'macos-latest'\n        timeout-minutes: 25\n        env:\n          TAURI_BUNDLER_DMG_IGNORE_CI: \"true\"\n        run: |\n          # Use title as app product name on macOS to preserve display casing in .app\n          ARGS=(\"${{ inputs.url }}\" \"--name\" \"${{ inputs.title }}\" \"--hide-title-bar\")\n\n          # Auto-detect local icon or use provided icon\n          if [ -n \"${{ inputs.icon }}\" ]; then\n            ARGS+=(\"--icon\" \"${{ inputs.icon }}\")\n          elif [ -f \"src-tauri/icons/${{ inputs.name }}.icns\" ]; then\n            ARGS+=(\"--icon\" \"src-tauri/icons/${{ inputs.name }}.icns\")\n          fi\n\n          if [ \"${{ inputs.new_window }}\" = \"true\" ]; then\n            ARGS+=(\"--new-window\")\n          fi\n\n          node dist/cli.js \"${ARGS[@]}\" --targets universal --multi-arch\n\n          mkdir -p output/macos\n\n          # The CLI copies the DMG to project root and removes it from bundle directory\n          # Check project root first, then fallback to bundle directory\n          if [ -f \"${{ inputs.title }}.dmg\" ]; then\n            mv \"${{ inputs.title }}.dmg\" output/macos/\"${{inputs.title}}\".dmg\n          elif [ -f \"${{ inputs.name }}.dmg\" ]; then\n            mv \"${{ inputs.name }}.dmg\" output/macos/\"${{inputs.title}}\".dmg\n          elif [ -n \"$(ls src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg 2>/dev/null)\" ]; then\n            mv src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg output/macos/\"${{inputs.title}}\".dmg\n          else\n            echo \"Error: No DMG file found\"\n            echo \"Searched locations:\"\n            echo \"  - ${{ inputs.name }}.dmg (project root)\"\n            echo \"  - src-tauri/target/universal-apple-darwin/release/bundle/dmg/\"\n            find . -name \"*.dmg\" -type f\n            exit 1\n          fi\n\n      - name: Notarize macOS DMG\n        if: matrix.os == 'macos-latest'\n        env:\n          PAKE_NOTARIZE_APPLE_ID: ${{ secrets.PAKE_NOTARIZE_APPLE_ID }}\n          PAKE_NOTARIZE_TEAM_ID: ${{ secrets.PAKE_NOTARIZE_TEAM_ID }}\n          PAKE_NOTARIZE_PASSWORD: ${{ secrets.PAKE_NOTARIZE_PASSWORD }}\n        run: |\n          set -euo pipefail\n\n          if [ -z \"${PAKE_NOTARIZE_APPLE_ID:-}\" ] || [ -z \"${PAKE_NOTARIZE_TEAM_ID:-}\" ] || [ -z \"${PAKE_NOTARIZE_PASSWORD:-}\" ]; then\n            echo \"Notarization secrets not configured, skipping notarization.\"\n            exit 0\n          fi\n\n          DMG_PATH=\"output/macos/${{inputs.title}}.dmg\"\n          if [ ! -f \"$DMG_PATH\" ]; then\n            echo \"Error: DMG file not found at $DMG_PATH\"\n            exit 1\n          fi\n\n          echo \"Submitting DMG to Apple notarization service...\"\n          xcrun notarytool submit \"$DMG_PATH\" \\\n            --apple-id \"$PAKE_NOTARIZE_APPLE_ID\" \\\n            --team-id \"$PAKE_NOTARIZE_TEAM_ID\" \\\n            --password \"$PAKE_NOTARIZE_PASSWORD\" \\\n            --wait\n\n          echo \"Stapling notarization ticket...\"\n          xcrun stapler staple \"$DMG_PATH\"\n\n      - name: Build for Windows\n        if: matrix.os == 'windows-latest'\n        timeout-minutes: 20\n        run: |\n          # Use title as app product name on Windows to preserve display casing\n          $args = \"${{ inputs.url }}\", \"--name\", \"${{ inputs.title }}\"\n\n          # Auto-detect local icon or use provided icon\n          if (\"${{ inputs.icon }}\" -ne \"\") {\n            $args += \"--icon\", \"${{ inputs.icon }}\"\n          } elseif (Test-Path \"src-tauri\\png\\${{ inputs.name }}_256.ico\") {\n            $args += \"--icon\", \"src-tauri\\png\\${{ inputs.name }}_256.ico\"\n          }\n\n          if (\"${{ inputs.new_window }}\" -eq \"true\") {\n            $args += \"--new-window\"\n          }\n\n          $args += \"--targets\", \"x64\"\n\n          node dist/cli.js $args\n\n          New-Item -Path \"output\\windows\" -ItemType Directory -Force\n\n          # The CLI copies the MSI to project root and removes it from bundle directory\n          # Check project root first, then fallback to bundle directories\n          $projectRootMsi = \"${{ inputs.title }}.msi\"\n          $legacyProjectRootMsi = \"${{ inputs.name }}.msi\"\n\n          if (Test-Path $projectRootMsi) {\n            Move-Item -Path $projectRootMsi -Destination \"output\\windows\\${{inputs.title}}_x64.msi\"\n          } elseif (Test-Path $legacyProjectRootMsi) {\n            Move-Item -Path $legacyProjectRootMsi -Destination \"output\\windows\\${{inputs.title}}_x64.msi\"\n          } else {\n            # Check architecture-specific path (x64 builds)\n            $msiFiles = Get-ChildItem -Path \"src-tauri\\target\\x86_64-pc-windows-msvc\\release\\bundle\\msi\\*.msi\" -ErrorAction SilentlyContinue\n\n            # Fallback to generic path\n            if (-not $msiFiles) {\n              $msiFiles = Get-ChildItem -Path \"src-tauri\\target\\release\\bundle\\msi\\*.msi\" -ErrorAction SilentlyContinue\n            }\n\n            if ($msiFiles) {\n              Move-Item -Path $msiFiles[0].FullName -Destination \"output\\windows\\${{inputs.title}}_x64.msi\"\n            } else {\n              Write-Error \"No MSI files found in expected locations\"\n              Write-Host \"Searched paths:\"\n              Write-Host \"  - $projectRootMsi (project root)\"\n              Write-Host \"  - $legacyProjectRootMsi (project root)\"\n              Write-Host \"  - src-tauri\\target\\x86_64-pc-windows-msvc\\release\\bundle\\msi\\\"\n              Write-Host \"  - src-tauri\\target\\release\\bundle\\msi\\\"\n              Write-Host \"`nAll MSI files in target directory:\"\n              Get-ChildItem -Path \"src-tauri\\target\\\" -Recurse -Name \"*.msi\" | Write-Host\n              exit 1\n            }\n          }\n\n          git checkout -- src-tauri/Cargo.lock\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v6\n        with:\n          name: ${{ inputs.title }}-${{ matrix.build }}\n          path: output/*/*.*\n          retention-days: 3\n\n      - name: Upload to release (Linux)\n        uses: ncipollo/release-action@v1 # cspell:disable-line\n        if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/')\n        with:\n          allowUpdates: true\n          omitBody: true\n          omitName: true\n          artifacts: \"output/linux/*.deb,output/linux/*.AppImage\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Upload to release (macOS)\n        uses: ncipollo/release-action@v1 # cspell:disable-line\n        if: matrix.os == 'macos-latest' && startsWith(github.ref, 'refs/tags/')\n        with:\n          allowUpdates: true\n          omitBody: true\n          omitName: true\n          artifacts: \"output/macos/*.dmg\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Upload to release (Windows)\n        uses: ncipollo/release-action@v1 # cspell:disable-line\n        if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')\n        with:\n          allowUpdates: true\n          omitBody: true\n          omitName: true\n          artifacts: \"output/windows/*.msi\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/update-contributors.yml",
    "content": "name: Update Contributors\n\non:\n  push:\n    branches: [main, dev]\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * 0\" # Every Sunday at midnight UTC\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  update-contributors:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          fetch-depth: 0\n\n      - name: Generate contributors SVG\n        uses: tw93/contributors-list@master\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          svgPath: CONTRIBUTORS.svg\n          svgWidth: 1000\n          avatarSize: 72\n          avatarMargin: 45\n          userNameHeight: 20\n          noFetch: false\n          noCommit: true\n          truncate: 0\n          includeBots: false\n          excludeUsers: \"github-actions web-flow dependabot claude\"\n          itemTemplate: |\n            <g transform=\"translate({{ x }}, {{ y }})\">\n              <defs>\n                <clipPath id=\"cp-{{ login }}\">\n                  <circle cx=\"36\" cy=\"36\" r=\"36\" />\n                </clipPath>\n              </defs>\n              <a xlink:href=\"{{{ url }}}\" href=\"{{{ url }}}\" class=\"contributor-link\" target=\"_blank\" rel=\"nofollow sponsored\" title=\"{{{ name }}}\">\n                <image width=\"72\" height=\"72\" xlink:href=\"{{{ avatar }}}\" href=\"{{{ avatar }}}\" clip-path=\"url(#cp-{{ login }})\" />\n                <text x=\"36\" y=\"86\" text-anchor=\"middle\" alignment-baseline=\"middle\" font-size=\"10\" fill=\"#666\" font-family=\"-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif\">{{{ name }}}</text>\n              </a>\n            </g>\n\n      - name: Commit & Push\n        uses: stefanzweifel/git-auto-commit-action@v7\n        with:\n          commit_message: \"chore: update contributors [skip ci]\"\n          file_pattern: CONTRIBUTORS.svg\n          commit_user_name: github-actions[bot]\n          commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com\n          commit_author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>\n          push_options: '--force'\n"
  },
  {
    "path": ".gitignore",
    "content": "\n!dist/.gitkeep\n!dist/about_pake.html\n!dist/cli.js\n.DS_Store\n.idea\n.next\n.vscode\n*.app\n*.AppImage\n*.deb\n*.desktop\n*.dmg\n*.local\n*.log\n*.msi\n*.njsproj\n*.ntvs*\n*.sln\n*.suo\n*.sw?\n*.tmp\n# AI assistant docs (do not commit)\n# AI Assistant files\n# Editor directories and files\n# Logs\n.claude/\nAGENT.md\nAGENTS.md\nCLAUDE.md\ndist\ndist-ssr\njournal/\nlerna-debug.log*\nlogs\nnode_modules\nnpm-debug.log*\noutput\npnpm-debug.log*\nsrc-tauri/.cargo/\nsrc-tauri/.cargo/config.toml\nsrc-tauri/.pake/\nsrc-tauri/gen\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": ".npmignore",
    "content": "# Development files\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n\n# Development directories\nnode_modules/\n.vscode/\n.idea/\n\n# Build artifacts\ndist-ssr/\n*.local\n\n# Development configs\n.env\n.env.local\n.env.development\n.env.test\n.env.production\n\n# Logs\nlogs/\n*.log\nnpm-debug.log*\nyarn-debug.log*\npnpm-debug.log*\n\n# OS files\n.DS_Store\nThumbs.db\n\n# Testing\ncoverage/\n.nyc_output/\n\n# Documentation source\ndocs/\n*.md\n!README.md\n\n# Development scripts\nscript/\nrollup.config.js\ntsconfig.json\n.prettierrc*\n.eslintrc*\n\n# Tauri development files\nsrc-tauri/target/\nsrc-tauri/.cargo/config.toml\nsrc-tauri/.pake/\nsrc-tauri/gen/\noutput/\n"
  },
  {
    "path": ".npmrc",
    "content": "# Suppress npm funding and audit messages during installation\nfund=false\naudit=false\n\n# Resolve sharp version conflicts\nprefer-dedupe=true\n"
  },
  {
    "path": ".pnpmrc",
    "content": "strict-peer-dependencies=false\nnode-linker=hoisted\nauto-install-peers=true\n"
  },
  {
    "path": ".prettierignore",
    "content": "src-tauri/target\nnode_modules\ndist/**/*\n*.ico\n*.icns\n*.png\n*.jpg\n*.jpeg\n*.gif\n*.svg\n*.bin\n*.exe\n*.dll\n*.so\n*.dylib\nCargo.lock\nsrc-tauri/Cargo.lock\npnpm-lock.yaml\ncli.js\n*.desktop\n*.wxs\n*.plist\n*.toml\n.github/workflows/\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the overall community\n\nExamples of unacceptable behavior include:\n\n- The use of erotic language or imagery, and sexual attention or advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\ntw93@qq.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## How to contribute to Pake\n\n**Welcome to create [pull requests](https://github.com/tw93/Pake/compare/) for bugfix, new component, doc, example, suggestion and anything.**\n\n## Branch Management\n\nAll development happens directly on `main`. Submit pull requests to `main`.\n\n## Development Setup\n\n### Prerequisites\n\n- Node.js ≥22.0.0 (recommended LTS, older versions ≥18.0.0 may work)\n- Rust ≥1.85.0 (required for edition2024 support in dependencies)\n- Platform-specific build tools:\n  - **macOS**: Xcode Command Line Tools (`xcode-select --install`)\n  - **Windows**: Visual Studio Build Tools with MSVC\n  - **Linux**: `build-essential`, `libwebkit2gtk`, system dependencies\n\n### Installation\n\n```bash\n# Clone the repository\ngit clone https://github.com/tw93/Pake.git\ncd Pake\n\n# Install dependencies\npnpm install\n\n# Start development (Tauri only)\npnpm run dev\n\n# Start development (CLI Wrapper + Tauri) - Recommended for CLI changes\npnpm run cli:dev -- https://web.telegram.org/k/\n```\n\n### Testing\n\n```bash\n# Run all tests (unit + integration + builder)\npnpm test\n\n# Build CLI for testing\npnpm run cli:build\n```\n\n### Tips\n\n- Use `--iterative-build` flag during development to skip some hefty checks and use app bundle format for faster debugging:\n\n  ```bash\n  pnpm run cli:dev --iterative-build\n  ```\n\n## Continuous Integration\n\nThe project uses streamlined GitHub Actions workflows:\n\n- **Quality & Testing**: Automatic code quality checks and comprehensive testing on all platforms\n- **Claude AI Integration**: Automated code review and interactive assistance\n- **Release Management**: Coordinated releases with app building and Docker publishing\n\n## Troubleshooting\n\n### macOS 26 Beta Compilation Issues\n\nIf you're running macOS 26 Beta and encounter compilation errors related to `mac-notification-sys` or system frameworks (errors about `CoreFoundation`, `_Builtin_float` modules), create a `src-tauri/.cargo/config.toml` file with:\n\n```toml\n[env]\n# Fix for macOS 26 Beta compatibility issues\n# Forces use of compatible SDK when building on macOS 26 Beta\nMACOSX_DEPLOYMENT_TARGET = \"15.0\"\nSDKROOT = \"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk\"\n```\n\nThis file is already in `.gitignore` and should not be committed to the repository.\n\n**Root Cause**: macOS 26 Beta uses newer system frameworks that aren't yet fully compatible with Tauri's dependencies. This configuration uses the universal SDK symlink which automatically points to your system's available SDK version.\n\n### Common Build Issues\n\n- **Rust compilation errors**: Run `cargo clean` in `src-tauri/` directory\n- **`cargo` command not found after installation**: Pake CLI now reloads the Rust environment automatically, but if the issue persists reopen your terminal or run `source ~/.cargo/env` (macOS/Linux) / `call %USERPROFILE%\\.cargo\\env` (Windows) before retrying\n- **Node dependency issues**: Delete `node_modules` and run `pnpm install`\n- **Permission errors on macOS**: Run `sudo xcode-select --reset`\n\nSee the [Advanced Usage Guide](docs/advanced-usage.md) for project structure and customization techniques.\n\n## More\n\nIt is a good habit to create a feature request issue to discuss whether the feature is necessary before you implement it. However, it's unnecessary to create an issue to claim that you found a typo or improved the readability of documentation, just create a pull request.\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:1.4\n# Cargo build stage - Updated to latest Rust for edition2024 support\nFROM rust:latest AS cargo-builder\n\n# Update Rust to ensure we have the latest version with edition2024 support\nRUN rustup update stable && rustup default stable\n\n# Verify Rust version supports edition2024\nRUN rustc --version && cargo --version\n\n# Install Rust dependencies\nRUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\\n    rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock && \\\n    apt-get update && apt-get install -y --no-install-recommends \\\n    libdbus-1-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev \\\n    libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev \\\n    libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev \\\n    gnome-video-effects && \\\n    apt-get clean && rm -rf /var/lib/apt/lists/*\n\n# Set PKG_CONFIG_PATH for GLib detection\nENV PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig\n\n# Verify Rust version\nRUN rustc --version && echo \"Rust version verified\"\n\nCOPY . /pake\nWORKDIR /pake/src-tauri\n# Build cargo packages and store cache\nRUN --mount=type=cache,target=/usr/local/cargo/registry \\\n    cargo fetch && \\\n    cargo build --release && \\\n    mkdir -p /cargo-cache && \\\n    cp -R /usr/local/cargo/registry /cargo-cache/ && \\\n    ([ -d \"/usr/local/cargo/git\" ] && cp -R /usr/local/cargo/git /cargo-cache/ || mkdir -p /usr/local/cargo/git) && \\\n    cp -R /usr/local/cargo/git /cargo-cache/\n# Verify the content of /cargo-cache && clean unnecessary files\nRUN ls -la /cargo-cache/registry && ls -la /cargo-cache/git && rm -rfd /cargo-cache/registry/src\n\n# Main build stage\nFROM rust:latest AS builder\n\n# Update Rust to ensure we have the latest version with edition2024 support\nRUN rustup update stable && rustup default stable\n\n# Install Rust dependencies\nRUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\\n    rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock && \\\n    apt-get update && apt-get install -y --no-install-recommends \\\n    libdbus-1-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev \\\n    libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev \\\n    libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev \\\n    gnome-video-effects && \\\n    apt-get clean && rm -rf /var/lib/apt/lists/*\n\n# Set PKG_CONFIG_PATH for GLib detection\nENV PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig\n\n# Verify Rust version in builder stage\nRUN rustc --version && echo \"Builder stage Rust version verified\"\n\n# Install Node.js 22.x and pnpm\nRUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\\n    rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock && \\\n    curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \\\n    apt-get update && apt-get install -y nodejs && \\\n    npm install -g pnpm && \\\n    apt-get clean && rm -rf /var/lib/apt/lists/*\n\n# Copy project files\nCOPY . /pake\nWORKDIR /pake\n\n# Copy Rust build artifacts\nCOPY --from=cargo-builder /pake/src-tauri /pake/src-tauri\nCOPY --from=cargo-builder /cargo-cache/git /usr/local/cargo/git\nCOPY --from=cargo-builder /cargo-cache/registry /usr/local/cargo/registry\n\n# Install dependencies and build pake-cli\nRUN --mount=type=cache,target=/root/.local/share/pnpm \\\n    pnpm install --frozen-lockfile && \\\n    pnpm run cli:build\n\n# Set up the entrypoint\nWORKDIR /output\nENTRYPOINT [\"node\", \"/pake/dist/cli.js\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Tw93\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h4 align=\"right\"><strong>English</strong> | <a href=\"README_CN.md\">简体中文</a></h4>\n<p align=\"center\">\n    <img src=https://gw.alipayobjects.com/zos/k/fa/logo-modified.png width=138/>\n</p>\n<h1 align=\"center\">Pake</h1>\n<p align=\"center\"><strong>Turn any webpage into a desktop app with one command, supports macOS, Windows, and Linux</strong></p>\n<div align=\"center\">\n    <a href=\"https://twitter.com/HiTw93\" target=\"_blank\">\n    <img alt=\"twitter\" src=\"https://img.shields.io/badge/follow-Tw93-red?style=flat-square&logo=Twitter\"></a>\n    <a href=\"https://t.me/+GclQS9ZnxyI2ODQ1\" target=\"_blank\">\n    <img alt=\"telegram\" src=\"https://img.shields.io/badge/chat-telegram-blueviolet?style=flat-square&logo=Telegram\"></a>\n    <a href=\"https://github.com/tw93/Pake/releases\" target=\"_blank\">\n    <img alt=\"GitHub downloads\" src=\"https://img.shields.io/github/downloads/tw93/Pake/total.svg?style=flat-square\"></a>\n    <a href=\"https://github.com/tw93/Pake/commits\" target=\"_blank\">\n    <img alt=\"GitHub commit\" src=\"https://img.shields.io/github/commit-activity/m/tw93/Pake?style=flat-square\"></a>\n    <a href=\"https://github.com/tw93/Pake/issues?q=is%3Aissue+is%3Aclosed\" target=\"_blank\">\n    <img alt=\"GitHub closed issues\" src=\"https://img.shields.io/github/issues-closed/tw93/Pake.svg?style=flat-square\"></a>\n</div>\n\n## Features\n\n- 🎐 **Lightweight**: Nearly 20 times smaller than Electron packages, typically around 5M\n- 🚀 **Fast**: Built with Rust Tauri, much faster than traditional JS frameworks with lower memory usage\n- ⚡ **Easy to use**: One-command packaging via CLI or online building, no complex configuration needed\n- 📦 **Feature-rich**: Supports shortcuts, immersive windows, drag & drop, style customization, ad removal\n\n## Getting Started\n\n- **Beginners**: Download ready-made [Popular Packages](#popular-packages) or use [Online Building](docs/github-actions-usage.md) with no environment setup required\n- **Developers**: Install [CLI Tool](docs/cli-usage.md) for one-command packaging of any website with customizable icons, window settings, and more\n- **Advanced Users**: Clone the project locally for [Custom Development](#development), or check [Advanced Usage](docs/advanced-usage.md) for style customization and feature enhancement\n- **Troubleshooting**: Check [FAQ](docs/faq.md) for common issues and solutions\n\n## Popular Packages\n\n<table>\n    <tr>\n        <td>WeRead\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/WeRead.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/WeRead_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/WeRead_x86_64.deb\">Linux</a>\n        </td>\n        <td>Twitter\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Twitter.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Twitter_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Twitter_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/WeRead.jpg width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Twitter.jpg width=600/></td>\n    </tr>\n    <tr>\n        <td>Grok\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Grok.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Grok_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Grok_x86_64.deb\">Linux</a>\n        </td>\n        <td>DeepSeek\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/DeepSeek.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/DeepSeek_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/DeepSeek_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Grok.png width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/DeepSeek.png width=600/></td>\n    </tr>\n    <tr>\n        <td>ChatGPT\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ChatGPT.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ChatGPT_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ChatGPT_x86_64.deb\">Linux</a>\n        </td>\n        <td>Gemini\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Gemini.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Gemini_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Gemini_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/ChatGPT.png width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Gemini.png width=600/></td>\n    </tr>\n    <tr>\n      <td>YouTube Music\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic_x86_64.deb\">Linux</a>\n      </td>\n      <td>YouTube\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTube.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTube_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTube_x86_64.deb\">Linux</a>\n      </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/YouTubeMusic.png width=600 /></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/YouTube.jpg width=600 /></td>\n    </tr>\n    <tr>\n        <td>LiZhi\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/LiZhi.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/LiZhi_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/LiZhi_x86_64.deb\">Linux</a>\n        </td>\n        <td>ProgramMusic\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ProgramMusic.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ProgramMusic_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ProgramMusic_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/LiZhi.jpg width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/ProgramMusic.jpg width=600/></td>\n    </tr>\n    <tr>\n        <td>Excalidraw\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Excalidraw.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Excalidraw_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Excalidraw_x86_64.deb\">Linux</a>\n        </td>\n        <td>XiaoHongShu\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Excalidraw.png width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/XiaoHongShu.png width=600/></td>\n    </tr>\n</table>\n\n<details>\n<summary>🏂 You can download more applications from <a href=\"https://github.com/tw93/Pake/releases\">Releases</a>. <b>Click here to expand the shortcuts reference!</b></summary>\n\n<br/>\n\n| Mac                                                       | Windows/Linux                                       | Function                            |\n| --------------------------------------------------------- | --------------------------------------------------- | ----------------------------------- |\n| <kbd>⌘</kbd> + <kbd>[</kbd>                               | <kbd>Ctrl</kbd> + <kbd>←</kbd>                      | Return to the previous page         |\n| <kbd>⌘</kbd> + <kbd>]</kbd>                               | <kbd>Ctrl</kbd> + <kbd>→</kbd>                      | Go to the next page                 |\n| <kbd>⌘</kbd> + <kbd>↑</kbd>                               | <kbd>Ctrl</kbd> + <kbd>↑</kbd>                      | Auto scroll to top of page          |\n| <kbd>⌘</kbd> + <kbd>↓</kbd>                               | <kbd>Ctrl</kbd> + <kbd>↓</kbd>                      | Auto scroll to bottom of page       |\n| <kbd>⌘</kbd> + <kbd>r</kbd>                               | <kbd>Ctrl</kbd> + <kbd>r</kbd>                      | Refresh Page                        |\n| <kbd>⌘</kbd> + <kbd>w</kbd>                               | <kbd>Ctrl</kbd> + <kbd>w</kbd>                      | Hide window, not quit               |\n| <kbd>⌘</kbd> + <kbd>-</kbd>                               | <kbd>Ctrl</kbd> + <kbd>-</kbd>                      | Zoom out the page                   |\n| <kbd>⌘</kbd> + <kbd>=</kbd>                               | <kbd>Ctrl</kbd> + <kbd>=</kbd>                      | Zoom in the Page                    |\n| <kbd>⌘</kbd> + <kbd>0</kbd>                               | <kbd>Ctrl</kbd> + <kbd>0</kbd>                      | Reset the page zoom                 |\n| <kbd>⌘</kbd> + <kbd>L</kbd>                               | <kbd>Ctrl</kbd> + <kbd>L</kbd>                      | Copy Current Page URL               |\n| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌥</kbd> + <kbd>V</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd>   | Paste and Match Style               |\n| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>H</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd>   | Go to Home Page                     |\n| <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>   | Toggle Developer Tools (Debug Only) |\n| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌫</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Del</kbd> | Clear Cache & Restart               |\n\nIn addition, double-click the title bar to switch to full-screen mode. For Mac users, you can also use the gesture to go to the previous or next page and drag the title bar to move the window. The new menu also offers options for navigation, zoom, and window controls.\n\n</details>\n\n## Command-Line Packaging\n\n![Pake](https://raw.githubusercontent.com/tw93/static/main/pake/pake1.gif)\n\n```bash\n# Install Pake CLI\npnpm install -g pake-cli\n\n# Basic usage - automatically fetches website icon\npake https://github.com --name GitHub\n\n# Advanced usage with custom options\npake https://weekly.tw93.fun --name Weekly --icon https://cdn.tw93.fun/pake/weekly.icns --width 1200 --height 800 --hide-title-bar\n```\n\nFirst-time packaging requires environment setup and may be slower, subsequent builds are fast. For complete parameter documentation, see [CLI Usage Guide](docs/cli-usage.md). Don't want to use CLI? Try [GitHub Actions Online Building](docs/github-actions-usage.md).\n\n## Development\n\nRequires Rust `>=1.85` and Node `>=22`. For detailed installation guide, see [Tauri documentation](https://v2.tauri.app/start/prerequisites/). If unfamiliar with development environment, use the CLI tool instead.\n\n```bash\n# Install dependencies\npnpm i\n\n# Local development [right-click to open debug mode]\npnpm run dev\n\n# Build application\npnpm run build\n```\n\nFor style customization, feature enhancement, container communication and other advanced features, see [Advanced Usage Documentation](docs/advanced-usage.md).\n\n## Developers\n\nPake's development can not be without these Hackers. They contributed a lot of capabilities for Pake. Also, welcome to follow them! ❤️\n\n<a href=\"https://github.com/tw93/Pake/graphs/contributors\">\n  <img src=\"./CONTRIBUTORS.svg?v=2\" alt=\"Contributors\" width=\"1000\" />\n</a>\n\n## Support\n\n<a href=\"https://miaoyan.app/cats.html?name=Pake\"><img src=\"https://miaoyan.app/assets/sponsors.svg\" width=\"1000px\" /></a>\n\n1. I have two cats, TangYuan and Coke. If you think Pake delights your life, you can feed them <a href=\"https://miaoyan.app/cats.html?name=Pake\" target=\"_blank\">food 🥩</a>.\n2. If you like Pake, you can star it on GitHub. Also, welcome to [recommend Pake](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=Pake%20-%20Turn%20any%20webpage%20into%20a%20desktop%20app%20with%20one%20command.%20Nearly%2020x%20smaller%20than%20Electron%20packages,%20supports%20macOS%20Windows%20Linux) to your friends.\n3. You can follow my [Twitter](https://twitter.com/HiTw93) to get the latest news of Pake or join our [Telegram](https://t.me/+GclQS9ZnxyI2ODQ1) chat group.\n4. I hope that you enjoy playing with it. Let us know if you find a website that would be great for a Mac App!\n"
  },
  {
    "path": "README_CN.md",
    "content": "<h4 align=\"right\"><a href=\"README.md\">English</a> | <strong>简体中文</strong></h4>\n<p align=\"center\">\n    <img src=https://gw.alipayobjects.com/zos/k/fa/logo-modified.png width=138/>\n</p>\n<h1 align=\"center\">Pake</h1>\n<p align=\"center\"><strong>一键打包网页生成轻量桌面应用，支持 macOS、Windows 和 Linux</strong></p>\n<div align=\"center\">\n    <a href=\"https://twitter.com/HiTw93\" target=\"_blank\">\n    <img alt=\"twitter\" src=\"https://img.shields.io/badge/follow-Tw93-red?style=flat-square&logo=Twitter\"></a>\n    <a href=\"https://t.me/+GclQS9ZnxyI2ODQ1\" target=\"_blank\">\n    <img alt=\"telegram\" src=\"https://img.shields.io/badge/chat-telegram-blueviolet?style=flat-square&logo=Telegram\"></a>\n    <a href=\"https://github.com/tw93/Pake/releases\" target=\"_blank\">\n    <img alt=\"GitHub downloads\" src=\"https://img.shields.io/github/downloads/tw93/Pake/total.svg?style=flat-square\"></a>\n    <a href=\"https://github.com/tw93/Pake/commits\" target=\"_blank\">\n    <img alt=\"GitHub commit\" src=\"https://img.shields.io/github/commit-activity/m/tw93/Pake?style=flat-square\"></a>\n    <a href=\"https://github.com/tw93/Pake/issues?q=is%3Aissue+is%3Aclosed\" target=\"_blank\">\n    <img alt=\"GitHub closed issues\" src=\"https://img.shields.io/github/issues-closed/tw93/Pake.svg?style=flat-square\"></a>\n</div>\n\n## 特征\n\n- 🎐 **体积小巧**：相比 Electron 应用小近 20 倍，通常只有 5M 左右\n- 🚀 **性能优异**：基于 Rust Tauri，比传统 JS 框架更快，内存占用更少\n- ⚡ **使用简单**：命令行一键打包，或在线构建，无需复杂配置\n- 📦 **功能丰富**：支持快捷键透传、沉浸式窗口、拖拽、样式定制、去广告\n\n## 快速开始\n\n- **新手用户**：直接下载现成的 [常用包](#常用包下载)，或通过 [在线构建](docs/github-actions-usage_CN.md) 无需环境配置即可打包\n- **开发者**：安装 [CLI 工具](docs/cli-usage_CN.md) 后一行命令打包任意网站，支持自定义图标、窗口等参数\n- **高级用户**：本地克隆项目进行 [定制开发](#定制开发)，或查看 [高级用法](docs/advanced-usage_CN.md) 实现样式定制、功能增强\n- **遇到问题**：查看 [常见问题](docs/faq_CN.md) 获取常见问题的解决方案\n\n## 常用包下载\n\n<table>\n    <tr>\n        <td>WeRead\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/WeRead.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/WeRead_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/WeRead_x86_64.deb\">Linux</a>\n        </td>\n        <td>Twitter\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Twitter.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Twitter_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Twitter_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/WeRead.jpg width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Twitter.jpg width=600/></td>\n    </tr>\n    <tr>\n        <td>Grok\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Grok.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Grok_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Grok_x86_64.deb\">Linux</a>\n        </td>\n        <td>DeepSeek\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/DeepSeek.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/DeepSeek_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/DeepSeek_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Grok.png width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/DeepSeek.png width=600/></td>\n    </tr>\n    <tr>\n        <td>ChatGPT\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ChatGPT.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ChatGPT_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ChatGPT_x86_64.deb\">Linux</a>\n        </td>\n        <td>Gemini\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Gemini.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Gemini_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Gemini_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/ChatGPT.png width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Gemini.png width=600/></td>\n    </tr>\n    <tr>\n      <td>YouTube Music\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic_x86_64.deb\">Linux</a>\n      </td>\n      <td>YouTube\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTube.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTube_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/YouTube_x86_64.deb\">Linux</a>\n      </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/YouTubeMusic.png width=600 /></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/YouTube.jpg width=600 /></td>\n    </tr>\n    <tr>\n        <td>LiZhi\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/LiZhi.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/LiZhi_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/LiZhi_x86_64.deb\">Linux</a>\n        </td>\n        <td>ProgramMusic\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ProgramMusic.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ProgramMusic_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/ProgramMusic_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/LiZhi.jpg width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/ProgramMusic.jpg width=600/></td>\n    </tr>\n    <tr>\n        <td>Excalidraw\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Excalidraw.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Excalidraw_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/Excalidraw_x86_64.deb\">Linux</a>\n        </td>\n        <td>XiaoHongShu\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu.dmg\">Mac</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu_x64.msi\">Windows</a>\n            <a href=\"https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu_x86_64.deb\">Linux</a>\n        </td>\n    </tr>\n    <tr>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Excalidraw.png width=600/></td>\n        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/XiaoHongShu.png width=600/></td>\n    </tr>\n</table>\n\n<details>\n\n<summary>🏂 更多应用可去 <a href=\"https://github.com/tw93/Pake/releases\">Release</a>下载，<b>此外点击可展开快捷键说明</b></summary>\n\n<br/>\n\n| Mac                                                       | Windows/Linux                                       | 功能                |\n| --------------------------------------------------------- | --------------------------------------------------- | ------------------- |\n| <kbd>⌘</kbd> + <kbd>[</kbd>                               | <kbd>Ctrl</kbd> + <kbd>←</kbd>                      | 返回上一个页面      |\n| <kbd>⌘</kbd> + <kbd>]</kbd>                               | <kbd>Ctrl</kbd> + <kbd>→</kbd>                      | 去下一个页面        |\n| <kbd>⌘</kbd> + <kbd>↑</kbd>                               | <kbd>Ctrl</kbd> + <kbd>↑</kbd>                      | 自动滚动到页面顶部  |\n| <kbd>⌘</kbd> + <kbd>↓</kbd>                               | <kbd>Ctrl</kbd> + <kbd>↓</kbd>                      | 自动滚动到页面底部  |\n| <kbd>⌘</kbd> + <kbd>r</kbd>                               | <kbd>Ctrl</kbd> + <kbd>r</kbd>                      | 刷新页面            |\n| <kbd>⌘</kbd> + <kbd>w</kbd>                               | <kbd>Ctrl</kbd> + <kbd>w</kbd>                      | 隐藏窗口,非退出     |\n| <kbd>⌘</kbd> + <kbd>-</kbd>                               | <kbd>Ctrl</kbd> + <kbd>-</kbd>                      | 缩小页面            |\n| <kbd>⌘</kbd> + <kbd>=</kbd>                               | <kbd>Ctrl</kbd> + <kbd>=</kbd>                      | 放大页面            |\n| <kbd>⌘</kbd> + <kbd>0</kbd>                               | <kbd>Ctrl</kbd> + <kbd>0</kbd>                      | 重置页面缩放        |\n| <kbd>⌘</kbd> + <kbd>L</kbd>                               | <kbd>Ctrl</kbd> + <kbd>L</kbd>                      | 复制当前页面网址    |\n| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌥</kbd> + <kbd>V</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd>   | 粘贴并匹配样式      |\n| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>H</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd>   | 回到首页            |\n| <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>   | 开启调试 (仅开发版) |\n| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌫</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Del</kbd> | 清除缓存并重启      |\n\n此外还支持双击头部全屏切换，拖拽头部移动窗口，Mac 用户支持手势返回和前进，新菜单也提供了导航、缩放和窗口控制等选项。\n\n</details>\n\n## 命令行一键打包\n\n![Pake](https://raw.githubusercontent.com/tw93/static/main/pake/pake1.gif)\n\n```bash\n# 安装 Pake CLI\npnpm install -g pake-cli\n\n# 基础用法 - 自动获取网站图标\npake https://github.com --name GitHub\n\n# 高级用法：自定义选项\npake https://weekly.tw93.fun --name Weekly --icon https://cdn.tw93.fun/pake/weekly.icns --width 1200 --height 800 --hide-title-bar\n```\n\n首次打包需要安装环境会比较慢，后续很快。完整参数说明查看 [CLI 使用指南](docs/cli-usage_CN.md)，不想用命令行可以试试 [GitHub Actions 在线构建](docs/github-actions-usage_CN.md)。\n\n## 定制开发\n\n需要 Rust `>=1.85` 和 Node `>=22`，详细安装指南参考 [Tauri 文档](https://tauri.app/start/prerequisites/)。不熟悉开发环境建议直接使用命令行工具。\n\n```bash\n# 安装依赖\npnpm i\n\n# 本地开发[右键可打开调试模式]\npnpm run dev\n\n# 打包应用\npnpm run build\n```\n\n想要样式定制、功能增强、容器通信等高级玩法，查看 [高级用法文档](docs/advanced-usage_CN.md)。\n\n## 开发者\n\nPake 的发展离不开这些优秀的贡献者 ❤️\n\n<a href=\"https://github.com/tw93/Pake/graphs/contributors\">\n  <img src=\"https://raw.githubusercontent.com/tw93/Pake/main/CONTRIBUTORS.svg?sanitize=true\" alt=\"Contributors\" width=\"1000\" />\n</a>\n\n## 支持\n\n<a href=\"https://miaoyan.app/cats.html?name=Pake\"><img src=\"https://miaoyan.app/assets/sponsors.svg\" width=\"1000px\" /></a>\n\n1. 我有两只猫，一只叫汤圆，一只可乐，假如 Pake 让你生活更美好，可以给她们 <a href=\"https://miaoyan.app/cats.html?name=Pake\" target=\"_blank\">喂罐头 🥩</a>。\n2. 如果你喜欢 Pake，可以在 Github Star，更欢迎 [推荐](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=Pake%20-%20一键打包网页生成轻量桌面应用，比%20Electron%20小%2020%20倍，支持%20macOS%20Windows%20Linux) 给志同道合的朋友使用。\n3. 可以关注我的 [Twitter](https://twitter.com/HiTw93) 获取最新的 Pake 更新消息，也欢迎加入 [Telegram](https://t.me/+GclQS9ZnxyI2ODQ1) 聊天群。\n4. 希望大伙玩的过程中有一种学习新技术的喜悦感，发现适合做成桌面 App 的网页也欢迎告诉我。\n"
  },
  {
    "path": "action.yml",
    "content": "name: \"Pake Web App Builder\"\ndescription: \"Transform any webpage into a lightweight desktop app using Rust and Tauri\"\nauthor: \"tw93\"\nbranding:\n  icon: \"package\"\n  color: \"blue\"\n\ninputs:\n  url:\n    description: \"Target URL to package\"\n    required: true\n\n  name:\n    description: \"Application name\"\n    required: true\n\n  output-dir:\n    description: \"Output directory for packages\"\n    required: false\n    default: \"dist\"\n\n  icon:\n    description: \"Custom app icon URL or path\"\n    required: false\n\n  width:\n    description: \"Window width\"\n    required: false\n    default: \"1200\"\n\n  height:\n    description: \"Window height\"\n    required: false\n    default: \"780\"\n\n  debug:\n    description: \"Enable debug mode\"\n    required: false\n    default: \"false\"\n\noutputs:\n  package-path:\n    description: \"Path to the generated package\"\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Setup Environment\n      shell: bash\n      run: |\n        # Install Node.js dependencies\n        npm install\n\n        # Build Pake CLI if not exists\n        if [ ! -f \"dist/cli.js\" ]; then\n          npm run cli:build\n        fi\n\n        # Ensure node is accessible in subsequent steps\n        echo \"$(npm bin)\" >> $GITHUB_PATH\n\n        # Install Rust/Cargo if needed\n        if ! command -v cargo &> /dev/null; then\n          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\n          source ~/.cargo/env\n          echo \"$HOME/.cargo/bin\" >> $GITHUB_PATH\n        fi\n\n    - name: Build Pake App\n      shell: bash\n      run: |\n        # Build arguments\n        ARGS=(\"${{ inputs.url }}\")\n\n        ARGS+=(\"--name\" \"${{ inputs.name }}\")\n\n        if [ -n \"${{ inputs.icon }}\" ]; then\n          ARGS+=(\"--icon\" \"${{ inputs.icon }}\")\n        fi\n\n        ARGS+=(\"--width\" \"${{ inputs.width }}\")\n        ARGS+=(\"--height\" \"${{ inputs.height }}\")\n\n        if [ \"${{ inputs.debug }}\" == \"true\" ]; then\n          ARGS+=(\"--debug\")\n        fi\n\n        # Create output directory\n        mkdir -p \"${{ inputs.output-dir }}\"\n        export PAKE_CREATE_APP=1\n\n        # Run Pake CLI\n        echo \"🔧 Running: node dist/cli.js ${ARGS[*]}\"\n        node dist/cli.js \"${ARGS[@]}\"\n\n        # Find generated package and set output\n        PACKAGE=$(find src-tauri/target -type f \\( -name \"*.deb\" -o -name \"*.exe\" -o -name \"*.msi\" -o -name \"*.dmg\" \\) 2>/dev/null | head -1)\n\n        # If no file packages found, look for .app directory (macOS)\n        if [ -z \"$PACKAGE\" ]; then\n          PACKAGE=$(find src-tauri/target -type d -name \"*.app\" 2>/dev/null | head -1)\n        fi\n        if [ -n \"$PACKAGE\" ]; then\n          # Move to output directory\n          BASENAME=$(basename \"$PACKAGE\")\n          mv \"$PACKAGE\" \"${{ inputs.output-dir }}/$BASENAME\" 2>/dev/null || cp -r \"$PACKAGE\" \"${{ inputs.output-dir }}/$BASENAME\"\n          echo \"package-path=${{ inputs.output-dir }}/$BASENAME\" >> $GITHUB_OUTPUT\n          echo \"✅ Package created: ${{ inputs.output-dir }}/$BASENAME\"\n        else\n          echo \"❌ No package found\"\n          exit 1\n        fi\n"
  },
  {
    "path": "bin/builders/BaseBuilder.ts",
    "content": "import path from 'path';\nimport fsExtra from 'fs-extra';\nimport chalk from 'chalk';\nimport prompts from 'prompts';\n\nimport { PakeAppOptions } from '@/types';\nimport { checkRustInstalled, ensureRustEnv, installRust } from '@/helpers/rust';\nimport { mergeConfig } from '@/helpers/merge';\nimport tauriConfig from '@/helpers/tauriConfig';\nimport {\n  generateIdentifierSafeName,\n  generateLinuxPackageName,\n} from '@/utils/name';\nimport { npmDirectory } from '@/utils/dir';\nimport { getSpinner } from '@/utils/info';\nimport { shellExec } from '@/utils/shell';\nimport { isChinaDomain } from '@/utils/ip';\nimport { IS_MAC } from '@/utils/platform';\nimport logger from '@/options/logger';\n\nexport default abstract class BaseBuilder {\n  protected options: PakeAppOptions;\n  private static packageManagerCache: string | null = null;\n\n  protected constructor(options: PakeAppOptions) {\n    this.options = options;\n  }\n\n  private getBuildEnvironment() {\n    return IS_MAC\n      ? {\n          CFLAGS: '-fno-modules',\n          CXXFLAGS: '-fno-modules',\n          MACOSX_DEPLOYMENT_TARGET: '14.0',\n        }\n      : undefined;\n  }\n\n  private getInstallTimeout(): number {\n    // Windows needs more time due to native compilation and antivirus scanning\n    return process.platform === 'win32' ? 900000 : 600000;\n  }\n\n  private getBuildTimeout(): number {\n    return 900000;\n  }\n\n  private async detectPackageManager(): Promise<string> {\n    if (BaseBuilder.packageManagerCache) {\n      return BaseBuilder.packageManagerCache;\n    }\n\n    const { execa } = await import('execa');\n\n    try {\n      await execa('pnpm', ['--version'], { stdio: 'ignore' });\n      logger.info('✺ Using pnpm for package management.');\n      BaseBuilder.packageManagerCache = 'pnpm';\n      return 'pnpm';\n    } catch {\n      try {\n        await execa('npm', ['--version'], { stdio: 'ignore' });\n        logger.info('✺ pnpm not available, using npm for package management.');\n        BaseBuilder.packageManagerCache = 'npm';\n        return 'npm';\n      } catch {\n        throw new Error(\n          'Neither pnpm nor npm is available. Please install a package manager.',\n        );\n      }\n    }\n  }\n\n  async prepare() {\n    const tauriSrcPath = path.join(npmDirectory, 'src-tauri');\n    const tauriTargetPath = path.join(tauriSrcPath, 'target');\n    const tauriTargetPathExists = await fsExtra.pathExists(tauriTargetPath);\n\n    if (!IS_MAC && !tauriTargetPathExists) {\n      logger.warn('✼ The first use requires installing system dependencies.');\n      logger.warn('✼ See more in https://tauri.app/start/prerequisites/.');\n    }\n\n    ensureRustEnv();\n\n    if (!checkRustInstalled()) {\n      const res = await prompts({\n        type: 'confirm',\n        message: 'Rust not detected. Install now?',\n        name: 'value',\n      });\n\n      if (res.value) {\n        await installRust();\n      } else {\n        logger.error('✕ Rust required to package your webapp.');\n        process.exit(0);\n      }\n    }\n\n    const isChina = await isChinaDomain('www.npmjs.com');\n    const spinner = getSpinner('Installing package...');\n    const rustProjectDir = path.join(tauriSrcPath, '.cargo');\n    const projectConf = path.join(rustProjectDir, 'config.toml');\n    await fsExtra.ensureDir(rustProjectDir);\n\n    // Detect available package manager\n    const packageManager = await this.detectPackageManager();\n    const registryOption = ' --registry=https://registry.npmmirror.com';\n    const peerDepsOption =\n      packageManager === 'npm' ? ' --legacy-peer-deps' : '';\n\n    const timeout = this.getInstallTimeout();\n    const buildEnv = this.getBuildEnvironment();\n\n    // Show helpful message for first-time users\n    if (!tauriTargetPathExists) {\n      logger.info(\n        process.platform === 'win32'\n          ? '✺ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'\n          : '✺ First-time setup may take 5-10 minutes (installing dependencies)...',\n      );\n    }\n\n    let usedMirror = isChina;\n\n    try {\n      if (isChina) {\n        logger.info(\n          `✺ Located in China, using ${packageManager}/rsProxy CN mirror.`,\n        );\n        const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');\n        await fsExtra.copy(projectCnConf, projectConf);\n        await shellExec(\n          `cd \"${npmDirectory}\" && ${packageManager} install${registryOption}${peerDepsOption}`,\n          timeout,\n          { ...buildEnv, CI: 'true' },\n        );\n      } else {\n        await shellExec(\n          `cd \"${npmDirectory}\" && ${packageManager} install${peerDepsOption}`,\n          timeout,\n          { ...buildEnv, CI: 'true' },\n        );\n      }\n      spinner.succeed(chalk.green('Package installed!'));\n    } catch (error: unknown) {\n      // If installation times out and we haven't tried the mirror yet, retry with mirror\n      if (\n        error instanceof Error &&\n        error.message.includes('timed out') &&\n        !usedMirror\n      ) {\n        spinner.fail(\n          chalk.yellow('Installation timed out, retrying with CN mirror...'),\n        );\n        logger.info(\n          '✺ Retrying installation with CN mirror for better speed...',\n        );\n\n        const retrySpinner = getSpinner('Retrying installation...');\n        usedMirror = true;\n\n        try {\n          const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');\n          await fsExtra.copy(projectCnConf, projectConf);\n          await shellExec(\n            `cd \"${npmDirectory}\" && ${packageManager} install${registryOption}${peerDepsOption}`,\n            timeout,\n            { ...buildEnv, CI: 'true' },\n          );\n          retrySpinner.succeed(\n            chalk.green('Package installed with CN mirror!'),\n          );\n        } catch (retryError) {\n          retrySpinner.fail(chalk.red('Installation failed'));\n          throw retryError;\n        }\n      } else {\n        spinner.fail(chalk.red('Installation failed'));\n        throw error;\n      }\n    }\n\n    if (!tauriTargetPathExists) {\n      logger.warn(\n        '✼ The first packaging may be slow, please be patient and wait, it will be faster afterwards.',\n      );\n    }\n  }\n\n  async build(url: string) {\n    await this.buildAndCopy(url, this.options.targets);\n  }\n\n  async start(url: string) {\n    logger.info('Pake dev server starting...');\n    await mergeConfig(url, this.options, tauriConfig);\n\n    const packageManager = await this.detectPackageManager();\n    const configPath = path.join(\n      npmDirectory,\n      'src-tauri',\n      '.pake',\n      'tauri.conf.json',\n    );\n\n    const features = this.getBuildFeatures();\n    const featureArgs =\n      features.length > 0 ? `--features ${features.join(',')}` : '';\n\n    const argSeparator = packageManager === 'npm' ? ' --' : '';\n    const command = `cd \"${npmDirectory}\" && ${packageManager} run tauri${argSeparator} dev --config \"${configPath}\" ${featureArgs}`;\n\n    await shellExec(command);\n  }\n\n  async buildAndCopy(url: string, target: string) {\n    const { name = 'pake-app' } = this.options;\n    await mergeConfig(url, this.options, tauriConfig);\n\n    // Detect available package manager\n    const packageManager = await this.detectPackageManager();\n\n    // Build app\n    const buildSpinner = getSpinner('Building app...');\n    // Let spinner run for a moment so user can see it, then stop before package manager command\n    await new Promise((resolve) => setTimeout(resolve, 500));\n    buildSpinner.stop();\n    // Show static message to keep the status visible\n    logger.warn('✸ Building app...');\n\n    const baseEnv = this.getBuildEnvironment();\n    let buildEnv: Record<string, string> = {\n      ...(baseEnv ?? {}),\n      ...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),\n    };\n\n    const resolveExecEnv = () =>\n      Object.keys(buildEnv).length > 0 ? buildEnv : undefined;\n\n    // Warn users about potential AppImage build failures on modern Linux systems.\n    // The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't\n    // recognize the .relr.dyn section introduced in glibc 2.38+.\n    if (process.platform === 'linux' && target === 'appimage') {\n      if (!buildEnv.NO_STRIP) {\n        logger.warn(\n          '⚠ Building AppImage on Linux may fail due to strip incompatibility with glibc 2.38+',\n        );\n        logger.warn(\n          '⚠ If build fails, retry with: NO_STRIP=1 pake <url> --targets appimage',\n        );\n      }\n    }\n\n    const buildCommand = `cd \"${npmDirectory}\" && ${this.getBuildCommand(packageManager)}`;\n    const buildTimeout = this.getBuildTimeout();\n\n    try {\n      await shellExec(buildCommand, buildTimeout, resolveExecEnv());\n    } catch (error) {\n      const shouldRetryWithoutStrip =\n        process.platform === 'linux' &&\n        target === 'appimage' &&\n        !buildEnv.NO_STRIP &&\n        this.isLinuxDeployStripError(error);\n\n      if (shouldRetryWithoutStrip) {\n        logger.warn(\n          '⚠ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.',\n        );\n        buildEnv = {\n          ...buildEnv,\n          NO_STRIP: '1',\n        };\n        await shellExec(buildCommand, buildTimeout, resolveExecEnv());\n      } else {\n        throw error;\n      }\n    }\n\n    // Copy app\n    const fileName = this.getFileName();\n    const fileType = this.getFileType(target);\n    const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);\n    const distPath = path.resolve(`${name}.${fileType}`);\n    await fsExtra.copy(appPath, distPath);\n\n    // Copy raw binary if requested\n    if (this.options.keepBinary) {\n      await this.copyRawBinary(npmDirectory, name);\n    }\n\n    await fsExtra.remove(appPath);\n    logger.success('✔ Build success!');\n    logger.success('✔ App installer located in', distPath);\n\n    // Log binary location if preserved\n    if (this.options.keepBinary) {\n      const binaryPath = this.getRawBinaryPath(name);\n      logger.success('✔ Raw binary located in', path.resolve(binaryPath));\n    }\n\n    if (IS_MAC && fileType === 'app' && this.options.install) {\n      await this.installAppToApplications(distPath, name);\n    }\n  }\n\n  private async installAppToApplications(\n    appBundlePath: string,\n    appName: string,\n  ): Promise<void> {\n    try {\n      logger.info(`- Installing ${appName} to /Applications...`);\n\n      const appBundleName = path.basename(appBundlePath);\n      const appDest = path.join('/Applications', appBundleName);\n\n      // fsExtra.move uses fs.rename (atomic on same filesystem) and falls back\n      // to copy+remove only when moving across volumes.\n      await fsExtra.move(appBundlePath, appDest, { overwrite: true });\n\n      logger.success(\n        `✔ ${appBundleName.replace(/\\.app$/, '')} installed to /Applications`,\n      );\n    } catch (error) {\n      logger.error(`✕ Failed to install ${appName}: ${error}`);\n      logger.info(`  App bundle still available at: ${appBundlePath}`);\n    }\n  }\n\n  protected getFileType(target: string): string {\n    return target;\n  }\n\n  abstract getFileName(): string;\n\n  private isLinuxDeployStripError(error: unknown): boolean {\n    if (!(error instanceof Error) || !error.message) {\n      return false;\n    }\n    const message = error.message.toLowerCase();\n    return (\n      message.includes('linuxdeploy') ||\n      message.includes('failed to run linuxdeploy') ||\n      message.includes('strip:') ||\n      message.includes('unable to recognise the format of the input file') ||\n      message.includes('appimage tool failed') ||\n      message.includes('strip tool')\n    );\n  }\n\n  // 架构映射配置\n  protected static readonly ARCH_MAPPINGS: Record<\n    string,\n    Record<string, string>\n  > = {\n    darwin: {\n      arm64: 'aarch64-apple-darwin',\n      x64: 'x86_64-apple-darwin',\n      universal: 'universal-apple-darwin',\n    },\n    win32: {\n      arm64: 'aarch64-pc-windows-msvc',\n      x64: 'x86_64-pc-windows-msvc',\n    },\n    linux: {\n      arm64: 'aarch64-unknown-linux-gnu',\n      x64: 'x86_64-unknown-linux-gnu',\n    },\n  };\n\n  // 架构名称映射（用于文件名生成）\n  protected static readonly ARCH_DISPLAY_NAMES: Record<string, string> = {\n    arm64: 'aarch64',\n    x64: 'x64',\n    universal: 'universal',\n  };\n\n  /**\n   * 解析目标架构\n   */\n  protected resolveTargetArch(requestedArch?: string): string {\n    if (requestedArch === 'auto' || !requestedArch) {\n      return process.arch;\n    }\n    return requestedArch;\n  }\n\n  /**\n   * 获取Tauri构建目标\n   */\n  protected getTauriTarget(\n    arch: string,\n    platform: NodeJS.Platform = process.platform,\n  ): string | null {\n    const platformMappings = BaseBuilder.ARCH_MAPPINGS[platform];\n    if (!platformMappings) return null;\n    return platformMappings[arch] || null;\n  }\n\n  /**\n   * 获取架构显示名称（用于文件名）\n   */\n  protected getArchDisplayName(arch: string): string {\n    return BaseBuilder.ARCH_DISPLAY_NAMES[arch] || arch;\n  }\n\n  /**\n   * 构建基础构建命令\n   */\n  protected buildBaseCommand(\n    packageManager: string,\n    configPath: string,\n    target?: string,\n  ): string {\n    const baseCommand = this.options.debug\n      ? `${packageManager} run build:debug`\n      : `${packageManager} run build`;\n\n    const argSeparator = packageManager === 'npm' ? ' --' : '';\n    let fullCommand = `${baseCommand}${argSeparator} -c \"${configPath}\"`;\n\n    if (target) {\n      fullCommand += ` --target ${target}`;\n    }\n\n    // Enable verbose output in debug mode to help diagnose build issues.\n    // This provides detailed logs from Tauri CLI and bundler tools.\n    if (this.options.debug) {\n      fullCommand += ' --verbose';\n    }\n\n    return fullCommand;\n  }\n\n  /**\n   * 获取构建特性列表\n   */\n  protected getBuildFeatures(): string[] {\n    const features = ['cli-build'];\n\n    // Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)\n    if (IS_MAC) {\n      const macOSVersion = this.getMacOSMajorVersion();\n      if (macOSVersion >= 23) {\n        features.push('macos-proxy');\n      }\n    }\n\n    return features;\n  }\n\n  protected getBuildCommand(packageManager: string = 'pnpm'): string {\n    // Use temporary config directory to avoid modifying source files\n    const configPath = path.join(\n      npmDirectory,\n      'src-tauri',\n      '.pake',\n      'tauri.conf.json',\n    );\n\n    let fullCommand = this.buildBaseCommand(packageManager, configPath);\n\n    // For macOS, use app bundles by default unless DMG is explicitly requested\n    if (IS_MAC && this.options.targets === 'app') {\n      fullCommand += ' --bundles app';\n    }\n\n    // Add features\n    const features = this.getBuildFeatures();\n    if (features.length > 0) {\n      fullCommand += ` --features ${features.join(',')}`;\n    }\n\n    return fullCommand;\n  }\n\n  protected getMacOSMajorVersion(): number {\n    try {\n      const os = require('os');\n      const release = os.release();\n      const majorVersion = parseInt(release.split('.')[0], 10);\n      return majorVersion;\n    } catch (error) {\n      return 0; // Disable proxy feature if version detection fails\n    }\n  }\n\n  protected getBasePath(): string {\n    const basePath = this.options.debug ? 'debug' : 'release';\n    return `src-tauri/target/${basePath}/bundle/`;\n  }\n\n  protected getBuildAppPath(\n    npmDirectory: string,\n    fileName: string,\n    fileType: string,\n  ): string {\n    // For app bundles on macOS, the directory is 'macos', not 'app'\n    const bundleDir =\n      fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();\n    return path.join(\n      npmDirectory,\n      this.getBasePath(),\n      bundleDir,\n      `${fileName}.${fileType}`,\n    );\n  }\n\n  /**\n   * Copy raw binary file to output directory\n   */\n  protected async copyRawBinary(\n    npmDirectory: string,\n    appName: string,\n  ): Promise<void> {\n    const binaryPath = this.getRawBinarySourcePath(npmDirectory, appName);\n    const outputPath = this.getRawBinaryPath(appName);\n\n    if (await fsExtra.pathExists(binaryPath)) {\n      await fsExtra.copy(binaryPath, outputPath);\n      // Make binary executable on Unix-like systems\n      if (process.platform !== 'win32') {\n        await fsExtra.chmod(outputPath, 0o755);\n      }\n    } else {\n      logger.warn(`✼ Raw binary not found at ${binaryPath}, skipping...`);\n    }\n  }\n\n  /**\n   * Get the source path of the raw binary file in the build directory\n   */\n  protected getRawBinarySourcePath(\n    npmDirectory: string,\n    appName: string,\n  ): string {\n    const basePath = this.options.debug ? 'debug' : 'release';\n    const binaryName = this.getBinaryName(appName);\n\n    // Handle cross-platform builds\n    if (this.options.multiArch || this.hasArchSpecificTarget()) {\n      return path.join(\n        npmDirectory,\n        this.getArchSpecificPath(),\n        basePath,\n        binaryName,\n      );\n    }\n\n    return path.join(npmDirectory, 'src-tauri/target', basePath, binaryName);\n  }\n\n  /**\n   * Get the output path for the raw binary file\n   */\n  protected getRawBinaryPath(appName: string): string {\n    const extension = process.platform === 'win32' ? '.exe' : '';\n    const suffix = process.platform === 'win32' ? '' : '-binary';\n    return `${appName}${suffix}${extension}`;\n  }\n\n  /**\n   * Get the binary name based on app name and platform\n   */\n  protected getBinaryName(appName: string): string {\n    const extension = process.platform === 'win32' ? '.exe' : '';\n\n    // Use unique binary name for all platforms to avoid conflicts\n    const nameToUse =\n      process.platform === 'linux'\n        ? generateLinuxPackageName(appName)\n        : generateIdentifierSafeName(appName);\n    return `pake-${nameToUse}${extension}`;\n  }\n\n  /**\n   * Check if this build has architecture-specific target\n   */\n  protected hasArchSpecificTarget(): boolean {\n    return false; // Override in subclasses if needed\n  }\n\n  /**\n   * Get architecture-specific path for binary\n   */\n  protected getArchSpecificPath(): string {\n    return 'src-tauri/target'; // Override in subclasses if needed\n  }\n}\n"
  },
  {
    "path": "bin/builders/BuilderProvider.ts",
    "content": "import BaseBuilder from './BaseBuilder';\nimport MacBuilder from './MacBuilder';\nimport WinBuilder from './WinBuilder';\nimport LinuxBuilder from './LinuxBuilder';\nimport { PakeAppOptions } from '@/types';\n\nconst { platform } = process;\n\nconst buildersMap: Record<\n  string,\n  new (options: PakeAppOptions) => BaseBuilder\n> = {\n  darwin: MacBuilder,\n  win32: WinBuilder,\n  linux: LinuxBuilder,\n};\n\nexport default class BuilderProvider {\n  static create(options: PakeAppOptions): BaseBuilder {\n    const Builder = buildersMap[platform];\n    if (!Builder) {\n      throw new Error('The current system is not supported!');\n    }\n    return new Builder(options);\n  }\n}\n"
  },
  {
    "path": "bin/builders/LinuxBuilder.ts",
    "content": "import path from 'path';\nimport BaseBuilder from './BaseBuilder';\nimport { PakeAppOptions } from '@/types';\nimport tauriConfig from '@/helpers/tauriConfig';\n\nexport default class LinuxBuilder extends BaseBuilder {\n  private buildFormat: string;\n  private buildArch: string;\n  private currentBuildType: string = '';\n\n  constructor(options: PakeAppOptions) {\n    super(options);\n\n    const target = options.targets || 'deb';\n    if (target.includes('-arm64')) {\n      this.buildFormat = target.replace('-arm64', '');\n      this.buildArch = 'arm64';\n    } else {\n      this.buildFormat = target;\n      this.buildArch = this.resolveTargetArch('auto');\n    }\n\n    this.options.targets = this.buildFormat;\n  }\n\n  getFileName() {\n    const { name = 'pake-app', targets } = this.options;\n    const version = tauriConfig.version;\n    const buildType =\n      this.currentBuildType || targets.split(',').map((t) => t.trim())[0];\n\n    let arch: string;\n    if (this.buildArch === 'arm64') {\n      arch =\n        buildType === 'rpm' || buildType === 'appimage' ? 'aarch64' : 'arm64';\n    } else {\n      if (this.buildArch === 'x64') {\n        arch = buildType === 'rpm' ? 'x86_64' : 'amd64';\n      } else {\n        arch = this.buildArch;\n        if (\n          this.buildArch === 'arm64' &&\n          (buildType === 'rpm' || buildType === 'appimage')\n        ) {\n          arch = 'aarch64';\n        }\n      }\n    }\n\n    if (this.currentBuildType === 'rpm') {\n      return `${name}-${version}-1.${arch}`;\n    }\n\n    return `${name}_${version}_${arch}`;\n  }\n\n  async build(url: string) {\n    const targetTypes = ['deb', 'appimage', 'rpm'];\n    const requestedTargets = this.options.targets\n      .split(',')\n      .map((t: string) => t.trim());\n\n    for (const target of targetTypes) {\n      if (requestedTargets.includes(target)) {\n        this.currentBuildType = target;\n        await this.buildAndCopy(url, target);\n      }\n    }\n  }\n\n  // Override buildAndCopy to ensure currentBuildType is synced if called directly, though the loop above handles it most of the time.\n  async buildAndCopy(url: string, target: string) {\n    this.currentBuildType = target;\n    await super.buildAndCopy(url, target);\n  }\n\n  protected getBuildCommand(packageManager: string = 'pnpm'): string {\n    const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');\n\n    const buildTarget =\n      this.buildArch === 'arm64'\n        ? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)\n        : undefined;\n\n    let fullCommand = this.buildBaseCommand(\n      packageManager,\n      configPath,\n      buildTarget,\n    );\n\n    const features = this.getBuildFeatures();\n    if (features.length > 0) {\n      fullCommand += ` --features ${features.join(',')}`;\n    }\n\n    if (this.currentBuildType) {\n      fullCommand += ` --bundles ${this.currentBuildType}`;\n    }\n\n    // Enable verbose output for AppImage builds when debugging or PAKE_VERBOSE is set.\n    // AppImage builds often fail with minimal error messages from linuxdeploy,\n    // so verbose mode helps diagnose issues like strip failures and missing dependencies.\n    if (\n      this.currentBuildType === 'appimage' &&\n      (this.options.targets.includes('appimage') ||\n        this.options.debug ||\n        process.env.PAKE_VERBOSE)\n    ) {\n      fullCommand += ' --verbose';\n    }\n\n    return fullCommand;\n  }\n\n  protected getBasePath(): string {\n    const basePath = this.options.debug ? 'debug' : 'release';\n\n    if (this.buildArch === 'arm64') {\n      const target = this.getTauriTarget(this.buildArch, 'linux');\n      return `src-tauri/target/${target}/${basePath}/bundle/`;\n    }\n\n    return super.getBasePath();\n  }\n\n  protected getFileType(target: string): string {\n    if (target === 'appimage') {\n      return 'AppImage';\n    }\n    return super.getFileType(target);\n  }\n\n  protected hasArchSpecificTarget(): boolean {\n    return this.buildArch === 'arm64';\n  }\n\n  protected getArchSpecificPath(): string {\n    if (this.buildArch === 'arm64') {\n      const target = this.getTauriTarget(this.buildArch, 'linux');\n      return `src-tauri/target/${target}`;\n    }\n    return super.getArchSpecificPath();\n  }\n}\n"
  },
  {
    "path": "bin/builders/MacBuilder.ts",
    "content": "import path from 'path';\nimport tauriConfig from '@/helpers/tauriConfig';\nimport { PakeAppOptions } from '@/types';\nimport BaseBuilder from './BaseBuilder';\n\nexport default class MacBuilder extends BaseBuilder {\n  private buildFormat: string;\n  private buildArch: string;\n\n  constructor(options: PakeAppOptions) {\n    super(options);\n\n    const validArchs = ['intel', 'apple', 'universal', 'auto', 'x64', 'arm64'];\n    this.buildArch = validArchs.includes(options.targets || '')\n      ? options.targets\n      : 'auto';\n\n    if (\n      options.iterativeBuild ||\n      options.install ||\n      process.env.PAKE_CREATE_APP === '1'\n    ) {\n      this.buildFormat = 'app';\n    } else {\n      this.buildFormat = 'dmg';\n    }\n\n    this.options.targets = this.buildFormat;\n  }\n\n  getFileName(): string {\n    const { name = 'pake-app' } = this.options;\n\n    if (this.buildFormat === 'app') {\n      return name;\n    }\n\n    let arch: string;\n    if (this.buildArch === 'universal' || this.options.multiArch) {\n      arch = 'universal';\n    } else if (this.buildArch === 'apple') {\n      arch = 'aarch64';\n    } else if (this.buildArch === 'intel') {\n      arch = 'x64';\n    } else {\n      arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));\n    }\n    return `${name}_${tauriConfig.version}_${arch}`;\n  }\n\n  private getActualArch(): string {\n    if (this.buildArch === 'universal' || this.options.multiArch) {\n      return 'universal';\n    } else if (this.buildArch === 'apple') {\n      return 'arm64';\n    } else if (this.buildArch === 'intel') {\n      return 'x64';\n    }\n    return this.resolveTargetArch(this.buildArch);\n  }\n\n  protected getBuildCommand(packageManager: string = 'pnpm'): string {\n    const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');\n    const actualArch = this.getActualArch();\n\n    const buildTarget = this.getTauriTarget(actualArch, 'darwin');\n    if (!buildTarget) {\n      throw new Error(`Unsupported architecture: ${actualArch} for macOS`);\n    }\n\n    let fullCommand = this.buildBaseCommand(\n      packageManager,\n      configPath,\n      buildTarget,\n    );\n\n    const features = this.getBuildFeatures();\n    if (features.length > 0) {\n      fullCommand += ` --features ${features.join(',')}`;\n    }\n\n    return fullCommand;\n  }\n\n  protected getBasePath(): string {\n    const basePath = this.options.debug ? 'debug' : 'release';\n    const actualArch = this.getActualArch();\n    const target = this.getTauriTarget(actualArch, 'darwin');\n\n    return `src-tauri/target/${target}/${basePath}/bundle`;\n  }\n\n  protected hasArchSpecificTarget(): boolean {\n    return true;\n  }\n\n  protected getArchSpecificPath(): string {\n    const actualArch = this.getActualArch();\n    const target = this.getTauriTarget(actualArch, 'darwin');\n    return `src-tauri/target/${target}`;\n  }\n}\n"
  },
  {
    "path": "bin/builders/WinBuilder.ts",
    "content": "import path from 'path';\nimport BaseBuilder from './BaseBuilder';\nimport { PakeAppOptions } from '@/types';\nimport tauriConfig from '@/helpers/tauriConfig';\n\nexport default class WinBuilder extends BaseBuilder {\n  private buildFormat: string = 'msi';\n  private buildArch: string;\n\n  constructor(options: PakeAppOptions) {\n    super(options);\n    const validArchs = ['x64', 'arm64', 'auto'];\n    this.buildArch = validArchs.includes(options.targets || '')\n      ? this.resolveTargetArch(options.targets)\n      : this.resolveTargetArch('auto');\n    this.options.targets = this.buildFormat;\n  }\n\n  getFileName(): string {\n    const { name } = this.options;\n    const language = tauriConfig.bundle.windows.wix.language[0];\n    const targetArch = this.getArchDisplayName(this.buildArch);\n    return `${name}_${tauriConfig.version}_${targetArch}_${language}`;\n  }\n\n  protected getBuildCommand(packageManager: string = 'pnpm'): string {\n    const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');\n    const buildTarget = this.getTauriTarget(this.buildArch, 'win32');\n\n    if (!buildTarget) {\n      throw new Error(\n        `Unsupported architecture: ${this.buildArch} for Windows`,\n      );\n    }\n\n    let fullCommand = this.buildBaseCommand(\n      packageManager,\n      configPath,\n      buildTarget,\n    );\n\n    const features = this.getBuildFeatures();\n    if (features.length > 0) {\n      fullCommand += ` --features ${features.join(',')}`;\n    }\n\n    return fullCommand;\n  }\n\n  protected getBasePath(): string {\n    const basePath = this.options.debug ? 'debug' : 'release';\n    const target = this.getTauriTarget(this.buildArch, 'win32');\n    return `src-tauri/target/${target}/${basePath}/bundle/`;\n  }\n\n  protected hasArchSpecificTarget(): boolean {\n    return true;\n  }\n\n  protected getArchSpecificPath(): string {\n    const target = this.getTauriTarget(this.buildArch, 'win32');\n    return `src-tauri/target/${target}`;\n  }\n}\n"
  },
  {
    "path": "bin/cli.ts",
    "content": "import log from 'loglevel';\nimport updateNotifier from 'update-notifier';\nimport packageJson from '../package.json';\nimport BuilderProvider from './builders/BuilderProvider';\nimport handleInputOptions from './options/index';\nimport { getCliProgram } from './helpers/cli-program';\nimport { PakeCliOptions } from './types';\n\nconst program = getCliProgram();\n\nasync function checkUpdateTips() {\n  updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({\n    isGlobal: true,\n  });\n}\n\nprogram.action(async (url: string, options: PakeCliOptions) => {\n  await checkUpdateTips();\n\n  if (!url) {\n    program.help({\n      error: false,\n    });\n    return;\n  }\n\n  log.setDefaultLevel('info');\n  log.setLevel('info');\n  if (options.debug) {\n    log.setLevel('debug');\n  }\n\n  const appOptions = await handleInputOptions(options, url);\n\n  const builder = BuilderProvider.create(appOptions);\n  await builder.prepare();\n  await builder.build(url);\n});\n\nprogram.parse();\n"
  },
  {
    "path": "bin/defaults.ts",
    "content": "import { PakeCliOptions } from './types.js';\n\nexport const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {\n  icon: '',\n  height: 780,\n  width: 1200,\n  fullscreen: false,\n  maximize: false,\n  resizable: true,\n  hideTitleBar: false,\n  alwaysOnTop: false,\n  appVersion: '1.0.0',\n  darkMode: false,\n  disabledWebShortcuts: false,\n  activationShortcut: '',\n  userAgent: '',\n  showSystemTray: false,\n  multiArch: false,\n  targets: (() => {\n    switch (process.platform) {\n      case 'linux':\n        return 'deb,appimage';\n      case 'darwin':\n        return 'dmg';\n      case 'win32':\n        return 'msi';\n      default:\n        return 'deb';\n    }\n  })(),\n  useLocalFile: false,\n  systemTrayIcon: '',\n  proxyUrl: '',\n  debug: false,\n  inject: [],\n  installerLanguage: 'en-US',\n  hideOnClose: undefined, // Platform-specific: true for macOS, false for others\n  incognito: false,\n  wasm: false,\n  enableDragDrop: false,\n  keepBinary: false,\n  multiInstance: false,\n  multiWindow: false,\n  startToTray: false,\n  forceInternalNavigation: false,\n  internalUrlRegex: '',\n  iterativeBuild: false,\n  zoom: 100,\n  minWidth: 0,\n  minHeight: 0,\n  ignoreCertificateErrors: false,\n  newWindow: false,\n  install: false,\n  camera: false,\n  microphone: false,\n};\n\n// Just for cli development\nexport const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = {\n  ...DEFAULT_PAKE_OPTIONS,\n  url: 'https://weekly.tw93.fun/en',\n  name: 'Weekly',\n  hideTitleBar: true,\n};\n"
  },
  {
    "path": "bin/dev.ts",
    "content": "import log from 'loglevel';\nimport { PakeCliOptions } from './types';\nimport handleInputOptions from './options/index';\nimport BuilderProvider from './builders/BuilderProvider';\nimport { getCliProgram } from './helpers/cli-program';\n\nconst program = getCliProgram();\n\nprogram.action(async (url: string, options: PakeCliOptions) => {\n  log.setDefaultLevel('debug');\n\n  const appOptions = await handleInputOptions(options, url);\n  log.debug('PakeAppOptions', appOptions);\n\n  const builder = BuilderProvider.create(appOptions);\n  await builder.prepare();\n  await builder.start(url);\n});\n\nprogram.parse();\n"
  },
  {
    "path": "bin/helpers/cli-program.ts",
    "content": "import chalk from 'chalk';\nimport { program, Option } from 'commander';\nimport packageJson from '../../package.json';\nimport {\n  DEFAULT_PAKE_OPTIONS as DEFAULT,\n  DEFAULT_PAKE_OPTIONS,\n} from '../defaults';\nimport { validateNumberInput, validateUrlInput } from '../utils/validate';\n\nexport function getCliProgram() {\n  const { green, yellow } = chalk;\n  const logo = `${chalk.green(' ____       _')}\n${green('|  _ \\\\ __ _| | _____')}\n${green('| |_) / _` | |/ / _ \\\\')}\n${green('|  __/ (_| |   <  __/')}  ${yellow('https://github.com/tw93/pake')}\n${green('|_|   \\\\__,_|_|\\\\_\\\\___|  can turn any webpage into a desktop app with Rust.')}\n`;\n\n  return program\n    .addHelpText('beforeAll', logo)\n    .usage(`[url] [options]`)\n    .showHelpAfterError()\n    .argument('[url]', 'The web URL you want to package', validateUrlInput)\n    .option('--name <string>', 'Application name')\n    .addOption(\n      new Option(\n        '--identifier <string>',\n        'Application identifier / bundle ID',\n      ).hideHelp(),\n    )\n    .option('--icon <string>', 'Application icon', DEFAULT.icon)\n    .option(\n      '--width <number>',\n      'Window width',\n      validateNumberInput,\n      DEFAULT.width,\n    )\n    .option(\n      '--height <number>',\n      'Window height',\n      validateNumberInput,\n      DEFAULT.height,\n    )\n    .option(\n      '--use-local-file',\n      'Use local file packaging',\n      DEFAULT.useLocalFile,\n    )\n    .option('--fullscreen', 'Start in full screen', DEFAULT.fullscreen)\n    .option('--hide-title-bar', 'For Mac, hide title bar', DEFAULT.hideTitleBar)\n    .option('--multi-arch', 'For Mac, both Intel and M1', DEFAULT.multiArch)\n    .option(\n      '--inject <files>',\n      'Inject local CSS/JS files into the page',\n      (val, previous) => {\n        if (!val) return DEFAULT.inject;\n\n        // Split by comma and trim whitespace, filter out empty strings\n        const files = val\n          .split(',')\n          .map((item) => item.trim())\n          .filter((item) => item.length > 0);\n\n        // If previous values exist (from multiple --inject options), merge them\n        return previous ? [...previous, ...files] : files;\n      },\n      DEFAULT.inject,\n    )\n    .option('--debug', 'Debug build and more output', DEFAULT.debug)\n    .addOption(\n      new Option(\n        '--proxy-url <url>',\n        'Proxy URL for all network requests (http://, https://, socks5://)',\n      )\n        .default(DEFAULT_PAKE_OPTIONS.proxyUrl)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--user-agent <string>', 'Custom user agent')\n        .default(DEFAULT.userAgent)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option(\n        '--targets <string>',\n        'Build target format for your system',\n      ).default(DEFAULT.targets),\n    )\n    .addOption(\n      new Option(\n        '--app-version <string>',\n        'App version, the same as package.json version',\n      )\n        .default(DEFAULT.appVersion)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--always-on-top', 'Always on the top level')\n        .default(DEFAULT.alwaysOnTop)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--maximize', 'Start window maximized')\n        .default(DEFAULT.maximize)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--dark-mode', 'Force Mac app to use dark mode')\n        .default(DEFAULT.darkMode)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--disabled-web-shortcuts', 'Disabled webPage shortcuts')\n        .default(DEFAULT.disabledWebShortcuts)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--activation-shortcut <string>', 'Shortcut key to active App')\n        .default(DEFAULT_PAKE_OPTIONS.activationShortcut)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--show-system-tray', 'Show system tray in app')\n        .default(DEFAULT.showSystemTray)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--system-tray-icon <string>', 'Custom system tray icon')\n        .default(DEFAULT.systemTrayIcon)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option(\n        '--hide-on-close [boolean]',\n        'Hide window on close instead of exiting (default: true for macOS, false for others)',\n      )\n        .default(DEFAULT.hideOnClose)\n        .argParser((value) => {\n          if (value === undefined) return true; // --hide-on-close without value\n          if (value === 'true') return true;\n          if (value === 'false') return false;\n          throw new Error('--hide-on-close must be true or false');\n        })\n        .hideHelp(),\n    )\n    .addOption(new Option('--title <string>', 'Window title').hideHelp())\n    .addOption(\n      new Option('--incognito', 'Launch app in incognito/private mode')\n        .default(DEFAULT.incognito)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--wasm', 'Enable WebAssembly support (Flutter Web, etc.)')\n        .default(DEFAULT.wasm)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--enable-drag-drop', 'Enable drag and drop functionality')\n        .default(DEFAULT.enableDragDrop)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--keep-binary', 'Keep raw binary file alongside installer')\n        .default(DEFAULT.keepBinary)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--multi-instance', 'Allow multiple app instances')\n        .default(DEFAULT.multiInstance)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option(\n        '--multi-window',\n        'Allow opening multiple windows within one app instance',\n      )\n        .default(DEFAULT.multiWindow)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--start-to-tray', 'Start app minimized to tray')\n        .default(DEFAULT.startToTray)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option(\n        '--force-internal-navigation',\n        'Keep every link inside the Pake window instead of opening external handlers',\n      )\n        .default(DEFAULT.forceInternalNavigation)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option(\n        '--internal-url-regex <string>',\n        'Regex pattern to match URLs that should be considered internal',\n      )\n        .default(DEFAULT.internalUrlRegex)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--installer-language <string>', 'Installer language')\n        .default(DEFAULT.installerLanguage)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--zoom <number>', 'Initial page zoom level (50-200)')\n        .default(DEFAULT.zoom)\n        .argParser((value) => {\n          const zoom = parseInt(value);\n          if (isNaN(zoom) || zoom < 50 || zoom > 200) {\n            throw new Error('--zoom must be a number between 50 and 200');\n          }\n          return zoom;\n        })\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--min-width <number>', 'Minimum window width')\n        .default(DEFAULT.minWidth)\n        .argParser(validateNumberInput)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--min-height <number>', 'Minimum window height')\n        .default(DEFAULT.minHeight)\n        .argParser(validateNumberInput)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option(\n        '--ignore-certificate-errors',\n        'Ignore certificate errors (for self-signed certificates)',\n      )\n        .default(DEFAULT.ignoreCertificateErrors)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option(\n        '--iterative-build',\n        'Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging',\n      )\n        .default(DEFAULT.iterativeBuild)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option(\n        '--new-window',\n        'Allow sites to open new windows (for auth flows, tabs, branches)',\n      )\n        .default(DEFAULT.newWindow)\n        .hideHelp(),\n    )\n    .option(\n      '--install',\n      'Auto-install app to /Applications (macOS) after build and remove local bundle',\n      DEFAULT.install,\n    )\n    .addOption(\n      new Option('--camera', 'Request camera permission on macOS')\n        .default(DEFAULT.camera)\n        .hideHelp(),\n    )\n    .addOption(\n      new Option('--microphone', 'Request microphone permission on macOS')\n        .default(DEFAULT.microphone)\n        .hideHelp(),\n    )\n    .version(packageJson.version, '-v, --version')\n    .configureHelp({\n      sortSubcommands: true,\n      optionTerm: (option) => {\n        if (option.flags === '-v, --version' || option.flags === '-h, --help')\n          return '';\n        return option.flags;\n      },\n      optionDescription: (option) => {\n        if (option.flags === '-v, --version' || option.flags === '-h, --help')\n          return '';\n        return option.description;\n      },\n    });\n}\n"
  },
  {
    "path": "bin/helpers/merge.ts",
    "content": "import path from 'path';\nimport fsExtra from 'fs-extra';\n\nimport combineFiles from '@/utils/combine';\nimport logger from '@/options/logger';\nimport {\n  generateSafeFilename,\n  generateIdentifierSafeName,\n  getSafeAppName,\n  generateLinuxPackageName,\n} from '@/utils/name';\nimport { PakeAppOptions, PlatformMap, WindowConfig } from '@/types';\nimport { tauriConfigDirectory, npmDirectory } from '@/utils/dir';\n\nexport async function mergeConfig(\n  url: string,\n  options: PakeAppOptions,\n  tauriConf: any,\n) {\n  // Ensure .pake directory exists and copy source templates if needed\n  const srcTauriDir = path.join(npmDirectory, 'src-tauri');\n  await fsExtra.ensureDir(tauriConfigDirectory);\n\n  // Copy source config files to .pake directory (as templates)\n  const sourceFiles = [\n    'tauri.conf.json',\n    'tauri.macos.conf.json',\n    'tauri.windows.conf.json',\n    'tauri.linux.conf.json',\n    'pake.json',\n  ];\n\n  await Promise.all(\n    sourceFiles.map(async (file) => {\n      const sourcePath = path.join(srcTauriDir, file);\n      const destPath = path.join(tauriConfigDirectory, file);\n\n      if (\n        (await fsExtra.pathExists(sourcePath)) &&\n        !(await fsExtra.pathExists(destPath))\n      ) {\n        await fsExtra.copy(sourcePath, destPath);\n      }\n    }),\n  );\n  const {\n    width,\n    height,\n    fullscreen,\n    maximize,\n    hideTitleBar,\n    alwaysOnTop,\n    appVersion,\n    darkMode,\n    disabledWebShortcuts,\n    activationShortcut,\n    userAgent,\n    showSystemTray,\n    systemTrayIcon,\n    useLocalFile,\n    identifier,\n    name = 'pake-app',\n    resizable = true,\n    inject,\n    proxyUrl,\n    installerLanguage,\n    hideOnClose,\n    incognito,\n    title,\n    wasm,\n    enableDragDrop,\n    multiInstance,\n    multiWindow,\n    startToTray,\n    forceInternalNavigation,\n    internalUrlRegex,\n    zoom,\n    minWidth,\n    minHeight,\n    ignoreCertificateErrors,\n    newWindow,\n    camera,\n    microphone,\n  } = options;\n\n  const { platform } = process;\n\n  const platformHideOnClose = hideOnClose ?? platform === 'darwin';\n\n  const tauriConfWindowOptions: Partial<WindowConfig> = {\n    width,\n    height,\n    fullscreen,\n    maximize,\n    resizable,\n    hide_title_bar: hideTitleBar,\n    activation_shortcut: activationShortcut,\n    always_on_top: alwaysOnTop,\n    dark_mode: darkMode,\n    disabled_web_shortcuts: disabledWebShortcuts,\n    hide_on_close: platformHideOnClose,\n    incognito: incognito,\n    title: title,\n    enable_wasm: wasm,\n    enable_drag_drop: enableDragDrop,\n    start_to_tray: startToTray && showSystemTray,\n    force_internal_navigation: forceInternalNavigation,\n    internal_url_regex: internalUrlRegex,\n    zoom,\n    min_width: minWidth,\n    min_height: minHeight,\n    ignore_certificate_errors: ignoreCertificateErrors,\n    new_window: newWindow,\n  };\n  Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });\n\n  tauriConf.productName = name;\n  tauriConf.identifier = identifier;\n  tauriConf.version = appVersion;\n\n  // Always set mainBinaryName to ensure binary uniqueness\n  const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;\n  tauriConf.mainBinaryName =\n    platform === 'linux'\n      ? linuxBinaryName\n      : `pake-${generateIdentifierSafeName(name)}`;\n\n  if (platform == 'win32') {\n    tauriConf.bundle.windows.wix.language[0] = installerLanguage;\n  }\n\n  const pathExists = await fsExtra.pathExists(url);\n  if (pathExists) {\n    logger.warn('✼ Your input might be a local file.');\n    tauriConf.pake.windows[0].url_type = 'local';\n\n    const fileName = path.basename(url);\n    const dirName = path.dirname(url);\n\n    const distDir = path.join(npmDirectory, 'dist');\n    const distBakDir = path.join(npmDirectory, 'dist_bak');\n\n    if (!useLocalFile) {\n      const urlPath = path.join(distDir, fileName);\n      await fsExtra.copy(url, urlPath);\n    } else {\n      fsExtra.moveSync(distDir, distBakDir, { overwrite: true });\n      fsExtra.copySync(dirName, distDir, { overwrite: true });\n\n      // ignore it, because about_pake.html have be erased.\n      // const filesToCopyBack = ['cli.js', 'about_pake.html'];\n      const filesToCopyBack = ['cli.js'];\n      await Promise.all(\n        filesToCopyBack.map((file) =>\n          fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file)),\n        ),\n      );\n    }\n\n    tauriConf.pake.windows[0].url = fileName;\n    tauriConf.pake.windows[0].url_type = 'local';\n  } else {\n    tauriConf.pake.windows[0].url_type = 'web';\n  }\n\n  const platformMap: PlatformMap = {\n    win32: 'windows',\n    linux: 'linux',\n    darwin: 'macos',\n  };\n  const currentPlatform = platformMap[platform];\n\n  if (userAgent.length > 0) {\n    tauriConf.pake.user_agent[currentPlatform] = userAgent;\n  }\n\n  tauriConf.pake.system_tray[currentPlatform] = showSystemTray;\n\n  // Processing targets are currently only open to Linux.\n  if (platform === 'linux') {\n    // Remove hardcoded desktop files and regenerate with correct app name\n    delete tauriConf.bundle.linux.deb.files;\n\n    // Generate correct desktop file configuration\n    const linuxName = generateLinuxPackageName(name);\n    const desktopFileName = `com.pake.${linuxName}.desktop`;\n    const iconName = `${linuxName}_512`;\n\n    // Create desktop file content\n    // Determine if title contains Chinese characters for Name[zh_CN]\n    const chineseName = title && /[\\u4e00-\\u9fa5]/.test(title) ? title : null;\n\n    const desktopContent = `[Desktop Entry]\nVersion=1.0\nType=Application\nName=${name}\n${chineseName ? `Name[zh_CN]=${chineseName}` : ''}\nComment=${name}\nExec=${linuxBinaryName}\nIcon=${iconName}\nCategories=Network;WebBrowser;Utility;\nMimeType=text/html;text/xml;application/xhtml_xml;\nStartupNotify=true\nTerminal=false\n`;\n\n    // Write desktop file to src-tauri/assets directory where Tauri expects it\n    const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');\n    const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);\n    await fsExtra.ensureDir(srcAssetsDir);\n    await fsExtra.writeFile(srcDesktopFilePath, desktopContent);\n\n    // Set up desktop file in bundle configuration\n    // Use absolute path from src-tauri directory to assets\n    const desktopInstallPath = `/usr/share/applications/${desktopFileName}`;\n    tauriConf.bundle.linux.deb.files = {\n      [desktopInstallPath]: `assets/${desktopFileName}`,\n    };\n\n    // Add desktop file support for RPM\n    if (!tauriConf.bundle.linux.rpm) {\n      tauriConf.bundle.linux.rpm = {};\n    }\n    tauriConf.bundle.linux.rpm.files = {\n      [desktopInstallPath]: `assets/${desktopFileName}`,\n    };\n\n    const validTargets = [\n      'deb',\n      'appimage',\n      'rpm',\n      'deb-arm64',\n      'appimage-arm64',\n      'rpm-arm64',\n    ];\n    const baseTarget = options.targets.includes('-arm64')\n      ? options.targets.replace('-arm64', '')\n      : options.targets;\n\n    if (validTargets.includes(options.targets)) {\n      tauriConf.bundle.targets = [baseTarget];\n    } else {\n      logger.warn(\n        `✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`,\n      );\n    }\n  }\n\n  // Set macOS bundle targets (for app vs dmg)\n  if (platform === 'darwin') {\n    const validMacTargets = ['app', 'dmg'];\n    if (validMacTargets.includes(options.targets)) {\n      tauriConf.bundle.targets = [options.targets];\n    }\n  }\n\n  // Set icon.\n  const safeAppName = getSafeAppName(name);\n  const platformIconMap: PlatformMap = {\n    win32: {\n      fileExt: '.ico',\n      path: `png/${safeAppName}_256.ico`,\n      defaultIcon: 'png/icon_256.ico',\n      message: 'Windows icon must be .ico and 256x256px.',\n    },\n    linux: {\n      fileExt: '.png',\n      path: `png/${generateLinuxPackageName(name)}_512.png`,\n      defaultIcon: 'png/icon_512.png',\n      message: 'Linux icon must be .png and 512x512px.',\n    },\n    darwin: {\n      fileExt: '.icns',\n      path: `icons/${safeAppName}.icns`,\n      defaultIcon: 'icons/icon.icns',\n      message: 'macOS icon must be .icns type.',\n    },\n  };\n  const iconInfo = platformIconMap[platform];\n  const resolvedIconPath = options.icon ? path.resolve(options.icon) : null;\n  const exists =\n    resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));\n  if (exists) {\n    let updateIconPath = true;\n    let customIconExt = path.extname(resolvedIconPath).toLowerCase();\n\n    if (customIconExt !== iconInfo.fileExt) {\n      updateIconPath = false;\n      logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);\n      tauriConf.bundle.icon = [iconInfo.defaultIcon];\n    } else {\n      const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);\n      tauriConf.bundle.resources = [iconInfo.path];\n\n      // Avoid copying if source and destination are the same\n      const absoluteDestPath = path.resolve(iconPath);\n      if (resolvedIconPath !== absoluteDestPath) {\n        await fsExtra.copy(resolvedIconPath, iconPath);\n      }\n    }\n\n    if (updateIconPath) {\n      tauriConf.bundle.icon = [iconInfo.path];\n    } else {\n      logger.warn(`✼ Icon will remain as default.`);\n    }\n  } else {\n    logger.warn(\n      '✼ Custom icon path may be invalid, default icon will be used instead.',\n    );\n    tauriConf.bundle.icon = [iconInfo.defaultIcon];\n  }\n\n  // Set tray icon path.\n  let trayIconPath =\n    platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];\n  if (systemTrayIcon.length > 0) {\n    try {\n      await fsExtra.pathExists(systemTrayIcon);\n      // 需要判断图标格式，默认只支持ico和png两种\n      let iconExt = path.extname(systemTrayIcon).toLowerCase();\n      if (iconExt == '.png' || iconExt == '.ico') {\n        const trayIcoPath = path.join(\n          npmDirectory,\n          `src-tauri/png/${safeAppName}${iconExt}`,\n        );\n        trayIconPath = `png/${safeAppName}${iconExt}`;\n        await fsExtra.copy(systemTrayIcon, trayIcoPath);\n      } else {\n        logger.warn(\n          `✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`,\n        );\n        logger.warn(`✼ Default system tray icon will be used.`);\n      }\n    } catch {\n      logger.warn(`✼ ${systemTrayIcon} not exists!`);\n      logger.warn(`✼ Default system tray icon will remain unchanged.`);\n    }\n  }\n\n  // Ensure trayIcon object exists before setting iconPath\n  if (!tauriConf.app.trayIcon) {\n    tauriConf.app.trayIcon = {};\n  }\n  tauriConf.app.trayIcon.iconPath = trayIconPath;\n  tauriConf.pake.system_tray_path = trayIconPath;\n\n  delete tauriConf.app.trayIcon;\n\n  const injectFilePath = path.join(\n    npmDirectory,\n    `src-tauri/src/inject/custom.js`,\n  );\n\n  // inject js or css files\n  if (inject?.length > 0) {\n    // Ensure inject is an array before calling .every()\n    const injectArray = Array.isArray(inject) ? inject : [inject];\n    if (\n      !injectArray.every(\n        (item) => item.endsWith('.css') || item.endsWith('.js'),\n      )\n    ) {\n      logger.error('The injected file must be in either CSS or JS format.');\n      return;\n    }\n    const files = injectArray.map((filepath) =>\n      path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath),\n    );\n    tauriConf.pake.inject = files;\n    await combineFiles(files, injectFilePath);\n  } else {\n    tauriConf.pake.inject = [];\n    await fsExtra.writeFile(injectFilePath, '');\n  }\n  tauriConf.pake.proxy_url = proxyUrl || '';\n  tauriConf.pake.multi_instance = multiInstance;\n  tauriConf.pake.multi_window = multiWindow;\n\n  // Configure WASM support with required HTTP headers\n  if (wasm) {\n    tauriConf.app.security = {\n      headers: {\n        'Cross-Origin-Opener-Policy': 'same-origin',\n        'Cross-Origin-Embedder-Policy': 'require-corp',\n      },\n    };\n  }\n\n  // Write entitlements dynamically on macOS so camera/microphone are opt-in\n  if (platform === 'darwin') {\n    const entitlementEntries: string[] = [];\n    if (camera) {\n      entitlementEntries.push(\n        '    <key>com.apple.security.device.camera</key>\\n    <true/>',\n      );\n    }\n    if (microphone) {\n      entitlementEntries.push(\n        '    <key>com.apple.security.device.audio-input</key>\\n    <true/>',\n      );\n    }\n    const entitlementsContent = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n${entitlementEntries.join('\\n')}\n  </dict>\n</plist>\n`;\n    const entitlementsPath = path.join(\n      npmDirectory,\n      'src-tauri',\n      'entitlements.plist',\n    );\n    await fsExtra.writeFile(entitlementsPath, entitlementsContent);\n  }\n\n  // Save config file.\n  const platformConfigPaths: PlatformMap = {\n    win32: 'tauri.windows.conf.json',\n    darwin: 'tauri.macos.conf.json',\n    linux: 'tauri.linux.conf.json',\n  };\n\n  const configPath = path.join(\n    tauriConfigDirectory,\n    platformConfigPaths[platform],\n  );\n\n  const bundleConf = { bundle: tauriConf.bundle };\n  await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });\n  const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');\n  await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });\n\n  let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));\n  delete tauriConf2.pake;\n\n  // delete tauriConf2.bundle;\n  if (process.env.NODE_ENV === 'development') {\n    tauriConf2.bundle = bundleConf.bundle;\n  }\n  const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');\n  await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });\n}\n"
  },
  {
    "path": "bin/helpers/rust.ts",
    "content": "import os from 'os';\nimport path from 'path';\nimport fsExtra from 'fs-extra';\nimport chalk from 'chalk';\nimport { execaSync } from 'execa';\n\nimport { getSpinner } from '@/utils/info';\nimport { IS_WIN } from '@/utils/platform';\nimport { shellExec } from '@/utils/shell';\nimport { isChinaDomain } from '@/utils/ip';\n\nfunction normalizePathForComparison(targetPath: string) {\n  const normalized = path.normalize(targetPath);\n  return IS_WIN ? normalized.toLowerCase() : normalized;\n}\n\nfunction getCargoHomeCandidates(): string[] {\n  const candidates = new Set<string>();\n  if (process.env.CARGO_HOME) {\n    candidates.add(process.env.CARGO_HOME);\n  }\n  const homeDir = os.homedir();\n  if (homeDir) {\n    candidates.add(path.join(homeDir, '.cargo'));\n  }\n  if (IS_WIN && process.env.USERPROFILE) {\n    candidates.add(path.join(process.env.USERPROFILE, '.cargo'));\n  }\n  return Array.from(candidates).filter(Boolean);\n}\n\nfunction ensureCargoBinOnPath() {\n  const currentPath = process.env.PATH || '';\n  const segments = currentPath.split(path.delimiter).filter(Boolean);\n  const normalizedSegments = new Set(\n    segments.map((segment) => normalizePathForComparison(segment)),\n  );\n\n  const additions: string[] = [];\n  let cargoHomeSet = Boolean(process.env.CARGO_HOME);\n\n  for (const cargoHome of getCargoHomeCandidates()) {\n    const binDir = path.join(cargoHome, 'bin');\n    if (\n      fsExtra.pathExistsSync(binDir) &&\n      !normalizedSegments.has(normalizePathForComparison(binDir))\n    ) {\n      additions.push(binDir);\n      normalizedSegments.add(normalizePathForComparison(binDir));\n    }\n\n    if (!cargoHomeSet && fsExtra.pathExistsSync(cargoHome)) {\n      process.env.CARGO_HOME = cargoHome;\n      cargoHomeSet = true;\n    }\n  }\n\n  if (additions.length) {\n    const prefix = additions.join(path.delimiter);\n    process.env.PATH = segments.length\n      ? `${prefix}${path.delimiter}${segments.join(path.delimiter)}`\n      : prefix;\n  }\n}\n\nexport function ensureRustEnv() {\n  ensureCargoBinOnPath();\n}\n\nexport async function installRust() {\n  const isActions = process.env.GITHUB_ACTIONS;\n  const isInChina = await isChinaDomain('sh.rustup.rs');\n  const rustInstallScriptForMac =\n    isInChina && !isActions\n      ? 'export RUSTUP_DIST_SERVER=\"https://rsproxy.cn\" && export RUSTUP_UPDATE_ROOT=\"https://rsproxy.cn/rustup\" && curl --proto \"=https\" --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh'\n      : \"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\";\n  const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';\n\n  const spinner = getSpinner('Downloading Rust...');\n\n  try {\n    await shellExec(\n      IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac,\n      300000,\n      undefined,\n    );\n    spinner.succeed(chalk.green('✔ Rust installed successfully!'));\n    ensureRustEnv();\n  } catch (error) {\n    spinner.fail(chalk.red('✕ Rust installation failed!'));\n    if (error instanceof Error) {\n      console.error(error.message);\n    } else {\n      console.error(error);\n    }\n    process.exit(1);\n  }\n}\n\nexport function checkRustInstalled() {\n  ensureCargoBinOnPath();\n  try {\n    execaSync('rustc', ['--version']);\n    return true;\n  } catch {\n    return false;\n  }\n}\n"
  },
  {
    "path": "bin/helpers/tauriConfig.ts",
    "content": "import path from 'path';\nimport fsExtra from 'fs-extra';\nimport { npmDirectory } from '@/utils/dir';\n\n// Load configs from npm package directory, not from project source\nconst tauriSrcDir = path.join(npmDirectory, 'src-tauri');\nconst pakeConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'pake.json'));\nconst CommonConf = fsExtra.readJSONSync(\n  path.join(tauriSrcDir, 'tauri.conf.json'),\n);\nconst WinConf = fsExtra.readJSONSync(\n  path.join(tauriSrcDir, 'tauri.windows.conf.json'),\n);\nconst MacConf = fsExtra.readJSONSync(\n  path.join(tauriSrcDir, 'tauri.macos.conf.json'),\n);\nconst LinuxConf = fsExtra.readJSONSync(\n  path.join(tauriSrcDir, 'tauri.linux.conf.json'),\n);\n\nconst platformConfigs = {\n  win32: WinConf,\n  darwin: MacConf,\n  linux: LinuxConf,\n};\n\nconst { platform } = process;\n// @ts-ignore\nconst platformConfig = platformConfigs[platform];\n\nlet tauriConfig = {\n  ...CommonConf,\n  bundle: platformConfig.bundle,\n  app: {\n    ...CommonConf.app,\n    trayIcon: {\n      ...(platformConfig?.app?.trayIcon ?? {}),\n    },\n  },\n  build: CommonConf.build,\n  pake: pakeConf,\n};\n\nexport default tauriConfig;\n"
  },
  {
    "path": "bin/options/icon.ts",
    "content": "import path from 'path';\nimport fsExtra from 'fs-extra';\nimport chalk from 'chalk';\nimport { dir } from 'tmp-promise';\nimport { fileTypeFromBuffer } from 'file-type';\nimport icongen from 'icon-gen';\nimport sharp from 'sharp';\n\nimport logger from './logger';\nimport { getSpinner } from '@/utils/info';\nimport { npmDirectory } from '@/utils/dir';\nimport { IS_LINUX, IS_WIN, IS_MAC } from '@/utils/platform';\nimport { PakeAppOptions } from '@/types';\nimport { writeIcoWithPreferredSize } from '@/utils/ico';\n\ntype PlatformIconConfig = {\n  format: string;\n  sizes?: number[];\n  size?: number;\n};\nconst ICON_CONFIG = {\n  minFileSize: 100,\n  supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp', 'icns'] as const,\n  whiteBackground: { r: 255, g: 255, b: 255 },\n  transparentBackground: { r: 255, g: 255, b: 255, alpha: 0 },\n  downloadTimeout: {\n    ci: 5000,\n    default: 15000,\n  },\n} as const;\n\nconst PLATFORM_CONFIG: Record<'win' | 'linux' | 'macos', PlatformIconConfig> = {\n  win: { format: '.ico', sizes: [16, 32, 48, 64, 128, 256] },\n  linux: { format: '.png', size: 512 },\n  macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },\n};\n\nconst API_KEYS = {\n  logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],\n  brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],\n};\n\n/**\n * Generates platform-specific icon paths and handles copying for Windows\n */\nimport { generateLinuxPackageName, generateSafeFilename } from '@/utils/name';\n\nfunction generateIconPath(appName: string, isDefault = false): string {\n  const safeName = isDefault ? 'icon' : getIconBaseName(appName);\n  const baseName = safeName;\n\n  if (IS_WIN) {\n    return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_256.ico`);\n  }\n  if (IS_LINUX) {\n    return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_512.png`);\n  }\n  return path.join(npmDirectory, 'src-tauri', 'icons', `${baseName}.icns`);\n}\n\nfunction getIconBaseName(appName: string): string {\n  const baseName = IS_LINUX\n    ? generateLinuxPackageName(appName)\n    : generateSafeFilename(appName).toLowerCase();\n  return baseName || 'pake-app';\n}\n\nasync function copyWindowsIconIfNeeded(\n  convertedPath: string,\n  appName: string,\n): Promise<string> {\n  if (!IS_WIN || !convertedPath.endsWith('.ico')) {\n    return convertedPath;\n  }\n\n  try {\n    const finalIconPath = generateIconPath(appName);\n    await fsExtra.ensureDir(path.dirname(finalIconPath));\n    // Reorder ICO to prioritize 256px icons for better Windows display\n    const reordered = await writeIcoWithPreferredSize(\n      convertedPath,\n      finalIconPath,\n      256,\n    );\n    if (!reordered) {\n      await fsExtra.copy(convertedPath, finalIconPath);\n    }\n    return finalIconPath;\n  } catch (error) {\n    logger.warn(\n      `Failed to copy Windows icon: ${error instanceof Error ? error.message : 'Unknown error'}`,\n    );\n    return convertedPath;\n  }\n}\n\n/**\n * Adds white background to transparent icons only\n */\nasync function preprocessIcon(inputPath: string): Promise<string> {\n  try {\n    const metadata = await sharp(inputPath).metadata();\n    if (metadata.channels !== 4) return inputPath; // No transparency\n\n    const { path: tempDir } = await dir();\n    const outputPath = path.join(tempDir, 'icon-with-background.png');\n\n    await sharp({\n      create: {\n        width: metadata.width || 512,\n        height: metadata.height || 512,\n        channels: 4,\n        background: { ...ICON_CONFIG.whiteBackground, alpha: 1 },\n      },\n    })\n      .composite([{ input: inputPath }])\n      .png()\n      .toFile(outputPath);\n\n    return outputPath;\n  } catch (error) {\n    if (error instanceof Error) {\n      logger.warn(`Failed to add background to icon: ${error.message}`);\n    }\n    return inputPath;\n  }\n}\n\n/**\n * Applies macOS squircle mask to icon\n */\nasync function applyMacOSMask(inputPath: string): Promise<string> {\n  try {\n    const { path: tempDir } = await dir();\n    const outputPath = path.join(tempDir, 'icon-macos-rounded.png');\n\n    // 1. Create a 1024x1024 rounded rect mask\n    // rx=\"224\" is closer to the smooth Apple squircle look for 1024px\n    const mask = Buffer.from(\n      '<svg width=\"1024\" height=\"1024\"><rect x=\"0\" y=\"0\" width=\"1024\" height=\"1024\" rx=\"224\" ry=\"224\" fill=\"white\"/></svg>',\n    );\n\n    // 2. Load input, resize to 1024, apply mask\n    const maskedBuffer = await sharp(inputPath)\n      .resize(1024, 1024, {\n        fit: 'contain',\n        background: { r: 0, g: 0, b: 0, alpha: 0 },\n      })\n      .composite([\n        {\n          input: mask,\n          blend: 'dest-in',\n        },\n      ])\n      .png()\n      .toBuffer();\n\n    // 3. Resize to 840x840 (~18% padding) to solve \"too big\" visual issue\n    // Native MacOS icons often leave some breathing room\n    await sharp(maskedBuffer)\n      .resize(840, 840, {\n        fit: 'contain',\n        background: { r: 0, g: 0, b: 0, alpha: 0 },\n      })\n      .extend({\n        top: 92,\n        bottom: 92,\n        left: 92,\n        right: 92,\n        background: { r: 0, g: 0, b: 0, alpha: 0 },\n      })\n      .toFile(outputPath);\n\n    return outputPath;\n  } catch (error) {\n    if (error instanceof Error) {\n      logger.warn(`Failed to apply macOS mask: ${error.message}`);\n    }\n    return inputPath;\n  }\n}\n\n/**\n * Converts icon to platform-specific format\n */\nasync function convertIconFormat(\n  inputPath: string,\n  appName: string,\n): Promise<string | null> {\n  try {\n    if (!(await fsExtra.pathExists(inputPath))) return null;\n\n    const { path: outputDir } = await dir();\n    const platformOutputDir = path.join(outputDir, 'converted-icons');\n    await fsExtra.ensureDir(platformOutputDir);\n\n    const processedInputPath = await preprocessIcon(inputPath);\n    const iconName = getIconBaseName(appName);\n\n    // Generate platform-specific format\n    if (IS_WIN) {\n      // Support multiple sizes for better Windows compatibility\n      await icongen(processedInputPath, platformOutputDir, {\n        report: false,\n        ico: {\n          name: `${iconName}_256`,\n          sizes: PLATFORM_CONFIG.win.sizes,\n        },\n      });\n      return path.join(\n        platformOutputDir,\n        `${iconName}_256${PLATFORM_CONFIG.win.format}`,\n      );\n    }\n\n    if (IS_LINUX) {\n      const outputPath = path.join(\n        platformOutputDir,\n        `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`,\n      );\n\n      // Ensure we convert to proper PNG format with correct size\n      await sharp(processedInputPath)\n        .resize(PLATFORM_CONFIG.linux.size, PLATFORM_CONFIG.linux.size, {\n          fit: 'contain',\n          background: ICON_CONFIG.transparentBackground,\n        })\n        .ensureAlpha()\n        .png()\n        .toFile(outputPath);\n\n      return outputPath;\n    }\n\n    // macOS\n    const macIconPath = await applyMacOSMask(processedInputPath);\n    await icongen(macIconPath, platformOutputDir, {\n      report: false,\n      icns: { name: iconName, sizes: PLATFORM_CONFIG.macos.sizes },\n    });\n    const outputPath = path.join(\n      platformOutputDir,\n      `${iconName}${PLATFORM_CONFIG.macos.format}`,\n    );\n    return (await fsExtra.pathExists(outputPath)) ? outputPath : null;\n  } catch (error) {\n    if (error instanceof Error) {\n      logger.warn(`Icon format conversion failed: ${error.message}`);\n    }\n    return null;\n  }\n}\n\n/**\n * Processes downloaded or local icon for platform-specific format\n */\nasync function processIcon(\n  iconPath: string,\n  appName: string,\n): Promise<string | null> {\n  if (!iconPath || !appName) return iconPath;\n\n  // Check if already in correct platform format\n  const ext = path.extname(iconPath).toLowerCase();\n  const isCorrectFormat =\n    (IS_WIN && ext === '.ico') ||\n    (IS_LINUX && ext === '.png') ||\n    (!IS_WIN && !IS_LINUX && ext === '.icns');\n\n  if (isCorrectFormat) {\n    return await copyWindowsIconIfNeeded(iconPath, appName);\n  }\n\n  // Convert to platform format\n  const convertedPath = await convertIconFormat(iconPath, appName);\n  if (convertedPath) {\n    return await copyWindowsIconIfNeeded(convertedPath, appName);\n  }\n\n  return iconPath;\n}\n\n/**\n * Gets default icon with platform-specific fallback logic\n */\nasync function getDefaultIcon(): Promise<string> {\n  logger.info('✼ No icon provided, using default icon.');\n\n  if (IS_WIN) {\n    const defaultIcoPath = generateIconPath('icon', true);\n    const defaultPngPath = path.join(\n      npmDirectory,\n      'src-tauri/png/icon_512.png',\n    );\n\n    // Try default ico first\n    if (await fsExtra.pathExists(defaultIcoPath)) {\n      return defaultIcoPath;\n    }\n\n    // Convert from png if ico doesn't exist\n    if (await fsExtra.pathExists(defaultPngPath)) {\n      logger.info('✼ Default ico not found, converting from png...');\n      try {\n        const convertedPath = await convertIconFormat(defaultPngPath, 'icon');\n        if (convertedPath && (await fsExtra.pathExists(convertedPath))) {\n          return await copyWindowsIconIfNeeded(convertedPath, 'icon');\n        }\n      } catch (error) {\n        logger.warn(\n          `Failed to convert default png to ico: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        );\n      }\n    }\n\n    // Fallback to png or empty\n    if (await fsExtra.pathExists(defaultPngPath)) {\n      logger.warn('✼ Using png as fallback for Windows (may cause issues).');\n      return defaultPngPath;\n    }\n\n    logger.warn('✼ No default icon found, will use pake default.');\n    return '';\n  }\n\n  // Linux and macOS defaults\n  const iconPath = IS_LINUX\n    ? 'src-tauri/png/icon_512.png'\n    : 'src-tauri/icons/icon.icns';\n  return path.join(npmDirectory, iconPath);\n}\n\n/**\n * Main icon handling function with simplified logic flow\n */\nexport async function handleIcon(\n  options: PakeAppOptions,\n  url?: string,\n): Promise<string> {\n  // Handle custom icon (local file or remote URL)\n  if (options.icon) {\n    if (options.icon.startsWith('http')) {\n      const downloadedPath = await downloadIcon(options.icon);\n      if (downloadedPath) {\n        const result = await processIcon(downloadedPath, options.name || '');\n        if (result) return result;\n      }\n      return '';\n    }\n    // Local file path\n    const resolvedPath = path.resolve(options.icon);\n    const result = await processIcon(resolvedPath, options.name || '');\n    return result || resolvedPath;\n  }\n\n  // Check for existing local icon before downloading\n  if (options.name) {\n    const localIconPath = generateIconPath(options.name);\n    if (await fsExtra.pathExists(localIconPath)) {\n      logger.info(`✼ Using existing local icon: ${localIconPath}`);\n      return localIconPath;\n    }\n  }\n\n  // Try favicon from website\n  if (url && options.name) {\n    const faviconPath = await tryGetFavicon(url, options.name);\n    if (faviconPath) return faviconPath;\n  }\n\n  // Use default icon\n  return await getDefaultIcon();\n}\n\n/**\n * Generates icon service URLs for a domain\n */\nfunction generateIconServiceUrls(domain: string): string[] {\n  const logoDevUrls = API_KEYS.logoDev\n    .sort(() => Math.random() - 0.5)\n    .map(\n      (token) =>\n        `https://img.logo.dev/${domain}?token=${token}&format=png&size=256`,\n    );\n\n  const brandfetchUrls = API_KEYS.brandfetch\n    .sort(() => Math.random() - 0.5)\n    .map((key) => `https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`);\n\n  return [\n    ...logoDevUrls,\n    ...brandfetchUrls,\n    `https://logo.clearbit.com/${domain}?size=256`,\n    `https://www.google.com/s2/favicons?domain=${domain}&sz=256`,\n    `https://favicon.is/${domain}`,\n    `https://${domain}/favicon.ico`,\n    `https://www.${domain}/favicon.ico`,\n  ];\n}\n\n/**\n * Attempts to fetch favicon from website\n */\nasync function tryGetFavicon(\n  url: string,\n  appName: string,\n): Promise<string | null> {\n  try {\n    const domain = new URL(url).hostname;\n    const spinner = getSpinner(`Fetching icon from ${domain}...`);\n\n    const serviceUrls = generateIconServiceUrls(domain);\n\n    const isCI =\n      process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';\n    const downloadTimeout = isCI\n      ? ICON_CONFIG.downloadTimeout.ci\n      : ICON_CONFIG.downloadTimeout.default;\n\n    for (const serviceUrl of serviceUrls) {\n      try {\n        const faviconPath = await downloadIcon(\n          serviceUrl,\n          false,\n          downloadTimeout,\n        );\n        if (!faviconPath) continue;\n\n        const convertedPath = await convertIconFormat(faviconPath, appName);\n        if (convertedPath) {\n          const finalPath = await copyWindowsIconIfNeeded(\n            convertedPath,\n            appName,\n          );\n          spinner.succeed(\n            chalk.green('Icon fetched and converted successfully!'),\n          );\n          return finalPath;\n        }\n      } catch (error: unknown) {\n        if (error instanceof Error) {\n          logger.debug(`Icon service ${serviceUrl} failed: ${error.message}`);\n        }\n        continue;\n      }\n    }\n\n    spinner.warn(`No favicon found for ${domain}. Using default.`);\n    return null;\n  } catch (error) {\n    if (error instanceof Error) {\n      logger.warn(`Failed to fetch favicon: ${error.message}`);\n    }\n    return null;\n  }\n}\n\n/**\n * Downloads icon from URL\n */\nexport async function downloadIcon(\n  iconUrl: string,\n  showSpinner = true,\n  customTimeout?: number,\n): Promise<string | null> {\n  const controller = new AbortController();\n  const timeoutId = setTimeout(() => {\n    controller.abort();\n  }, customTimeout || 10000);\n\n  try {\n    const response = await fetch(iconUrl, {\n      signal: controller.signal,\n    });\n\n    clearTimeout(timeoutId);\n\n    if (!response.ok) {\n      if (response.status === 404 && !showSpinner) {\n        return null;\n      }\n      throw new Error(`HTTP ${response.status} ${response.statusText}`);\n    }\n\n    const arrayBuffer = await response.arrayBuffer();\n\n    if (!arrayBuffer || arrayBuffer.byteLength < ICON_CONFIG.minFileSize)\n      return null;\n\n    const fileDetails = await fileTypeFromBuffer(arrayBuffer);\n    if (\n      !fileDetails ||\n      !ICON_CONFIG.supportedFormats.includes(fileDetails.ext as any)\n    ) {\n      return null;\n    }\n\n    return await saveIconFile(arrayBuffer, fileDetails.ext);\n  } catch (error: unknown) {\n    clearTimeout(timeoutId);\n    if (showSpinner) {\n      if (error instanceof Error && error.name === 'AbortError') {\n        logger.error('Icon download timed out!');\n      } else {\n        logger.error(\n          'Icon download failed!',\n          error instanceof Error ? error.message : String(error),\n        );\n      }\n    }\n    return null;\n  }\n}\n\n/**\n * Saves icon file to temporary location\n */\nasync function saveIconFile(\n  iconData: ArrayBuffer,\n  extension: string,\n): Promise<string> {\n  const buffer = Buffer.from(iconData);\n  const { path: tempPath } = await dir();\n\n  // Always save with the original extension first\n  const originalIconPath = path.join(tempPath, `icon.${extension}`);\n  await fsExtra.outputFile(originalIconPath, buffer);\n\n  return originalIconPath;\n}\n"
  },
  {
    "path": "bin/options/index.ts",
    "content": "import path from 'path';\nimport fsExtra from 'fs-extra';\nimport logger from '@/options/logger';\n\nimport { handleIcon } from './icon';\nimport { getDomain } from '@/utils/url';\nimport {\n  promptText,\n  capitalizeFirstLetter,\n  resolveIdentifier,\n} from '@/utils/info';\nimport { generateLinuxPackageName } from '@/utils/name';\nimport { PakeAppOptions, PakeCliOptions, PlatformMap } from '@/types';\n\nfunction resolveAppName(name: string, platform: NodeJS.Platform): string {\n  const domain = getDomain(name) || 'pake';\n  return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;\n}\n\nfunction resolveLocalAppName(\n  filePath: string,\n  platform: NodeJS.Platform,\n): string {\n  const baseName = path.parse(filePath).name || 'pake-app';\n  if (platform === 'linux') {\n    return generateLinuxPackageName(baseName) || 'pake-app';\n  }\n  const normalized = baseName\n    .replace(/[^a-zA-Z0-9\\u4e00-\\u9fff -]/g, '')\n    .replace(/^[ -]+/, '')\n    .replace(/\\s+/g, ' ')\n    .trim();\n  return normalized || 'pake-app';\n}\n\nfunction isValidName(name: string, platform: NodeJS.Platform): boolean {\n  const platformRegexMapping: PlatformMap = {\n    linux: /^[a-z0-9\\u4e00-\\u9fff][a-z0-9\\u4e00-\\u9fff-]*$/,\n    default: /^[a-zA-Z0-9\\u4e00-\\u9fff][a-zA-Z0-9\\u4e00-\\u9fff- ]*$/,\n  };\n  const reg = platformRegexMapping[platform] || platformRegexMapping.default;\n  return !!name && reg.test(name);\n}\n\nexport default async function handleOptions(\n  options: PakeCliOptions,\n  url: string,\n): Promise<PakeAppOptions> {\n  const { platform } = process;\n  const isActions = process.env.GITHUB_ACTIONS;\n  let name = options.name;\n\n  const pathExists = await fsExtra.pathExists(url);\n  if (!options.name) {\n    const defaultName = pathExists\n      ? resolveLocalAppName(url, platform)\n      : resolveAppName(url, platform);\n    const promptMessage = 'Enter your application name';\n    const namePrompt = await promptText(promptMessage, defaultName);\n    name = namePrompt?.trim() || defaultName;\n  }\n\n  if (name && platform === 'linux') {\n    name = generateLinuxPackageName(name);\n  }\n\n  if (name && !isValidName(name, platform)) {\n    const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;\n    const DEFAULT_NAME_ERROR = `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;\n    const errorMsg =\n      platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;\n    logger.error(errorMsg);\n    if (isActions) {\n      name = resolveAppName(url, platform);\n      logger.warn(`✼ Inside github actions, use the default name: ${name}`);\n    } else {\n      process.exit(1);\n    }\n  }\n\n  const resolvedName = name || 'pake-app';\n\n  const appOptions: PakeAppOptions = {\n    ...options,\n    name: resolvedName,\n    identifier: resolveIdentifier(url, options.name, options.identifier),\n  };\n\n  const iconPath = await handleIcon(appOptions, url);\n  appOptions.icon = iconPath || '';\n\n  return appOptions;\n}\n"
  },
  {
    "path": "bin/options/logger.ts",
    "content": "import chalk from 'chalk';\nimport log from 'loglevel';\n\nconst logger = {\n  info(...msg: any[]) {\n    log.info(...msg.map((m) => chalk.white(m)));\n  },\n  debug(...msg: any[]) {\n    log.debug(...msg);\n  },\n  error(...msg: any[]) {\n    log.error(...msg.map((m) => chalk.red(m)));\n  },\n  warn(...msg: any[]) {\n    log.info(...msg.map((m) => chalk.yellow(m)));\n  },\n  success(...msg: any[]) {\n    log.info(...msg.map((m) => chalk.green(m)));\n  },\n};\n\nexport default logger;\n"
  },
  {
    "path": "bin/types.ts",
    "content": "export interface PlatformMap {\n  [key: string]: any;\n}\n\nexport interface PakeCliOptions {\n  // Application name\n  name?: string;\n\n  // Explicit app identifier / bundle id\n  identifier?: string;\n\n  // Window title (supports Chinese characters)\n  title?: string;\n\n  // Application icon\n  icon: string;\n\n  // Application window width, default 1200px\n  width: number;\n\n  // Application window height, default 780px\n  height: number;\n\n  // Whether the window is resizable, default true\n  resizable: boolean;\n\n  // Whether the window can be fullscreen, default false\n  fullscreen: boolean;\n\n  // Start window maximized, default false\n  maximize: boolean;\n\n  // Enable immersive header, default false.\n  hideTitleBar: boolean;\n\n  // Enable windows always on top, default false\n  alwaysOnTop: boolean;\n\n  // App version, the same as package.json version, default 1.0.0\n  appVersion: string;\n\n  // Force Mac to use dark mode, default false\n  darkMode: boolean;\n\n  // Disable web shortcuts, default false\n  disabledWebShortcuts: boolean;\n\n  // Set a shortcut key to wake up the app, default empty\n  activationShortcut: string;\n\n  // Custom User-Agent, default off\n  userAgent: string;\n\n  // Enable system tray, default off for macOS, on for Windows and Linux\n  showSystemTray: boolean;\n\n  // Tray icon, default same as app icon for Windows and Linux, macOS requires separate png or ico\n  systemTrayIcon: string;\n\n  // Recursive copy, when url is a local file path, if this option is enabled, the url path file and all its subFiles will be copied to the pake static file folder, default off\n  useLocalFile: false;\n\n  // Multi arch, supports both Intel and M1 chips, only for Mac\n  multiArch: boolean;\n\n  // Build target architecture/format:\n  // Linux: \"deb\", \"appimage\", \"deb-arm64\", \"appimage-arm64\"; Windows: \"x64\", \"arm64\"; macOS: \"intel\", \"apple\", \"universal\"\n  targets: string;\n\n  // Debug mode, outputs more logs\n  debug: boolean;\n\n  /** External scripts that need to be injected into the page. */\n  inject: string[];\n\n  // Set Api Proxy\n  proxyUrl: string;\n\n  // Installer language, valid for Windows users, default is en-US\n  installerLanguage: string;\n\n  // Hide window on close instead of exiting, platform-specific: true for macOS, false for others\n  hideOnClose: boolean | undefined;\n\n  // Launch app in incognito/private mode, default false\n  incognito: boolean;\n\n  // Enable WebAssembly support (Flutter Web, etc.), default false\n  wasm: boolean;\n\n  // Enable drag and drop functionality, default false\n  enableDragDrop: boolean;\n\n  // Keep raw binary file alongside installer, default false\n  keepBinary: boolean;\n\n  // Allow multiple instances, default false (single instance)\n  multiInstance: boolean;\n\n  // Allow opening multiple windows in one app instance, default false\n  multiWindow: boolean;\n\n  // Start app minimized to tray, default false\n  startToTray: boolean;\n\n  // Force navigation to stay inside the Pake window even for external links\n  forceInternalNavigation: boolean;\n\n  // Regex pattern to match URLs that should be considered internal\n  internalUrlRegex: string;\n\n  // Initial page zoom level (50-200), default 100\n  zoom: number;\n\n  // Minimum window width, default 0 (no limit)\n  minWidth: number;\n\n  // Minimum window height, default 0 (no limit)\n  minHeight: number;\n\n  // Ignore certificate errors (for self-signed certs), default false\n  ignoreCertificateErrors: boolean;\n\n  // Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging\n  iterativeBuild: boolean;\n\n  // Allow sites to open new windows, default false\n  newWindow: boolean;\n\n  // Auto-install app to /Applications (macOS) after build, default false\n  install: boolean;\n\n  // Request camera entitlement on macOS, default false\n  camera: boolean;\n\n  // Request microphone entitlement on macOS, default false\n  microphone: boolean;\n}\n\nexport interface PakeAppOptions extends PakeCliOptions {\n  identifier: string;\n}\n\nexport interface PlatformSpecific<T> {\n  macos: T;\n  linux: T;\n  windows: T;\n}\n\nexport interface WindowConfig {\n  url: string;\n  hide_title_bar: boolean;\n  fullscreen: boolean;\n  maximize: boolean;\n  width: number;\n  height: number;\n  resizable: boolean;\n  url_type: string;\n  always_on_top: boolean;\n  dark_mode: boolean;\n  disabled_web_shortcuts: boolean;\n  activation_shortcut: string;\n  hide_on_close: boolean;\n  incognito: boolean;\n  title?: string;\n  enable_wasm: boolean;\n  enable_drag_drop: boolean;\n  start_to_tray: boolean;\n  force_internal_navigation: boolean;\n  internal_url_regex: string;\n  zoom: number;\n  min_width: number;\n  min_height: number;\n  ignore_certificate_errors: boolean;\n  new_window: boolean;\n}\n\nexport interface PakeConfig {\n  windows: WindowConfig[];\n  user_agent: PlatformSpecific<string>;\n  system_tray: PlatformSpecific<boolean>;\n  system_tray_path: string;\n  proxy_url: string;\n  multi_instance: boolean;\n  multi_window: boolean;\n}\n"
  },
  {
    "path": "bin/utils/combine.ts",
    "content": "import fs from 'fs';\n\nexport default async function combineFiles(files: string[], output: string) {\n  const contents = files.map((file) => {\n    if (file.endsWith('.css')) {\n      const fileContent = fs.readFileSync(file, 'utf-8');\n      return `window.addEventListener('DOMContentLoaded', (_event) => {\n        const css = ${JSON.stringify(fileContent)};\n        const style = document.createElement('style');\n        style.innerHTML = css;\n        document.head.appendChild(style);\n      });`;\n    }\n\n    const fileContent = fs.readFileSync(file);\n    return (\n      \"window.addEventListener('DOMContentLoaded', (_event) => { \" +\n      fileContent +\n      ' });'\n    );\n  });\n  fs.writeFileSync(output, contents.join('\\n'));\n  return files;\n}\n"
  },
  {
    "path": "bin/utils/dir.ts",
    "content": "import path from 'path';\nimport { fileURLToPath } from 'url';\n\n// Convert the current module URL to a file path\nconst currentModulePath = fileURLToPath(import.meta.url);\n\n// Resolve the parent directory of the current module\nexport const npmDirectory = path.join(path.dirname(currentModulePath), '..');\n\nexport const tauriConfigDirectory = path.join(\n  npmDirectory,\n  'src-tauri',\n  '.pake',\n);\n"
  },
  {
    "path": "bin/utils/ico.ts",
    "content": "import path from 'path';\nimport fsExtra from 'fs-extra';\n\nconst ICO_HEADER_SIZE = 6;\nconst ICO_DIR_ENTRY_SIZE = 16;\nconst ICO_TYPE_ICON = 1;\n\nexport type IcoEntry = {\n  index: number;\n  width: number;\n  height: number;\n  bitCount: number;\n  bytesInRes: number;\n  imageOffset: number;\n  directory: Buffer;\n  data: Buffer;\n};\n\nfunction decodeDimension(value: number): number {\n  return value === 0 ? 256 : value;\n}\n\nfunction compareByPreferredSize(\n  preferredSize: number,\n): (a: IcoEntry, b: IcoEntry) => number {\n  return (a, b) => {\n    const aSize = Math.max(a.width, a.height);\n    const bSize = Math.max(b.width, b.height);\n\n    const aExact = aSize === preferredSize ? 0 : 1;\n    const bExact = bSize === preferredSize ? 0 : 1;\n    if (aExact !== bExact) return aExact - bExact;\n\n    const aDistance = Math.abs(aSize - preferredSize);\n    const bDistance = Math.abs(bSize - preferredSize);\n    if (aDistance !== bDistance) return aDistance - bDistance;\n\n    const aSmaller = aSize < preferredSize ? 1 : 0;\n    const bSmaller = bSize < preferredSize ? 1 : 0;\n    if (aSmaller !== bSmaller) return aSmaller - bSmaller;\n\n    if (a.bitCount !== b.bitCount) return b.bitCount - a.bitCount;\n    if (aSize !== bSize) return bSize - aSize;\n\n    return a.index - b.index;\n  };\n}\n\nexport function parseIcoBuffer(buffer: Buffer): IcoEntry[] {\n  if (buffer.length < ICO_HEADER_SIZE) {\n    throw new Error('Invalid ICO: header too short.');\n  }\n\n  const reserved = buffer.readUInt16LE(0);\n  const type = buffer.readUInt16LE(2);\n  const count = buffer.readUInt16LE(4);\n\n  if (reserved !== 0 || type !== ICO_TYPE_ICON || count < 1) {\n    throw new Error('Invalid ICO: invalid header.');\n  }\n\n  const tableSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;\n  if (buffer.length < tableSize) {\n    throw new Error('Invalid ICO: directory table too short.');\n  }\n\n  const entries: IcoEntry[] = [];\n\n  for (let i = 0; i < count; i++) {\n    const offset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;\n    const widthByte = buffer.readUInt8(offset);\n    const heightByte = buffer.readUInt8(offset + 1);\n    const bitCount = buffer.readUInt16LE(offset + 6);\n    const bytesInRes = buffer.readUInt32LE(offset + 8);\n    const imageOffset = buffer.readUInt32LE(offset + 12);\n\n    if (bytesInRes < 1 || imageOffset + bytesInRes > buffer.length) {\n      throw new Error('Invalid ICO: frame out of bounds.');\n    }\n\n    entries.push({\n      index: i,\n      width: decodeDimension(widthByte),\n      height: decodeDimension(heightByte),\n      bitCount,\n      bytesInRes,\n      imageOffset,\n      directory: buffer.subarray(offset, offset + ICO_DIR_ENTRY_SIZE),\n      data: buffer.subarray(imageOffset, imageOffset + bytesInRes),\n    });\n  }\n\n  return entries;\n}\n\nexport function buildReorderedIcoBuffer(\n  buffer: Buffer,\n  preferredSize: number,\n): Buffer {\n  const entries = parseIcoBuffer(buffer);\n  const ordered = [...entries].sort(compareByPreferredSize(preferredSize));\n  const count = ordered.length;\n  const tableSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;\n  const payloadSize = ordered.reduce(\n    (acc, entry) => acc + entry.data.length,\n    0,\n  );\n  const output = Buffer.alloc(tableSize + payloadSize);\n\n  output.writeUInt16LE(0, 0);\n  output.writeUInt16LE(ICO_TYPE_ICON, 2);\n  output.writeUInt16LE(count, 4);\n\n  let currentOffset = tableSize;\n  for (let i = 0; i < count; i++) {\n    const entry = ordered[i];\n    const entryOffset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;\n\n    entry.directory.copy(output, entryOffset, 0, 8);\n    output.writeUInt32LE(entry.data.length, entryOffset + 8);\n    output.writeUInt32LE(currentOffset, entryOffset + 12);\n    entry.data.copy(output, currentOffset);\n    currentOffset += entry.data.length;\n  }\n\n  return output;\n}\n\nexport async function writeIcoWithPreferredSize(\n  sourcePath: string,\n  outputPath: string,\n  preferredSize: number,\n): Promise<boolean> {\n  try {\n    const sourceBuffer = await fsExtra.readFile(sourcePath);\n    const reordered = buildReorderedIcoBuffer(sourceBuffer, preferredSize);\n    await fsExtra.ensureDir(path.dirname(outputPath));\n    await fsExtra.outputFile(outputPath, reordered);\n    return true;\n  } catch {\n    return false;\n  }\n}\n"
  },
  {
    "path": "bin/utils/info.ts",
    "content": "import crypto from 'crypto';\nimport prompts from 'prompts';\nimport ora from 'ora';\nimport chalk from 'chalk';\n\n// Generates a stable identifier based on the app URL (and optionally name).\n// When name is provided it is included in the hash so two apps wrapping\n// the same URL can coexist. Omitting name preserves backward compatibility\n// with identifiers generated before V3.10.1.\nexport function getIdentifier(url: string, name?: string) {\n  const hashInput = name ? `${url}::${name}` : url;\n  const postFixHash = crypto\n    .createHash('md5')\n    .update(hashInput)\n    .digest('hex')\n    .substring(0, 6);\n  return `com.pake.${postFixHash}`;\n}\n\nexport function resolveIdentifier(\n  url: string,\n  explicitName: string | undefined,\n  customIdentifier?: string,\n) {\n  const trimmedIdentifier = customIdentifier?.trim();\n  if (trimmedIdentifier) {\n    return trimmedIdentifier;\n  }\n\n  return getIdentifier(url, explicitName);\n}\n\nexport async function promptText(\n  message: string,\n  initial?: string,\n): Promise<string> {\n  const response = await prompts({\n    type: 'text',\n    name: 'content',\n    message,\n    initial,\n  });\n  return response.content;\n}\n\nexport function capitalizeFirstLetter(string: string) {\n  return string.charAt(0).toUpperCase() + string.slice(1);\n}\n\nexport function getSpinner(text: string) {\n  const loadingType = {\n    interval: 80,\n    frames: ['✦', '✶', '✺', '✵', '✸', '✹', '✺'],\n  };\n  return ora({\n    text: `${chalk.cyan(text)}\\n`,\n    spinner: loadingType,\n    color: 'cyan',\n  }).start();\n}\n"
  },
  {
    "path": "bin/utils/ip.ts",
    "content": "import dns from 'dns';\nimport http from 'http';\nimport { promisify } from 'util';\n\nimport logger from '@/options/logger';\n\nconst resolve = promisify(dns.resolve);\n\nconst ping = async (host: string) => {\n  const lookup = promisify(dns.lookup);\n  const ip = await lookup(host);\n  const start = new Date();\n\n  // Prevent timeouts from affecting user experience.\n  const requestPromise = new Promise<number>((resolve, reject) => {\n    const req = http.get(`http://${ip.address}`, (res) => {\n      const delay = new Date().getTime() - start.getTime();\n      res.resume();\n      resolve(delay);\n    });\n\n    req.on('error', (err) => {\n      reject(err);\n    });\n  });\n\n  const timeoutPromise = new Promise<number>((_, reject) => {\n    setTimeout(() => {\n      reject(new Error('Request timed out after 3 seconds'));\n    }, 1000);\n  });\n\n  return Promise.race([requestPromise, timeoutPromise]);\n};\n\nasync function isChinaDomain(domain: string): Promise<boolean> {\n  try {\n    const [ip] = await resolve(domain);\n    return await isChinaIP(ip, domain);\n  } catch (error) {\n    logger.debug(`${domain} can't be parse!`);\n    return true;\n  }\n}\n\nasync function isChinaIP(ip: string, domain: string): Promise<boolean> {\n  try {\n    const delay = await ping(ip);\n    logger.debug(`${domain} latency is ${delay} ms`);\n    return delay > 1000;\n  } catch (error) {\n    logger.debug(`ping ${domain} failed!`);\n    return true;\n  }\n}\n\nexport { isChinaDomain, isChinaIP };\n"
  },
  {
    "path": "bin/utils/name.ts",
    "content": "export function generateSafeFilename(name: string): string {\n  return name\n    .replace(/[<>:\"/\\\\|?*]/g, '_')\n    .replace(/\\s+/g, '_')\n    .replace(/\\.+$/g, '')\n    .slice(0, 255);\n}\n\nexport function getSafeAppName(name: string): string {\n  return generateSafeFilename(name).toLowerCase();\n}\n\nexport function generateLinuxPackageName(name: string): string {\n  return name\n    .toLowerCase()\n    .replace(/[^a-z0-9\\u4e00-\\u9fff]+/g, '-')\n    .replace(/^-+|-+$/g, '')\n    .replace(/-+/g, '-');\n}\n\nexport function generateIdentifierSafeName(name: string): string {\n  const cleaned = name.replace(/[^a-zA-Z0-9\\u4e00-\\u9fff]/g, '').toLowerCase();\n\n  if (cleaned === '') {\n    const fallback = Array.from(name)\n      .map((char) => {\n        const code = char.charCodeAt(0);\n        if (\n          (code >= 48 && code <= 57) ||\n          (code >= 65 && code <= 90) ||\n          (code >= 97 && code <= 122)\n        ) {\n          return char.toLowerCase();\n        }\n        return code.toString(16);\n      })\n      .join('')\n      .slice(0, 50);\n\n    return fallback || 'pake-app';\n  }\n\n  return cleaned;\n}\n\nexport function generateWindowsFilename(name: string): string {\n  return name\n    .replace(/[<>:\"/\\\\|?*]/g, '_')\n    .replace(/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i, '$&_')\n    .slice(0, 255);\n}\n\nexport function generateMacOSFilename(name: string): string {\n  return name.replace(/[:]/g, '_').slice(0, 255);\n}\n"
  },
  {
    "path": "bin/utils/platform.ts",
    "content": "const { platform } = process;\n\nexport const IS_MAC = platform === 'darwin';\nexport const IS_WIN = platform === 'win32';\nexport const IS_LINUX = platform === 'linux';\n"
  },
  {
    "path": "bin/utils/shell.ts",
    "content": "import { execa } from 'execa';\nimport { npmDirectory } from './dir';\n\nexport async function shellExec(\n  command: string,\n  timeout: number = 300000,\n  env?: Record<string, string>,\n) {\n  try {\n    const { exitCode } = await execa(command, {\n      cwd: npmDirectory,\n      // Use 'inherit' to show all output directly to user in real-time.\n      // This ensures linuxdeploy and other tool outputs are visible during builds.\n      stdio: 'inherit',\n      shell: true,\n      timeout,\n      env: env ? { ...process.env, ...env } : process.env,\n    });\n    return exitCode;\n  } catch (error: any) {\n    const exitCode = error.exitCode ?? 'unknown';\n    const errorMessage = error.message || 'Unknown error occurred';\n\n    if (error.timedOut) {\n      throw new Error(\n        `Command timed out after ${timeout}ms: \"${command}\". Try increasing timeout or check network connectivity.`,\n      );\n    }\n\n    let errorMsg = `Error occurred while executing command \"${command}\". Exit code: ${exitCode}. Details: ${errorMessage}`;\n\n    // Provide helpful guidance for common Linux AppImage build failures\n    // caused by strip tool incompatibility with modern glibc (2.38+)\n    const lowerError = errorMessage.toLowerCase();\n\n    if (\n      process.platform === 'linux' &&\n      (lowerError.includes('linuxdeploy') ||\n        lowerError.includes('appimage') ||\n        lowerError.includes('strip'))\n    ) {\n      errorMsg +=\n        '\\n\\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n' +\n        'Linux AppImage Build Failed\\n' +\n        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n\\n' +\n        'Cause: Strip tool incompatibility with glibc 2.38+\\n' +\n        '       (affects Debian Trixie, Arch Linux, and other modern distros)\\n\\n' +\n        'Quick fix:\\n' +\n        '  NO_STRIP=1 pake <url> --targets appimage --debug\\n\\n' +\n        'Alternatives:\\n' +\n        '  • Use DEB format: pake <url> --targets deb\\n' +\n        '  • Update binutils: sudo apt install binutils (or pacman -S binutils)\\n' +\n        '  • Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\\n' +\n        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';\n\n      if (\n        lowerError.includes('fuse') ||\n        lowerError.includes('operation not permitted') ||\n        lowerError.includes('/dev/fuse')\n      ) {\n        errorMsg +=\n          '\\n\\nDocker / Container hint:\\n' +\n          '  AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\\n' +\n          '    --privileged --device /dev/fuse --security-opt apparmor=unconfined\\n' +\n          '  or run on the host directly.';\n      }\n    }\n\n    throw new Error(errorMsg);\n  }\n}\n"
  },
  {
    "path": "bin/utils/url.ts",
    "content": "import * as psl from 'psl';\n\n// Extracts the domain from a given URL.\nexport function getDomain(inputUrl: string): string | null {\n  try {\n    const url = new URL(inputUrl);\n    // Use PSL to parse domain names.\n    const parsed = psl.parse(url.hostname);\n\n    // If domain is available, split it and return the SLD.\n    if ('domain' in parsed && parsed.domain) {\n      return parsed.domain.split('.')[0];\n    } else {\n      return null;\n    }\n  } catch (error) {\n    return null;\n  }\n}\n\n// Appends 'https://' protocol to the URL if not present.\nexport function appendProtocol(inputUrl: string): string {\n  try {\n    new URL(inputUrl);\n    return inputUrl;\n  } catch {\n    return `https://${inputUrl}`;\n  }\n}\n\n// Normalizes the URL by ensuring it has a protocol and is valid.\nexport function normalizeUrl(urlToNormalize: string): string {\n  const urlWithProtocol = appendProtocol(urlToNormalize);\n  try {\n    new URL(urlWithProtocol);\n    return urlWithProtocol;\n  } catch (err) {\n    throw new Error(\n      `Your url \"${urlWithProtocol}\" is invalid: ${(err as Error).message}`,\n    );\n  }\n}\n"
  },
  {
    "path": "bin/utils/validate.ts",
    "content": "import fs from 'fs';\nimport { InvalidArgumentError } from 'commander';\nimport { normalizeUrl } from './url';\n\nexport function validateNumberInput(value: string) {\n  const parsedValue = Number(value);\n  if (isNaN(parsedValue)) {\n    throw new InvalidArgumentError('Not a number.');\n  }\n  return parsedValue;\n}\n\nexport function validateUrlInput(url: string) {\n  const isFile = fs.existsSync(url);\n\n  if (!isFile) {\n    try {\n      return normalizeUrl(url);\n    } catch (error) {\n      if (error instanceof Error) {\n        throw new InvalidArgumentError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  return url;\n}\n"
  },
  {
    "path": "default_app_list.json",
    "content": "[\n  {\n    \"name\": \"deepseek\",\n    \"title\": \"DeepSeek\",\n    \"name_zh\": \"DeepSeek\",\n    \"url\": \"https://chat.deepseek.com/\"\n  },\n  {\n    \"name\": \"grok\",\n    \"title\": \"Grok\",\n    \"name_zh\": \"Grok\",\n    \"url\": \"https://grok.com/\"\n  },\n  {\n    \"name\": \"gemini\",\n    \"title\": \"Gemini\",\n    \"name_zh\": \"Gemini\",\n    \"url\": \"https://gemini.google.com/\"\n  },\n  {\n    \"name\": \"excalidraw\",\n    \"title\": \"Excalidraw\",\n    \"name_zh\": \"Excalidraw\",\n    \"url\": \"https://excalidraw.com/\"\n  },\n  {\n    \"name\": \"programmusic\",\n    \"title\": \"ProgramMusic\",\n    \"name_zh\": \"ProgramMusic\",\n    \"url\": \"https://musicforprogramming.net/\"\n  },\n  {\n    \"name\": \"twitter\",\n    \"title\": \"Twitter\",\n    \"name_zh\": \"推特\",\n    \"url\": \"https://twitter.com/\"\n  },\n  {\n    \"name\": \"youtube\",\n    \"title\": \"YouTube\",\n    \"name_zh\": \"YouTube\",\n    \"url\": \"https://www.youtube.com\"\n  },\n  {\n    \"name\": \"chatgpt\",\n    \"title\": \"ChatGPT\",\n    \"name_zh\": \"ChatGPT\",\n    \"url\": \"https://chatgpt.com/\",\n    \"new_window\": true\n  },\n  {\n    \"name\": \"flomo\",\n    \"title\": \"Flomo\",\n    \"name_zh\": \"浮墨\",\n    \"url\": \"https://v.flomoapp.com/mine\"\n  },\n  {\n    \"name\": \"qwerty\",\n    \"title\": \"Qwerty\",\n    \"name_zh\": \"Qwerty\",\n    \"url\": \"https://qwerty.kaiyi.cool/\"\n  },\n  {\n    \"name\": \"lizhi\",\n    \"title\": \"LiZhi\",\n    \"name_zh\": \"李志\",\n    \"url\": \"https://lizhi.dengdengju.com/?from=pake\"\n  },\n  {\n    \"name\": \"xiaohongshu\",\n    \"title\": \"XiaoHongShu\",\n    \"name_zh\": \"小红书\",\n    \"url\": \"https://www.xiaohongshu.com/explore\"\n  },\n  {\n    \"name\": \"youtubemusic\",\n    \"title\": \"YouTubeMusic\",\n    \"name_zh\": \"YouTubeMusic\",\n    \"url\": \"https://music.youtube.com/\"\n  },\n  {\n    \"name\": \"weread\",\n    \"title\": \"WeRead\",\n    \"name_zh\": \"微信阅读\",\n    \"url\": \"https://weread.qq.com/\"\n  }\n]\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Pake Documentation\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"README_CN.md\">简体中文</a></h4>\n\nWelcome to Pake documentation! Here you'll find comprehensive guides and documentation to help you start working with Pake as quickly as possible.\n\n## User Guides\n\n- **[CLI Command Reference](cli-usage.md)** - Complete command-line parameters and basic usage\n- **[GitHub Actions Online Build](github-actions-usage.md)** - Online build without local environment setup\n- **[Pake Action Integration](pake-action.md)** - Use Pake as a GitHub Action in your projects\n\n## Developer Guides\n\n- **[Advanced Usage & Development](advanced-usage.md)** - Code customization, project structure, development environment setup and testing guides\n- **[Contributing Guide](../CONTRIBUTING.md)** - How to contribute to Pake development\n\n## Quick Links\n\n- [Main Repository](https://github.com/tw93/Pake)\n- [Releases](https://github.com/tw93/Pake/releases)\n- [Discussions](https://github.com/tw93/Pake/discussions)\n- [Issues](https://github.com/tw93/Pake/issues)\n"
  },
  {
    "path": "docs/README_CN.md",
    "content": "# Pake 文档\n\n<h4 align=\"right\"><a href=\"README.md\">English</a> | <strong>简体中文</strong></h4>\n\n欢迎使用 Pake 文档！在这里您可以找到全面的指南和文档，帮助您快速开始使用 Pake。\n\n## 使用指南\n\n- **[CLI命令参考](cli-usage_CN.md)** - 完整的命令行参数说明和基础用法\n- **[GitHub Actions在线构建](github-actions-usage_CN.md)** - 无需本地环境的在线构建方式\n- **[Pake Action集成](pake-action.md)** - 在你的项目中使用 Pake 作为 GitHub Action\n\n## 开发指南\n\n- **[高级用法与开发](advanced-usage_CN.md)** - 代码自定义、项目结构、开发环境配置和测试指南\n- **[贡献指南](../CONTRIBUTING.md)** - 如何为 Pake 开发做贡献\n\n## 快捷链接\n\n- [主仓库](https://github.com/tw93/Pake)\n- [发布页面](https://github.com/tw93/Pake/releases)\n- [讨论区](https://github.com/tw93/Pake/discussions)\n- [问题反馈](https://github.com/tw93/Pake/issues)\n"
  },
  {
    "path": "docs/advanced-usage.md",
    "content": "# Advanced Usage\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"advanced-usage_CN.md\">简体中文</a></h4>\n\nCustomize Pake apps with style modifications, JavaScript injection, and container communication.\n\n## Style Customization\n\nRemove ads or customize appearance by modifying CSS.\n\n**Quick Process:**\n\n1. Run `pnpm run dev` for development\n2. Use DevTools to find elements to modify\n3. Edit `src-tauri/src/inject/style.js`:\n\n```javascript\nconst css = `\n  .ads-banner { display: none !important; }\n  .header { background: #1a1a1a !important; }\n`;\n```\n\n## JavaScript Injection\n\nAdd custom functionality like keyboard shortcuts.\n\n**Implementation:**\n\n1. Edit `src-tauri/src/inject/event.js`\n2. Add event listeners:\n\n```javascript\ndocument.addEventListener(\"keydown\", (e) => {\n  if (e.ctrlKey && e.key === \"k\") {\n    // Custom action\n  }\n});\n```\n\n## Built-in Features\n\n### Download Error Notifications\n\nPake automatically provides user-friendly download error notifications:\n\n**Features:**\n\n- **Bilingual Support**: Automatically detects browser language (Chinese/English)\n- **System Notifications**: Uses native OS notifications when permission is granted\n- **Graceful Fallback**: Falls back to console logging if notifications are unavailable\n- **Comprehensive Coverage**: Handles all download types (HTTP, Data URI, Blob)\n\n**User Experience:**\n\nWhen a download fails, users will see a notification:\n\n- English: \"Download Error - Download failed: filename.pdf\"\n- Chinese: \"下载错误 - 下载失败: filename.pdf\"\n\n**Requesting Notification Permission:**\n\nTo enable notifications, add this to your injected JavaScript:\n\n```javascript\n// Request notification permission on app start\nif (window.Notification && Notification.permission === \"default\") {\n  Notification.requestPermission();\n}\n```\n\nThe download system automatically handles:\n\n- Regular HTTP(S) downloads\n- Data URI downloads (base64 encoded files)\n- Blob URL downloads (dynamically generated files)\n- Context menu initiated downloads\n\n## Container Communication\n\nSend messages between web content and Pake container.\n\n**Web Side (JavaScript):**\n\n```javascript\nwindow.__TAURI__.invoke(\"handle_scroll\", {\n  scrollY: window.scrollY,\n  scrollX: window.scrollX,\n});\n```\n\n**Container Side (Rust):**\n\n```rust\n#[tauri::command]\nfn handle_scroll(scroll_y: f64, scroll_x: f64) {\n  println!(\"Scroll: {}, {}\", scroll_x, scroll_y);\n}\n```\n\n## Window Configuration\n\nConfigure window properties in `pake.json`:\n\n```json\n{\n  \"windows\": {\n    \"width\": 1200,\n    \"height\": 780,\n    \"fullscreen\": false,\n    \"resizable\": true\n  },\n  \"hideTitleBar\": true\n}\n```\n\n## Static File Packaging\n\nPackage local HTML/CSS/JS files:\n\n```bash\npake ./my-app/index.html --name my-static-app --use-local-file\n```\n\nRequirements: Pake CLI >= 3.0.0\n\n## macOS Media Permissions\n\nBy default, apps built with Pake do not request camera or microphone access. For sites that require these (for example, video conferencing or voice input), pass the relevant flags at build time:\n\n```bash\npake https://chatgpt.com --name ChatGPT --microphone\npake https://meet.google.com --name GoogleMeet --camera --microphone\n```\n\n- `--microphone` — grants microphone access (`com.apple.security.device.audio-input`)\n- `--camera` — grants camera access (`com.apple.security.device.camera`)\n\nmacOS will prompt the user for permission on first use. Only add these flags for sites that actually need them.\n\n## Multiple Apps For The Same Site\n\nIf you need separate apps for the same site, for example two Gmail accounts with different login state, build them with different app names:\n\n```bash\npake https://gmail.com --name \"Gmail Work\"\npake https://gmail.com --name \"Gmail Personal\"\n```\n\nPake now generates a different app identifier for each `URL + name` pair, so these apps can be installed as separate desktop apps instead of resolving to the same app.\n\nFor advanced cases, Pake also supports a hidden `--identifier` option if you need to pin the bundle identifier explicitly:\n\n```bash\npake https://gmail.com --name \"Gmail Work\" --identifier com.example.gmail.work\n```\n\n`--multi-instance` is different. It only allows multiple processes for the same packaged app, it does not create separate app identities.\n\n## Project Structure\n\nUnderstanding Pake's codebase structure will help you navigate and contribute effectively:\n\n```tree\n├── bin/                    # CLI source code (TypeScript)\n│   ├── builders/          # Platform-specific builders\n│   ├── helpers/           # Utility functions\n│   └── options/           # CLI option processing\n├── docs/                  # Project documentation\n├── src-tauri/             # Tauri application core\n│   ├── src/\n│   │   ├── app/           # Core modules (window, tray, shortcuts)\n│   │   ├── inject/        # Web page injection logic\n│   │   └── lib.rs         # Application entry point\n│   ├── icons/             # macOS icons (.icns)\n│   ├── png/               # Windows/Linux icons (.ico, .png)\n│   ├── pake.json          # App configuration\n│   └── tauri.*.conf.json  # Platform-specific configs\n├── scripts/               # Build and utility scripts\n└── tests/                 # Test suites\n```\n\n### Key Components\n\n- **CLI Tool** (`bin/`): TypeScript-based command interface for packaging apps\n- **Tauri App** (`src-tauri/`): Rust-based desktop framework\n- **Injection System** (`src-tauri/src/inject/`): Custom CSS/JS injection for webpages\n- **Configuration**: Multi-platform app settings and build configurations\n\n## Development Workflow\n\n### Prerequisites\n\n- Node.js ≥22.0.0 (recommended LTS, older versions ≥18.0.0 may work)\n- Rust ≥1.85.0 (recommended stable)\n\n#### Platform-Specific Requirements\n\n**macOS:**\n\n- Xcode Command Line Tools: `xcode-select --install`\n\n**Windows:**\n\n- **CRITICAL**: Consult [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) before proceeding\n- Windows 10 SDK (10.0.19041.0) and Visual Studio Build Tools 2022 (≥17.2)\n- Required redistributables:\n  1. Microsoft Visual C++ 2015-2022 Redistributable (x64)\n  2. Microsoft Visual C++ 2015-2022 Redistributable (x86)\n  3. Microsoft Visual C++ 2012 Redistributable (x86) (optional)\n  4. Microsoft Visual C++ 2013 Redistributable (x86) (optional)\n  5. Microsoft Visual C++ 2008 Redistributable (x86) (optional)\n\n- **Windows ARM (ARM64) support**: Install C++ ARM64 build tools in Visual Studio Installer under \"Individual Components\" → \"MSVC v143 - VS 2022 C++ ARM64 build tools\"\n\n**Linux (Ubuntu):**\n\n```bash\nsudo apt install libdbus-1-dev \\\n    libsoup-3.0-dev \\\n    libjavascriptcoregtk-4.1-dev \\\n    libwebkit2gtk-4.1-dev \\\n    build-essential \\\n    curl \\\n    wget \\\n    file \\\n    libxdo-dev \\\n    libssl-dev \\\n    libgtk-3-dev \\\n    libayatana-appindicator3-dev \\\n    librsvg2-dev \\\n    gnome-video-effects \\\n    gnome-video-effects-extra \\\n    libglib2.0-dev \\\n    pkg-config\n```\n\n### Installation\n\n```bash\n# Clone the repository\ngit clone https://github.com/tw93/Pake.git\ncd Pake\n\n# Install dependencies\npnpm install\n\n# Start development\npnpm run dev\n```\n\n### Development Commands\n\n1. **CLI Changes**: Edit files in `bin/`, then run `pnpm run cli:build`\n2. **Core App Changes**: Edit files in `src-tauri/src/`, then run `pnpm run dev`\n3. **Injection Logic**: Modify files in `src-tauri/src/inject/` for web customizations\n4. **Testing**: Run `pnpm test` for comprehensive validation\n\n#### Command Reference\n\n- **Dev mode**: `pnpm run dev` (hot reload)\n- **Build**: `pnpm run build`\n- **Debug build**: `pnpm run build:debug`\n- **CLI build**: `pnpm run cli:build`\n\n#### CLI Development\n\nFor CLI development with hot reloading, modify the `DEFAULT_DEV_PAKE_OPTIONS` configuration in `bin/defaults.ts`:\n\n```typescript\nexport const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = {\n  ...DEFAULT_PAKE_OPTIONS,\n  url: \"https://weekly.tw93.fun/en\",\n  name: \"Weekly\",\n};\n```\n\nThen run:\n\n```bash\npnpm run cli:dev\n```\n\nThis script reads the configuration and packages the specified app in watch mode, with hot updates for `pake-cli` code changes.\n\n### Testing Guide\n\nComprehensive CLI build and release validation guidance for multi-platform packaging.\n\n#### Running Tests\n\n```bash\n# Complete test suite (recommended)\npnpm test                   # Build the CLI, run the Vitest suite, then run real build + release workflow smoke tests\n\n# Skip the real build and release workflow smoke tests\npnpm test -- --no-build\n\n# Run the fast Vitest suite only\nnpx vitest run\n\n# Build the CLI explicitly\npnpm run cli:build\n\n# Run the release workflow smoke test directly\nnode ./tests/release.js\n```\n\n#### 🚀 Complete Test Suite Includes\n\n- ✅ **Vitest suite**: unit, integration, builder, and CLI option coverage\n- ✅ **Real build smoke test**: platform-aware packaging validation\n- ✅ **Release workflow smoke test**: verifies the release build path used for popular apps\n\n#### Test Details\n\n- `pnpm test` runs the main CLI test runner in [`tests/index.js`](../tests/index.js), which:\n- builds the CLI,\n- runs the Vitest suite,\n- runs the real build smoke test unless `--no-build` is passed,\n- and then runs the release workflow smoke test when the real build phase succeeds.\n\nUseful optional flags:\n\n- `--no-unit`: skip unit tests\n- `--no-integration`: skip integration tests\n- `--no-builder`: skip builder tests\n- `--no-build`: skip the real build smoke test and the follow-up release workflow smoke test\n- `--e2e`: add end-to-end configuration tests\n- `--pake-cli`: add GitHub Actions related checks\n\nIf you only want the release workflow smoke test, run `node ./tests/release.js` directly.\n\n#### Troubleshooting\n\n- **CLI file not found**: Run `pnpm run cli:build`\n- **Test timeout**: Build tests require extended time to complete\n- **Build failures**: Check Rust toolchain with `rustup update`\n- **Permission errors**: Ensure write permissions are available\n\n### Common Build Issues\n\n- **Rust compilation errors**: Run `cargo clean` in `src-tauri/` directory\n- **Node dependency issues**: Delete `node_modules` and run `pnpm install`\n- **Permission errors on macOS**: Run `sudo xcode-select --reset`\n\n## Links\n\n- [CLI Documentation](cli-usage.md)\n- [GitHub Discussions](https://github.com/tw93/Pake/discussions)\n"
  },
  {
    "path": "docs/advanced-usage_CN.md",
    "content": "# 高级用法\n\n<h4 align=\"right\"><strong><a href=\"advanced-usage.md\">English</a></strong> | 简体中文</h4>\n\n通过样式修改、JavaScript 注入和容器通信等方式自定义 Pake 应用。\n\n## 样式自定义\n\n通过修改 CSS 移除广告或自定义外观。\n\n**快速流程：**\n\n1. 运行 `pnpm run dev` 进入开发模式\n2. 使用开发者工具找到要修改的元素\n3. 编辑 `src-tauri/src/inject/style.js`：\n\n   ```javascript\n   const css = `\n     .ads-banner { display: none !important; }\n     .header { background: #1a1a1a !important; }\n   `;\n   ```\n\n## JavaScript 注入\n\n添加自定义功能，如键盘快捷键。\n\n**实现方式：**\n\n1. 编辑 `src-tauri/src/inject/event.js`\n2. 添加事件监听器：\n\n```javascript\ndocument.addEventListener(\"keydown\", (e) => {\n  if (e.ctrlKey && e.key === \"k\") {\n    // 自定义操作\n  }\n});\n```\n\n## 内置功能\n\n### 下载错误通知\n\nPake 自动提供用户友好的下载错误通知：\n\n**功能特性：**\n\n- **双语支持**：自动检测浏览器语言（中文/英文）\n- **系统通知**：在授予权限后使用原生操作系统通知\n- **优雅降级**：如果通知不可用则降级到控制台日志\n- **全面覆盖**：处理所有下载类型（HTTP、Data URI、Blob）\n\n**用户体验：**\n\n当下载失败时，用户将看到通知：\n\n- 英文：\"Download Error - Download failed: filename.pdf\"\n- 中文：\"下载错误 - 下载失败: filename.pdf\"\n\n**请求通知权限：**\n\n要启用通知，请在注入的 JavaScript 中添加：\n\n```javascript\n// 在应用启动时请求通知权限\nif (window.Notification && Notification.permission === \"default\") {\n  Notification.requestPermission();\n}\n```\n\n下载系统自动处理：\n\n- 常规 HTTP(S) 下载\n- Data URI 下载（base64 编码文件）\n- Blob URL 下载（动态生成的文件）\n- 右键菜单发起的下载\n\n## 容器通信\n\n在网页内容和 Pake 容器之间发送消息。\n\n**网页端（JavaScript）：**\n\n```javascript\nwindow.__TAURI__.invoke(\"handle_scroll\", {\n  scrollY: window.scrollY,\n  scrollX: window.scrollX,\n});\n```\n\n**容器端（Rust）：**\n\n```rust\n#[tauri::command]\nfn handle_scroll(scroll_y: f64, scroll_x: f64) {\n  println!(\"滚动位置: {}, {}\", scroll_x, scroll_y);\n}\n```\n\n## 窗口配置\n\n在 `pake.json` 中配置窗口属性：\n\n```json\n{\n  \"windows\": {\n    \"width\": 1200,\n    \"height\": 780,\n    \"fullscreen\": false,\n    \"resizable\": true\n  },\n  \"hideTitleBar\": true\n}\n```\n\n## 静态文件打包\n\n打包本地 HTML/CSS/JS 文件：\n\n```bash\npake ./my-app/index.html --name my-static-app --use-local-file\n```\n\n要求：Pake CLI >= 3.0.0\n\n## macOS 摄像头与麦克风权限\n\nPake 构建的应用默认不申请摄像头或麦克风权限。对于需要这些权限的站点（例如视频会议或语音输入），在构建时传入对应的标志：\n\n```bash\npake https://chatgpt.com --name ChatGPT --microphone\npake https://meet.google.com --name GoogleMeet --camera --microphone\n```\n\n- `--microphone` — 申请麦克风权限（`com.apple.security.device.audio-input`）\n- `--camera` — 申请摄像头权限（`com.apple.security.device.camera`）\n\nmacOS 会在首次使用时向用户弹出权限确认对话框。请仅在确实需要的站点上添加这些标志。\n\n## 同一站点生成多个独立应用\n\n如果你需要为同一个站点生成多个彼此独立的应用，例如两个不同登录态的 Gmail，可以直接使用不同的应用名称进行构建：\n\n```bash\npake https://gmail.com --name \"Gmail Work\"\npake https://gmail.com --name \"Gmail Personal\"\n```\n\nPake 现在会基于 `URL + name` 生成不同的应用标识，因此这两个应用会被当作两个独立桌面应用安装，而不是落到同一个应用上。\n\n对于需要固定 bundle identifier 的高级场景，Pake 也支持一个隐藏参数 `--identifier`：\n\n```bash\npake https://gmail.com --name \"Gmail Work\" --identifier com.example.gmail.work\n```\n\n`--multi-instance` 和这个能力不同，它只是允许同一个已打包应用启动多个进程，并不会创建多个独立应用身份。\n\n## 项目结构\n\n了解 Pake 的代码库结构将帮助您有效地进行导航和贡献：\n\n```tree\n├── bin/                    # CLI 源代码 (TypeScript)\n│   ├── builders/          # 平台特定的构建器\n│   ├── helpers/           # 实用函数\n│   └── options/           # CLI 选项处理\n├── docs/                  # 项目文档\n├── src-tauri/             # Tauri 应用核心\n│   ├── src/\n│   │   ├── app/           # 核心模块（窗口、托盘、快捷键）\n│   │   ├── inject/        # 网页注入逻辑\n│   │   └── lib.rs         # 应用程序入口点\n│   ├── icons/             # macOS 图标 (.icns)\n│   ├── png/               # Windows/Linux 图标 (.ico, .png)\n│   ├── pake.json          # 应用配置\n│   └── tauri.*.conf.json  # 平台特定配置\n├── scripts/               # 构建和实用脚本\n└── tests/                 # 测试套件\n```\n\n### 关键组件\n\n- **CLI 工具** (`bin/`): 基于 TypeScript 的命令接口，用于打包应用\n- **Tauri 应用** (`src-tauri/`): 基于 Rust 的桌面框架\n- **注入系统** (`src-tauri/src/inject/`): 用于网页的自定义 CSS/JS 注入\n- **配置**: 多平台应用设置和构建配置\n\n## 开发工作流\n\n### 前置条件\n\n- Node.js ≥22.0.0 (推荐 LTS，较旧版本 ≥18.0.0 可能可用)\n- Rust ≥1.85.0 (推荐稳定版)\n\n#### 平台特定要求\n\n**macOS:**\n\n- Xcode 命令行工具：`xcode-select --install`\n\n**Windows:**\n\n- **重要**：请先参阅 [Tauri 依赖项指南](https://v2.tauri.app/start/prerequisites/)\n- Windows 10 SDK (10.0.19041.0) 和 Visual Studio Build Tools 2022 (≥17.2)\n- 必需的运行库：\n  1. Microsoft Visual C++ 2015-2022 Redistributable (x64)\n  2. Microsoft Visual C++ 2015-2022 Redistributable (x86)\n  3. Microsoft Visual C++ 2012 Redistributable (x86)（可选）\n  4. Microsoft Visual C++ 2013 Redistributable (x86)（可选）\n  5. Microsoft Visual C++ 2008 Redistributable (x86)（可选）\n\n- **Windows ARM (ARM64) 支持**：在 Visual Studio Installer 中的\"单个组件\"下安装\"MSVC v143 - VS 2022 C++ ARM64 构建工具\"\n\n**Linux (Ubuntu):**\n\n```bash\nsudo apt install libdbus-1-dev \\\n    libsoup-3.0-dev \\\n    libjavascriptcoregtk-4.1-dev \\\n    libwebkit2gtk-4.1-dev \\\n    build-essential \\\n    curl \\\n    wget \\\n    file \\\n    libxdo-dev \\\n    libssl-dev \\\n    libgtk-3-dev \\\n    libayatana-appindicator3-dev \\\n    librsvg2-dev \\\n    gnome-video-effects \\\n    gnome-video-effects-extra \\\n    libglib2.0-dev \\\n    pkg-config\n```\n\n### 安装\n\n```bash\n# 克隆仓库\ngit clone https://github.com/tw93/Pake.git\ncd Pake\n\n# 安装依赖\npnpm install\n\n# 开始开发\npnpm run dev\n```\n\n### 开发命令\n\n1. **CLI 更改**: 编辑 `bin/` 中的文件，然后运行 `pnpm run cli:build`\n2. **核心应用更改**: 编辑 `src-tauri/src/` 中的文件，然后运行 `pnpm run dev`\n3. **注入逻辑**: 修改 `src-tauri/src/inject/` 中的文件以进行网页自定义\n4. **测试**: 运行 `pnpm test` 进行综合验证\n\n#### 命令参考\n\n- **开发模式**：`pnpm run dev`（热重载）\n- **构建**：`pnpm run build`\n- **调试构建**：`pnpm run build:debug`\n- **CLI 构建**：`pnpm run cli:build`\n\n#### CLI 开发调试\n\n对于需要热重载的 CLI 开发，可修改 `bin/defaults.ts` 中的 `DEFAULT_DEV_PAKE_OPTIONS` 配置：\n\n```typescript\nexport const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = {\n  ...DEFAULT_PAKE_OPTIONS,\n  url: \"https://weekly.tw93.fun/en\",\n  name: \"Weekly\",\n};\n```\n\n然后运行：\n\n```bash\npnpm run cli:dev\n```\n\n此脚本会读取上述配置并使用 watch 模式打包指定的应用，对 `pake-cli` 代码修改可实时热更新。\n\n### 测试指南\n\n统一的 CLI 构建与发布验证指南，用于验证多平台打包功能。\n\n#### 运行测试\n\n```bash\n# 完整测试套件（推荐）\npnpm test                   # 构建 CLI，运行 Vitest 套件，再执行真实构建和发布流程 smoke test\n\n# 跳过真实构建和发布流程 smoke test\npnpm test -- --no-build\n\n# 仅运行快速 Vitest 套件\nnpx vitest run\n\n# 构建 CLI 以供测试\npnpm run cli:build\n\n# 单独运行发布流程 smoke test\nnode ./tests/release.js\n```\n\n#### 🚀 完整测试套件包含\n\n- ✅ **Vitest 套件**：单元、集成、构建器和 CLI 选项覆盖\n- ✅ **真实构建 smoke test**：按平台验证实际打包流程\n- ✅ **发布流程 smoke test**：验证 popular apps 的发布构建路径\n\n#### 测试内容详情\n\n- `pnpm test` 会运行 [`tests/index.js`](../tests/index.js) 这个主测试入口，它会：\n- 先构建 CLI，\n- 再运行 Vitest 套件，\n- 如果没有传 `--no-build`，继续执行真实构建 smoke test，\n- 然后在真实构建成功后继续执行发布流程 smoke test。\n\n常用可选参数：\n\n- `--no-unit`：跳过单元测试\n- `--no-integration`：跳过集成测试\n- `--no-builder`：跳过构建器测试\n- `--no-build`：跳过真实构建 smoke test 以及后续的发布流程 smoke test\n- `--e2e`：增加端到端配置测试\n- `--pake-cli`：增加 GitHub Actions 相关检查\n\n如果只想单独验证发布流程，可以直接运行 `node ./tests/release.js`。\n\n#### 故障排除\n\n- **CLI 文件不存在**：运行 `pnpm run cli:build`\n- **测试超时**：构建测试需要较长时间完成\n- **构建失败**：检查 Rust 工具链 `rustup update`\n- **权限错误**：确保有写入权限\n\n### 常见构建问题\n\n- **Rust 编译错误**: 在 `src-tauri/` 目录中运行 `cargo clean`\n- **Node 依赖问题**: 删除 `node_modules` 并运行 `pnpm install`\n- **macOS 权限错误**: 运行 `sudo xcode-select --reset`\n\n## 链接\n\n- [CLI 文档](cli-usage_CN.md)\n- [GitHub 讨论区](https://github.com/tw93/Pake/discussions)\n"
  },
  {
    "path": "docs/cli-usage.md",
    "content": "# CLI Usage Guide\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"cli-usage_CN.md\">简体中文</a></h4>\n\nComplete command-line reference and basic usage for Pake CLI.\n\n## Installation\n\nEnsure that your Node.js version is 22.0 or higher (e.g., 22.11.0). _Note: Older versions ≥18.0.0 may also work._\n\n**Recommended (pnpm):**\n\n```bash\npnpm install -g pake-cli\n```\n\n**Alternative (npm):**\n\n```bash\nnpm install -g pake-cli\n```\n\n**If you encounter permission issues:**\n\n```bash\n# Use npx to run without global installation\nnpx pake-cli [url] [options]\n\n# Or fix npm permissions permanently\nnpm config set prefix ~/.npm-global\necho 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc\nsource ~/.bashrc\n```\n\n**Prerequisites:**\n\n- Node.js ≥18.0.0\n- Rust ≥1.85.0 (installed automatically if missing)\n- **macOS/Linux**: `curl`, `wget`, `file` and `tar` used for dependency management\n\n## Quick Start\n\n```bash\n# Basic usage - automatically fetches website icon\npake https://github.com --name \"GitHub\"\n\n# Advanced usage with custom options\npake https://weekly.tw93.fun --name \"Weekly\" --icon https://cdn.tw93.fun/pake/weekly.icns --width 1200 --height 800 --hide-title-bar\n\n# Complete example with multiple options\npake https://github.com --name \"GitHub Desktop\" --width 1400 --height 900 --show-system-tray --debug\n\n```\n\n## CLI Usage\n\n```bash\npake [url] [options]\n```\n\nThe packaged application will be located in the current working directory by default. The first packaging might take some time due to environment configuration. Please be patient.\n\n> **macOS Output**: On macOS, Pake creates DMG installers by default. To create `.app` bundles for testing (to avoid user interaction), set the environment variable `PAKE_CREATE_APP=1`. If you want Pake to install the app directly into `/Applications`, use `--install`, which builds an `.app`, copies it into `/Applications`, and removes the local bundle after a successful install.\n>\n> **Note**: Packaging requires the Rust environment. If Rust is not installed, you will be prompted for installation confirmation. In case of installation failure or timeout, you can [install it manually](https://www.rust-lang.org/tools/install).\n\n### [url]\n\nThe URL is the link to the web page you want to package or the path to a local HTML file. This is mandatory.\n\n### [options]\n\nVarious options are available for customization. Here are the most commonly used ones:\n\n| Option             | Description                                     | Example                                        |\n| ------------------ | ----------------------------------------------- | ---------------------------------------------- |\n| `--name`           | Application name                                | `--name \"Weekly\"`                              |\n| `--icon`           | Custom icon (optional, auto-fetch website icon) | `--icon https://cdn.tw93.fun/pake/weekly.icns` |\n| `--width`          | Window width (default: 1200px)                  | `--width 1400`                                 |\n| `--height`         | Window height (default: 780px)                  | `--height 900`                                 |\n| `--hide-title-bar` | Immersive header (macOS only)                   | `--hide-title-bar`                             |\n| `--debug`          | Enable development tools                        | `--debug`                                      |\n\nFor complete options, see detailed sections below.\n\n#### [name]\n\nSpecify the application name. If not provided, you will be prompted to enter it. It is recommended to use English.\n\n**Note**: Also supports multiple words with automatic platform-specific handling:\n\n- **Windows/macOS**: Preserves spaces and case (e.g., `\"Google Translate\"`)\n- **Linux**: Converts to lowercase with hyphens (e.g., `\"google-translate\"`)\n\n```shell\n--name <string>\n--name MyApp\n\n# Multiple words (if needed):\n--name \"Google Translate\"\n```\n\n#### [icon]\n\n**Optional parameter**: If not provided, Pake will automatically fetch the website's icon and convert to the appropriate format. For custom icons, visit [icon-icons](https://icon-icons.com) or [macOSicons](https://macosicons.com/#/).\n\nSupports both local and remote files, automatically converts to platform-specific formats:\n\n- macOS: `.icns` format\n- Windows: `.ico` format\n- Linux: `.png` format\n\n```shell\n--icon <path>\n\n# Examples:\n# Without --icon parameter, auto-fetch website icon\npake https://github.com --name GitHub\n\n# With custom icons\n--icon ./my-icon.png\n--icon https://cdn.tw93.fun/pake/weekly.icns  # Remote icon (.icns for macOS)\n```\n\n#### [height]\n\nSet the height of the application window. Default is `780px`.\n\n```shell\n--height <number>\n```\n\n#### [width]\n\nSet the width of the application window. Default is `1200px`.\n\n```shell\n--width <number>\n```\n\n#### [min-width]\n\nSet the minimum width that the window can be resized to. Keeps layouts usable when the window is dragged small.\n\n```shell\n--min-width <number>\n```\n\n#### [min-height]\n\nSet the minimum height that the window can be resized to. Prevents UI breakage caused by very short windows.\n\n```shell\n--min-height <number>\n```\n\n#### [zoom]\n\nSet initial page zoom level (50-200). Default is `100`. Users can still adjust with `Cmd/Ctrl +/-/0` shortcuts.\n\n```shell\n--zoom <number>\n--zoom 80   # 80%\n--zoom 120  # 120%\n```\n\n#### [hide-title-bar]\n\nEnable or disable immersive header. Default is `false`. Use the following command to enable this feature, macOS only.\n\n```shell\n--hide-title-bar\n```\n\n#### [fullscreen]\n\nDetermine whether the application launches in full screen. Default is `false`. Use the following command to enable full\nscreen.\n\n```shell\n--fullscreen\n```\n\n#### [maximize]\n\nDetermine whether the application launches with a maximized window. Default is `false`. Use the following command to enable\nmaximize.\n\n```shell\n--maximize\n```\n\n#### [activation-shortcut]\n\nSet the activation shortcut for the application. Default is empty, so it does not take effect. You can customize the activation shortcut with the following commands, e.g. `CmdOrControl+Shift+P`. Usage can refer to [available-modifiers](https://www.electronjs.org/docs/latest/api/accelerator#available-modifiers).\n\n```shell\n--activation-shortcut <string>\n```\n\n#### [always-on-top]\n\nSets whether the window is always at the top level, defaults to `false`.\n\n```shell\n--always-on-top\n```\n\n#### [app-version]\n\nSet the version number of the packaged application to be consistent with the naming format of version in package.json, defaulting to `1.0.0`.\n\n```shell\n--app-version <string>\n```\n\n#### [dark-mode]\n\nForce Mac to package applications using dark mode, default is `false`.\n\n```shell\n--dark-mode\n```\n\n#### [disabled-web-shortcuts]\n\nSets whether to disable web shortcuts in the original Pake container, defaults to `false`.\n\n```shell\n--disabled-web-shortcuts\n```\n\n#### [force-internal-navigation]\n\nKeeps every clicked link (even pointing to other domains) inside the Pake window instead of letting the OS open an external browser or helper. Default is `false`.\n\n```shell\n--force-internal-navigation\n```\n\n#### [internal-url-regex]\n\nSet a regex pattern to determine which URLs should be considered internal (opened within the app). When set, this pattern takes precedence over the default domain-based matching. Useful when you want to limit internal navigation to specific paths on a domain.\n\n```shell\n--internal-url-regex <pattern>\n\n# Example: Only treat facebook.com/messages paths as internal\n--internal-url-regex \"^https://www\\\\.facebook\\\\.com/messages(/.*)?$\"\n\n# Example: Only treat specific subdomains as internal\n--internal-url-regex \"^https://(app|api)\\\\.example\\\\.com\"\n```\n\n#### [multi-arch]\n\nPackage the application to support both Intel and M1 chips, exclusively for macOS. Default is `false`.\n\n##### Prerequisites\n\n- Note: After enabling this option, Rust must be installed using rustup from the official Rust website. Installation via brew is not supported.\n- For Intel chip users, install the arm64 cross-platform package to support M1 chips using the following command:\n\n  ```shell\n  rustup target add aarch64-apple-darwin\n  ```\n\n- For M1 chip users, install the x86 cross-platform package to support Intel chips using the following command:\n\n  ```shell\n  rustup target add x86_64-apple-darwin\n  ```\n\n##### Usage\n\n```shell\n--multi-arch\n```\n\n#### [targets]\n\nSpecify the build target architecture or format:\n\n- **Linux**: `deb`, `appimage`, `rpm`, `deb-arm64`, `appimage-arm64`, `rpm-arm64` (default: `deb`, `appimage`)\n- **Windows**: `x64`, `arm64` (auto-detects if not specified)\n- **macOS**: `intel`, `apple`, `universal` (auto-detects if not specified)\n\n```shell\n--targets <target>\n\n# Examples:\n--targets arm64          # Windows ARM64\n--targets x64            # Windows x64\n--targets universal      # macOS Universal (Intel + Apple Silicon)\n--targets apple          # macOS Apple Silicon only\n--targets intel          # macOS Intel only\n--targets deb            # Linux DEB package (x64)\n--targets rpm            # Linux RPM package (x64)\n--targets appimage       # Linux AppImage (x64)\n--targets deb-arm64      # Linux DEB package (ARM64)\n--targets rpm-arm64      # Linux RPM package (ARM64)\n--targets appimage-arm64 # Linux AppImage (ARM64)\n```\n\n**Note for Linux ARM64**:\n\n- Cross-compilation requires additional setup. Install `gcc-aarch64-linux-gnu` and configure environment variables for cross-compilation.\n- ARM64 support enables Pake apps to run on ARM-based Linux devices, including Linux phones (postmarketOS, Ubuntu Touch), Raspberry Pi, and other ARM64 Linux systems.\n- Use `--target appimage-arm64` for portable ARM64 applications that work across different ARM64 Linux distributions.\n\n#### [user-agent]\n\nCustomize the browser user agent. Default is empty.\n\n```shell\n--user-agent <string>\n```\n\n#### [show-system-tray]\n\nDisplay the application in system tray. Default is `false`.\n\n```shell\n--show-system-tray\n```\n\n#### [system-tray-icon]\n\nSpecify the system tray icon. This is only effective when the system tray is enabled. The icon must be in `.ico` or `.png` format and should be an image with dimensions ranging from 32x32 to 256x256 pixels.\n\n```shell\n--system-tray-icon <path>\n```\n\n#### [hide-on-close]\n\nHide window instead of closing the application when clicking close button. Platform-specific default: `true` for macOS, `false` for Windows/Linux.\n\n```shell\n# Hide on close (default behavior on macOS)\n--hide-on-close\n--hide-on-close true\n\n# Close application immediately (default behavior on Windows/Linux)\n--hide-on-close false\n```\n\n#### [start-to-tray]\n\nStart the application minimized to system tray instead of showing the window. Must be used with `--show-system-tray`. Default is `false`.\n\n```shell\n--start-to-tray\n\n# Example: Start hidden to tray (must use with --show-system-tray)\npake https://github.com --name GitHub --show-system-tray --start-to-tray\n```\n\n**Note**: Double-click the tray icon to show/hide the window. If used without `--show-system-tray`, this option is ignored.\n\n#### [title]\n\nSet the window title bar text. macOS shows no title if not specified; Windows/Linux fallback to app name.\n\n```shell\n--title <string>\n\n# Examples:\n--title \"My Application\"\n--title \"Google Translate\"\n```\n\n#### [incognito]\n\nLaunch the application in incognito/private browsing mode. Default is `false`. When enabled, the webview will run in private mode, which means it won't store cookies, local storage, or browsing history. This is useful for privacy-sensitive applications.\n\n```shell\n--incognito\n```\n\n#### [wasm]\n\nEnable WebAssembly support with cross-origin isolation headers. Required for Flutter Web applications and other web applications that use WebAssembly modules like `sqlite3.wasm`, `canvaskit.wasm`. Default is `false`.\n\nThis option adds necessary HTTP headers (`Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`) and browser flags to enable SharedArrayBuffer and WebAssembly features.\n\n```shell\n--wasm\n\n# Example: Package a Flutter Web app with WASM support\npake https://flutter.dev --name FlutterApp --wasm\n```\n\n#### [enable-drag-drop]\n\nEnable native drag and drop functionality within the application. Default is `false`. When enabled, allows drag and drop operations like reordering items, file uploads, and other interactive drag behaviors that work in regular browsers.\n\n```shell\n--enable-drag-drop\n\n# Example: Package an app that requires drag-drop functionality\npake https://planka.example.com --name PlankApp --enable-drag-drop\n```\n\n#### [keep-binary]\n\nKeep the raw binary file alongside the installer. Default is `false`. When enabled, also outputs a standalone executable that can run without installation.\n\n```shell\n--keep-binary\n\n# Example: Package app with both installer and standalone binary\npake https://github.com --name GitHub --keep-binary\n```\n\n**Output**: Creates both installer and standalone executable (`AppName-binary` on Unix, `AppName.exe` on Windows).\n\n#### [iterative-build]\n\nTurn on rapid build mode (app only, no dmg/deb/msi), good for debugging. Default is `false`.\n\n```shell\n--iterative-build\n```\n\n#### [install]\n\nInstall the built macOS app directly into `/Applications`. Default is `false`.\n\nThis option is macOS-only and is intended for local development or quick testing. When enabled, Pake builds an `.app` bundle, copies it into `/Applications`, replaces any existing app with the same name, and removes the local bundle after a successful install. If the install fails, the local `.app` is kept in the current working directory.\n\n```shell\n--install\n\n# Example: Build and install directly to /Applications\npake https://github.com --name GitHub --install\n```\n\n#### [multi-instance]\n\nAllow the packaged app to run more than one instance at the same time. Default is `false`, which means launching a second instance simply focuses the existing window. Enable this when you need to open several windows of the same app simultaneously.\n\n```shell\n--multi-instance\n\n# Example: Allow multiple chat windows\npake https://chat.example.com --name ChatApp --multi-instance\n```\n\n#### [multi-window]\n\nAllow opening multiple windows within a single running app instance. Default is `false`.\n\nThis is different from `--multi-instance`:\n\n- `--multi-instance`: starts multiple app processes.\n- `--multi-window`: keeps one process and opens extra windows from that process.\n\nWhen enabled, relaunching an already running app opens a new window instead of only focusing the existing one.\n\nThis can improve popup-based authentication flows, but it cannot bypass provider policy. Some providers, especially Google, may still reject sign-in inside embedded webviews.\n\n```shell\n--multi-window\n\n# Example: Keep one process but open multiple windows\npake https://chat.example.com --name ChatApp --multi-window\n```\n\n#### [installer-language]\n\nSet the Windows Installer language. Options include `zh-CN`, `ja-JP`, More at [Tauri docs](https://v2.tauri.app/distribute/windows-installer/#internationalization). Default is `en-US`.\n\n```shell\n--installer-language <language>\n```\n\n#### [use-local-file]\n\nEnable recursive copying. When the URL is a local file path, enabling this option will copy the folder containing the file specified in the URL, as well as all sub-files, to the Pake static folder. This is disabled by default.\n\n```shell\n--use-local-file\n\n# Basic static file packaging\npake ./my-app/index.html --name \"my-app\" --use-local-file\n```\n\n#### [inject]\n\nUsing `inject`, you can inject local absolute and relative path `css` and `js` files into the page you specify the `url` to customize it. For example, an adblock script that can be applied to any web page, or a `css` that optimizes the `UI` of a page, you can write it once to customize it. would only need to write the `app` once to generalize it to any other page.\n\nSupports both comma-separated and multiple option formats:\n\n```shell\n# Comma-separated (recommended)\n--inject ./tools/style.css,./tools/hotkey.js\n\n# Multiple options\n--inject ./tools/style.css --inject ./tools/hotkey.js\n\n# Single file\n--inject ./tools/style.css\n```\n\n#### [proxy-url]\n\nSet proxy server for all network requests. Supports HTTP, HTTPS, and SOCKS5. Available on Windows and Linux. On macOS, requires macOS 14+.\n\n```shell\n--proxy-url http://127.0.0.1:7890\n--proxy-url socks5://127.0.0.1:7891\n```\n\n#### [debug]\n\nEnable developer tools and detailed logging for debugging.\n\n```shell\n--debug\n```\n\n#### [ignore-certificate-errors]\n\nIgnore TLS certificate validation errors when loading the target URL. Useful for intranet apps, dev servers, or self-signed certificates.\n\n```shell\n--ignore-certificate-errors\n```\n\n#### [new-window]\n\nAllow sites to open new windows, such as authentication popups, extra tabs, or branch views.\n\nThis can help sites that rely on popup auth windows, but it does not guarantee in-app sign-in. Some providers, especially Google, may block authentication inside embedded webviews regardless of this option.\n\n```shell\n--new-window\n```\n\n### Packaging Complete\n\nAfter completing the above steps, your application should be successfully packaged. Please note that the packaging process may take some time depending on your system configuration and network conditions. Be patient, and once the packaging is complete, you can find the application installer in the specified directory.\n\n## Docker\n\n```shell\n# Run the Pake CLI via Docker (AppImage builds need FUSE access)\ndocker run --rm --privileged \\\n    --device /dev/fuse \\\n    --security-opt apparmor=unconfined \\\n    -v YOUR_DIR:/output \\\n    ghcr.io/tw93/pake \\\n    <arguments>\n\n# For example:\ndocker run --rm --privileged \\\n    --device /dev/fuse \\\n    --security-opt apparmor=unconfined \\\n    -v ./packages:/output \\\n    ghcr.io/tw93/pake \\\n    https://example.com --name myapp --icon ./icon.png --targets appimage\n```\n"
  },
  {
    "path": "docs/cli-usage_CN.md",
    "content": "# CLI 使用指南\n\n<h4 align=\"right\"><strong><a href=\"cli-usage.md\">English</a></strong> | 简体中文</h4>\n\n完整的命令行参数说明和基础用法指南。\n\n## 安装\n\n请确保您的 Node.js 版本为 22 或更高版本（例如 22.11.0）。_注意：较旧的版本 ≥18.0.0 也可能可以工作。_\n\n**推荐方式 (pnpm)：**\n\n```bash\npnpm install -g pake-cli\n```\n\n**备选方式 (npm)：**\n\n```bash\nnpm install -g pake-cli\n```\n\n**如果遇到权限问题：**\n\n```bash\n# 使用 npx 运行，无需全局安装\nnpx pake-cli [url] [选项]\n\n# 或者永久修复 npm 权限\nnpm config set prefix ~/.npm-global\necho 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc\nsource ~/.bashrc\n```\n\n**前置条件：**\n\n- Node.js ≥18.0.0\n- Rust ≥1.85.0（如缺失将自动安装）\n- **macOS/Linux**：`curl`、`wget`、`file` 和 `tar`（用于依赖管理）\n\n## 快速开始\n\n```bash\n# 基础用法 - 自动获取网站图标\npake https://github.com --name \"GitHub\"\n\n# 高级用法：自定义选项\npake https://weekly.tw93.fun --name \"Weekly\" --icon https://cdn.tw93.fun/pake/weekly.icns --width 1200 --height 800 --hide-title-bar\n\n# 完整示例：多个选项组合使用\npake https://github.com --name \"GitHub Desktop\" --width 1400 --height 900 --show-system-tray --debug\n\n```\n\n## 命令行使用\n\n```bash\npake [url] [options]\n```\n\n应用程序的打包结果将默认保存在当前工作目录。由于首次打包需要配置环境，这可能需要一些时间，请耐心等待。\n\n> **macOS 输出**：在 macOS 上，Pake 默认创建 DMG 安装程序。如需创建 `.app` 包进行测试（避免用户交互），请设置环境变量 `PAKE_CREATE_APP=1`。如果希望 Pake 直接将应用安装到 `/Applications`，可以使用 `--install`；该选项会构建 `.app`、复制到 `/Applications`，并在安装成功后删除当前目录中的本地 `.app`。\n>\n> **注意**：打包过程需要使用 `Rust` 环境。如果您没有安装 `Rust`，系统会提示您是否要安装。如果遇到安装失败或超时的问题，您可以 [手动安装](https://www.rust-lang.org/tools/install)。\n\n### [url]\n\n`url` 是您需要打包的网页链接 🔗 或本地 HTML 文件的路径，此参数为必填。\n\n### [options]\n\n您可以通过传递以下选项来定制打包过程。以下是最常用的选项：\n\n| 选项               | 描述                                 | 示例                                           |\n| ------------------ | ------------------------------------ | ---------------------------------------------- |\n| `--name`           | 应用程序名称                         | `--name \"Weekly\"`                              |\n| `--icon`           | 自定义图标（可选，自动获取网站图标） | `--icon https://cdn.tw93.fun/pake/weekly.icns` |\n| `--width`          | 窗口宽度（默认：1200px）             | `--width 1400`                                 |\n| `--height`         | 窗口高度（默认：780px）              | `--height 900`                                 |\n| `--hide-title-bar` | 沉浸式标题栏（仅macOS）              | `--hide-title-bar`                             |\n| `--debug`          | 启用开发者工具                       | `--debug`                                      |\n\n完整选项请参见下面的详细说明：\n\n#### [name]\n\n指定应用程序的名称，如果未指定，系统会提示您输入，建议使用英文单词。\n\n**注意**: 支持带空格的名称，会自动处理不同平台的命名规范:\n\n- **Windows/macOS**: 保持空格和大小写（如 `\"Google Translate\"`）\n- **Linux**: 自动转换为小写并用连字符连接（如 `\"google-translate\"`）\n\n```shell\n--name <string>\n--name MyApp\n\n# 带空格的名称:\n--name \"Google Translate\"\n```\n\n#### [icon]\n\n**可选参数**：不传此参数时，Pake 会自动获取网站图标并转换为对应格式。如需自定义图标，可访问 [icon-icons](https://icon-icons.com) 或 [macOSicons](https://macosicons.com/#/) 下载。\n\n支持本地或远程文件，自动转换为平台所需格式：\n\n- macOS：`.icns` 格式\n- Windows：`.ico` 格式\n- Linux：`.png` 格式\n\n```shell\n--icon <path>\n\n# 示例：\n# 不传 --icon 参数，自动获取网站图标\npake https://github.com --name GitHub\n\n# 使用自定义图标\n--icon ./my-icon.png\n--icon https://cdn.tw93.fun/pake/weekly.icns  # 远程图标（.icns适用于macOS）\n```\n\n#### [height]\n\n设置应用窗口的高度，默认为 `780px`。\n\n```shell\n--height <number>\n```\n\n#### [width]\n\n设置应用窗口的宽度，默认为 `1200px`。\n\n```shell\n--width <number>\n```\n\n#### [min-width]\n\n设置窗口可以缩放到的最小宽度，防止窗口被拖得过小导致控件错位。\n\n```shell\n--min-width <number>\n```\n\n#### [min-height]\n\n设置窗口可以缩放到的最小高度，避免界面内容因高度过小而错乱。\n\n```shell\n--min-height <number>\n```\n\n#### [zoom]\n\n设置初始页面缩放级别（50-200），默认为 `100`。用户仍可通过快捷键（`Cmd/Ctrl +/-/0`）调整。\n\n```shell\n--zoom <number>\n--zoom 80   # 80%\n--zoom 120  # 120%\n```\n\n#### [hide-title-bar]\n\n设置是否启用沉浸式头部，默认为 `false`（不启用）。当前只对 macOS 上有效。\n\n```shell\n--hide-title-bar\n```\n\n#### [fullscreen]\n\n设置应用程序是否在启动时自动全屏，默认为 `false`。使用以下命令可以设置应用程序启动时自动全屏。\n\n```shell\n--fullscreen\n```\n\n#### [maximize]\n\n设置应用程序是否在启动时最大化窗口，默认为 `false`。使用以下命令可以设置应用程序启动时窗口最大化。\n\n```shell\n--maximize\n```\n\n#### [activation-shortcut]\n\n设置应用程序的激活快捷键。默认为空，不生效，可以使用以下命令自定义激活快捷键，例如 `CmdOrControl+Shift+P`，使用可参考 [available-modifiers](https://www.electronjs.org/docs/latest/api/accelerator#available-modifiers)。\n\n```shell\n--activation-shortcut <string>\n```\n\n#### [always-on-top]\n\n设置是否窗口一直在最顶层，默认为 `false`。\n\n```shell\n--always-on-top\n```\n\n#### [app-version]\n\n设置打包应用的版本号，和 package.json 里面 version 命名格式一致，默认为 `1.0.0`。\n\n```shell\n--app-version <string>\n```\n\n#### [dark-mode]\n\n强制 Mac 打包应用使用黑暗模式，默认为 `false`。\n\n```shell\n--dark-mode\n```\n\n#### [disabled-web-shortcuts]\n\n设置是否禁用原有 Pake 容器里面的网页操作快捷键，默认为 `false`。\n\n```shell\n--disabled-web-shortcuts\n```\n\n#### [force-internal-navigation]\n\n启用后所有点击的链接（即使是跨域）都会在 Pake 窗口内打开，不会再调用外部浏览器或辅助程序。默认 `false`。\n\n```shell\n--force-internal-navigation\n```\n\n#### [internal-url-regex]\n\n设置一个正则表达式来判断哪些 URL 应被视为内部链接（在应用内打开）。设置后，此正则表达式将优先于默认的域名匹配逻辑。适用于只想让特定路径在应用内打开的场景。\n\n```shell\n--internal-url-regex <pattern>\n\n# 示例：只把 facebook.com/messages 路径视为内部链接\n--internal-url-regex \"^https://www\\\\.facebook\\\\.com/messages(/.*)?$\"\n\n# 示例：只把特定子域名视为内部链接\n--internal-url-regex \"^https://(app|api)\\\\.example\\\\.com\"\n```\n\n#### [multi-arch]\n\n设置打包结果同时支持 Intel 和 M1 芯片，仅适用于 macOS，默认为 `false`。\n\n##### 准备工作\n\n- 注意：启用此选项后，需要使用 rust 官网的 rustup 安装 rust，不支持通过 brew 安装。\n- 对于 Intel 芯片用户，需要安装 arm64 跨平台包，以使安装包支持 M1 芯片。使用以下命令安装：\n\n  ```shell\n  rustup target add aarch64-apple-darwin\n  ```\n\n- 对于 M1 芯片用户，需要安装 x86 跨平台包，以使安装包支持 Intel 芯片。使用以下命令安装：\n\n  ```shell\n  rustup target add x86_64-apple-darwin\n  ```\n\n##### 使用方法\n\n```shell\n--multi-arch\n```\n\n#### [targets]\n\n指定构建目标架构或格式：\n\n- **Linux**: `deb`, `appimage`, `rpm`, `deb-arm64`, `appimage-arm64`, `rpm-arm64`（默认：`deb`, `appimage`）\n- **Windows**: `x64`, `arm64`（未指定时自动检测）\n- **macOS**: `intel`, `apple`, `universal`（未指定时自动检测）\n\n```shell\n--targets <target>\n\n# 示例：\n--targets arm64          # Windows ARM64\n--targets x64            # Windows x64\n--targets universal      # macOS 通用版本（Intel + Apple Silicon）\n--targets apple          # 仅 macOS Apple Silicon\n--targets intel          # 仅 macOS Intel\n--targets deb            # Linux DEB 包（x64）\n--targets rpm            # Linux RPM 包（x64）\n--targets appimage       # Linux AppImage（x64）\n--targets deb-arm64      # Linux DEB 包（ARM64）\n--targets rpm-arm64      # Linux RPM 包（ARM64）\n--targets appimage-arm64 # Linux AppImage（ARM64）\n```\n\n**Linux ARM64 注意事项**：\n\n- 交叉编译需要额外设置。需要安装 `gcc-aarch64-linux-gnu` 并配置交叉编译环境变量。\n- ARM64 支持让 Pake 应用可以在基于 ARM 的 Linux 设备上运行，包括 Linux 手机（postmarketOS、Ubuntu Touch）、树莓派和其他 ARM64 Linux 系统。\n- 使用 `--target appimage-arm64` 可以创建便携式 ARM64 应用，在不同的 ARM64 Linux 发行版上运行。\n\n#### [user-agent]\n\n自定义浏览器的用户代理请求头，默认为空。\n\n```shell\n--user-agent <string>\n```\n\n#### [show-system-tray]\n\n设置应用程序显示在系统托盘，默认为 `false`。\n\n```shell\n--show-system-tray\n```\n\n#### [system-tray-icon]\n\n设置通知栏托盘图标，仅在启用通知栏托盘时有效。图标必须为 `.ico` 或 `.png` 格式，分辨率为 32x32 到 256x256 像素。\n\n```shell\n--system-tray-icon <path>\n```\n\n#### [hide-on-close]\n\n点击关闭按钮时隐藏窗口而不是退出应用程序。平台特定默认值：macOS 为 `true`，Windows/Linux 为 `false`。\n\n```shell\n# 关闭时隐藏（macOS 默认行为）\n--hide-on-close\n--hide-on-close true\n\n# 立即关闭应用程序（Windows/Linux 默认行为）\n--hide-on-close false\n```\n\n#### [start-to-tray]\n\n启动时将应用程序最小化到系统托盘而不是显示窗口。必须与 `--show-system-tray` 一起使用。默认为 `false`。\n\n```shell\n--start-to-tray\n\n# 示例：启动时隐藏到托盘（必须与 --show-system-tray 一起使用）\npake https://github.com --name GitHub --show-system-tray --start-to-tray\n```\n\n**注意**：双击托盘图标可以显示/隐藏窗口。如果不与 `--show-system-tray` 一起使用，此选项将被忽略。\n\n#### [title]\n\n设置窗口标题栏文本，macOS 未指定时不显示标题，Windows/Linux 回退使用应用名称。\n\n```shell\n--title <string>\n\n# 示例：\n--title \"我的应用\"\n--title \"音乐播放器\"\n```\n\n#### [incognito]\n\n以隐私/隐身浏览模式启动应用程序。默认为 `false`。启用后，webview 将在隐私模式下运行，这意味着它不会存储 cookie、本地存储或浏览历史记录。这对于注重隐私的应用程序很有用。\n\n```shell\n--incognito\n```\n\n#### [wasm]\n\n启用 WebAssembly 支持，添加跨域隔离头部，适用于 Flutter Web 应用以及其他使用 WebAssembly 模块（如 `sqlite3.wasm`、`canvaskit.wasm`）的 Web 应用，默认为 `false`。\n\n此选项会添加必要的 HTTP 头部（`Cross-Origin-Opener-Policy: same-origin` 和 `Cross-Origin-Embedder-Policy: require-corp`）以及浏览器标志，以启用 SharedArrayBuffer 和 WebAssembly 功能。\n\n```shell\n--wasm\n\n# 示例：打包支持 WASM 的 Flutter Web 应用\npake https://flutter.dev --name FlutterApp --wasm\n```\n\n#### [enable-drag-drop]\n\n启用原生拖拽功能。默认为 `false`。启用后，允许在应用中进行拖拽操作，如重新排序项目、文件上传以及其他在常规浏览器中有效的交互式拖拽行为。\n\n```shell\n--enable-drag-drop\n\n# 示例：打包需要拖拽功能的应用\npake https://planka.example.com --name PlankApp --enable-drag-drop\n```\n\n#### [keep-binary]\n\n保留原始二进制文件与安装包一起。默认为 `false`。启用后，除了平台特定的安装包外，还会输出一个可独立运行的可执行文件。\n\n```shell\n--keep-binary\n\n# 示例：同时生成安装包和独立可执行文件\npake https://github.com --name GitHub --keep-binary\n```\n\n**输出结果**：同时创建安装包和独立可执行文件（Unix 系统为 `AppName-binary`，Windows 为 `AppName.exe`）。\n\n#### [iterative-build]\n\n开启快速构建模式（仅生成 app，不生成 dmg/deb/msi），适用于调试。默认为 `false`。\n\n```shell\n--iterative-build\n```\n\n#### [install]\n\n将构建出的 macOS 应用直接安装到 `/Applications`。默认为 `false`。\n\n该选项仅适用于 macOS，适合本地开发和快速验证。启用后，Pake 会构建 `.app` 包，将其复制到 `/Applications`，如果已存在同名应用则先替换，并在安装成功后删除当前工作目录中的本地 `.app`。如果安装失败，当前目录中的 `.app` 会被保留。\n\n```shell\n--install\n\n# 示例：构建后直接安装到 /Applications\npake https://github.com --name GitHub --install\n```\n\n#### [multi-instance]\n\n允许打包后的应用同时运行多个实例。默认为 `false`，此时再次启动只会聚焦已有窗口。启用该选项后，可以同时打开同一个应用的多个窗口。\n\n```shell\n--multi-instance\n\n# 示例：允许聊天应用同时开多个窗口\npake https://chat.example.com --name ChatApp --multi-instance\n```\n\n#### [multi-window]\n\n允许在单个运行中的应用实例内打开多个窗口，默认值为 `false`。\n\n它和 `--multi-instance` 的区别：\n\n- `--multi-instance`：启动多个应用进程。\n- `--multi-window`：保持单进程，在该进程内打开多个窗口。\n\n启用后，如果应用已在运行，再次启动会新开一个窗口，而不是仅聚焦已有窗口。\n\n这个选项可以改善基于弹窗的认证流程，但不能绕过认证提供方的策略限制。某些提供方，尤其是 Google，仍然可能拒绝在嵌入式 WebView 中完成登录。\n\n```shell\n--multi-window\n\n# 示例：单进程多窗口\npake https://chat.example.com --name ChatApp --multi-window\n```\n\n#### [installer-language]\n\n设置 Windows 安装包语言。支持 `zh-CN`、`ja-JP`，更多在 [Tauri 文档](https://v2.tauri.app/distribute/windows-installer/#internationalization)。默认为 `en-US`。\n\n```shell\n--installer-language <language>\n```\n\n#### [use-local-file]\n\n当 `url` 为本地文件路径时，如果启用此选项，则会递归地将 `url` 路径文件所在的文件夹及其所有子文件复制到 Pake 的静态文件夹。默认不启用。\n\n```shell\n--use-local-file\n\n# 基础静态文件打包\npake ./my-app/index.html --name \"my-app\" --use-local-file\n```\n\n#### [inject]\n\n使用 `inject` 可以通过本地的绝对、相对路径的 `css` `js` 文件注入到你所指定 `url` 的页面中，从而为其做定制化改造。举个例子：一段可以通用到任何网页的广告屏蔽脚本，或者是优化页面 `UI` 展示的 `css`，你只需要书写一次可以将其通用到任何其他网页打包的 `app`。\n\n支持逗号分隔和多个选项两种格式：\n\n```shell\n# 逗号分隔（推荐）\n--inject ./tools/style.css,./tools/hotkey.js\n\n# 多个选项\n--inject ./tools/style.css --inject ./tools/hotkey.js\n\n# 单个文件\n--inject ./tools/style.css\n```\n\n#### [proxy-url]\n\n为所有网络请求设置代理服务器。支持 HTTP、HTTPS 和 SOCKS5。在 Windows 和 Linux 上可用。在 macOS 上需要 macOS 14+。\n\n```shell\n--proxy-url http://127.0.0.1:7890\n--proxy-url socks5://127.0.0.1:7891\n```\n\n#### [debug]\n\n启用开发者工具和详细日志输出，用于调试。\n\n```shell\n--debug\n```\n\n#### [ignore-certificate-errors]\n\n忽略目标 URL 的 TLS 证书校验错误，适用于内网应用、开发环境、自签名证书。\n\n```shell\n--ignore-certificate-errors\n```\n\n#### [new-window]\n\n允许网站打开新窗口，例如登录授权弹窗、额外标签页或分支会话页面。\n\n这个选项可以帮助依赖弹窗授权窗口的网站，但不能保证一定能在应用内完成登录。某些提供方，尤其是 Google，可能仍然会阻止在嵌入式 WebView 中进行认证。\n\n```shell\n--new-window\n```\n\n### 打包完成\n\n完成上述步骤后，您的应用程序应该已经成功打包。请注意，根据您的系统配置和网络状况，打包过程可能需要一些时间。请耐心等待，一旦打包完成，您就可以在指定的目录中找到应用程序安装包。\n\n## Docker 使用\n\n```shell\n# 在 Linux 上通过 Docker 运行 Pake CLI（AppImage 构建需要 FUSE 权限）\ndocker run --rm --privileged \\\n    --device /dev/fuse \\\n    --security-opt apparmor=unconfined \\\n    -v YOUR_DIR:/output \\\n    ghcr.io/tw93/pake \\\n    <arguments>\n\n# 例如：\ndocker run --rm --privileged \\\n    --device /dev/fuse \\\n    --security-opt apparmor=unconfined \\\n    -v ./packages:/output \\\n    ghcr.io/tw93/pake \\\n    https://example.com --name MyApp --icon ./icon.png --targets appimage\n```\n"
  },
  {
    "path": "docs/faq.md",
    "content": "# Frequently Asked Questions (FAQ)\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"faq_CN.md\">简体中文</a></h4>\n\nCommon issues and solutions when using Pake.\n\n## Table of Contents\n\n- [Build Issues](#build-issues)\n  - [Rust Version Error: \"feature 'edition2024' is required\"](#rust-version-error-feature-edition2024-is-required)\n  - [Linux: Build Error \"Can't detect any appindicator library\" on Ubuntu 24.04](#linux-build-error-cant-detect-any-appindicator-library-on-ubuntu-2404)\n  - [Linux: AppImage Build Fails with \"failed to run linuxdeploy\"](#linux-appimage-build-fails-with-failed-to-run-linuxdeploy)\n  - [Linux: \"cargo: command not found\" After Installing Rust](#linux-cargo-command-not-found-after-installing-rust)\n  - [Windows: Installation Timeout During First Build](#windows-installation-timeout-during-first-build)\n  - [Windows: Missing Visual Studio Build Tools](#windows-missing-visual-studio-build-tools)\n  - [macOS: Build Fails with Module Compilation Errors](#macos-build-fails-with-module-compilation-errors)\n- [Runtime Issues](#runtime-issues)\n  - [App Window is Too Small/Large](#app-window-is-too-smalllarge)\n  - [App Icon Not Showing Correctly](#app-icon-not-showing-correctly)\n  - [Website Features Not Working (Login, Upload, etc.)](#website-features-not-working-login-upload-etc)\n- [Installation Issues](#installation-issues)\n  - [Permission Denied When Installing Globally](#permission-denied-when-installing-globally)\n- [Getting Help](#getting-help)\n\n---\n\n## Build Issues\n\n### Rust Version Error: \"feature 'edition2024' is required\"\n\n**Problem:**\nWhen building Pake or using the CLI, you encounter an error like:\n\n```txt\nerror: failed to parse manifest\nCaused by:\n  feature `edition2024` is required\n  this Cargo does not support nightly features, but if you switch to nightly channel you can add `cargo-features = [\"edition2024\"]\n  to enable this feature\n```\n\n**Why This Happens:**\n\nPake's dependencies require Rust edition2024 support, which is only available in Rust 1.85.0 or later. Specifically:\n\n- The dependency chain includes: `tauri` → `image` → `moxcms` → `pxfm v0.1.25` (requires edition2024)\n- Rust edition2024 became stable in Rust 1.85.0 (released February 2025)\n- If your Rust version is older (e.g., 1.82.0 from August 2024), you'll see this error\n\n**Solution:**\n\nUpdate your Rust toolchain to version 1.85.0 or later:\n\n```bash\n# Update to the latest stable Rust version\nrustup update stable\n\n# Or install the latest stable version\nrustup install stable\n\n# Verify the update\nrustc --version\n# Should show: rustc 1.85.0 or higher\n```\n\nAfter updating, retry your build command.\n\n**For Development Setup:**\n\nIf you're setting up a development environment, ensure:\n\n- Rust ≥1.85.0 (check with `rustc --version`)\n- Node.js ≥22.0.0 (check with `node --version`)\n\nSee [CONTRIBUTING.md](../CONTRIBUTING.md) for complete prerequisites.\n\n---\n\n### Linux: Build Error \"Can't detect any appindicator library\" on Ubuntu 24.04\n\n**Problem:**\nWhen building on Ubuntu 24.04 or newer, you may encounter:\n\n```txt\nCan't detect any appindicator library\n```\n\nOr potentially errors related to Icon RGBA in older versions.\n\n**Solution:**\n\nUbuntu 24.04+ replaced `libappindicator3-dev` with `libayatana-appindicator3-dev`.\n\nInstall the correct dependency:\n\n```bash\nsudo apt-get update\nsudo apt-get install -y libayatana-appindicator3-dev\n```\n\n---\n\n### Linux: AppImage Build Fails with \"failed to run linuxdeploy\"\n\n**Problem:**\nWhen building AppImage on Linux (Debian, Ubuntu, Arch, etc.), you may encounter errors like:\n\n```txt\nError: failed to run linuxdeploy\nError: strip: Unable to recognise the format of the input file\n```\n\n**Solution 1: Automatic NO_STRIP Retry (Recommended)**\n\nPake CLI now automatically retries AppImage builds with `NO_STRIP=1` when linuxdeploy fails to strip the binary. To skip the strip step from the very first attempt (or when scripting your own builds), set the variable manually:\n\n```bash\nNO_STRIP=1 pake https://example.com --name MyApp --targets appimage\n```\n\nThis bypasses the library stripping process that often causes issues on certain Linux distributions.\n\n**Solution 2: Install System Dependencies**\n\nIf NO_STRIP doesn't work, ensure you have all required system dependencies:\n\n```bash\nsudo apt update\nsudo apt install -y \\\n  libdbus-1-dev \\\n  libsoup-3.0-dev \\\n  libjavascriptcoregtk-4.1-dev \\\n  libwebkit2gtk-4.1-dev \\\n  build-essential \\\n  curl wget file \\\n  libxdo-dev \\\n  libssl-dev \\\n  libgtk-3-dev \\\n  libayatana-appindicator3-dev \\\n  librsvg2-dev \\\n  gnome-video-effects \\\n  libglib2.0-dev \\\n  libgirepository1.0-dev \\\n  pkg-config\n```\n\nThen try building again (you can still pre-set `NO_STRIP=1` if you prefer).\n\n**Solution 3: Use DEB Format Instead**\n\nDEB packages are more stable on Debian-based systems:\n\n```bash\npake https://example.com --name MyApp --targets deb\n```\n\n**Solution 4: Use Docker (with FUSE access)**\n\nBuild in a clean environment without installing dependencies. AppImage tooling needs access to `/dev/fuse`, so run the container in privileged mode (or grant FUSE explicitly):\n\n```bash\ndocker run --rm --privileged \\\n  --device /dev/fuse \\\n  --security-opt apparmor=unconfined \\\n  -v $(pwd)/output:/output \\\n  ghcr.io/tw93/pake:latest \\\n  https://example.com --name MyApp --targets appimage\n```\n\n> **Tip:** The generated AppImage may be owned by root. Run `sudo chown $(id -nu):$(id -ng) ./output/MyApp.AppImage` afterwards.\n\n**Why This Happens:**\n\nThis is a known issue with Tauri's linuxdeploy tool, which can fail when:\n\n- System libraries have incompatible formats for stripping\n- Building on newer distributions (Arch, Debian Trixie, etc.)\n- Missing WebKit2GTK or GTK development libraries\n\nThe `NO_STRIP=1` environment variable is the official workaround recommended by the Tauri community.\n\n---\n\n### Linux: \"cargo: command not found\" After Installing Rust\n\n**Problem:**\nYou installed Rust but Pake still reports \"cargo: command not found\".\n\n**Solution:**\n\nPake CLI automatically reloads the Rust environment, but if issues persist:\n\n```bash\n# Reload environment in current terminal\nsource ~/.cargo/env\n\n# Or restart your terminal\n```\n\nThen try building again.\n\n---\n\n### Windows: Installation Timeout During First Build\n\n**Problem:**\nWhen building for the first time on Windows, you may encounter:\n\n```txt\nError: Command timed out after 900000ms: \"cd ... && pnpm install\"\n```\n\n**Why This Happens:**\n\nFirst-time installation on Windows can be slow due to:\n\n- Native module compilation (requires Visual Studio Build Tools)\n- Large dependency downloads (Tauri, Rust toolchain)\n- Windows Defender real-time scanning\n- Network connectivity issues\n\n**Solution 1: Automatic Retry (Built-in)**\n\nPake CLI now automatically retries with CN mirror if the initial installation times out. Simply wait for the retry to complete.\n\n**Solution 2: Manual Installation**\n\nIf automatic retry fails, manually install dependencies:\n\n```bash\n# Navigate to pake-cli installation directory\ncd %LOCALAPPDATA%\\pnpm\\global\\5\\.pnpm\\pake-cli@VERSION\\node_modules\\pake-cli\n\n# Install with CN mirror\npnpm install --registry=https://registry.npmmirror.com\n\n# Then retry your build\npake https://github.com --name GitHub\n```\n\n**Solution 3: Improve Network Speed**\n\n- Use a stable network connection\n- Temporarily disable antivirus software during installation\n- Use a VPN or proxy if needed\n\n**Expected Time:**\n\n- First installation: 10-15 minutes on Windows\n- Subsequent builds: Much faster (dependencies cached)\n\n---\n\n### Windows: Missing Visual Studio Build Tools\n\n**Problem:**\nBuild fails with errors about missing MSVC or Windows SDK.\n\n**Solution:**\n\nInstall Visual Studio Build Tools:\n\n1. Download [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022)\n2. During installation, select \"Desktop development with C++\"\n3. For ARM64 support: Also select \"MSVC v143 - VS 2022 C++ ARM64 build tools\" under Individual Components\n\n---\n\n### macOS: Build Fails with Module Compilation Errors\n\n**Problem:**\nOn macOS 26 Beta or newer, you may see errors related to `CoreFoundation` or `_Builtin_float` modules.\n\n**Solution:**\n\nCreate a configuration file to use compatible SDK:\n\n```bash\ncat > src-tauri/.cargo/config.toml << 'EOF'\n[env]\nMACOSX_DEPLOYMENT_TARGET = \"15.0\"\nSDKROOT = \"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk\"\nEOF\n```\n\nThis file is already in `.gitignore` and won't be committed.\n\n---\n\n## Runtime Issues\n\n### App Window is Too Small/Large\n\n**Solution:**\n\nSpecify custom dimensions when building:\n\n```bash\npake https://example.com --width 1200 --height 800\n```\n\nSee [CLI Usage Guide](cli-usage.md#window-options) for all window options.\n\n---\n\n### App Icon Not Showing Correctly\n\n**Problem:**\nCustom icon doesn't appear or shows default icon.\n\n**Solution:**\n\nEnsure you're using the correct icon format for your platform:\n\n- **macOS**: `.icns` format\n- **Windows**: `.ico` format\n- **Linux**: `.png` format\n\n```bash\n# macOS\npake https://example.com --icon ./icon.icns\n\n# Windows\npake https://example.com --icon ./icon.ico\n\n# Linux\npake https://example.com --icon ./icon.png\n```\n\nPake can automatically convert icons, but providing the correct format is more reliable.\n\n---\n\n### Website Features Not Working (Login, Upload, etc.)\n\n**Problem:**\nSome website features don't work in the Pake app.\n\n**Solution:**\n\nThis is usually due to web compatibility issues. Try:\n\n1. **Set custom User Agent:**\n\n   ```bash\n   pake https://example.com --user-agent \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\"\n   ```\n\n2. **Inject custom JavaScript:**\n\n   ```bash\n   pake https://example.com --inject ./fix.js\n   ```\n\n   For pages that need periodic reloads, you can keep this behavior in a small injected script instead of adding a dedicated Pake option:\n\n   ```javascript\n   function isEditing(element) {\n     if (!element) return false;\n     const tagName = element.tagName;\n     return (\n       element.isContentEditable ||\n       tagName === \"INPUT\" ||\n       tagName === \"TEXTAREA\" ||\n       tagName === \"SELECT\"\n     );\n   }\n\n   setInterval(() => {\n     if (!document.hidden && !isEditing(document.activeElement)) {\n       window.location.reload();\n     }\n   }, 300000);\n   ```\n\n   Save it as `refresh.js` and package with:\n\n   ```bash\n   pake https://news.ycombinator.com --name HackerNews --inject ./refresh.js\n   ```\n\n3. **Check if the site requires specific permissions** that may not be available in WebView\n\n4. **Be aware of embedded-webview sign-in limits**\n\n   Some authentication providers, especially Google, may block sign-in inside embedded webviews. Because Pake packages sites into a desktop webview, Google properties or sites that rely on Google OAuth may still fail to sign in even when `--new-window` or `--multi-window` is enabled. This is provider policy, not a packaging bug. In those cases, use the normal browser, a browser-installed app, or a native desktop client.\n\n---\n\n## Installation Issues\n\n### Permission Denied When Installing Globally\n\n**Problem:**\n`npm install -g pake-cli` fails with permission errors.\n\n**Solution:**\n\nUse one of these approaches:\n\n```bash\n# Option 1: Use npx (no installation needed)\nnpx pake-cli https://example.com\n\n# Option 2: Fix npm permissions\nnpm config set prefix ~/.npm-global\necho 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc\nsource ~/.bashrc\nnpm install -g pake-cli\n\n# Option 3: Use pnpm (recommended)\npnpm install -g pake-cli\n```\n\n---\n\n## Getting Help\n\nIf your issue isn't covered here:\n\n1. Check the [CLI Usage Guide](cli-usage.md) for detailed parameter documentation\n2. See [Advanced Usage](advanced-usage.md) for prerequisites and system setup\n3. Search [existing GitHub issues](https://github.com/tw93/Pake/issues)\n4. [Open a new issue](https://github.com/tw93/Pake/issues/new) with:\n   - Your OS and version\n   - Node.js and Rust versions (`node --version`, `rustc --version`)\n   - Complete error message\n   - Build command you used\n"
  },
  {
    "path": "docs/faq_CN.md",
    "content": "# 常见问题 (FAQ)\n\n<h4 align=\"right\"><a href=\"faq.md\">English</a> | <strong>简体中文</strong></h4>\n\n使用 Pake 时的常见问题和解决方案。\n\n## 目录\n\n- [构建问题](#构建问题)\n  - [Rust 版本错误:\"feature 'edition2024' is required\"](#rust-版本错误feature-edition2024-is-required)\n  - [Linux：Ubuntu 24.04 构建报错 \"Can't detect any appindicator library\"](#linuxubuntu-2404-构建报错-cant-detect-any-appindicator-library)\n  - [Linux：AppImage 构建失败，提示 \"failed to run linuxdeploy\"](#linuxappimage-构建失败提示-failed-to-run-linuxdeploy)\n  - [Linux:\"cargo: command not found\" 即使已安装 Rust](#linuxcargo-command-not-found-即使已安装-rust)\n  - [Windows：首次构建时安装超时](#windows首次构建时安装超时)\n  - [Windows：缺少 Visual Studio 构建工具](#windows缺少-visual-studio-构建工具)\n  - [macOS：构建失败，出现模块编译错误](#macos构建失败出现模块编译错误)\n- [运行时问题](#运行时问题)\n  - [应用窗口太小/太大](#应用窗口太小太大)\n  - [应用图标显示不正确](#应用图标显示不正确)\n  - [网站功能不工作（登录、上传等）](#网站功能不工作登录上传等)\n- [安装问题](#安装问题)\n  - [全局安装时权限被拒绝](#全局安装时权限被拒绝)\n- [获取帮助](#获取帮助)\n\n---\n\n## 构建问题\n\n### Rust 版本错误:\"feature 'edition2024' is required\"\n\n**问题描述：**\n在构建 Pake 或使用 CLI 时，遇到如下错误：\n\n```txt\nerror: failed to parse manifest\nCaused by:\n  feature `edition2024` is required\n  this Cargo does not support nightly features, but if you switch to nightly channel you can add `cargo-features = [\"edition2024\"]`\n  to enable this feature\n```\n\n**原因分析：**\n\nPake 的依赖项需要 Rust edition2024 支持，该特性仅在 Rust 1.85.0 或更高版本中可用。具体来说：\n\n- 依赖链包括：`tauri` → `image` → `moxcms` → `pxfm v0.1.25`（需要 edition2024）\n- Rust edition2024 在 Rust 1.85.0（2025 年 2 月发布）中成为稳定版\n- 如果您的 Rust 版本较旧（例如 2024 年 8 月的 1.82.0），就会看到此错误\n\n**解决方案：**\n\n将 Rust 工具链更新到 1.85.0 或更高版本：\n\n```bash\n# 更新到最新稳定版 Rust\nrustup update stable\n\n# 或者安装最新稳定版\nrustup install stable\n\n# 验证更新\nrustc --version\n# 应显示：rustc 1.85.0 或更高版本\n```\n\n更新后，重新执行构建命令。\n\n**对于开发环境设置：**\n\n如果您正在设置开发环境，请确保：\n\n- Rust ≥1.85.0（使用 `rustc --version` 检查）\n- Node.js ≥22.0.0（使用 `node --version` 检查）\n\n详见 [CONTRIBUTING.md](../CONTRIBUTING.md) 获取完整的前置条件。\n\n---\n\n### Linux：Ubuntu 24.04 构建报错 \"Can't detect any appindicator library\"\n\n**问题描述：**\n在 Ubuntu 24.04 或更新版本上构建时，可能遇到以下错误：\n\n```txt\nCan't detect any appindicator library\n```\n\n或者在之前的版本中可能看到关于 Icon RGBA 的报错。\n\n**解决方案：**\n\n这是因为 Ubuntu 24.04+ 使用 `libayatana-appindicator3-dev` 替代了旧的 `libappindicator3-dev`。\n\n请安装正确的依赖库：\n\n```bash\nsudo apt-get update\nsudo apt-get install -y libayatana-appindicator3-dev\n```\n\n---\n\n### Linux：AppImage 构建失败，提示 \"failed to run linuxdeploy\"\n\n**问题描述：**\n在 Linux 系统（Debian、Ubuntu、Arch 等）上构建 AppImage 时，可能遇到如下错误：\n\n```txt\nError: failed to run linuxdeploy\nError: strip: Unable to recognise the format of the input file\n```\n\n**解决方案 1：自动 NO_STRIP 重试（推荐）**\n\nPake CLI 已在 linuxdeploy 剥离失败时自动使用 `NO_STRIP=1` 进行二次构建。如果你希望一开始就跳过剥离步骤（或在脚本中使用），可以手动设置该变量：\n\n```bash\nNO_STRIP=1 pake https://example.com --name MyApp --targets appimage\n```\n\n这会绕过经常在某些 Linux 发行版上出现问题的库文件剥离过程。\n\n**解决方案 2：安装系统依赖**\n\n如果 NO_STRIP 不起作用，确保已安装所有必需的系统依赖：\n\n```bash\nsudo apt update\nsudo apt install -y \\\n  libdbus-1-dev \\\n  libsoup-3.0-dev \\\n  libjavascriptcoregtk-4.1-dev \\\n  libwebkit2gtk-4.1-dev \\\n  build-essential \\\n  curl wget file \\\n  libxdo-dev \\\n  libssl-dev \\\n  libgtk-3-dev \\\n  libayatana-appindicator3-dev \\\n  librsvg2-dev \\\n  gnome-video-effects \\\n  libglib2.0-dev \\\n  libgirepository1.0-dev \\\n  pkg-config\n```\n\n然后再次尝试构建（也可以提前设置 `NO_STRIP=1`）。\n\n**解决方案 3：改用 DEB 格式**\n\nDEB 包在基于 Debian 的系统上更稳定：\n\n```bash\npake https://example.com --name MyApp --targets deb\n```\n\n**解决方案 4：使用 Docker（需开放 FUSE）**\n\n在干净的环境中构建，无需安装依赖。AppImage 工具需要访问 `/dev/fuse`，因此需要以特权模式运行（或显式授权 FUSE）：\n\n```bash\ndocker run --rm --privileged \\\n  --device /dev/fuse \\\n  --security-opt apparmor=unconfined \\\n  -v $(pwd)/output:/output \\\n  ghcr.io/tw93/pake:latest \\\n  https://example.com --name MyApp --targets appimage\n```\n\n> **提示：** 生成的 AppImage 可能属于 root，需要执行 `sudo chown $(id -nu):$(id -ng) ./output/MyApp.AppImage` 调整所有权。\n\n**原因：**\n\n这是 Tauri 的 linuxdeploy 工具的已知问题，在以下情况下可能失败：\n\n- 系统库的格式不兼容剥离操作\n- 在较新的发行版上构建（Arch、Debian Trixie 等）\n- 缺少 WebKit2GTK 或 GTK 开发库\n\n`NO_STRIP=1` 环境变量是 Tauri 社区推荐的官方解决方法。\n\n---\n\n### Linux:\"cargo: command not found\" 即使已安装 Rust\n\n**问题描述：**\n已安装 Rust 但 Pake 仍然提示 \"cargo: command not found\"。\n\n**解决方案：**\n\nPake CLI 会自动重新加载 Rust 环境，但如果问题仍然存在：\n\n```bash\n# 在当前终端重新加载环境\nsource ~/.cargo/env\n\n# 或者重启终端\n```\n\n然后再次尝试构建。\n\n---\n\n### Windows：首次构建时安装超时\n\n**问题描述：**\n在 Windows 上首次构建时，可能遇到：\n\n```txt\nError: Command timed out after 900000ms: \"cd ... && pnpm install\"\n```\n\n**原因分析：**\n\nWindows 首次安装可能较慢，原因包括：\n\n- 本地模块编译（需要 Visual Studio Build Tools）\n- 大量依赖下载（Tauri、Rust 工具链）\n- Windows Defender 实时扫描\n- 网络连接问题\n\n**解决方案 1：自动重试（内置）**\n\nPake CLI 现在会在初次安装超时后自动使用国内镜像重试。只需等待重试完成即可。\n\n**解决方案 2：手动安装依赖**\n\n如果自动重试失败，可手动安装依赖：\n\n```bash\n# 进入 pake-cli 安装目录\ncd %LOCALAPPDATA%\\pnpm\\global\\5\\.pnpm\\pake-cli@版本号\\node_modules\\pake-cli\n\n# 使用国内镜像安装\npnpm install --registry=https://registry.npmmirror.com\n\n# 然后重新构建\npake https://github.com --name GitHub\n```\n\n**解决方案 3：改善网络环境**\n\n- 使用稳定的网络连接\n- 安装过程中临时关闭杀毒软件\n- 必要时使用 VPN 或代理\n\n**预期时间：**\n\n- 首次安装：Windows 上需要 10-15 分钟\n- 后续构建：依赖已缓存，速度会快很多\n\n---\n\n### Windows：缺少 Visual Studio 构建工具\n\n**问题描述：**\n构建失败，提示缺少 MSVC 或 Windows SDK。\n\n**解决方案：**\n\n安装 Visual Studio 构建工具：\n\n1. 下载 [Visual Studio Build Tools](https://visualstudio.microsoft.com/zh-hans/downloads/#build-tools-for-visual-studio-2022)\n2. 安装时选择\"使用 C++ 的桌面开发\"\n3. ARM64 支持：在\"单个组件\"下额外选择\"MSVC v143 - VS 2022 C++ ARM64 构建工具\"\n\n---\n\n### macOS：构建失败，出现模块编译错误\n\n**问题描述：**\n在 macOS 26 Beta 或更新版本上，可能看到与 `CoreFoundation` 或 `_Builtin_float` 模块相关的错误。\n\n**解决方案：**\n\n创建配置文件以使用兼容的 SDK：\n\n```bash\ncat > src-tauri/.cargo/config.toml << 'EOF'\n[env]\nMACOSX_DEPLOYMENT_TARGET = \"15.0\"\nSDKROOT = \"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk\"\nEOF\n```\n\n此文件已在 `.gitignore` 中，不会被提交。\n\n---\n\n## 运行时问题\n\n### 应用窗口太小/太大\n\n**解决方案：**\n\n构建时指定自定义尺寸：\n\n```bash\npake https://example.com --width 1200 --height 800\n```\n\n查看 [CLI 使用指南](cli-usage_CN.md#窗口选项) 了解所有窗口选项。\n\n---\n\n### 应用图标显示不正确\n\n**问题描述：**\n自定义图标没有显示或显示默认图标。\n\n**解决方案：**\n\n确保为您的平台使用正确的图标格式：\n\n- **macOS**：`.icns` 格式\n- **Windows**：`.ico` 格式\n- **Linux**：`.png` 格式\n\n```bash\n# macOS\npake https://example.com --icon ./icon.icns\n\n# Windows\npake https://example.com --icon ./icon.ico\n\n# Linux\npake https://example.com --icon ./icon.png\n```\n\nPake 可以自动转换图标，但提供正确的格式更可靠。\n\n---\n\n### 网站功能不工作（登录、上传等）\n\n**问题描述：**\n某些网站功能在 Pake 应用中无法工作。\n\n**解决方案：**\n\n这通常是由于 Web 兼容性问题。尝试：\n\n1. **设置自定义 User Agent：**\n\n   ```bash\n   pake https://example.com --user-agent \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\"\n   ```\n\n2. **注入自定义 JavaScript：**\n\n   ```bash\n   pake https://example.com --inject ./fix.js\n   ```\n\n   对于需要定时刷新的页面，建议把这类行为放在一个小的注入脚本里，而不是增加专门的 Pake 参数：\n\n   ```javascript\n   function isEditing(element) {\n     if (!element) return false;\n     const tagName = element.tagName;\n     return (\n       element.isContentEditable ||\n       tagName === \"INPUT\" ||\n       tagName === \"TEXTAREA\" ||\n       tagName === \"SELECT\"\n     );\n   }\n\n   setInterval(() => {\n     if (!document.hidden && !isEditing(document.activeElement)) {\n       window.location.reload();\n     }\n   }, 300000);\n   ```\n\n   将其保存为 `refresh.js`，然后这样打包：\n\n   ```bash\n   pake https://news.ycombinator.com --name HackerNews --inject ./refresh.js\n   ```\n\n3. **检查网站是否需要 WebView 中可能不可用的特定权限**\n\n4. **注意嵌入式 WebView 的登录限制**\n\n   某些认证提供方，尤其是 Google，可能会阻止在嵌入式 WebView 中完成登录。由于 Pake 是把网站包装进桌面 WebView，Google 自家站点或依赖 Google OAuth 的网站，即使启用了 `--new-window` 或 `--multi-window`，也仍然可能无法在应用内完成登录。这属于提供方策略限制，不是打包逻辑错误。遇到这种情况时，建议改用普通浏览器、浏览器安装版站点应用，或官方原生桌面客户端。\n\n---\n\n## 安装问题\n\n### 全局安装时权限被拒绝\n\n**问题描述：**\n`npm install -g pake-cli` 失败，提示权限错误。\n\n**解决方案：**\n\n使用以下方法之一：\n\n```bash\n# 方案 1：使用 npx（无需安装）\nnpx pake-cli https://example.com\n\n# 方案 2：修复 npm 权限\nnpm config set prefix ~/.npm-global\necho 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc\nsource ~/.bashrc\nnpm install -g pake-cli\n\n# 方案 3：使用 pnpm（推荐）\npnpm install -g pake-cli\n```\n\n---\n\n## 获取帮助\n\n如果您的问题未在此处涵盖：\n\n1. 查看 [CLI 使用指南](cli-usage_CN.md) 了解详细参数文档\n2. 参阅 [高级用法](advanced-usage_CN.md) 了解前置条件和系统设置\n3. 搜索 [现有的 GitHub issues](https://github.com/tw93/Pake/issues)\n4. [提交新 issue](https://github.com/tw93/Pake/issues/new) 时请包含：\n   - 您的操作系统和版本\n   - Node.js 和 Rust 版本（`node --version`、`rustc --version`）\n   - 完整的错误信息\n   - 您使用的构建命令\n\n### Linux: 打包失败，提示 `Can't detect any appindicator library`\n\n**问题描述：**\n在 Linux 上打包时，构建失败并显示以下错误：\n\n```txt\nCan't detect any appindicator library\n```\n\n**原因分析：**\n这个错误表示您的 Linux 系统缺少创建“系统托盘图标”所需的核心库 `libappindicator`。Pake 打包的应用支持系统托盘功能，因此该库是必需的。\n\n**解决方案：**\n您需要在您的 Linux 系统上安装这个缺失的开发库。\n\n- **对于 Debian / Ubuntu 系统：**\n\n  ```bash\n  sudo apt-get update && sudo apt-get install -y libappindicator3-dev\n  ```\n\n- **对于 Fedora / CentOS / RHEL 系统：**\n\n  ```bash\n  sudo dnf install -y libappindicator-devel\n  ```\n\n为了确保打包环境的完整性，推荐一次性安装所有 Tauri 所需的依赖。请参考本文档中关于 `failed to run linuxdeploy` 问题的解决方案，其中包含了完整的依赖列表。\n"
  },
  {
    "path": "docs/github-actions-usage.md",
    "content": "# GitHub Actions Usage Guide\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"github-actions-usage_CN.md\">简体中文</a></h4>\n\nBuild Pake apps online without installing development tools locally.\n\n## Quick Steps\n\n### 1. Fork Repository\n\n[Fork this project](https://github.com/tw93/Pake/fork)\n\n### 2. Run Workflow\n\n1. Go to Actions tab in your forked repository\n2. Select `Build App With Pake CLI`\n3. Fill in the form (same parameters as [CLI options](cli-usage.md))\n4. Click `Run Workflow`\n\n   ![Actions Interface](https://raw.githubusercontent.com/tw93/static/main/pake/action.png)\n\n### 3. Download App\n\n- Green checkmark = build success\n- Click the workflow name to view details\n- Find `Artifacts` section and download your app\n\n  ![Build Success](https://raw.githubusercontent.com/tw93/static/main/pake/action2.png)\n\n### 4. Build Times\n\n- **First run**: ~10-15 minutes (sets up cache)\n- **Subsequent runs**: ~5 minutes (uses cache)\n- Cache size: 400-600MB when complete\n\n## Tips\n\n- Be patient on first run - let cache build completely\n- Stable network connection recommended\n- If build fails, delete cache and retry\n\n## Links\n\n- [CLI Documentation](cli-usage.md)\n- [Advanced Usage](advanced-usage.md)\n"
  },
  {
    "path": "docs/github-actions-usage_CN.md",
    "content": "# GitHub Actions 使用指南\n\n<h4 align=\"right\"><strong><a href=\"github-actions-usage.md\">English</a></strong> | 简体中文</h4>\n\n无需本地安装开发工具，在线构建 Pake 应用。\n\n## 快速步骤\n\n### 1. Fork 仓库\n\n[Fork 此项目](https://github.com/tw93/Pake/fork)\n\n### 2. 运行工作流\n\n1. 前往你 Fork 的仓库的 Actions 页面\n2. 选择 `Build App With Pake CLI`\n3. 填写表单（参数与 [CLI 选项](cli-usage_CN.md) 相同）\n4. 点击 `Run Workflow`\n\n   ![Actions 界面](https://raw.githubusercontent.com/tw93/static/main/pake/action.png)\n\n### 3. 下载应用\n\n- 绿色勾号 = 构建成功\n- 点击工作流名称查看详情\n- 在 `Artifacts` 部分下载应用\n\n  ![构建成功](https://raw.githubusercontent.com/tw93/static/main/pake/action2.png)\n\n### 4. 构建时间\n\n- **首次运行**：约 10-15 分钟（建立缓存）\n- **后续运行**：约 5 分钟（使用缓存）\n- 缓存大小：完成时为 400-600MB\n\n## 提示\n\n- 首次运行需要耐心等待，让缓存完全建立\n- 建议网络连接稳定\n- 如果构建失败，删除缓存后重试\n\n## 链接\n\n- [CLI 文档](cli-usage_CN.md)\n- [高级用法](advanced-usage_CN.md)\n"
  },
  {
    "path": "docs/pake-action.md",
    "content": "# Pake Action\n\nTransform any webpage into a lightweight desktop app with a single GitHub Actions step.\n\n> This guide shows how to use Pake as a GitHub Action in your own projects. For using our project's built-in GitHub Actions workflow, see [GitHub Actions Usage](github-actions-usage.md).\n\n## Quick Start\n\n```yaml\n- name: Build Pake App\n  uses: tw93/Pake@v3\n  with:\n    url: \"https://example.com\"\n    name: \"MyApp\"\n```\n\n## Inputs\n\n| Parameter    | Description              | Required | Default |\n| ------------ | ------------------------ | -------- | ------- |\n| `url`        | Target URL to package    | ✅       |         |\n| `name`       | Application name         | ✅       |         |\n| `output-dir` | Output directory         |          | `dist`  |\n| `icon`       | Custom app icon URL/path |          |         |\n| `width`      | Window width             |          | `1200`  |\n| `height`     | Window height            |          | `780`   |\n| `debug`      | Enable debug mode        |          | `false` |\n\n## Outputs\n\n| Output         | Description                   |\n| -------------- | ----------------------------- |\n| `package-path` | Path to the generated package |\n\n## Examples\n\n### Basic Usage\n\n```yaml\nname: Build Web App\non: [push]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: tw93/Pake@v3\n        with:\n          url: \"https://weekly.tw93.fun\"\n          name: \"WeeklyApp\"\n```\n\n### With Custom Icon\n\n```yaml\n- uses: tw93/Pake@v3\n  with:\n    url: \"https://example.com\"\n    name: \"MyApp\"\n    icon: \"https://example.com/icon.png\"\n    width: 1400\n    height: 900\n```\n\n### Multi-Platform Build\n\n```yaml\njobs:\n  build:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: tw93/Pake@v3\n        with:\n          url: \"https://example.com\"\n          name: \"CrossPlatformApp\"\n```\n\n## How It Works\n\n1. **Auto Setup**: Installs Rust, Node.js dependencies, builds Pake CLI\n2. **Build App**: Runs `pake` command with your parameters\n3. **Package Output**: Finds and moves the generated package to output directory\n\n## Supported Platforms\n\n- **Linux**: `.deb` packages (Ubuntu runners)\n- **macOS**: `.app` and `.dmg` packages (macOS runners)\n- **Windows**: `.exe` and `.msi` packages (Windows runners)\n\nUse GitHub's matrix strategy to build for multiple platforms simultaneously.\n\n## Related Documentation\n\n- [GitHub Actions Usage](github-actions-usage.md) - Using Pake's built-in workflow\n- [CLI Usage](cli-usage.md) - Command-line interface reference\n- [Advanced Usage](advanced-usage.md) - Customization options\n"
  },
  {
    "path": "icns2png.py",
    "content": "\"\"\"\n批量将icns文件转成png文件\nBatch convert ICNS files to PNG files\n\"\"\"\nimport os\n\ntry:\n    from PIL import Image\nexcept ImportError:\n    os.system(\"pip install Pillow\")\n    from PIL import Image\n\nif __name__ == \"__main__\":\n    now_dir = os.path.dirname(os.path.abspath(__file__))\n    icons_dir = os.path.join(now_dir, \"src-tauri\", \"icons\")\n    png_dir = os.path.join(now_dir, \"src-tauri\", \"png\")\n    if not os.path.exists(png_dir):\n        os.mkdir(png_dir)\n    file_list = os.listdir(icons_dir)\n    file_list = [file for file in file_list if file.endswith(\".icns\")]\n    for file in file_list:\n        icns_path = os.path.join(icons_dir, file)\n        image = Image.open(icns_path)\n        image_512 = image.copy().resize((512, 512))\n        image_256 = image.copy().resize((256, 256))\n        image_32 = image.copy().resize((32, 32))\n        image_name = os.path.splitext(file)[0]\n        image_512_path = os.path.join(png_dir, image_name + \"_512.png\")\n        image_256_path = os.path.join(png_dir, image_name + \"_256.ico\")\n        image_32_path = os.path.join(png_dir, image_name + \"_32.ico\")\n        image_512.save(image_512_path, \"PNG\")\n        image_256.save(image_256_path, \"ICO\")\n        image_32.save(image_32_path, \"ICO\")\n    print(\"png file write success.\")\n    print(f\"There are {len(os.listdir(png_dir))} png picture in \", png_dir)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"pake-cli\",\n  \"version\": \"3.10.1\",\n  \"description\": \"🤱🏻 Turn any webpage into a desktop app with one command. 🤱🏻 一键打包网页生成轻量桌面应用。\",\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"packageManager\": \"pnpm@10.26.2\",\n  \"bin\": {\n    \"pake\": \"./dist/cli.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/tw93/pake.git\"\n  },\n  \"author\": {\n    \"name\": \"Tw93\",\n    \"email\": \"tw93@qq.com\"\n  },\n  \"keywords\": [\n    \"pake\",\n    \"pake-cli\",\n    \"rust\",\n    \"tauri\",\n    \"no-electron\",\n    \"productivity\"\n  ],\n  \"files\": [\n    \"dist\",\n    \"src-tauri\"\n  ],\n  \"scripts\": {\n    \"start\": \"pnpm run dev\",\n    \"dev\": \"pnpm run tauri dev\",\n    \"build\": \"tauri build\",\n    \"build:debug\": \"tauri build --debug\",\n    \"build:mac\": \"tauri build --target universal-apple-darwin\",\n    \"analyze\": \"cd src-tauri && cargo bloat --release --crates\",\n    \"tauri\": \"tauri\",\n    \"cli\": \"cross-env NODE_ENV=development rollup -c -w\",\n    \"cli:build\": \"cross-env NODE_ENV=production rollup -c\",\n    \"test\": \"pnpm run cli:build && cross-env PAKE_CREATE_APP=1 node tests/index.js\",\n    \"format\": \"prettier --write . --ignore-unknown && find tests -name '*.js' -exec sed -i '' 's/[[:space:]]*$//' {} \\\\; && cd src-tauri && cargo fmt --verbose\",\n    \"format:check\": \"prettier --check . --ignore-unknown\",\n    \"update\": \"pnpm update --verbose && cd src-tauri && cargo update\",\n    \"prepublishOnly\": \"pnpm run cli:build\"\n  },\n  \"type\": \"module\",\n  \"exports\": \"./dist/cli.js\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@tauri-apps/api\": \"~2.10.1\",\n    \"@tauri-apps/cli\": \"^2.10.0\",\n    \"chalk\": \"^5.6.2\",\n    \"commander\": \"^14.0.3\",\n    \"execa\": \"^9.6.1\",\n    \"file-type\": \"^21.3.0\",\n    \"fs-extra\": \"^11.3.3\",\n    \"icon-gen\": \"^5.0.0\",\n    \"loglevel\": \"^1.9.2\",\n    \"ora\": \"^9.3.0\",\n    \"prompts\": \"^2.4.2\",\n    \"psl\": \"^1.15.0\",\n    \"sharp\": \"^0.34.5\",\n    \"tmp-promise\": \"^3.0.3\",\n    \"update-notifier\": \"^7.3.1\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-alias\": \"^6.0.0\",\n    \"@rollup/plugin-commonjs\": \"^29.0.0\",\n    \"@rollup/plugin-json\": \"^6.1.0\",\n    \"@rollup/plugin-replace\": \"^6.0.3\",\n    \"@rollup/plugin-terser\": \"^0.4.4\",\n    \"@types/fs-extra\": \"^11.0.4\",\n    \"@types/node\": \"^25.3.2\",\n    \"@types/page-icon\": \"^0.3.6\",\n    \"@types/prompts\": \"^2.4.9\",\n    \"@types/tmp\": \"^0.2.6\",\n    \"@types/update-notifier\": \"^6.0.8\",\n    \"app-root-path\": \"^3.1.0\",\n    \"cross-env\": \"^10.1.0\",\n    \"prettier\": \"^3.8.1\",\n    \"rollup\": \"^4.59.0\",\n    \"rollup-plugin-typescript2\": \"^0.36.0\",\n    \"tslib\": \"^2.8.1\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\"\n  },\n  \"pnpm\": {\n    \"overrides\": {\n      \"sharp\": \"^0.34.5\"\n    },\n    \"onlyBuiltDependencies\": [\n      \"esbuild\",\n      \"sharp\"\n    ]\n  }\n}\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import path from \"path\";\nimport fs from \"fs\";\nimport appRootPath from \"app-root-path\";\nimport typescript from \"rollup-plugin-typescript2\";\nimport alias from \"@rollup/plugin-alias\";\nimport commonjs from \"@rollup/plugin-commonjs\";\nimport json from \"@rollup/plugin-json\";\nimport replace from \"@rollup/plugin-replace\";\nimport chalk from \"chalk\";\nimport { spawn, exec } from \"child_process\";\n\n// Set macOS SDK environment variables for compatibility\nif (process.platform === \"darwin\") {\n  process.env.MACOSX_DEPLOYMENT_TARGET =\n    process.env.MACOSX_DEPLOYMENT_TARGET || \"14.0\";\n  process.env.CFLAGS = process.env.CFLAGS || \"-fno-modules\";\n  process.env.CXXFLAGS = process.env.CXXFLAGS || \"-fno-modules\";\n}\n\nconst isProduction = process.env.NODE_ENV === \"production\";\nconst devPlugins = !isProduction ? [pakeCliDevPlugin()] : [];\n\nexport default {\n  input: isProduction ? \"bin/cli.ts\" : \"bin/dev.ts\",\n  output: {\n    file: isProduction ? \"dist/cli.js\" : \"dist/dev.js\",\n    format: \"es\",\n    sourcemap: !isProduction,\n    banner: isProduction ? \"#!/usr/bin/env node\" : \"\",\n  },\n  watch: {\n    include: \"bin/**\",\n    exclude: \"node_modules/**\",\n  },\n  external: (id) => {\n    if (id === \"bin/cli.ts\" || id === \"bin/dev.ts\") return false;\n    if (id.startsWith(\".\") || path.isAbsolute(id) || id.startsWith(\"@/\"))\n      return false;\n    return true;\n  },\n  onwarn(warning, warn) {\n    if (warning.code === \"UNRESOLVED_IMPORT\") {\n      return;\n    }\n    warn(warning);\n  },\n  plugins: [\n    typescript({\n      tsconfig: \"./tsconfig.json\",\n      sourceMap: !isProduction,\n      inlineSources: !isProduction,\n      noEmitOnError: false,\n      compilerOptions: {\n        target: \"es2020\",\n        module: \"esnext\",\n        moduleResolution: \"node\",\n        esModuleInterop: true,\n        allowSyntheticDefaultImports: true,\n      },\n    }),\n    json(),\n    commonjs(),\n    replace({\n      \"process.env.NODE_ENV\": JSON.stringify(process.env.NODE_ENV),\n      preventAssignment: true,\n    }),\n    alias({\n      entries: [{ find: \"@\", replacement: path.join(appRootPath.path, \"bin\") }],\n    }),\n    ...devPlugins,\n  ],\n};\n\nfunction pakeCliDevPlugin() {\n  let devChildProcess;\n  let cliChildProcess;\n\n  let devHasStarted = false;\n\n  // 智能检测包管理器\n  const detectPackageManager = () => {\n    if (fs.existsSync(\"pnpm-lock.yaml\")) return \"pnpm\";\n    if (fs.existsSync(\"yarn.lock\")) return \"yarn\";\n    return \"npm\";\n  };\n\n  return {\n    name: \"pake-cli-dev-plugin\",\n    buildEnd() {\n      const command = \"node\";\n      // Pass through arguments, ignoring the first 2 (node rollup) and filtering out rollup-specifics\n      // We need to keep only arguments meant for our CLI script\n      const args = process.argv.slice(2).filter((arg) => {\n        // Filter out typical rollup flags if they are mixed in\n        // This is a simplistic filter, might need adjustment based on how npm script invokes rollup\n        return ![\"-c\", \"-w\", \"--config\", \"--watch\"].includes(arg);\n      });\n      const cliCmdArgs = [\"./dist/dev.js\", ...args];\n\n      cliChildProcess = spawn(command, cliCmdArgs, { detached: true });\n\n      cliChildProcess.stdout.on(\"data\", (data) => {\n        console.log(chalk.green(data.toString()));\n      });\n\n      cliChildProcess.stderr.on(\"data\", (data) => {\n        console.error(chalk.yellow(data.toString()));\n      });\n\n      cliChildProcess.on(\"close\", async (code) => {\n        console.log(chalk.yellow(`cli running end with code: ${code}`));\n        if (devHasStarted) return;\n        devHasStarted = true;\n\n        const packageManager = detectPackageManager();\n        const command = `${packageManager} run tauri dev --config ./src-tauri/.pake/tauri.conf.json -- --features cli-build`;\n\n        devChildProcess = exec(command);\n\n        devChildProcess.stdout.on(\"data\", (data) => {\n          console.log(chalk.green(data.toString()));\n        });\n\n        devChildProcess.stderr.on(\"data\", (data) => {\n          console.error(chalk.yellow(data.toString()));\n        });\n\n        devChildProcess.on(\"close\", (code) => {\n          console.log(chalk.yellow(`dev running end: ${code}`));\n          process.exit(code);\n        });\n      });\n    },\n  };\n}\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"1.93.0\"\ncomponents = [\"rustfmt\", \"clippy\"]\n"
  },
  {
    "path": "src-tauri/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "content": "[package]\nname = \"pake\"\nversion = \"3.10.1\"\ndescription = \"🤱🏻 Turn any webpage into a desktop app with Rust.\"\nauthors = [\"Tw93\"]\nlicense = \"MIT\"\nrepository = \"https://github.com/tw93/Pake\"\nedition = \"2021\"\nrust-version = \"1.85.0\"\n\n[lib]\nname = \"app_lib\"\ncrate-type = [\"staticlib\", \"cdylib\", \"lib\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[build-dependencies]\ntauri-build = { version = \"2.5.5\", features = [] }\n\n[dependencies]\nserde_json = \"1.0.149\"\nserde = { version = \"1.0.228\", features = [\"derive\"] }\ntokio = { version = \"1.49.0\", features = [\"full\"] }\ntauri = { version = \"2.10.2\", features = [\n  \"tray-icon\",\n  \"image-ico\",\n  \"image-png\",\n  \"macos-proxy\",\n] }\ntauri-plugin-window-state = \"2.4.1\"\ntauri-plugin-oauth = \"2.0.0\"\ntauri-plugin-http = \"2.5.7\"\ntauri-plugin-global-shortcut = { version = \"2.3.1\" }\ntauri-plugin-shell = { version = \"2.3.5\" }\ntauri-plugin-opener = { version = \"2.5.3\" }\ntauri-plugin-single-instance = \"2.4.0\"\ntauri-plugin-notification = \"2.3.3\"\n\n[features]\n# this feature is used for development builds from development cli\ncli-build = []\n# by default Tauri runs in production mode\n# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL\ndefault = [\"custom-protocol\"]\n# this feature is used for production builds where `devPath` points to the filesystem\n# DO NOT remove this\ncustom-protocol = [\"tauri/custom-protocol\"]\n\n[profile.release]\npanic = \"abort\"\ncodegen-units = 16\nlto = \"thin\"\nopt-level = \"z\"\nstrip = true\n"
  },
  {
    "path": "src-tauri/Info.plist",
    "content": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>NSCameraUsageDescription</key>\n    <string>Request camera access</string>\n    <key>NSMicrophoneUsageDescription</key>\n    <string>Request microphone access</string>\n    <key>NSAppTransportSecurity</key>\n    <dict>\n      <key>NSAllowsArbitraryLoads</key>\n      <true/>\n    </dict>\n  </dict>\n</plist>\n"
  },
  {
    "path": "src-tauri/assets/main.wxs",
    "content": "<?if $(sys.BUILDARCH)=\"x86\"?>\n    <?define Win64 = \"no\" ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFilesFolder\" ?>\n<?elseif $(sys.BUILDARCH)=\"x64\"?>\n    <?define Win64 = \"yes\" ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFiles64Folder\" ?>\n<?elseif $(sys.BUILDARCH)=\"arm64\"?>\n    <?define Win64 = \"yes\" ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFiles64Folder\" ?>\n<?else?>\n    <?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>\n<?endif?>\n\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n    <Product\n            Id=\"*\"\n            Name=\"{{product_name}}\"\n            UpgradeCode=\"{{upgrade_code}}\"\n            Language=\"!(loc.TauriLanguage)\"\n            Manufacturer=\"{{manufacturer}}\"\n            Version=\"{{version}}\">\n\n        <Package Id=\"*\"\n                 Keywords=\"Installer\"\n                 InstallerVersion=\"450\"\n                 Languages=\"0\"\n                 Compressed=\"yes\"\n                 InstallScope=\"perMachine\"\n                 SummaryCodepage=\"!(loc.TauriCodepage)\"/>\n\n        <!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->\n        <!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->\n        <Property Id=\"REINSTALLMODE\" Value=\"amus\" />\n\n        <!-- Auto launch app after installation, useful for passive mode which usually used in updates -->\n        <Property Id=\"AUTOLAUNCHAPP\" Secure=\"yes\" />\n        <!-- Property to forward cli args to the launched app to not lose those of the pre-update instance -->\n        <Property Id=\"LAUNCHAPPARGS\" Secure=\"yes\" />\n\n        {{#if allow_downgrades}}\n            <MajorUpgrade Schedule=\"afterInstallInitialize\" AllowDowngrades=\"yes\" />\n        {{else}}\n            <MajorUpgrade Schedule=\"afterInstallInitialize\" DowngradeErrorMessage=\"!(loc.DowngradeErrorMessage)\" AllowSameVersionUpgrades=\"yes\" />\n        {{/if}}\n\n        <InstallExecuteSequence>\n            <RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>\n        </InstallExecuteSequence>\n\n        <Media Id=\"1\" Cabinet=\"app.cab\" EmbedCab=\"yes\" />\n\n        {{#if banner_path}}\n        <WixVariable Id=\"WixUIBannerBmp\" Value=\"{{banner_path}}\" />\n        {{/if}}\n        {{#if dialog_image_path}}\n        <WixVariable Id=\"WixUIDialogBmp\" Value=\"{{dialog_image_path}}\" />\n        {{/if}}\n        {{#if license}}\n        <WixVariable Id=\"WixUILicenseRtf\" Value=\"{{license}}\" />\n        {{/if}}\n\n        <Icon Id=\"ProductIcon\" SourceFile=\"{{icon_path}}\"/>\n        <Property Id=\"ARPPRODUCTICON\" Value=\"ProductIcon\" />\n        <Property Id=\"ARPNOREPAIR\" Value=\"yes\" Secure=\"yes\" />      <!-- Remove repair -->\n        <SetProperty Id=\"ARPNOMODIFY\" Value=\"1\" After=\"InstallValidate\" Sequence=\"execute\"/>\n\n        {{#if homepage}}\n        <Property Id=\"ARPURLINFOABOUT\" Value=\"{{homepage}}\"/>\n        <Property Id=\"ARPHELPLINK\" Value=\"{{homepage}}\"/>\n        <Property Id=\"ARPURLUPDATEINFO\" Value=\"{{homepage}}\"/>\n        {{/if}}\n\n        <!-- initialize with previous InstallDir -->\n        <Property Id=\"INSTALLDIR\">\n            <RegistrySearch Id=\"PrevInstallDirReg\" Root=\"HKCU\" Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\" Name=\"InstallDir\" Type=\"raw\"/>\n        </Property>\n\n        <!-- launch app checkbox -->\n        <Property Id=\"WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT\" Value=\"!(loc.LaunchApp)\" />\n        <Property Id=\"WIXUI_EXITDIALOGOPTIONALCHECKBOX\" Value=\"1\"/>\n        <CustomAction Id=\"LaunchApplication\" Impersonate=\"yes\" FileKey=\"Path\" ExeCommand=\"[LAUNCHAPPARGS]\" Return=\"asyncNoWait\" />\n\n        <UI>\n            <!-- launch app checkbox -->\n            <Publish Dialog=\"ExitDialog\" Control=\"Finish\" Event=\"DoAction\" Value=\"LaunchApplication\">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>\n\n            <Property Id=\"WIXUI_INSTALLDIR\" Value=\"INSTALLDIR\" />\n\n            {{#unless license}}\n            <!-- Skip license dialog -->\n            <Publish Dialog=\"WelcomeDlg\"\n                     Control=\"Next\"\n                     Event=\"NewDialog\"\n                     Value=\"InstallDirDlg\"\n                     Order=\"2\">1</Publish>\n            <Publish Dialog=\"InstallDirDlg\"\n                     Control=\"Back\"\n                     Event=\"NewDialog\"\n                     Value=\"WelcomeDlg\"\n                     Order=\"2\">1</Publish>\n            {{/unless}}\n        </UI>\n\n        <UIRef Id=\"WixUI_InstallDir\" />\n\n        <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">\n            <Directory Id=\"DesktopFolder\" Name=\"Desktop\">\n                <Component Id=\"ApplicationShortcutDesktop\" Guid=\"*\">\n                    <Shortcut Id=\"ApplicationDesktopShortcut\" Name=\"{{product_name}}\" Description=\"Runs {{product_name}}\" Target=\"[!Path]\" WorkingDirectory=\"INSTALLDIR\" />\n                    <RemoveFolder Id=\"DesktopFolder\" On=\"uninstall\" />\n                    <RegistryValue Root=\"HKCU\" Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\" Name=\"Desktop Shortcut\" Type=\"integer\" Value=\"1\" KeyPath=\"yes\" />\n                </Component>\n            </Directory>\n            <Directory Id=\"$(var.PlatformProgramFilesFolder)\" Name=\"PFiles\">\n                <Directory Id=\"INSTALLDIR\" Name=\"{{product_name}}\"/>\n            </Directory>\n            <Directory Id=\"ProgramMenuFolder\">\n                <Directory Id=\"ApplicationProgramsFolder\" Name=\"{{product_name}}\"/>\n            </Directory>\n        </Directory>\n\n        <DirectoryRef Id=\"INSTALLDIR\">\n            <Component Id=\"RegistryEntries\" Guid=\"*\">\n                <RegistryKey Root=\"HKCU\" Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\">\n                    <RegistryValue Name=\"InstallDir\" Type=\"string\" Value=\"[INSTALLDIR]\" KeyPath=\"yes\" />\n                </RegistryKey>\n                <!-- Change the Root to HKCU for perUser installations -->\n                {{#each deep_link_protocols as |protocol| ~}}\n                <RegistryKey Root=\"HKLM\" Key=\"Software\\Classes\\\\{{protocol}}\">\n                    <RegistryValue Type=\"string\" Name=\"URL Protocol\" Value=\"\"/>\n                    <RegistryValue Type=\"string\" Value=\"URL:{{bundle_id}} protocol\"/>\n                    <RegistryKey Key=\"DefaultIcon\">\n                        <RegistryValue Type=\"string\" Value=\"&quot;[!Path]&quot;,0\" />\n                    </RegistryKey>\n                    <RegistryKey Key=\"shell\\open\\command\">\n                        <RegistryValue Type=\"string\" Value=\"&quot;[!Path]&quot; &quot;%1&quot;\" />\n                    </RegistryKey>\n                </RegistryKey>\n                {{/each~}}\n            </Component>\n            <Component Id=\"Path\" Guid=\"{{path_component_guid}}\" Win64=\"$(var.Win64)\">\n                <File Id=\"Path\" Source=\"{{main_binary_path}}\" KeyPath=\"yes\" Checksum=\"yes\"/>\n                {{#each file_associations as |association| ~}}\n                {{#each association.ext as |ext| ~}}\n                <ProgId Id=\"{{../../product_name}}.{{ext}}\" Advertise=\"yes\" Description=\"{{association.description}}\">\n                    <Extension Id=\"{{ext}}\" Advertise=\"yes\">\n                        <Verb Id=\"open\" Command=\"Open with {{../../product_name}}\" Argument=\"&quot;%1&quot;\" />\n                    </Extension>\n                </ProgId>\n                {{/each~}}\n                {{/each~}}\n            </Component>\n            {{#each binaries as |bin| ~}}\n            <Component Id=\"{{ bin.id }}\" Guid=\"{{bin.guid}}\" Win64=\"$(var.Win64)\">\n                <File Id=\"Bin_{{ bin.id }}\" Source=\"{{bin.path}}\" KeyPath=\"yes\"/>\n            </Component>\n            {{/each~}}\n            {{#if enable_elevated_update_task}}\n            <Component Id=\"UpdateTask\" Guid=\"C492327D-9720-4CD5-8DB8-F09082AF44BE\" Win64=\"$(var.Win64)\">\n                <File Id=\"UpdateTask\" Source=\"update.xml\" KeyPath=\"yes\" Checksum=\"yes\"/>\n            </Component>\n            <Component Id=\"UpdateTaskInstaller\" Guid=\"011F25ED-9BE3-50A7-9E9B-3519ED2B9932\" Win64=\"$(var.Win64)\">\n                <File Id=\"UpdateTaskInstaller\" Source=\"install-task.ps1\" KeyPath=\"yes\" Checksum=\"yes\"/>\n            </Component>\n            <Component Id=\"UpdateTaskUninstaller\" Guid=\"D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1\" Win64=\"$(var.Win64)\">\n                <File Id=\"UpdateTaskUninstaller\" Source=\"uninstall-task.ps1\" KeyPath=\"yes\" Checksum=\"yes\"/>\n            </Component>\n            {{/if}}\n            {{resources}}\n            <Component Id=\"CMP_UninstallShortcut\" Guid=\"*\">\n\n                <Shortcut Id=\"UninstallShortcut\"\n\t\t\t\t\t\t  Name=\"Uninstall {{product_name}}\"\n\t\t\t\t\t\t  Description=\"Uninstalls {{product_name}}\"\n\t\t\t\t\t\t  Target=\"[System64Folder]msiexec.exe\"\n\t\t\t\t\t\t  Arguments=\"/x [ProductCode]\" />\n\n\t\t\t\t<RemoveFolder Id=\"INSTALLDIR\"\n\t\t\t\t\t\t\t  On=\"uninstall\" />\n\n\t\t\t\t<RegistryValue Root=\"HKCU\"\n\t\t\t\t\t\t\t   Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\"\n\t\t\t\t\t\t\t   Name=\"Uninstaller Shortcut\"\n\t\t\t\t\t\t\t   Type=\"integer\"\n\t\t\t\t\t\t\t   Value=\"1\"\n\t\t\t\t\t\t\t   KeyPath=\"yes\" />\n            </Component>\n        </DirectoryRef>\n\n        <DirectoryRef Id=\"ApplicationProgramsFolder\">\n            <Component Id=\"ApplicationShortcut\" Guid=\"*\">\n                <Shortcut Id=\"ApplicationStartMenuShortcut\"\n                    Name=\"{{product_name}}\"\n                    Description=\"Runs {{product_name}}\"\n                    Target=\"[!Path]\"\n                    Icon=\"ProductIcon\"\n                    WorkingDirectory=\"INSTALLDIR\">\n                    <ShortcutProperty Key=\"System.AppUserModel.ID\" Value=\"{{bundle_id}}\"/>\n                </Shortcut>\n                <RemoveFolder Id=\"ApplicationProgramsFolder\" On=\"uninstall\"/>\n                <RegistryValue Root=\"HKCU\" Key=\"Software\\\\{{manufacturer}}\\\\{{product_name}}\" Name=\"Start Menu Shortcut\" Type=\"integer\" Value=\"1\" KeyPath=\"yes\"/>\n           </Component>\n        </DirectoryRef>\n\n        {{#each merge_modules as |msm| ~}}\n        <DirectoryRef Id=\"TARGETDIR\">\n            <Merge Id=\"{{ msm.name }}\" SourceFile=\"{{ msm.path }}\" DiskId=\"1\" Language=\"!(loc.TauriLanguage)\" />\n        </DirectoryRef>\n\n        <Feature Id=\"{{ msm.name }}\" Title=\"{{ msm.name }}\" AllowAdvertise=\"no\" Display=\"hidden\" Level=\"1\">\n            <MergeRef Id=\"{{ msm.name }}\"/>\n        </Feature>\n        {{/each~}}\n\n        <Feature\n                Id=\"MainProgram\"\n                Title=\"Application\"\n                Description=\"!(loc.InstallAppFeature)\"\n                Level=\"1\"\n                ConfigurableDirectory=\"INSTALLDIR\"\n                AllowAdvertise=\"no\"\n                Display=\"expand\"\n                Absent=\"disallow\">\n\n            <ComponentRef Id=\"RegistryEntries\"/>\n\n            {{#each resource_file_ids as |resource_file_id| ~}}\n                <ComponentRef Id=\"{{ resource_file_id }}\"/>\n            {{/each~}}\n\n            {{#if enable_elevated_update_task}}\n                <ComponentRef Id=\"UpdateTask\" />\n                <ComponentRef Id=\"UpdateTaskInstaller\" />\n                <ComponentRef Id=\"UpdateTaskUninstaller\" />\n            {{/if}}\n\n            <Feature Id=\"ShortcutsFeature\"\n                Title=\"Shortcuts\"\n                Level=\"1\">\n                <ComponentRef Id=\"Path\"/>\n                <ComponentRef Id=\"CMP_UninstallShortcut\" />\n                <ComponentRef Id=\"ApplicationShortcut\" />\n                <ComponentRef Id=\"ApplicationShortcutDesktop\" />\n            </Feature>\n\n            <Feature\n                Id=\"Environment\"\n                Title=\"PATH Environment Variable\"\n                Description=\"!(loc.PathEnvVarFeature)\"\n                Level=\"1\"\n                Absent=\"allow\">\n            <ComponentRef Id=\"Path\"/>\n            {{#each binaries as |bin| ~}}\n            <ComponentRef Id=\"{{ bin.id }}\"/>\n            {{/each~}}\n            </Feature>\n        </Feature>\n\n        <Feature Id=\"External\" AllowAdvertise=\"no\" Absent=\"disallow\">\n            {{#each component_group_refs as |id| ~}}\n            <ComponentGroupRef Id=\"{{ id }}\"/>\n            {{/each~}}\n            {{#each component_refs as |id| ~}}\n            <ComponentRef Id=\"{{ id }}\"/>\n            {{/each~}}\n            {{#each feature_group_refs as |id| ~}}\n            <FeatureGroupRef Id=\"{{ id }}\"/>\n            {{/each~}}\n            {{#each feature_refs as |id| ~}}\n            <FeatureRef Id=\"{{ id }}\"/>\n            {{/each~}}\n            {{#each merge_refs as |id| ~}}\n            <MergeRef Id=\"{{ id }}\"/>\n            {{/each~}}\n        </Feature>\n\n        {{#if install_webview}}\n        <!-- WebView2 -->\n        <Property Id=\"WVRTINSTALLED\">\n            <RegistrySearch Id=\"WVRTInstalledSystem\" Root=\"HKLM\" Key=\"SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}\" Name=\"pv\" Type=\"raw\" Win64=\"no\" />\n            <RegistrySearch Id=\"WVRTInstalledUser\" Root=\"HKCU\" Key=\"SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}\" Name=\"pv\" Type=\"raw\"/>\n        </Property>\n\n        {{#if download_bootstrapper}}\n        <CustomAction Id='DownloadAndInvokeBootstrapper' Directory=\"INSTALLDIR\" Execute=\"deferred\" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\\{] [\\[]Net.ServicePointManager[\\]]::SecurityProtocol = [\\[]Net.SecurityProtocolType[\\]]::Tls12 [\\}] catch [\\{][\\}]; Invoke-WebRequest -Uri \"https://go.microsoft.com/fwlink/p/?LinkId=2124703\" -OutFile \"$env:TEMP\\MicrosoftEdgeWebview2Setup.exe\" ; Start-Process -FilePath \"$env:TEMP\\MicrosoftEdgeWebview2Setup.exe\" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>\n        <InstallExecuteSequence>\n            <Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>\n                <![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>\n            </Custom>\n        </InstallExecuteSequence>\n        {{/if}}\n\n        <!-- Embedded webview bootstrapper mode -->\n        {{#if webview2_bootstrapper_path}}\n        <Binary Id=\"MicrosoftEdgeWebview2Setup.exe\" SourceFile=\"{{webview2_bootstrapper_path}}\"/>\n        <CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute=\"deferred\" ExeCommand='{{webview_installer_args}} /install' Return='check' />\n        <InstallExecuteSequence>\n            <Custom Action='InvokeBootstrapper' Before='InstallFinalize'>\n                <![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>\n            </Custom>\n        </InstallExecuteSequence>\n        {{/if}}\n\n        <!-- Embedded offline installer -->\n        {{#if webview2_installer_path}}\n        <Binary Id=\"MicrosoftEdgeWebView2RuntimeInstaller.exe\" SourceFile=\"{{webview2_installer_path}}\"/>\n        <CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute=\"deferred\" ExeCommand='{{webview_installer_args}} /install' Return='check' />\n        <InstallExecuteSequence>\n            <Custom Action='InvokeStandalone' Before='InstallFinalize'>\n                <![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>\n            </Custom>\n        </InstallExecuteSequence>\n        {{/if}}\n\n        {{/if}}\n\n        {{#if enable_elevated_update_task}}\n        <!-- Install an elevated update task within Windows Task Scheduler -->\n        <CustomAction\n            Id=\"CreateUpdateTask\"\n            Return=\"check\"\n            Directory=\"INSTALLDIR\"\n            Execute=\"commit\"\n            Impersonate=\"yes\"\n            ExeCommand=\"powershell.exe -WindowStyle hidden .\\install-task.ps1\" />\n        <InstallExecuteSequence>\n            <Custom Action='CreateUpdateTask' Before='InstallFinalize'>\n                NOT(REMOVE)\n            </Custom>\n        </InstallExecuteSequence>\n        <!-- Remove elevated update task during uninstall -->\n        <CustomAction\n            Id=\"DeleteUpdateTask\"\n            Return=\"check\"\n            Directory=\"INSTALLDIR\"\n            ExeCommand=\"powershell.exe -WindowStyle hidden .\\uninstall-task.ps1\" />\n        <InstallExecuteSequence>\n            <Custom Action=\"DeleteUpdateTask\" Before='InstallFinalize'>\n                (REMOVE = \"ALL\") AND NOT UPGRADINGPRODUCTCODE\n            </Custom>\n        </InstallExecuteSequence>\n        {{/if}}\n\n        <InstallExecuteSequence>\n          <Custom Action=\"LaunchApplication\" After=\"InstallFinalize\">AUTOLAUNCHAPP AND NOT Installed</Custom>\n        </InstallExecuteSequence>\n\n        <SetProperty Id=\"ARPINSTALLLOCATION\" Value=\"[INSTALLDIR]\" After=\"CostFinalize\"/>\n    </Product>\n</Wix>\n"
  },
  {
    "path": "src-tauri/build.rs",
    "content": "fn main() {\n    tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/capabilities/default.json",
    "content": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"pake-capability\",\n  \"description\": \"Capability for the pake app.\",\n  \"webviews\": [\"pake\"],\n  \"remote\": {\n    \"urls\": [\"https://*.*\"]\n  },\n  \"permissions\": [\n    \"shell:allow-open\",\n    \"core:window:allow-theme\",\n    \"core:window:allow-start-dragging\",\n    \"core:window:allow-toggle-maximize\",\n    \"core:window:allow-is-fullscreen\",\n    \"core:window:allow-set-fullscreen\",\n    \"core:window:allow-set-resizable\",\n    \"core:window:allow-maximize\",\n    \"core:window:allow-minimize\",\n    \"core:window:allow-close\",\n    \"core:webview:allow-internal-toggle-devtools\",\n    \"notification:allow-is-permission-granted\",\n    \"notification:allow-notify\",\n    \"notification:allow-get-active\",\n    \"notification:allow-register-listener\",\n    \"notification:allow-register-action-types\",\n    \"notification:default\",\n    \"core:path:default\"\n  ]\n}\n"
  },
  {
    "path": "src-tauri/entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n\n  </dict>\n</plist>\n"
  },
  {
    "path": "src-tauri/pake.json",
    "content": "{\n  \"windows\": [\n    {\n      \"url\": \"https://weekly.tw93.fun/en\",\n      \"url_type\": \"web\",\n      \"hide_title_bar\": true,\n      \"fullscreen\": false,\n      \"width\": 1200,\n      \"height\": 780,\n      \"resizable\": true,\n      \"always_on_top\": false,\n      \"dark_mode\": false,\n      \"activation_shortcut\": \"\",\n      \"disabled_web_shortcuts\": false,\n      \"hide_on_close\": true,\n      \"incognito\": false,\n      \"enable_wasm\": false,\n      \"enable_drag_drop\": false,\n      \"maximize\": false,\n      \"start_to_tray\": false,\n      \"force_internal_navigation\": false,\n      \"internal_url_regex\": \"\",\n      \"new_window\": false\n    }\n  ],\n  \"user_agent\": {\n    \"macos\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15\",\n    \"linux\": \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\",\n    \"windows\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\"\n  },\n  \"system_tray\": {\n    \"macos\": false,\n    \"linux\": true,\n    \"windows\": true\n  },\n  \"system_tray_path\": \"icons/icon.png\",\n  \"inject\": [],\n  \"proxy_url\": \"\",\n  \"multi_instance\": false,\n  \"multi_window\": false\n}\n"
  },
  {
    "path": "src-tauri/rust_proxy.toml",
    "content": "[source.crates-io]\nreplace-with = 'rsproxy-sparse'\n[source.rsproxy]\nregistry = \"https://rsproxy.cn/crates.io-index\"\n[source.rsproxy-sparse]\nregistry = \"sparse+https://rsproxy.cn/index/\"\n[registries.rsproxy]\nindex = \"https://rsproxy.cn/crates.io-index\"\n[net]\ngit-fetch-with-cli = true\n"
  },
  {
    "path": "src-tauri/src/app/config.rs",
    "content": "use serde::{Deserialize, Serialize};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct WindowConfig {\n    pub url: String,\n    pub hide_title_bar: bool,\n    pub fullscreen: bool,\n    pub maximize: bool,\n    pub width: f64,\n    pub height: f64,\n    pub resizable: bool,\n    pub url_type: String,\n    pub always_on_top: bool,\n    pub dark_mode: bool,\n    pub disabled_web_shortcuts: bool,\n    pub activation_shortcut: String,\n    pub hide_on_close: bool,\n    pub incognito: bool,\n    pub title: Option<String>,\n    pub enable_wasm: bool,\n    pub enable_drag_drop: bool,\n    #[serde(default)]\n    pub new_window: bool,\n    pub start_to_tray: bool,\n    #[serde(default)]\n    pub force_internal_navigation: bool,\n    #[serde(default)]\n    pub internal_url_regex: String,\n    #[serde(default = \"default_zoom\")]\n    pub zoom: u32,\n    #[serde(default)]\n    pub min_width: f64,\n    #[serde(default)]\n    pub min_height: f64,\n    #[serde(default)]\n    pub ignore_certificate_errors: bool,\n}\n\nfn default_zoom() -> u32 {\n    100\n}\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct PlatformSpecific<T> {\n    pub macos: T,\n    pub linux: T,\n    pub windows: T,\n}\n\nimpl<T> PlatformSpecific<T> {\n    pub const fn get(&self) -> &T {\n        #[cfg(target_os = \"macos\")]\n        let platform = &self.macos;\n        #[cfg(target_os = \"linux\")]\n        let platform = &self.linux;\n        #[cfg(target_os = \"windows\")]\n        let platform = &self.windows;\n\n        platform\n    }\n}\n\nimpl<T> PlatformSpecific<T>\nwhere\n    T: Copy,\n{\n    pub const fn copied(&self) -> T {\n        *self.get()\n    }\n}\n\npub type UserAgent = PlatformSpecific<String>;\npub type FunctionON = PlatformSpecific<bool>;\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct PakeConfig {\n    pub windows: Vec<WindowConfig>,\n    pub user_agent: UserAgent,\n    pub system_tray: FunctionON,\n    pub system_tray_path: String,\n    pub proxy_url: String,\n    #[serde(default)]\n    pub multi_instance: bool,\n    #[serde(default)]\n    pub multi_window: bool,\n}\n\nimpl PakeConfig {\n    pub fn show_system_tray(&self) -> bool {\n        self.system_tray.copied()\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/app/invoke.rs",
    "content": "use crate::util::{check_file_or_append, get_download_message_with_lang, show_toast, MessageType};\nuse std::fs::{self, File};\nuse std::io::Write;\nuse std::str::FromStr;\nuse tauri::http::Method;\nuse tauri::{command, AppHandle, Manager, Url, WebviewWindow};\nuse tauri_plugin_http::reqwest::{ClientBuilder, Request};\n\n#[cfg(target_os = \"macos\")]\nuse tauri::Theme;\n\n#[derive(serde::Deserialize)]\npub struct DownloadFileParams {\n    url: String,\n    filename: String,\n    language: Option<String>,\n}\n\n#[derive(serde::Deserialize)]\npub struct BinaryDownloadParams {\n    filename: String,\n    binary: Vec<u8>,\n    language: Option<String>,\n}\n\n#[derive(serde::Deserialize)]\npub struct NotificationParams {\n    title: String,\n    body: String,\n    icon: String,\n}\n\n#[command]\npub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> {\n    let window: WebviewWindow = app.get_webview_window(\"pake\").ok_or(\"Window not found\")?;\n\n    show_toast(\n        &window,\n        &get_download_message_with_lang(MessageType::Start, params.language.clone()),\n    );\n\n    let download_dir = app\n        .path()\n        .download_dir()\n        .map_err(|e| format!(\"Failed to get download dir: {}\", e))?;\n\n    let output_path = download_dir.join(&params.filename);\n\n    let path_str = output_path.to_str().ok_or(\"Invalid output path\")?;\n\n    let file_path = check_file_or_append(path_str);\n\n    let client = ClientBuilder::new()\n        .build()\n        .map_err(|e| format!(\"Failed to build client: {}\", e))?;\n\n    let url = Url::from_str(&params.url).map_err(|e| format!(\"Invalid URL: {}\", e))?;\n\n    let request = Request::new(Method::GET, url);\n\n    let response = client.execute(request).await;\n\n    match response {\n        Ok(mut res) => {\n            let mut file =\n                File::create(file_path).map_err(|e| format!(\"Failed to create file: {}\", e))?;\n\n            while let Some(chunk) = res\n                .chunk()\n                .await\n                .map_err(|e| format!(\"Failed to get chunk: {}\", e))?\n            {\n                file.write_all(&chunk)\n                    .map_err(|e| format!(\"Failed to write chunk: {}\", e))?;\n            }\n\n            show_toast(\n                &window,\n                &get_download_message_with_lang(MessageType::Success, params.language.clone()),\n            );\n            Ok(())\n        }\n        Err(e) => {\n            show_toast(\n                &window,\n                &get_download_message_with_lang(MessageType::Failure, params.language),\n            );\n            Err(e.to_string())\n        }\n    }\n}\n\n#[command]\npub async fn download_file_by_binary(\n    app: AppHandle,\n    params: BinaryDownloadParams,\n) -> Result<(), String> {\n    let window: WebviewWindow = app.get_webview_window(\"pake\").ok_or(\"Window not found\")?;\n\n    show_toast(\n        &window,\n        &get_download_message_with_lang(MessageType::Start, params.language.clone()),\n    );\n\n    let download_dir = app\n        .path()\n        .download_dir()\n        .map_err(|e| format!(\"Failed to get download dir: {}\", e))?;\n\n    let output_path = download_dir.join(&params.filename);\n\n    let path_str = output_path.to_str().ok_or(\"Invalid output path\")?;\n\n    let file_path = check_file_or_append(path_str);\n\n    match fs::write(file_path, &params.binary) {\n        Ok(_) => {\n            show_toast(\n                &window,\n                &get_download_message_with_lang(MessageType::Success, params.language.clone()),\n            );\n            Ok(())\n        }\n        Err(e) => {\n            show_toast(\n                &window,\n                &get_download_message_with_lang(MessageType::Failure, params.language),\n            );\n            Err(e.to_string())\n        }\n    }\n}\n\n#[command]\npub fn send_notification(app: AppHandle, params: NotificationParams) -> Result<(), String> {\n    use tauri_plugin_notification::NotificationExt;\n    app.notification()\n        .builder()\n        .title(&params.title)\n        .body(&params.body)\n        .icon(&params.icon)\n        .show()\n        .map_err(|e| format!(\"Failed to show notification: {}\", e))?;\n    Ok(())\n}\n\n#[command]\npub async fn update_theme_mode(app: AppHandle, mode: String) {\n    #[cfg(target_os = \"macos\")]\n    {\n        if let Some(window) = app.get_webview_window(\"pake\") {\n            let theme = if mode == \"dark\" {\n                Theme::Dark\n            } else {\n                Theme::Light\n            };\n            let _ = window.set_theme(Some(theme));\n        }\n    }\n    #[cfg(not(target_os = \"macos\"))]\n    {\n        let _ = app;\n        let _ = mode;\n    }\n}\n\n#[command]\n#[allow(unreachable_code)]\npub fn clear_cache_and_restart(app: AppHandle) -> Result<(), String> {\n    if let Some(window) = app.get_webview_window(\"pake\") {\n        match window.clear_all_browsing_data() {\n            Ok(_) => {\n                // Clear all browsing data successfully\n                app.restart();\n                Ok(())\n            }\n            Err(e) => {\n                eprintln!(\"Failed to clear browsing data: {}\", e);\n                Err(format!(\"Failed to clear browsing data: {}\", e))\n            }\n        }\n    } else {\n        Err(\"Main window not found\".to_string())\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/app/menu.rs",
    "content": "// Menu functionality is only used on macOS\n#![cfg(target_os = \"macos\")]\n\nuse crate::app::window::open_additional_window_safe;\nuse tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu};\nuse tauri::{AppHandle, Manager, Wry};\nuse tauri_plugin_opener::OpenerExt;\n\npub fn get_menu(app: &AppHandle<Wry>, allow_multi_window: bool) -> tauri::Result<Menu<Wry>> {\n    let pake_version = env!(\"CARGO_PKG_VERSION\");\n    let pake_menu_item_title = format!(\"Built with Pake V{}\", pake_version);\n\n    let menu = Menu::with_items(\n        app,\n        &[\n            &app_menu(app)?,\n            &file_menu(app, allow_multi_window)?,\n            &edit_menu(app)?,\n            &view_menu(app)?,\n            &navigation_menu(app)?,\n            &window_menu(app)?,\n            &help_menu(app, &pake_menu_item_title)?,\n        ],\n    )?;\n\n    Ok(menu)\n}\n\nfn app_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {\n    let app_menu = Submenu::new(app, \"Pake\", true)?;\n    let about_metadata = AboutMetadata::default();\n    app_menu.append(&PredefinedMenuItem::about(\n        app,\n        Some(\"Pake\"),\n        Some(about_metadata),\n    )?)?;\n    app_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    app_menu.append(&PredefinedMenuItem::services(app, None)?)?;\n    app_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    app_menu.append(&PredefinedMenuItem::hide(app, None)?)?;\n    app_menu.append(&PredefinedMenuItem::hide_others(app, None)?)?;\n    app_menu.append(&PredefinedMenuItem::show_all(app, None)?)?;\n    app_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    app_menu.append(&PredefinedMenuItem::quit(app, None)?)?;\n    Ok(app_menu)\n}\n\nfn file_menu(app: &AppHandle<Wry>, allow_multi_window: bool) -> tauri::Result<Submenu<Wry>> {\n    let file_menu = Submenu::new(app, \"File\", true)?;\n    if allow_multi_window {\n        file_menu.append(&MenuItem::with_id(\n            app,\n            \"new_window\",\n            \"New Window\",\n            true,\n            Some(\"CmdOrCtrl+N\"),\n        )?)?;\n        file_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    }\n    file_menu.append(&PredefinedMenuItem::close_window(app, None)?)?;\n    file_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    file_menu.append(&MenuItem::with_id(\n        app,\n        \"clear_cache_restart\",\n        \"Clear Cache & Restart\",\n        true,\n        Some(\"CmdOrCtrl+Shift+Backspace\"),\n    )?)?;\n    Ok(file_menu)\n}\n\nfn edit_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {\n    let edit_menu = Submenu::new(app, \"Edit\", true)?;\n    edit_menu.append(&PredefinedMenuItem::undo(app, None)?)?;\n    edit_menu.append(&PredefinedMenuItem::redo(app, None)?)?;\n    edit_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    edit_menu.append(&PredefinedMenuItem::cut(app, None)?)?;\n    edit_menu.append(&PredefinedMenuItem::copy(app, None)?)?;\n    edit_menu.append(&PredefinedMenuItem::paste(app, None)?)?;\n    edit_menu.append(&MenuItem::with_id(\n        app,\n        \"paste_and_match_style\",\n        \"Paste and Match Style\",\n        true,\n        Some(\"CmdOrCtrl+Shift+Option+V\"),\n    )?)?;\n    edit_menu.append(&PredefinedMenuItem::select_all(app, None)?)?;\n    edit_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    edit_menu.append(&MenuItem::with_id(\n        app,\n        \"copy_url\",\n        \"Copy URL\",\n        true,\n        Some(\"CmdOrCtrl+L\"),\n    )?)?;\n    Ok(edit_menu)\n}\n\nfn view_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {\n    let view_menu = Submenu::new(app, \"View\", true)?;\n    view_menu.append(&MenuItem::with_id(\n        app,\n        \"reload\",\n        \"Reload\",\n        true,\n        Some(\"CmdOrCtrl+R\"),\n    )?)?;\n    view_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    view_menu.append(&MenuItem::with_id(\n        app,\n        \"zoom_in\",\n        \"Zoom In\",\n        true,\n        Some(\"CmdOrCtrl+=\"),\n    )?)?;\n    view_menu.append(&MenuItem::with_id(\n        app,\n        \"zoom_out\",\n        \"Zoom Out\",\n        true,\n        Some(\"CmdOrCtrl+-\"),\n    )?)?;\n    view_menu.append(&MenuItem::with_id(\n        app,\n        \"zoom_reset\",\n        \"Actual Size\",\n        true,\n        Some(\"CmdOrCtrl+0\"),\n    )?)?;\n    view_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    view_menu.append(&PredefinedMenuItem::fullscreen(app, None)?)?;\n    view_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    view_menu.append(&MenuItem::with_id(\n        app,\n        \"toggle_devtools\",\n        \"Toggle Developer Tools\",\n        cfg!(debug_assertions),\n        Some(\"CmdOrCtrl+Option+I\"),\n    )?)?;\n    Ok(view_menu)\n}\n\nfn navigation_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {\n    let navigation_menu = Submenu::new(app, \"Navigation\", true)?;\n    navigation_menu.append(&MenuItem::with_id(\n        app,\n        \"go_back\",\n        \"Back\",\n        true,\n        Some(\"CmdOrCtrl+[\"),\n    )?)?;\n    navigation_menu.append(&MenuItem::with_id(\n        app,\n        \"go_forward\",\n        \"Forward\",\n        true,\n        Some(\"CmdOrCtrl+]\"),\n    )?)?;\n    navigation_menu.append(&MenuItem::with_id(\n        app,\n        \"go_home\",\n        \"Go Home\",\n        true,\n        Some(\"CmdOrCtrl+Shift+H\"),\n    )?)?;\n    Ok(navigation_menu)\n}\n\nfn window_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {\n    let window_menu = Submenu::new(app, \"Window\", true)?;\n    window_menu.append(&PredefinedMenuItem::minimize(app, None)?)?;\n    window_menu.append(&PredefinedMenuItem::maximize(app, None)?)?;\n    window_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    window_menu.append(&MenuItem::with_id(\n        app,\n        \"always_on_top\",\n        \"Toggle Always on Top\",\n        true,\n        None::<&str>,\n    )?)?;\n    window_menu.append(&PredefinedMenuItem::separator(app)?)?;\n    window_menu.append(&PredefinedMenuItem::close_window(app, None)?)?;\n    Ok(window_menu)\n}\n\nfn help_menu(app: &AppHandle<Wry>, title: &str) -> tauri::Result<Submenu<Wry>> {\n    let help_menu = Submenu::new(app, \"Help\", true)?;\n    let github_item = MenuItem::with_id(app, \"pake_github_link\", title, true, None::<&str>)?;\n    help_menu.append(&github_item)?;\n    Ok(help_menu)\n}\n\npub fn handle_menu_click(app_handle: &AppHandle, id: &str) {\n    match id {\n        \"new_window\" => {\n            open_additional_window_safe(app_handle);\n        }\n        \"pake_github_link\" => {\n            let _ = app_handle\n                .opener()\n                .open_url(\"https://github.com/tw93/Pake\", None::<&str>);\n        }\n        \"reload\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"window.location.reload()\");\n            }\n        }\n        \"toggle_devtools\" => {\n            #[cfg(debug_assertions)] // Only allow in debug builds\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                if window.is_devtools_open() {\n                    window.close_devtools();\n                } else {\n                    window.open_devtools();\n                }\n            }\n        }\n        \"zoom_in\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"zoomIn()\");\n            }\n        }\n        \"zoom_out\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"zoomOut()\");\n            }\n        }\n        \"zoom_reset\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"setZoom('100%')\");\n            }\n        }\n        \"go_back\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"window.history.back()\");\n            }\n        }\n        \"go_forward\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"window.history.forward()\");\n            }\n        }\n        \"go_home\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"window.location.href = window.pakeConfig.url\");\n            }\n        }\n        \"copy_url\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"navigator.clipboard.writeText(window.location.href)\");\n            }\n        }\n        \"paste_and_match_style\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let _ = window.eval(\"triggerPasteAsPlainText()\");\n            }\n        }\n        \"clear_cache_restart\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                if let Ok(_) = window.clear_all_browsing_data() {\n                    app_handle.restart();\n                }\n            }\n        }\n        \"always_on_top\" => {\n            if let Some(window) = app_handle.get_webview_window(\"pake\") {\n                let is_on_top = window.is_always_on_top().unwrap_or(false);\n                let _ = window.set_always_on_top(!is_on_top);\n            }\n        }\n        _ => {}\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/app/mod.rs",
    "content": "pub mod config;\npub mod invoke;\n#[cfg(target_os = \"macos\")]\npub mod menu;\npub mod setup;\npub mod window;\n"
  },
  {
    "path": "src-tauri/src/app/setup.rs",
    "content": "use crate::app::window::open_additional_window_safe;\nuse std::str::FromStr;\nuse std::sync::{Arc, Mutex};\nuse std::time::{Duration, Instant};\nuse tauri::{\n    menu::{MenuBuilder, MenuItemBuilder},\n    tray::{TrayIconBuilder, TrayIconEvent},\n    AppHandle, Manager,\n};\nuse tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};\nuse tauri_plugin_window_state::{AppHandleExt, StateFlags};\n\npub fn set_system_tray(\n    app: &AppHandle,\n    show_system_tray: bool,\n    tray_icon_path: &str,\n    _init_fullscreen: bool,\n    allow_multi_window: bool,\n) -> tauri::Result<()> {\n    if !show_system_tray {\n        app.remove_tray_by_id(\"pake-tray\");\n        return Ok(());\n    }\n\n    let new_window = MenuItemBuilder::with_id(\"new_window\", \"New Window\").build(app)?;\n    let hide_app = MenuItemBuilder::with_id(\"hide_app\", \"Hide\").build(app)?;\n    let show_app = MenuItemBuilder::with_id(\"show_app\", \"Show\").build(app)?;\n    let quit = MenuItemBuilder::with_id(\"quit\", \"Quit\").build(app)?;\n\n    let menu = if allow_multi_window {\n        MenuBuilder::new(app)\n            .items(&[&new_window, &hide_app, &show_app, &quit])\n            .build()?\n    } else {\n        MenuBuilder::new(app)\n            .items(&[&hide_app, &show_app, &quit])\n            .build()?\n    };\n\n    app.app_handle().remove_tray_by_id(\"pake-tray\");\n\n    let tray = TrayIconBuilder::new()\n        .menu(&menu)\n        .on_menu_event(move |app, event| match event.id().as_ref() {\n            \"new_window\" => {\n                open_additional_window_safe(app);\n            }\n            \"hide_app\" => {\n                if let Some(window) = app.get_webview_window(\"pake\") {\n                    window.minimize().unwrap();\n                }\n            }\n            \"show_app\" => {\n                if let Some(window) = app.get_webview_window(\"pake\") {\n                    window.show().unwrap();\n                    #[cfg(target_os = \"linux\")]\n                    if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {\n                        let _ = window.set_fullscreen(true);\n                        let _ = window.set_focus();\n                    }\n                }\n            }\n            \"quit\" => {\n                app.save_window_state(StateFlags::all()).unwrap();\n                std::process::exit(0);\n            }\n            _ => (),\n        })\n        .on_tray_icon_event(move |tray, event| match event {\n            TrayIconEvent::Click { button, .. } => {\n                if button == tauri::tray::MouseButton::Left {\n                    if let Some(window) = tray.app_handle().get_webview_window(\"pake\") {\n                        let is_visible = window.is_visible().unwrap_or(false);\n                        if is_visible {\n                            window.hide().unwrap();\n                        } else {\n                            window.show().unwrap();\n                            window.set_focus().unwrap();\n                            #[cfg(target_os = \"linux\")]\n                            if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {\n                                let _ = window.set_fullscreen(true);\n                            }\n                        }\n                    }\n                }\n            }\n            _ => {}\n        })\n        .icon(if tray_icon_path.is_empty() {\n            app.default_window_icon()\n                .unwrap_or_else(|| panic!(\"Failed to get default window icon\"))\n                .clone()\n        } else {\n            tauri::image::Image::from_path(tray_icon_path).unwrap_or_else(|_| {\n                // If custom tray icon fails to load, fallback to default\n                app.default_window_icon()\n                    .unwrap_or_else(|| panic!(\"Failed to get default window icon\"))\n                    .clone()\n            })\n        })\n        .build(app)?;\n\n    tray.set_icon_as_template(false)?;\n    Ok(())\n}\n\npub fn set_global_shortcut(\n    app: &AppHandle,\n    shortcut: String,\n    _init_fullscreen: bool,\n) -> tauri::Result<()> {\n    if shortcut.is_empty() {\n        return Ok(());\n    }\n\n    let app_handle = app.clone();\n    let shortcut_hotkey = Shortcut::from_str(&shortcut).unwrap();\n    let last_triggered = Arc::new(Mutex::new(Instant::now()));\n\n    app_handle\n        .plugin(\n            tauri_plugin_global_shortcut::Builder::new()\n                .with_handler({\n                    let last_triggered = Arc::clone(&last_triggered);\n                    move |app, event, _shortcut| {\n                        let mut last_triggered = last_triggered.lock().unwrap();\n                        if Instant::now().duration_since(*last_triggered)\n                            < Duration::from_millis(300)\n                        {\n                            return;\n                        }\n                        *last_triggered = Instant::now();\n\n                        if shortcut_hotkey.eq(event) {\n                            if let Some(window) = app.get_webview_window(\"pake\") {\n                                let is_visible = window.is_visible().unwrap();\n                                if is_visible {\n                                    window.hide().unwrap();\n                                } else {\n                                    window.show().unwrap();\n                                    window.set_focus().unwrap();\n                                    #[cfg(target_os = \"linux\")]\n                                    if _init_fullscreen && !window.is_fullscreen().unwrap_or(false)\n                                    {\n                                        let _ = window.set_fullscreen(true);\n                                    }\n                                }\n                            }\n                        }\n                    }\n                })\n                .build(),\n        )\n        .expect(\"Failed to set global shortcut\");\n\n    app.global_shortcut().register(shortcut_hotkey).unwrap();\n\n    Ok(())\n}\n"
  },
  {
    "path": "src-tauri/src/app/window.rs",
    "content": "use crate::app::config::PakeConfig;\nuse crate::util::get_data_dir;\nuse std::{path::PathBuf, str::FromStr, sync::Mutex};\nuse tauri::{\n    webview::{NewWindowFeatures, NewWindowResponse},\n    AppHandle, Config, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder,\n};\n\n#[cfg(target_os = \"macos\")]\nuse tauri::{Theme, TitleBarStyle};\n\n#[cfg(target_os = \"windows\")]\nfn build_proxy_browser_arg(url: &Url) -> Option<String> {\n    let host = url.host_str()?;\n    let scheme = url.scheme();\n    let port = url.port().or_else(|| match scheme {\n        \"http\" => Some(80),\n        \"socks5\" => Some(1080),\n        _ => None,\n    })?;\n\n    match scheme {\n        \"http\" | \"socks5\" => Some(format!(\"--proxy-server={scheme}://{host}:{port}\")),\n        _ => None,\n    }\n}\n\npub struct MultiWindowState {\n    pub pake_config: PakeConfig,\n    pub tauri_config: Config,\n    next_window_index: Mutex<u32>,\n}\n\nimpl MultiWindowState {\n    pub fn new(pake_config: PakeConfig, tauri_config: Config) -> Self {\n        Self {\n            pake_config,\n            tauri_config,\n            next_window_index: Mutex::new(0),\n        }\n    }\n\n    fn next_window_label(&self) -> String {\n        let mut index = self.next_window_index.lock().unwrap();\n        *index += 1;\n        format!(\"pake-{}\", *index)\n    }\n}\n\npub fn set_window(app: &AppHandle, config: &PakeConfig, tauri_config: &Config) -> WebviewWindow {\n    build_window_with_label(app, config, tauri_config, \"pake\").expect(\"Failed to build window\")\n}\n\npub fn open_additional_window(app: &AppHandle) -> tauri::Result<WebviewWindow> {\n    let state = app.state::<MultiWindowState>();\n    let label = state.next_window_label();\n    build_window_with_label(app, &state.pake_config, &state.tauri_config, &label)\n}\n\nstruct WindowBuildOptions<'a> {\n    label: &'a str,\n    url: WebviewUrl,\n    visible: bool,\n    new_window_features: Option<NewWindowFeatures>,\n}\n\nfn open_requested_window(\n    app: &AppHandle,\n    config: &PakeConfig,\n    tauri_config: &Config,\n    target_url: Url,\n    features: NewWindowFeatures,\n) -> tauri::Result<WebviewWindow> {\n    let state = app.state::<MultiWindowState>();\n    let label = state.next_window_label();\n    let window = build_window(\n        app,\n        config,\n        tauri_config,\n        WindowBuildOptions {\n            label: &label,\n            url: WebviewUrl::External(\"about:blank\".parse().unwrap()),\n            visible: true,\n            new_window_features: Some(features),\n        },\n    )?;\n\n    let title = target_url.host_str().unwrap_or(target_url.as_str());\n    let _ = window.set_title(title);\n    let _ = window.set_focus();\n\n    Ok(window)\n}\n\npub fn open_additional_window_safe(app: &AppHandle) {\n    #[cfg(target_os = \"windows\")]\n    {\n        let app_handle = app.clone();\n        std::thread::spawn(move || {\n            if let Ok(window) = open_additional_window(&app_handle) {\n                let _ = window.show();\n                let _ = window.set_focus();\n            }\n        });\n    }\n\n    #[cfg(not(target_os = \"windows\"))]\n    {\n        if let Ok(window) = open_additional_window(app) {\n            let _ = window.show();\n            let _ = window.set_focus();\n        }\n    }\n}\n\nfn build_window_with_label(\n    app: &AppHandle,\n    config: &PakeConfig,\n    tauri_config: &Config,\n    label: &str,\n) -> tauri::Result<WebviewWindow> {\n    let window_config = config\n        .windows\n        .first()\n        .expect(\"At least one window configuration is required\");\n    let url = match window_config.url_type.as_str() {\n        \"web\" => WebviewUrl::App(window_config.url.parse().unwrap()),\n        \"local\" => WebviewUrl::App(PathBuf::from(&window_config.url)),\n        _ => panic!(\"url type can only be web or local\"),\n    };\n\n    build_window(\n        app,\n        config,\n        tauri_config,\n        WindowBuildOptions {\n            label,\n            url,\n            visible: false,\n            new_window_features: None,\n        },\n    )\n}\n\nfn build_window(\n    app: &AppHandle,\n    config: &PakeConfig,\n    tauri_config: &Config,\n    opts: WindowBuildOptions,\n) -> tauri::Result<WebviewWindow> {\n    let WindowBuildOptions {\n        label,\n        url,\n        visible,\n        new_window_features,\n    } = opts;\n    let package_name = tauri_config.clone().product_name.unwrap();\n    let _data_dir = get_data_dir(app, package_name);\n\n    let window_config = config\n        .windows\n        .first()\n        .expect(\"At least one window configuration is required\");\n\n    let user_agent = config.user_agent.get();\n\n    let config_script = format!(\n        \"window.pakeConfig = {}\",\n        serde_json::to_string(&window_config).unwrap()\n    );\n\n    // Platform-specific title: macOS prefers empty, others fallback to product name\n    let effective_title = window_config.title.as_deref().unwrap_or_else(|| {\n        if cfg!(target_os = \"macos\") {\n            \"\"\n        } else {\n            tauri_config.product_name.as_deref().unwrap_or(\"\")\n        }\n    });\n\n    let mut window_builder = WebviewWindowBuilder::new(app, label, url)\n        .title(effective_title)\n        .visible(visible)\n        .user_agent(user_agent)\n        .resizable(window_config.resizable)\n        .maximized(window_config.maximize);\n\n    #[cfg(target_os = \"windows\")]\n    {\n        let scale_factor = app\n            .primary_monitor()\n            .ok()\n            .flatten()\n            .map(|m| m.scale_factor())\n            .unwrap_or(1.0);\n        let logical_width = window_config.width / scale_factor;\n        let logical_height = window_config.height / scale_factor;\n        window_builder = window_builder.inner_size(logical_width, logical_height);\n    }\n\n    #[cfg(not(target_os = \"windows\"))]\n    {\n        window_builder = window_builder.inner_size(window_config.width, window_config.height);\n    }\n\n    window_builder = window_builder\n        .always_on_top(window_config.always_on_top)\n        .incognito(window_config.incognito);\n\n    #[cfg(any(target_os = \"windows\", target_os = \"macos\"))]\n    {\n        window_builder = window_builder.fullscreen(window_config.fullscreen);\n    }\n\n    if window_config.min_width > 0.0 || window_config.min_height > 0.0 {\n        let min_w = if window_config.min_width > 0.0 {\n            window_config.min_width\n        } else {\n            window_config.width\n        };\n        let min_h = if window_config.min_height > 0.0 {\n            window_config.min_height\n        } else {\n            window_config.height\n        };\n        window_builder = window_builder.min_inner_size(min_w, min_h);\n    }\n\n    if !window_config.enable_drag_drop {\n        window_builder = window_builder.disable_drag_drop_handler();\n    }\n\n    if window_config.new_window {\n        let app_handle = app.clone();\n        let popup_config = config.clone();\n        let popup_tauri_config = tauri_config.clone();\n        window_builder = window_builder.on_new_window(move |target_url, features| {\n            match open_requested_window(\n                &app_handle,\n                &popup_config,\n                &popup_tauri_config,\n                target_url,\n                features,\n            ) {\n                Ok(window) => NewWindowResponse::Create { window },\n                Err(error) => {\n                    eprintln!(\"[Pake] Failed to open requested window: {error}\");\n                    NewWindowResponse::Deny\n                }\n            }\n        });\n    }\n\n    // Add initialization scripts\n    window_builder = window_builder\n        .initialization_script(&config_script)\n        .initialization_script(include_str!(\"../inject/component.js\"))\n        .initialization_script(include_str!(\"../inject/event.js\"))\n        .initialization_script(include_str!(\"../inject/style.js\"))\n        .initialization_script(include_str!(\"../inject/theme_refresh.js\"))\n        .initialization_script(include_str!(\"../inject/auth.js\"))\n        .initialization_script(include_str!(\"../inject/custom.js\"));\n\n    #[cfg(target_os = \"windows\")]\n    let mut windows_browser_args = String::from(\"--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --disable-blink-features=AutomationControlled\");\n\n    #[cfg(target_os = \"linux\")]\n    let mut linux_browser_args = String::from(\"--disable-blink-features=AutomationControlled\");\n\n    if window_config.ignore_certificate_errors {\n        #[cfg(target_os = \"windows\")]\n        {\n            windows_browser_args.push_str(\" --ignore-certificate-errors\");\n        }\n\n        #[cfg(target_os = \"linux\")]\n        {\n            linux_browser_args.push_str(\" --ignore-certificate-errors\");\n        }\n\n        #[cfg(target_os = \"macos\")]\n        {\n            window_builder = window_builder.additional_browser_args(\"--ignore-certificate-errors\");\n        }\n    }\n\n    if window_config.enable_wasm {\n        #[cfg(target_os = \"windows\")]\n        {\n            windows_browser_args.push_str(\" --enable-features=SharedArrayBuffer\");\n            windows_browser_args.push_str(\" --enable-unsafe-webgpu\");\n        }\n\n        #[cfg(target_os = \"linux\")]\n        {\n            linux_browser_args.push_str(\" --enable-features=SharedArrayBuffer\");\n            linux_browser_args.push_str(\" --enable-unsafe-webgpu\");\n        }\n\n        #[cfg(target_os = \"macos\")]\n        {\n            window_builder = window_builder\n                .additional_browser_args(\"--enable-features=SharedArrayBuffer\")\n                .additional_browser_args(\"--enable-unsafe-webgpu\");\n        }\n    }\n\n    let mut parsed_proxy_url: Option<Url> = None;\n\n    // Platform-specific configuration must be set before proxy on Windows/Linux\n    #[cfg(target_os = \"macos\")]\n    {\n        let title_bar_style = if window_config.hide_title_bar {\n            TitleBarStyle::Overlay\n        } else {\n            TitleBarStyle::Visible\n        };\n        window_builder = window_builder.title_bar_style(title_bar_style);\n\n        // Default to following system theme (None), only force dark when explicitly set\n        let theme = if window_config.dark_mode {\n            Some(Theme::Dark)\n        } else {\n            None // Follow system theme\n        };\n        window_builder = window_builder.theme(theme);\n    }\n\n    // Windows and Linux: set data_directory before proxy_url\n    #[cfg(not(target_os = \"macos\"))]\n    {\n        window_builder = window_builder.data_directory(_data_dir).theme(None);\n\n        if !config.proxy_url.is_empty() {\n            if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {\n                parsed_proxy_url = Some(proxy_url.clone());\n                #[cfg(target_os = \"windows\")]\n                {\n                    if let Some(arg) = build_proxy_browser_arg(&proxy_url) {\n                        windows_browser_args.push(' ');\n                        windows_browser_args.push_str(&arg);\n                    }\n                }\n            }\n        }\n\n        #[cfg(target_os = \"windows\")]\n        {\n            window_builder = window_builder.additional_browser_args(&windows_browser_args);\n        }\n\n        #[cfg(target_os = \"linux\")]\n        {\n            window_builder = window_builder.additional_browser_args(&linux_browser_args);\n        }\n    }\n\n    // Set proxy after platform-specific configs (required for Windows/Linux)\n    if parsed_proxy_url.is_none() && !config.proxy_url.is_empty() {\n        if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {\n            parsed_proxy_url = Some(proxy_url);\n        }\n    }\n\n    if let Some(proxy_url) = parsed_proxy_url {\n        window_builder = window_builder.proxy_url(proxy_url);\n        #[cfg(debug_assertions)]\n        println!(\"Proxy configured: {}\", config.proxy_url);\n    }\n\n    if let Some(features) = new_window_features {\n        window_builder = window_builder.window_features(features).focused(true);\n    }\n\n    // Allow navigation to OAuth/authentication domains\n    window_builder = window_builder.on_navigation(|url| {\n        let url_str = url.as_str();\n\n        // Always allow same-origin navigation\n        if url_str.starts_with(\"http://localhost\") || url_str.starts_with(\"http://127.0.0.1\") {\n            return true;\n        }\n\n        // Check for OAuth/authentication domains\n        let auth_patterns = [\n            \"accounts.google.com\",\n            \"login.microsoftonline.com\",\n            \"github.com/login\",\n            \"appleid.apple.com\",\n            \"facebook.com\",\n            \"twitter.com\",\n        ];\n\n        let auth_paths = [\"/oauth/\", \"/auth/\", \"/authorize\", \"/login\"];\n\n        // Allow if matches auth patterns\n        for pattern in &auth_patterns {\n            if url_str.contains(pattern) {\n                #[cfg(debug_assertions)]\n                println!(\"Allowing OAuth navigation to: {}\", url_str);\n                return true;\n            }\n        }\n\n        for path in &auth_paths {\n            if url_str.contains(path) {\n                #[cfg(debug_assertions)]\n                println!(\"Allowing auth path navigation to: {}\", url_str);\n                return true;\n            }\n        }\n\n        // Allow all other navigation by default\n        true\n    });\n\n    window_builder.build()\n}\n"
  },
  {
    "path": "src-tauri/src/inject/auth.js",
    "content": "// OAuth and Authentication Logic\n\n// Check if URL matches OAuth/authentication patterns\nfunction matchesAuthUrl(url, baseUrl = window.location.href) {\n  try {\n    const urlObj = new URL(url, baseUrl);\n    const hostname = urlObj.hostname.toLowerCase();\n    const pathname = urlObj.pathname.toLowerCase();\n    const fullUrl = urlObj.href.toLowerCase();\n\n    // Common OAuth providers and paths\n    const oauthPatterns = [\n      /accounts\\.google\\.com/,\n      /accounts\\.google\\.[a-z]+/,\n      /login\\.microsoftonline\\.com/,\n      /github\\.com\\/login/,\n      /facebook\\.com\\/.*\\/dialog/,\n      /twitter\\.com\\/oauth/,\n      /appleid\\.apple\\.com/,\n      /\\/oauth\\//,\n      /\\/auth\\//,\n      /\\/authorize/,\n      /\\/login\\/oauth/,\n      /\\/signin/,\n      /\\/login/,\n      /servicelogin/,\n      /\\/o\\/oauth2/,\n    ];\n\n    const isMatch = oauthPatterns.some(\n      (pattern) =>\n        pattern.test(hostname) ||\n        pattern.test(pathname) ||\n        pattern.test(fullUrl),\n    );\n\n    if (isMatch) {\n      console.log(\"[Pake] OAuth URL detected:\", url);\n    }\n\n    return isMatch;\n  } catch (e) {\n    return false;\n  }\n}\n\n// Check if URL is an OAuth/authentication link\nfunction isAuthLink(url) {\n  return matchesAuthUrl(url);\n}\n\n// Check if this is an OAuth/authentication popup\nfunction isAuthPopup(url, name) {\n  // Check for known authentication window names\n  const authWindowNames = [\n    \"AppleAuthentication\",\n    \"oauth2\",\n    \"oauth\",\n    \"google-auth\",\n    \"auth-popup\",\n    \"signin\",\n    \"login\",\n  ];\n\n  if (authWindowNames.includes(name)) {\n    return true;\n  }\n\n  return matchesAuthUrl(url);\n}\n\n// Export functions to global scope\nwindow.matchesAuthUrl = matchesAuthUrl;\nwindow.isAuthLink = isAuthLink;\nwindow.isAuthPopup = isAuthPopup;\n"
  },
  {
    "path": "src-tauri/src/inject/component.js",
    "content": "document.addEventListener(\"DOMContentLoaded\", () => {\n  // Toast\n  function pakeToast(msg) {\n    const m = document.createElement(\"div\");\n    m.innerHTML = msg;\n    m.style.cssText =\n      \"max-width:60%;min-width: 80px;padding:0 12px;height: 32px;color: rgb(255, 255, 255);line-height: 32px;text-align: center;border-radius: 8px;position: fixed; bottom:24px;right: 28px;z-index: 999999;background: rgba(0, 0, 0,.8);font-size: 13px;\";\n    document.body.appendChild(m);\n    setTimeout(function () {\n      const d = 0.5;\n      m.style.transition =\n        \"transform \" + d + \"s ease-in, opacity \" + d + \"s ease-in\";\n      m.style.opacity = \"0\";\n      setTimeout(function () {\n        document.body.removeChild(m);\n      }, d * 1000);\n    }, 3000);\n  }\n\n  window.pakeToast = pakeToast;\n});\n\n// Polyfill for HTML5 Fullscreen API in Tauri webview\n// This bridges the HTML5 Fullscreen API to Tauri's native window fullscreen\n// Works for all video sites (YouTube, Vimeo, Bilibili, etc.)\n(function () {\n  if (window.__PAKE_FULLSCREEN_POLYFILL__) return;\n  window.__PAKE_FULLSCREEN_POLYFILL__ = true;\n\n  function initFullscreenPolyfill() {\n    if (!window.__TAURI__ || !document.head) {\n      setTimeout(initFullscreenPolyfill, 100);\n      return;\n    }\n\n    const appWindow = window.__TAURI__.window.getCurrentWindow();\n    let fullscreenElement = null;\n    let actualFullscreenElement = null;\n    let originalStyles = null;\n    let originalParent = null;\n    let originalNextSibling = null;\n    let wasInBody = false;\n    let monitorId = null;\n\n    // Inject fullscreen styles\n    if (!document.getElementById(\"pake-fullscreen-style\")) {\n      const styleEl = document.createElement(\"style\");\n      styleEl.id = \"pake-fullscreen-style\";\n      styleEl.textContent = `\n      body.pake-fullscreen-active {\n        overflow: hidden !important;\n      }\n      .pake-fullscreen-element {\n        position: fixed !important;\n        top: 0 !important;\n        left: 0 !important;\n        width: 100vw !important;\n        height: 100vh !important;\n        max-width: 100vw !important;\n        max-height: 100vh !important;\n        margin: 0 !important;\n        padding: 0 !important;\n        z-index: 2147483647 !important;\n        background: #000 !important;\n        object-fit: contain !important;\n      }\n      .pake-fullscreen-element video {\n        width: 100% !important;\n        height: 100% !important;\n        object-fit: contain !important;\n      }\n    `;\n      document.head.appendChild(styleEl);\n    }\n\n    function startFullscreenMonitor() {\n      if (monitorId) return;\n      monitorId = setInterval(() => {\n        appWindow\n          .isFullscreen()\n          .then((isFullscreen) => {\n            if (fullscreenElement && !isFullscreen) {\n              exitFullscreen();\n            }\n          })\n          .catch(() => {});\n      }, 500);\n    }\n\n    function stopFullscreenMonitor() {\n      if (!monitorId) return;\n      clearInterval(monitorId);\n      monitorId = null;\n    }\n\n    // Find the actual video element\n    function findMediaElement() {\n      const videos = document.querySelectorAll(\"video\");\n      if (videos.length > 0) {\n        let largestVideo = videos[0];\n        let maxArea = 0;\n        videos.forEach((video) => {\n          const rect = video.getBoundingClientRect();\n          const area = rect.width * rect.height;\n          if (area > maxArea || !video.paused) {\n            maxArea = area;\n            largestVideo = video;\n          }\n        });\n        return largestVideo;\n      }\n      return null;\n    }\n\n    // Enter fullscreen\n    function enterFullscreen(element) {\n      fullscreenElement = element;\n\n      // If html/body element, find the video instead\n      let targetElement = element;\n      if (element === document.documentElement || element === document.body) {\n        const mediaElement = findMediaElement();\n        if (mediaElement) {\n          targetElement = mediaElement;\n          actualFullscreenElement = mediaElement;\n        } else {\n          actualFullscreenElement = element;\n        }\n      } else {\n        actualFullscreenElement = element;\n      }\n\n      // Save original state\n      originalStyles = {\n        position: targetElement.style.position,\n        top: targetElement.style.top,\n        left: targetElement.style.left,\n        width: targetElement.style.width,\n        height: targetElement.style.height,\n        maxWidth: targetElement.style.maxWidth,\n        maxHeight: targetElement.style.maxHeight,\n        margin: targetElement.style.margin,\n        padding: targetElement.style.padding,\n        zIndex: targetElement.style.zIndex,\n        background: targetElement.style.background,\n        objectFit: targetElement.style.objectFit,\n      };\n\n      wasInBody = targetElement.parentNode === document.body;\n      if (!wasInBody) {\n        originalParent = targetElement.parentNode;\n        originalNextSibling = targetElement.nextSibling;\n      }\n\n      // Apply fullscreen\n      targetElement.classList.add(\"pake-fullscreen-element\");\n      document.body.classList.add(\"pake-fullscreen-active\");\n\n      if (!wasInBody) {\n        document.body.appendChild(targetElement);\n      }\n\n      // Fullscreen window\n      appWindow.setFullscreen(true).then(() => {\n        startFullscreenMonitor();\n        const event = new Event(\"fullscreenchange\", { bubbles: true });\n        document.dispatchEvent(event);\n        element.dispatchEvent(event);\n\n        const webkitEvent = new Event(\"webkitfullscreenchange\", {\n          bubbles: true,\n        });\n        document.dispatchEvent(webkitEvent);\n        element.dispatchEvent(webkitEvent);\n      });\n\n      return Promise.resolve();\n    }\n\n    // Exit fullscreen\n    function exitFullscreen() {\n      if (!fullscreenElement) {\n        return Promise.resolve();\n      }\n\n      stopFullscreenMonitor();\n\n      const exitingElement = fullscreenElement;\n      const targetElement = actualFullscreenElement;\n\n      // Restore styles and position\n      targetElement.classList.remove(\"pake-fullscreen-element\");\n      document.body.classList.remove(\"pake-fullscreen-active\");\n\n      if (originalStyles) {\n        Object.keys(originalStyles).forEach((key) => {\n          targetElement.style[key] = originalStyles[key];\n        });\n      }\n\n      if (!wasInBody && originalParent) {\n        if (\n          originalNextSibling &&\n          originalNextSibling.parentNode === originalParent\n        ) {\n          originalParent.insertBefore(targetElement, originalNextSibling);\n        } else if (originalParent.isConnected) {\n          originalParent.appendChild(targetElement);\n        }\n      }\n\n      // Reset state\n      fullscreenElement = null;\n      actualFullscreenElement = null;\n      originalStyles = null;\n      originalParent = null;\n      originalNextSibling = null;\n      wasInBody = false;\n\n      // Exit window fullscreen\n      return appWindow.setFullscreen(false).then(() => {\n        const event = new Event(\"fullscreenchange\", { bubbles: true });\n        document.dispatchEvent(event);\n        exitingElement.dispatchEvent(event);\n\n        const webkitEvent = new Event(\"webkitfullscreenchange\", {\n          bubbles: true,\n        });\n        document.dispatchEvent(webkitEvent);\n        exitingElement.dispatchEvent(webkitEvent);\n      });\n    }\n\n    // Override fullscreenEnabled\n    Object.defineProperty(document, \"fullscreenEnabled\", {\n      get: () => true,\n      configurable: true,\n    });\n    Object.defineProperty(document, \"webkitFullscreenEnabled\", {\n      get: () => true,\n      configurable: true,\n    });\n\n    // Override fullscreenElement\n    Object.defineProperty(document, \"fullscreenElement\", {\n      get: () => fullscreenElement,\n      configurable: true,\n    });\n    Object.defineProperty(document, \"webkitFullscreenElement\", {\n      get: () => fullscreenElement,\n      configurable: true,\n    });\n    Object.defineProperty(document, \"webkitCurrentFullScreenElement\", {\n      get: () => fullscreenElement,\n      configurable: true,\n    });\n\n    // Override requestFullscreen\n    Element.prototype.requestFullscreen = function () {\n      return enterFullscreen(this);\n    };\n    Element.prototype.webkitRequestFullscreen = function () {\n      return enterFullscreen(this);\n    };\n    Element.prototype.webkitRequestFullScreen = function () {\n      return enterFullscreen(this);\n    };\n\n    // Override exitFullscreen\n    document.exitFullscreen = exitFullscreen;\n    document.webkitExitFullscreen = exitFullscreen;\n    document.webkitCancelFullScreen = exitFullscreen;\n\n    // Handle Escape key\n    document.addEventListener(\n      \"keydown\",\n      (e) => {\n        if (e.key === \"Escape\" && fullscreenElement) {\n          exitFullscreen();\n        }\n      },\n      true,\n    );\n  }\n\n  initFullscreenPolyfill();\n})();\n"
  },
  {
    "path": "src-tauri/src/inject/custom.js",
    "content": ""
  },
  {
    "path": "src-tauri/src/inject/event.js",
    "content": "const shortcuts = {\n  \"[\": () => window.history.back(),\n  \"]\": () => window.history.forward(),\n  \"-\": () => zoomOut(),\n  \"=\": () => zoomIn(),\n  \"+\": () => zoomIn(),\n  0: () => setZoom(\"100%\"),\n  r: () => window.location.reload(),\n  ArrowUp: () => scrollTo(0, 0),\n  ArrowDown: () => scrollTo(0, document.body.scrollHeight),\n};\n\nfunction setZoom(zoom) {\n  const html = document.getElementsByTagName(\"html\")[0];\n  const body = document.body;\n  const zoomValue = parseFloat(zoom) / 100;\n  const isWindows = /windows/i.test(navigator.userAgent);\n\n  if (isWindows) {\n    body.style.transform = `scale(${zoomValue})`;\n    body.style.transformOrigin = \"top left\";\n    body.style.width = `${100 / zoomValue}%`;\n    body.style.height = `${100 / zoomValue}%`;\n  } else {\n    html.style.zoom = zoom;\n  }\n\n  window.localStorage.setItem(\"htmlZoom\", zoom);\n}\n\nfunction zoomCommon(zoomChange) {\n  const currentZoom = window.localStorage.getItem(\"htmlZoom\") || \"100%\";\n  setZoom(zoomChange(currentZoom));\n}\n\nfunction zoomIn() {\n  zoomCommon((currentZoom) => `${Math.min(parseInt(currentZoom) + 10, 200)}%`);\n}\n\nfunction zoomOut() {\n  zoomCommon((currentZoom) => `${Math.max(parseInt(currentZoom) - 10, 30)}%`);\n}\n\nlet pasteAsPlainTextPending = false;\n\nfunction triggerPasteAsPlainText() {\n  pasteAsPlainTextPending = true;\n  document.execCommand(\"paste\");\n  setTimeout(() => {\n    pasteAsPlainTextPending = false;\n  }, 100);\n}\n\nfunction handleShortcut(event) {\n  if (shortcuts[event.key]) {\n    event.preventDefault();\n    shortcuts[event.key]();\n  }\n}\n\nconst DOWNLOADABLE_FILE_EXTENSIONS = {\n  documents: [\n    \"pdf\",\n    \"doc\",\n    \"docx\",\n    \"xls\",\n    \"xlsx\",\n    \"ppt\",\n    \"pptx\",\n    \"txt\",\n    \"rtf\",\n    \"odt\",\n    \"ods\",\n    \"odp\",\n    \"pages\",\n    \"numbers\",\n    \"key\",\n    \"epub\",\n    \"mobi\",\n  ],\n  archives: [\n    \"zip\",\n    \"rar\",\n    \"7z\",\n    \"tar\",\n    \"gz\",\n    \"gzip\",\n    \"bz2\",\n    \"xz\",\n    \"lzma\",\n    \"deb\",\n    \"rpm\",\n    \"pkg\",\n    \"msi\",\n    \"exe\",\n    \"dmg\",\n    \"apk\",\n    \"ipa\",\n  ],\n  data: [\n    \"json\",\n    \"xml\",\n    \"csv\",\n    \"sql\",\n    \"db\",\n    \"sqlite\",\n    \"yaml\",\n    \"yml\",\n    \"toml\",\n    \"ini\",\n    \"cfg\",\n    \"conf\",\n    \"log\",\n  ],\n  code: [\n    \"js\",\n    \"ts\",\n    \"jsx\",\n    \"tsx\",\n    \"css\",\n    \"scss\",\n    \"sass\",\n    \"less\",\n    \"sh\",\n    \"bat\",\n    \"ps1\",\n  ],\n  fonts: [\"ttf\", \"otf\", \"woff\", \"woff2\", \"eot\"],\n  design: [\"ai\", \"psd\", \"sketch\", \"fig\", \"xd\"],\n  system: [\n    \"iso\",\n    \"img\",\n    \"bin\",\n    \"torrent\",\n    \"jar\",\n    \"war\",\n    \"indd\",\n    \"fla\",\n    \"swf\",\n    \"raw\",\n  ],\n};\n\nconst ALL_DOWNLOADABLE_EXTENSIONS = Object.values(\n  DOWNLOADABLE_FILE_EXTENSIONS,\n).flat();\n\nconst PREVIEWABLE_MEDIA_EXTENSIONS = [\n  \"png\",\n  \"jpg\",\n  \"jpeg\",\n  \"gif\",\n  \"webp\",\n  \"svg\",\n  \"bmp\",\n  \"tiff\",\n  \"tif\",\n  \"avif\",\n  \"heic\",\n  \"heif\",\n  \"mp4\",\n  \"webm\",\n  \"mov\",\n  \"m4v\",\n  \"mkv\",\n  \"avi\",\n  \"ogv\",\n  \"mp3\",\n  \"wav\",\n  \"ogg\",\n  \"flac\",\n  \"aac\",\n  \"m4a\",\n];\n\nconst DOWNLOAD_PATH_PATTERNS = [\n  \"/download/\",\n  \"/files/\",\n  \"/attachments/\",\n  \"/assets/\",\n  \"/releases/\",\n  \"/dist/\",\n];\n\n// Language detection utilities\nfunction getUserLanguage() {\n  return navigator.language || navigator.userLanguage;\n}\n\nfunction isChineseLanguage(language = getUserLanguage()) {\n  return (\n    language &&\n    (language.startsWith(\"zh\") ||\n      language.includes(\"CN\") ||\n      language.includes(\"TW\") ||\n      language.includes(\"HK\"))\n  );\n}\n\n// User notification helper\nfunction showDownloadError(filename) {\n  const isChinese = isChineseLanguage();\n  const message = isChinese\n    ? `下载失败: ${filename}`\n    : `Download failed: ${filename}`;\n\n  if (window.Notification && Notification.permission === \"granted\") {\n    new Notification(isChinese ? \"下载错误\" : \"Download Error\", {\n      body: message,\n    });\n  } else {\n    console.error(message);\n  }\n}\n\nfunction getExtension(url) {\n  try {\n    const pathname = new URL(url).pathname.toLowerCase();\n    const extensionIndex = pathname.lastIndexOf(\".\");\n    return extensionIndex > -1 ? pathname.slice(extensionIndex + 1) : \"\";\n  } catch (e) {\n    return \"\";\n  }\n}\n\nfunction isPreviewableMedia(url) {\n  const extension = getExtension(url);\n  return PREVIEWABLE_MEDIA_EXTENSIONS.includes(extension);\n}\n\n// Unified file detection - replaces both isDownloadLink and isFileLink\nfunction isDownloadableFile(url) {\n  try {\n    const extension = getExtension(url);\n    if (PREVIEWABLE_MEDIA_EXTENSIONS.includes(extension)) {\n      return false;\n    }\n\n    const urlObj = new URL(url);\n    const hasDownloadHints =\n      urlObj.searchParams.has(\"download\") ||\n      urlObj.searchParams.has(\"attachment\");\n\n    if (hasDownloadHints) {\n      return true;\n    }\n\n    return (\n      ALL_DOWNLOADABLE_EXTENSIONS.includes(extension) ||\n      DOWNLOAD_PATH_PATTERNS.some((pattern) =>\n        urlObj.pathname.toLowerCase().includes(pattern),\n      )\n    );\n  } catch (e) {\n    return false;\n  }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n  const tauri = window.__TAURI__;\n  const appWindow = tauri.window.getCurrentWindow();\n  const invoke = tauri.core.invoke;\n  const pakeConfig = window[\"pakeConfig\"] || {};\n  const forceInternalNavigation = pakeConfig.force_internal_navigation === true;\n  const internalUrlRegex = pakeConfig.internal_url_regex || \"\";\n  let internalUrlPattern = null;\n  if (internalUrlRegex) {\n    try {\n      internalUrlPattern = new RegExp(internalUrlRegex);\n    } catch (e) {\n      console.error(\"[Pake] Invalid internal_url_regex pattern:\", e);\n    }\n  }\n\n  if (!document.getElementById(\"pake-top-dom\")) {\n    const topDom = document.createElement(\"div\");\n    topDom.id = \"pake-top-dom\";\n    document.body.appendChild(topDom);\n  }\n\n  const domEl = document.getElementById(\"pake-top-dom\");\n\n  domEl.addEventListener(\"touchstart\", () => {\n    appWindow.startDragging();\n  });\n\n  domEl.addEventListener(\"mousedown\", (e) => {\n    e.preventDefault();\n    if (e.buttons === 1 && e.detail !== 2) {\n      appWindow.startDragging();\n    }\n  });\n\n  domEl.addEventListener(\"dblclick\", () => {\n    appWindow.isFullscreen().then((fullscreen) => {\n      appWindow.setFullscreen(!fullscreen);\n    });\n  });\n\n  if (window[\"pakeConfig\"]?.disabled_web_shortcuts !== true) {\n    document.addEventListener(\"keyup\", (event) => {\n      if (/windows|linux/i.test(navigator.userAgent) && event.ctrlKey) {\n        handleShortcut(event);\n      }\n      if (/macintosh|mac os x/i.test(navigator.userAgent) && event.metaKey) {\n        handleShortcut(event);\n      }\n    });\n  }\n\n  document.addEventListener(\n    \"paste\",\n    (event) => {\n      if (pasteAsPlainTextPending) {\n        event.preventDefault();\n        event.stopImmediatePropagation();\n\n        const text = event.clipboardData?.getData(\"text/plain\") || \"\";\n        if (text) {\n          document.execCommand(\"insertText\", false, text);\n        }\n      }\n    },\n    true,\n  );\n\n  // Collect blob urls to blob by overriding window.URL.createObjectURL\n  function collectUrlToBlobs() {\n    const backupCreateObjectURL = window.URL.createObjectURL;\n    window.blobToUrlCaches = new Map();\n    window.URL.createObjectURL = (blob) => {\n      const url = backupCreateObjectURL.call(window.URL, blob);\n      window.blobToUrlCaches.set(url, blob);\n      return url;\n    };\n  }\n\n  function convertBlobUrlToBinary(blobUrl) {\n    return new Promise((resolve) => {\n      const blob = window.blobToUrlCaches.get(blobUrl);\n      const reader = new FileReader();\n\n      reader.readAsArrayBuffer(blob);\n      reader.onload = () => {\n        resolve(Array.from(new Uint8Array(reader.result)));\n      };\n    });\n  }\n\n  function downloadFromDataUri(dataURI, filename) {\n    try {\n      const byteString = atob(dataURI.split(\",\")[1]);\n      // write the bytes of the string to an ArrayBuffer\n      const bufferArray = new ArrayBuffer(byteString.length);\n\n      // create a view into the buffer\n      const binary = new Uint8Array(bufferArray);\n\n      // set the bytes of the buffer to the correct values\n      for (let i = 0; i < byteString.length; i++) {\n        binary[i] = byteString.charCodeAt(i);\n      }\n\n      // write the ArrayBuffer to a binary, and you're done\n      const userLanguage = getUserLanguage();\n      invoke(\"download_file_by_binary\", {\n        params: {\n          filename,\n          binary: Array.from(binary),\n          language: userLanguage,\n        },\n      }).catch((error) => {\n        console.error(\"Failed to download data URI file:\", filename, error);\n        showDownloadError(filename);\n      });\n    } catch (error) {\n      console.error(\"Failed to process data URI:\", dataURI, error);\n      showDownloadError(filename || \"file\");\n    }\n  }\n\n  function downloadFromBlobUrl(blobUrl, filename) {\n    convertBlobUrlToBinary(blobUrl)\n      .then((binary) => {\n        const userLanguage = getUserLanguage();\n        invoke(\"download_file_by_binary\", {\n          params: {\n            filename,\n            binary,\n            language: userLanguage,\n          },\n        }).catch((error) => {\n          console.error(\"Failed to download blob file:\", filename, error);\n          showDownloadError(filename);\n        });\n      })\n      .catch((error) => {\n        console.error(\"Failed to convert blob to binary:\", blobUrl, error);\n        showDownloadError(filename);\n      });\n  }\n\n  // detect blob download by createElement(\"a\")\n  function detectDownloadByCreateAnchor() {\n    const createEle = document.createElement;\n    document.createElement = (el) => {\n      if (el !== \"a\") return createEle.call(document, el);\n      const anchorEle = createEle.call(document, el);\n\n      // use addEventListener to avoid overriding the original click event.\n      anchorEle.addEventListener(\n        \"click\",\n        (e) => {\n          const url = anchorEle.href;\n          const filename = anchorEle.download || getFilenameFromUrl(url);\n          if (window.blobToUrlCaches.has(url)) {\n            e.preventDefault();\n            e.stopImmediatePropagation();\n            downloadFromBlobUrl(url, filename);\n            // case: download from dataURL -> convert dataURL ->\n          } else if (url.startsWith(\"data:\")) {\n            e.preventDefault();\n            e.stopImmediatePropagation();\n            downloadFromDataUri(url, filename);\n          }\n        },\n        true,\n      );\n\n      return anchorEle;\n    };\n  }\n\n  // process special download protocol['data:','blob:']\n  const isSpecialDownload = (url) =>\n    [\"blob\", \"data\"].some((protocol) => url.startsWith(protocol));\n\n  const isDownloadRequired = (url, anchorElement, e) =>\n    anchorElement.download || e.metaKey || e.ctrlKey || isDownloadableFile(url);\n\n  const handleExternalLink = (url) => {\n    // Don't try to open blob: or data: URLs with shell\n    if (isSpecialDownload(url)) {\n      console.warn(\"Cannot open special URL with shell:\", url);\n      return;\n    }\n\n    invoke(\"plugin:shell|open\", {\n      path: url,\n    }).catch((error) => {\n      console.error(\"Failed to open URL with shell:\", url, error);\n    });\n  };\n\n  // Check if URL belongs to the same domain (including subdomains)\n  const isSameDomain = (url) => {\n    try {\n      const linkUrl = new URL(url);\n      const currentUrl = new URL(window.location.href);\n\n      if (linkUrl.hostname === currentUrl.hostname) return true;\n\n      // Extract root domain (e.g., bilibili.com from www.bilibili.com)\n      const getRootDomain = (hostname) => {\n        const parts = hostname.split(\".\");\n        return parts.length >= 2 ? parts.slice(-2).join(\".\") : hostname;\n      };\n\n      return (\n        getRootDomain(currentUrl.hostname) === getRootDomain(linkUrl.hostname)\n      );\n    } catch (e) {\n      return false;\n    }\n  };\n\n  // Check if URL should be treated as internal based on regex pattern or domain\n  const isInternalUrl = (url) => {\n    // If regex pattern is configured, use it as the primary check\n    if (internalUrlPattern) {\n      try {\n        return internalUrlPattern.test(url);\n      } catch (e) {\n        console.error(\"[Pake] Error testing internal_url_regex:\", e);\n        // Fall back to domain check on error\n        return isSameDomain(url);\n      }\n    }\n    // Default to domain-based check\n    return isSameDomain(url);\n  };\n\n  const detectAnchorElementClick = (e) => {\n    // Safety check: ensure e.target exists and is an Element with closest method\n    if (!e.target || typeof e.target.closest !== \"function\") {\n      return;\n    }\n    const anchorElement = e.target.closest(\"a\");\n\n    if (anchorElement && anchorElement.href) {\n      const target = anchorElement.target;\n      const hrefUrl = new URL(anchorElement.href);\n      const absoluteUrl = hrefUrl.href;\n      let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl);\n\n      // Keep OAuth/authentication flows inside the app when popup support is enabled.\n      if (window.isAuthLink(absoluteUrl)) {\n        console.log(\"[Pake] Handling OAuth navigation in-app:\", absoluteUrl);\n\n        if (window.pakeConfig?.new_window) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n\n          const authWindow = originalWindowOpen.call(\n            window,\n            absoluteUrl,\n            \"_blank\",\n            \"width=1200,height=800,scrollbars=yes,resizable=yes\",\n          );\n\n          if (!authWindow) {\n            window.location.href = absoluteUrl;\n          }\n        }\n\n        return;\n      }\n\n      // Handle _blank links: same domain navigates in-app, cross-domain opens new window\n      if (target === \"_blank\") {\n        if (forceInternalNavigation) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          window.location.href = absoluteUrl;\n          return;\n        }\n\n        if (isInternalUrl(absoluteUrl)) {\n          // For internal links (based on regex or domain), let the browser handle it naturally\n          return;\n        }\n\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        const newWindow = originalWindowOpen.call(\n          window,\n          absoluteUrl,\n          \"_blank\",\n          \"width=1200,height=800,scrollbars=yes,resizable=yes\",\n        );\n        if (!newWindow) handleExternalLink(absoluteUrl);\n        return;\n      }\n\n      if (target === \"_new\") {\n        if (forceInternalNavigation) {\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          window.location.href = absoluteUrl;\n          return;\n        }\n\n        e.preventDefault();\n        handleExternalLink(absoluteUrl);\n        return;\n      }\n\n      // Process download links for Rust to handle.\n      if (\n        isDownloadRequired(absoluteUrl, anchorElement, e) &&\n        !isSpecialDownload(absoluteUrl)\n      ) {\n        e.preventDefault();\n        e.stopImmediatePropagation();\n        const userLanguage = getUserLanguage();\n        invoke(\"download_file\", {\n          params: { url: absoluteUrl, filename, language: userLanguage },\n        });\n        return;\n      }\n\n      // Handle regular links: internal URLs allow normal navigation, external opens new window\n      if (!target || target === \"_self\") {\n        // Optimization: Allow previewable media to be handled by the app/browser directly\n        // This fixes issues where CDN links are treated as external\n        if (isPreviewableMedia(absoluteUrl)) {\n          return;\n        }\n\n        if (!isInternalUrl(absoluteUrl)) {\n          if (forceInternalNavigation) {\n            return;\n          }\n\n          e.preventDefault();\n          e.stopImmediatePropagation();\n          const newWindow = originalWindowOpen.call(\n            window,\n            absoluteUrl,\n            \"_blank\",\n            \"width=1200,height=800,scrollbars=yes,resizable=yes\",\n          );\n          if (!newWindow) handleExternalLink(absoluteUrl);\n        }\n      }\n    }\n  };\n\n  // Prevent some special websites from executing in advance, before the click event is triggered.\n  document.addEventListener(\"click\", detectAnchorElementClick, true);\n\n  collectUrlToBlobs();\n  detectDownloadByCreateAnchor();\n\n  // Rewrite the window.open function.\n  const originalWindowOpen = window.open;\n  window.open = function (url, name, specs) {\n    // Allow authentication popups to open normally\n    if (window.isAuthPopup(url, name)) {\n      return originalWindowOpen.call(window, url, name, specs);\n    }\n\n    try {\n      const baseUrl = window.location.origin + window.location.pathname;\n      const hrefUrl = new URL(url, baseUrl);\n      const absoluteUrl = hrefUrl.href;\n\n      if (!isInternalUrl(absoluteUrl)) {\n        if (forceInternalNavigation) {\n          return originalWindowOpen.call(window, absoluteUrl, name, specs);\n        }\n\n        handleExternalLink(absoluteUrl);\n        return null;\n      }\n\n      return originalWindowOpen.call(window, absoluteUrl, name, specs);\n    } catch (error) {\n      return originalWindowOpen.call(window, url, name, specs);\n    }\n  };\n\n  // Set the default zoom, There are problems with Loop without using try-catch.\n  try {\n    setDefaultZoom();\n  } catch (e) {\n    console.log(e);\n  }\n\n  // Fix Chinese input method \"Enter\" on Safari\n  document.addEventListener(\n    \"keydown\",\n    (e) => {\n      if (e.key === \"Process\") e.stopPropagation();\n    },\n    true,\n  );\n\n  // Language detection and texts\n  const isChinese = isChineseLanguage();\n\n  const menuTexts = {\n    // Media operations\n    downloadImage: isChinese ? \"下载图片\" : \"Download Image\",\n    downloadVideo: isChinese ? \"下载视频\" : \"Download Video\",\n    downloadFile: isChinese ? \"下载文件\" : \"Download File\",\n    copyAddress: isChinese ? \"复制地址\" : \"Copy Address\",\n    openInBrowser: isChinese ? \"浏览器打开\" : \"Open in Browser\",\n  };\n\n  // Menu theme configuration\n  const MENU_THEMES = {\n    dark: {\n      menu: {\n        background: \"#2d2d2d\",\n        border: \"1px solid #404040\",\n        color: \"#ffffff\",\n        shadow: \"0 4px 16px rgba(0, 0, 0, 0.4)\",\n      },\n      item: {\n        divider: \"#404040\",\n        hoverBg: \"#404040\",\n      },\n    },\n    light: {\n      menu: {\n        background: \"#ffffff\",\n        border: \"1px solid #e0e0e0\",\n        color: \"#333333\",\n        shadow: \"0 4px 16px rgba(0, 0, 0, 0.15)\",\n      },\n      item: {\n        divider: \"#f0f0f0\",\n        hoverBg: \"#d0d0d0\",\n      },\n    },\n  };\n\n  // Theme detection and menu styles\n  function getTheme() {\n    const prefersDark = window.matchMedia(\n      \"(prefers-color-scheme: dark)\",\n    ).matches;\n    return prefersDark ? \"dark\" : \"light\";\n  }\n\n  function getMenuStyles(theme = getTheme()) {\n    return MENU_THEMES[theme] || MENU_THEMES.light;\n  }\n\n  // Menu configuration constants\n  const MENU_CONFIG = {\n    id: \"pake-context-menu\",\n    minWidth: \"120px\", // Compact width for better UX\n    borderRadius: \"6px\", // Slightly more rounded for modern look\n    fontSize: \"13px\",\n    zIndex: \"999999\",\n    fontFamily:\n      \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\",\n    // Menu item dimensions\n    itemPadding: \"8px 16px\", // Increased vertical padding for better comfort\n    itemLineHeight: \"1.2\",\n    itemBorderRadius: \"3px\", // Subtle rounded corners for menu items\n    itemTransition: \"background-color 0.1s ease\",\n  };\n\n  // Create custom context menu\n  function createContextMenu() {\n    const contextMenu = document.createElement(\"div\");\n    contextMenu.id = MENU_CONFIG.id;\n    const styles = getMenuStyles();\n\n    contextMenu.style.cssText = `\n      position: fixed;\n      background: ${styles.menu.background};\n      border: ${styles.menu.border};\n      border-radius: ${MENU_CONFIG.borderRadius};\n      box-shadow: ${styles.menu.shadow};\n      padding: 4px 0;\n      min-width: ${MENU_CONFIG.minWidth};\n      font-family: ${MENU_CONFIG.fontFamily};\n      font-size: ${MENU_CONFIG.fontSize};\n      color: ${styles.menu.color};\n      z-index: ${MENU_CONFIG.zIndex};\n      display: none;\n      user-select: none;\n    `;\n    document.body.appendChild(contextMenu);\n    return contextMenu;\n  }\n\n  function createMenuItem(text, onClick, divider = false) {\n    const item = document.createElement(\"div\");\n    const styles = getMenuStyles();\n\n    item.style.cssText = `\n      padding: ${MENU_CONFIG.itemPadding};\n      cursor: pointer;\n      user-select: none;\n      font-weight: 400;\n      line-height: ${MENU_CONFIG.itemLineHeight};\n      transition: ${MENU_CONFIG.itemTransition};\n      white-space: nowrap;\n      border-radius: ${MENU_CONFIG.itemBorderRadius};\n      margin: 2px 4px;\n      border-bottom: ${divider ? `1px solid ${styles.item.divider}` : \"none\"};\n    `;\n    item.textContent = text;\n\n    item.addEventListener(\"mouseenter\", () => {\n      item.style.backgroundColor = styles.item.hoverBg;\n    });\n\n    item.addEventListener(\"mouseleave\", () => {\n      item.style.backgroundColor = \"transparent\";\n    });\n\n    item.addEventListener(\"click\", (e) => {\n      e.preventDefault();\n      e.stopPropagation();\n      onClick();\n      hideContextMenu();\n    });\n\n    return item;\n  }\n\n  function showContextMenu(x, y, items) {\n    let contextMenu = document.getElementById(MENU_CONFIG.id);\n\n    // Always recreate menu to ensure theme is up-to-date\n    if (contextMenu) {\n      contextMenu.remove();\n    }\n    contextMenu = createContextMenu();\n\n    items.forEach((item) => {\n      contextMenu.appendChild(item);\n    });\n\n    contextMenu.style.left = x + \"px\";\n    contextMenu.style.top = y + \"px\";\n    contextMenu.style.display = \"block\";\n\n    // Adjust position if menu goes off screen\n    const rect = contextMenu.getBoundingClientRect();\n    if (rect.right > window.innerWidth) {\n      contextMenu.style.left = x - rect.width + \"px\";\n    }\n    if (rect.bottom > window.innerHeight) {\n      contextMenu.style.top = y - rect.height + \"px\";\n    }\n  }\n\n  function hideContextMenu() {\n    const contextMenu = document.getElementById(MENU_CONFIG.id);\n    if (contextMenu) {\n      contextMenu.style.display = \"none\";\n    }\n  }\n\n  function downloadImage(imageUrl) {\n    // Convert relative URLs to absolute\n    if (imageUrl.startsWith(\"/\")) {\n      imageUrl = window.location.origin + imageUrl;\n    } else if (imageUrl.startsWith(\"./\")) {\n      imageUrl = new URL(imageUrl, window.location.href).href;\n    } else if (\n      !imageUrl.startsWith(\"http\") &&\n      !imageUrl.startsWith(\"data:\") &&\n      !imageUrl.startsWith(\"blob:\")\n    ) {\n      imageUrl = new URL(imageUrl, window.location.href).href;\n    }\n\n    // Generate filename from URL\n    const filename = getFilenameFromUrl(imageUrl) || \"image\";\n\n    // Handle different URL types\n    if (imageUrl.startsWith(\"data:\")) {\n      downloadFromDataUri(imageUrl, filename);\n    } else if (imageUrl.startsWith(\"blob:\")) {\n      if (window.blobToUrlCaches && window.blobToUrlCaches.has(imageUrl)) {\n        downloadFromBlobUrl(imageUrl, filename);\n      }\n    } else {\n      // Regular HTTP(S) image\n      const userLanguage = getUserLanguage();\n      invoke(\"download_file\", {\n        params: {\n          url: imageUrl,\n          filename: filename,\n          language: userLanguage,\n        },\n      }).catch((error) => {\n        console.error(\"Failed to download image:\", filename, error);\n        showDownloadError(filename);\n      });\n    }\n  }\n\n  // Check if element is media (image or video)\n  function getMediaInfo(target) {\n    // Check for img tags\n    if (target.tagName.toLowerCase() === \"img\") {\n      return { isMedia: true, url: target.src, type: \"image\" };\n    }\n\n    // Check for video tags\n    if (target.tagName.toLowerCase() === \"video\") {\n      return {\n        isMedia: true,\n        url: target.src || target.currentSrc,\n        type: \"video\",\n      };\n    }\n\n    // Check for elements with background images\n    if (target.style && target.style.backgroundImage) {\n      const bgImage = target.style.backgroundImage;\n      const urlMatch = bgImage.match(/url\\([\"']?([^\"')]+)[\"']?\\)/);\n      if (urlMatch) {\n        return { isMedia: true, url: urlMatch[1], type: \"image\" };\n      }\n    }\n\n    // Check for parent elements with background images\n    const parentWithBg =\n      target && typeof target.closest === \"function\"\n        ? target.closest('[style*=\"background-image\"]')\n        : null;\n    if (parentWithBg) {\n      const bgImage = parentWithBg.style.backgroundImage;\n      const urlMatch = bgImage.match(/url\\([\"']?([^\"')]+)[\"']?\\)/);\n      if (urlMatch) {\n        return { isMedia: true, url: urlMatch[1], type: \"image\" };\n      }\n    }\n\n    return { isMedia: false, url: \"\", type: \"\" };\n  }\n\n  // Simplified menu builder\n  function buildMenuItems(type, data) {\n    const userLanguage = getUserLanguage();\n    const items = [];\n\n    switch (type) {\n      case \"media\":\n        const downloadText =\n          data.type === \"image\"\n            ? menuTexts.downloadImage\n            : menuTexts.downloadVideo;\n        items.push(\n          createMenuItem(downloadText, () => downloadImage(data.url)),\n          createMenuItem(menuTexts.copyAddress, () =>\n            navigator.clipboard.writeText(data.url),\n          ),\n          createMenuItem(menuTexts.openInBrowser, () =>\n            invoke(\"plugin:shell|open\", { path: data.url }),\n          ),\n        );\n        break;\n\n      case \"link\":\n        if (data.isFile) {\n          items.push(\n            createMenuItem(menuTexts.downloadFile, () => {\n              const filename = getFilenameFromUrl(data.url);\n              invoke(\"download_file\", {\n                params: { url: data.url, filename, language: userLanguage },\n              }).catch((error) => {\n                console.error(\"Failed to download file:\", filename, error);\n                showDownloadError(filename);\n              });\n            }),\n          );\n        }\n        items.push(\n          createMenuItem(menuTexts.copyAddress, () =>\n            navigator.clipboard.writeText(data.url),\n          ),\n          createMenuItem(menuTexts.openInBrowser, () =>\n            invoke(\"plugin:shell|open\", { path: data.url }),\n          ),\n        );\n        break;\n    }\n\n    return items;\n  }\n\n  // Handle right-click context menu\n  document.addEventListener(\n    \"contextmenu\",\n    function (event) {\n      const target = event.target;\n\n      // Check for media elements (images/videos)\n      const mediaInfo = getMediaInfo(target);\n\n      // Check for links (but not if it's media)\n      const linkElement =\n        target && typeof target.closest === \"function\"\n          ? target.closest(\"a\")\n          : null;\n      const isLink = linkElement && linkElement.href && !mediaInfo.isMedia;\n\n      // Only show custom menu for media or links\n      if (mediaInfo.isMedia || isLink) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        let menuItems = [];\n\n        if (mediaInfo.isMedia) {\n          menuItems = buildMenuItems(\"media\", mediaInfo);\n        } else if (isLink) {\n          const linkUrl = linkElement.href;\n          menuItems = buildMenuItems(\"link\", {\n            url: linkUrl,\n            isFile: isDownloadableFile(linkUrl),\n          });\n        }\n\n        showContextMenu(event.clientX, event.clientY, menuItems);\n      }\n      // For all other elements, let browser's default context menu handle it\n    },\n    true,\n  );\n\n  // Hide context menu when clicking elsewhere\n  document.addEventListener(\"click\", hideContextMenu);\n  document.addEventListener(\"keydown\", (e) => {\n    if (e.key === \"Escape\") {\n      hideContextMenu();\n    }\n  });\n});\n\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n  let permVal = \"granted\";\n  window.Notification = function (title, options) {\n    const { invoke } = window.__TAURI__.core;\n    const body = options?.body || \"\";\n    let icon = options?.icon || \"\";\n\n    // If the icon is a relative path, convert to full path using URI\n    if (icon.startsWith(\"/\")) {\n      icon = window.location.origin + icon;\n    }\n\n    invoke(\"send_notification\", {\n      params: {\n        title,\n        body,\n        icon,\n      },\n    });\n  };\n\n  window.Notification.requestPermission = async () => \"granted\";\n\n  Object.defineProperty(window.Notification, \"permission\", {\n    enumerable: true,\n    get: () => permVal,\n    set: (v) => {\n      permVal = v;\n    },\n  });\n});\n\nfunction setDefaultZoom() {\n  const htmlZoom = window.localStorage.getItem(\"htmlZoom\");\n  if (htmlZoom) {\n    setZoom(htmlZoom);\n  } else if (window.pakeConfig?.zoom && window.pakeConfig.zoom !== 100) {\n    setZoom(`${window.pakeConfig.zoom}%`);\n  }\n}\n\nfunction getFilenameFromUrl(url) {\n  try {\n    const urlPath = new URL(url).pathname;\n    let filename = urlPath.substring(urlPath.lastIndexOf(\"/\") + 1);\n\n    // If no filename or no extension, generate one\n    if (!filename || !filename.includes(\".\")) {\n      const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\n      // Detect image type from URL or data URI\n      if (url.startsWith(\"data:image/\")) {\n        const mimeType = url.substring(11, url.indexOf(\";\"));\n        filename = `image-${timestamp}.${mimeType}`;\n      } else {\n        // Default to common image extensions based on common patterns\n        if (url.includes(\"jpg\") || url.includes(\"jpeg\")) {\n          filename = `image-${timestamp}.jpg`;\n        } else if (url.includes(\"png\")) {\n          filename = `image-${timestamp}.png`;\n        } else if (url.includes(\"gif\")) {\n          filename = `image-${timestamp}.gif`;\n        } else if (url.includes(\"webp\")) {\n          filename = `image-${timestamp}.webp`;\n        } else if (url.includes(\"svg\")) {\n          filename = `image-${timestamp}.svg`;\n        } else {\n          filename = `image-${timestamp}.png`; // default\n        }\n      }\n    }\n\n    return filename;\n  } catch (e) {\n    // Fallback for invalid URLs\n    const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n    return `image-${timestamp}.png`;\n  }\n}\n"
  },
  {
    "path": "src-tauri/src/inject/style.js",
    "content": "window.addEventListener(\"DOMContentLoaded\", (_event) => {\n  // Customize and transform existing functions\n  const contentCSS = `\n    #page #footer-wrapper,\n    .drawing-board .toolbar .toolbar-action,\n    .c-swiper-container,\n    .download_entry,\n    .lang, .copyright,\n    .wwads-cn, .adsbygoogle,\n    #Bottom > div.content > div.inner,\n    #Rightbar .sep20:nth-of-type(5),\n    #Rightbar > div.box:nth-child(4),\n    #Main > div.box:nth-child(8) > div\n    #Wrapper > div.sep20,\n    #Main > div.box:nth-child(8),\n    #masthead-ad,\n    #app > header > div > div.menu,\n    #root > div > div.fixed.top-0.left-0.w-64.h-screen.p-10.pb-0.flex.flex-col.justify-between > div > div.space-y-4 > a:nth-child(3),\n    #app > div.layout > div.main-container > div.side-bar > li.divider,\n    #Rightbar > div:nth-child(6) > div.sidebar_compliance,\n    #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside > div > div > a.ChatPageFollowTwitterLink_followLink__Gl2tt,\n    #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside > div > div > a.Button_buttonBase__0QP_m.Button_primary__pIDjn.ChatPageDownloadLinks_downloadButton__amBRh,\n    #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside > div > div > section a[href*=\"/contact\"],\n    .dc04ec1d .c7f51894 .a1e75851, .a7f3a288 .b91228e4, .efe408db .a24007f4{\n      display: none !important;\n    }\n\n    #app > header .right .avatar.logged-in{\n      opacity: 0;\n      transition: opacity 0.3s;\n    }\n\n    #app > header .right .avatar.logged-in:hover{\n      opacity: 1;\n    }\n\n    html::-webkit-scrollbar {\n      display: none !important;\n    }\n\n    #__next .ChatPageSidebar_menuFooter__E1KTY,#__next > div.PageWithSidebarLayout_centeringDiv___L9br > div > aside > div > menu > section:nth-child(6) {\n      display: none;\n    }\n\n    #__next > div.overflow-hidden.w-full.h-full  .min-h-\\\\[20px\\\\].items-start.gap-4.whitespace-pre-wrap.break-words {\n      word-break: break-all;\n    }\n\n    #__next .PageWithSidebarLayout_mainSection__i1yOg {\n      width: 100%;\n      max-width: 1000px;\n    }\n\n    #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside{\n      min-width: 260px;\n    }\n\n    #__next > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div.relative.flex.h-full.max-w-full.flex-1.overflow-hidden > div > main > div.absolute.left-2.top-2.z-10.hidden.md\\\\:inline-block{\n      margin-top:20px;\n      margin-left: 10px;\n    }\n\n    .a7f3a288.f0d4f23d {\n      padding-top: 34px;\n    }\n\n    .ec92d1d3 {\n      padding-top: 48px;\n    }\n\n    .chakra-ui-light #app .chakra-heading,\n    .chakra-ui-dark #app .chakra-heading,\n    .chakra-ui-light #app .chakra-stack,\n    .chakra-ui-dark #app .chakra-stack,\n    .app-main .sidebar-mouse-in-out,\n    .chakra-modal__content-container .chakra-modal__header > div > div,\n    #__next > div.PageWithSidebarLayout_centeringDiv___L9br > section > header {\n      padding-top: 10px;\n    }\n\n    #__next .overflow-hidden>.hidden.bg-gray-900 span.rounded-md.bg-yellow-200 {\n      display: none;\n    }\n\n    #__next .absolute .px-3.pt-2.pb-3.text-center {\n      visibility: hidden;\n      padding-bottom: 4px;\n    }\n\n    #__next .h-full.w-full .text-center.text-xs.text-gray-600>span {\n      visibility: hidden;\n      height: 15px;\n    }\n\n    #__next > div.overflow-hidden.w-full.h-full.relative.flex > div.dark.hidden.flex-shrink-0.bg-gray-900.md\\\\:flex.md\\\\:w-\\\\[260px\\\\].md\\\\:flex-col > div > div > nav {\n      width: 100%;\n    }\n\n    .panel.give_me .nav_view {\n      top: 164px !important;\n    }\n\n    #Wrapper{\n      background-color: #F8F8F8 !important;\n      background-image:none !important;\n    }\n\n    #Top {\n      border-bottom: none;\n    }\n\n    #global > div.header-container.showSearchBoxOrHeaderFixed > header > div.right > div > div.dropdown-nav{\n      display: none;\n    }\n\n    #__next > div.AnnouncementWrapper_container__Z51yh > div > aside > div > div > menu > section:nth-child(4) > section, #__next > div.AnnouncementWrapper_container__Z51yh > div > aside > div > div > menu > section:nth-child(4){\n      display: none;\n    }\n\n    #react-root [data-testid=\"placementTracking\"] article,\n    #react-root a[href*=\"quick_promote_web\"],\n    #react-root [data-testid=\"AppTabBar_Explore_Link\"],\n    #react-root a[href*=\"/lists\"][role=\"link\"][aria-label],\n    #react-root a[href*=\"/i/communitynotes\"][role=\"link\"][aria-label],\n    #react-root a[role=\"link\"][aria-label=\"Communities\"],\n    #react-root a[href*=\"/i/verified-orgs-signup\"][role=\"link\"][aria-label] {\n      display: none !important;\n    }\n\n    #react-root [data-testid=\"DMDrawer\"],\n    #root > main > footer.justify-center.ease-in {\n      visibility: hidden !important;\n    }\n\n    #__next > div.overflow-hidden.w-full.h-full .absolute.bottom-0.left-0.w-full > div.text-center.text-xs {\n      visibility: hidden !important;\n      height: 0px !important;\n    }\n\n    #react-root [data-testid=\"primaryColumn\"] > div > div {\n      position: relative !important;\n    }\n\n    #react-root [data-testid=\"sidebarColumn\"] {\n      visibility: hidden !important;\n      width: 0 !important;\n      margin: 0 !important;\n      padding: 0 !important;\n      z-index: 1 !important;\n    }\n\n    @media only screen and (min-width: 1000px) {\n      #react-root main[role=\"main\"] {\n        align-items: center !important;\n        overflow-x: clip !important;\n      }\n\n      #react-root [data-testid=\"primaryColumn\"] {\n        width: 700px !important;\n        max-width: 700px !important;\n        margin: 0 auto !important;\n      }\n      #react-root [data-testid=\"primaryColumn\"] > div > div:last-child,\n      #react-root [data-testid=\"primaryColumn\"] > div > div:last-child div {\n        max-width: unset !important;\n      }\n\n      #react-root div[aria-label][role=\"group\"][id^=\"id__\"] {\n        margin-right: 81px !important;\n      }\n\n      #react-root header[role=\"banner\"] {\n        position: fixed !important;\n        left: 0 !important;\n      }\n\n      #react-root header[role=\"banner\"] > div > div > div {\n        justify-content: center !important;\n        padding-top: 0;\n        overflow-x: hidden;\n      }\n\n      #react-root form[role=\"search\"] > div:nth-child(1) > div {\n        background-color: transparent !important;\n      }\n\n      #react-root h1[role=\"heading\"] {\n        padding-top: 4px !important;\n      }\n\n      #react-root header[role=\"banner\"]\n        nav[role=\"navigation\"]\n        *\n        div[dir=\"auto\"]:not([aria-label])\n        > span,\n      #react-root [data-testid=\"SideNav_AccountSwitcher_Button\"] > div:not(:first-child) {\n        display: inline-block !important;\n        opacity: 0 !important;\n        transition: 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);\n      }\n      #react-root header[role=\"banner\"]\n        nav[role=\"navigation\"]:hover\n        *\n        div[dir=\"auto\"]:not([aria-label])\n        > span,\n      #react-root [data-testid=\"SideNav_AccountSwitcher_Button\"]:hover > div:not(:first-child) {\n        opacity: 1 !important;\n      }\n      #react-root header[role=\"banner\"] nav[role=\"navigation\"]:hover > * > div {\n        backdrop-filter: blur(12px) !important;\n      }\n      #react-root header[role=\"banner\"] nav[role=\"navigation\"] > a {\n        position: relative;\n      }\n\n      #react-root header[role=\"banner\"] nav[role=\"navigation\"] > a::before {\n        content: \"\";\n        position: absolute;\n        top: 0px;\n        right: -40px;\n        bottom: 0px;\n        left: 0px;\n      }\n      #react-root [data-testid=\"SideNav_AccountSwitcher_Button\"] {\n        bottom: 18px !important;\n        left: 1px !important;\n      }\n\n      #react-root [data-testid=\"SideNav_NewTweet_Button\"], #react-root [aria-label=\"Twitter Blue\"]{\n        display: none;\n      }\n    }\n\n    @media only screen and (min-width: 1265px) {\n      #react-root [data-testid=\"sidebarColumn\"] form[role=\"search\"] {\n        visibility: visible !important;\n        position: fixed !important;\n        top: 12px !important;\n        right: 16px !important;\n      }\n\n      #react-root [data-testid=\"sidebarColumn\"] input[placeholder=\"Search Twitter\"] {\n        width: 150px;\n      }\n\n      #react-root [data-testid=\"sidebarColumn\"] form[role=\"search\"]:focus-within {\n        width: 374px !important;\n        backdrop-filter: blur(12px) !important;\n      }\n\n      #react-root [data-testid=\"sidebarColumn\"] input[placeholder=\"Search Twitter\"]:focus {\n        width: 328px !important;\n      }\n\n      #react-root div[style*=\"left: -12px\"] {\n        left: unset !important;\n      }\n\n      #react-root div[style=\"left: -8px; width: 306px;\"] {\n        left: unset !important;\n        width: 374px !important;\n      }\n\n      #react-root .searchFilters {\n        visibility: visible !important;\n        position: fixed;\n        top: 12px;\n        right: 16px;\n        width: 240px;\n      }\n      #react-root .searchFilters > div > div:first-child {\n        display: none;\n      }\n    }\n\n    @media (min-width:1280px){\n      #__next .text-base.xl\\\\:max-w-3xl, #__next form.stretch.xl\\\\:max-w-3xl {\n        max-width: 48rem;\n      }\n    }\n\n    #__next .prose ol li p {\n      margin: 0;\n      display: inline;\n    }\n\n    .AppHeader .AppHeader-globalBar.js-global-bar {\n      padding-top: 35px;\n    }\n\n    .header-overlay .header-logged-out {\n      margin-top: 15px;\n    }\n\n    .w-full #stage-slideover-sidebar {\n      padding-top: 16px;\n    }\n    .w-full #thread #page-header {\n      padding-top: 36px;\n    }\n  `;\n  const contentStyleElement = document.createElement(\"style\");\n  contentStyleElement.innerHTML = contentCSS;\n  document.head.appendChild(contentStyleElement);\n\n  // Top spacing adapts to head-hiding scenarios\n  const topPaddingCSS = `\n    #layout > ytmusic-nav-bar{\n      padding-top: 20px;\n    }\n\n    .columns .column #header,\n    .main > div > div.panel.give_me > div.header {\n      padding-top: 30px;\n    }\n\n    ytd-masthead>#container.style-scope.ytd-masthead {\n      padding-top: 12px;\n    }\n\n    #__next header.HeaderBar_header__jn5ju {\n      padding-top: 16px;\n    }\n\n    #root > .excalidraw-app> .excalidraw-container .App-menu.App-menu_top{\n      margin-top: 15px;\n    }\n\n    .geist-page nav.dashboard_nav__PRmJv,\n    #app > div.layout > div.header-container.showSearchBoxOrHeaderFixed > header > a {\n      padding-top:10px;\n    }\n\n    .geist-page .submenu button{\n      margin-top:24px;\n    }\n\n    .container-with-note #home, .container-with-note #switcher{\n      top: 30px;\n    }\n\n    #__next .overflow-hidden>.overflow-x-hidden .scrollbar-trigger > nav {\n      padding-top: 12px;\n    }\n\n    #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.relative.flex.h-full.max-w-full.flex-1.flex-col.overflow-hidden > main > div.flex.h-full.flex-col > div.flex-1.overflow-hidden > div > div.absolute.left-0.right-0 > div > div.flex.items-center.gap-2 > button{\n      margin-left: 60px;\n      margin-right: -10px;\n    }\n\n    #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.dark.flex-shrink-0.overflow-x-hidden.bg-black > div > div > div > div > nav > div.flex.flex-col.pt-2.empty\\\\:hidden.dark\\\\:border-white\\\\/20 > a,\n    #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.relative.flex.h-full.max-w-full.flex-1.flex-col.overflow-hidden > main > div.group.fixed.bottom-3.right-3.z-10.hidden.gap-1.lg\\\\:flex > div,\n    #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary > div > div > div > div > nav > div.flex.flex-col.pt-2.empty\\\\:hidden.dark\\\\:border-white\\\\/20 > a {\n      display: none;\n    }\n\n    #__next .md\\\\:px-\\\\[60px\\\\].text-token-text-secondary.text-xs.text-center.py-2.px-2.relative{\n      visibility:hidden;\n    }\n\n    #__next>div>div>.flex.h-screen.w-full.flex-col.items-center {\n      padding-top: 20px;\n    }\n\n    .h-dvh.flex-grow .bg-gradient-to-b.from-background.via-background {\n      padding-top: 40px;\n    }\n\n    body > div.relative.flex.h-full.w-full.overflow-hidden.transition-colors.z-0 > div.z-\\\\[21\\\\].flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary.max-md\\\\:\\\\!w-0 > div > div > div > nav > div.flex.justify-between.h-\\\\[60px\\\\].items-center.md\\\\:h-header-height {\n      padding-top: 25px;\n    }\n\n    body > div.relative.flex.h-full.w-full.overflow-hidden.transition-colors.z-0 > div.relative.flex.h-full.max-w-full.flex-1.flex-col.overflow-hidden > main > div.composer-parent.flex.h-full.flex-col.focus-visible\\\\:outline-0 > div.flex-1.overflow-hidden.\\\\@container\\\\/thread > div > div.absolute.left-0.right-0 > div{\n      padding-top: 35px;\n    }\n\n    #__next .sticky.left-0.right-0.top-0.z-20.bg-black{\n      padding-top: 0px;\n    }\n\n    #header-area > div > .css-gtiexd > div:nth-child(1) > div, #header-area .logoIcon .user-info{\n      padding-top: 20px;\n    }\n\n    #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary > div > div > div > div > nav, #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.relative.flex.h-full.max-w-full.flex-1.flex-col.overflow-hidden > main {\n      padding-top: 6px;\n    }\n\n    #__next > div.AnnouncementWrapper_container__Z51yh > div > aside.SidebarLayout_sidebar__SXeDJ.SidebarLayout_left__k163a > div > div > header{\n      padding-left: 84px;\n      padding-top: 10px;\n    }\n\n    #page .main_header, .cb-layout-basic--navbar,\n    #app .splitpanes.splitpanes--horizontal.no-splitter header,\n    .fui-FluentProvider .fui-Button[data-testid=\"HomeButton\"],\n    #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside .ChatPageSidebar_logo__9PIXq {\n      padding-top: 20px;\n    }\n\n    #tabs-sidebar--tabpanel-0 > div.tw-flex.tw-items-center.tw-mb-\\\\[12px\\\\].tw-mt-\\\\[14px\\\\].tw-px-4 {\n      padding-top: 15px;\n    }\n\n    #tabs-sidebar--tabpanel-1 > div > div.tw-p-\\\\[16px\\\\].tw-flex.tw-flex-col.tw-gap-1\\\\.5{\n      padding-top: 30px;\n    }\n\n    #tabs-sidebar--tabpanel-2 > div > h2 {\n      padding-top: 20px;\n      height: 70px;\n    }\n\n    .lark > .dashboard-sidebar, .lark > .dashboard-sidebar > .sidebar-user-info , .lark > .dashboard-sidebar .index-module_wrapper_F-Wbq{\n      padding-top:15px;\n    }\n\n    #app-root .mat-mdc-tooltip-trigger.main-menu-button.mdc-icon-button {\n      margin-top: 15px;\n    }\n\n    .lark > .main-wrapper [data-testid=\"aside\"] {\n      top: 15px;\n    }\n\n    #global > div.header-container > .mask-paper {\n      padding-top: 20px;\n    }\n\n    #background.ytd-masthead {\n      height: 68px;\n    }\n\n    .wrap.h1body-exist.max-container > div.menu-tocs > div.menu-btn{\n      top: 28px;\n    }\n\n    .flex.w-full.h-full.overflow-hidden{\n      padding-top:20px;\n    }\n\n    .text-sidebar-foreground .bg-sidebar{\n      padding-top:30px;\n    }\n\n    #pake-top-dom:active {\n      cursor: grabbing;\n      cursor: -webkit-grabbing;\n    }\n\n    #pake-top-dom{\n      position:fixed;\n      background:transparent;\n      top:0;\n      width: 100%;\n      height: 20px;\n      cursor: grab;\n      -webkit-app-region: drag;\n      user-select: none;\n      -webkit-user-select: none;\n      z-index: 99999;\n    }\n\n    @media (max-width:767px){\n      #__next .overflow-hidden.w-full .max-w-full>.sticky.top-0 {\n        padding-top: 20px;\n      }\n\n      #__next > div.overflow-hidden.w-full.h-full  main.relative.h-full.w-full.flex-1 > .flex-1.overflow-hidden .h-32.md\\\\:h-48.flex-shrink-0{\n        height: 0px;\n      }\n    }\n  `;\n  const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n  if (window[\"pakeConfig\"]?.hide_title_bar && isMac) {\n    const topPaddingStyleElement = document.createElement(\"style\");\n    topPaddingStyleElement.innerHTML = topPaddingCSS;\n    document.head.appendChild(topPaddingStyleElement);\n  }\n});\n"
  },
  {
    "path": "src-tauri/src/inject/theme_refresh.js",
    "content": "document.addEventListener(\"DOMContentLoaded\", () => {\n  const debounce = (func, wait) => {\n    let timeout;\n    return (...args) => {\n      clearTimeout(timeout);\n      timeout = setTimeout(() => func(...args), wait);\n    };\n  };\n\n  const updateTheme = () => {\n    const doc = document.documentElement;\n    const body = document.body;\n    let mode = null;\n\n    // Check for explicit theme classes or attributes\n    const isDark =\n      doc.classList.contains(\"dark\") ||\n      body.classList.contains(\"dark\") ||\n      doc.getAttribute(\"data-theme\") === \"dark\" ||\n      body.getAttribute(\"data-theme\") === \"dark\" ||\n      doc.style.colorScheme === \"dark\";\n\n    const isLight =\n      doc.classList.contains(\"light\") ||\n      body.classList.contains(\"light\") ||\n      doc.getAttribute(\"data-theme\") === \"light\" ||\n      body.getAttribute(\"data-theme\") === \"light\" ||\n      doc.style.colorScheme === \"light\";\n\n    if (isDark) mode = \"dark\";\n    else if (isLight) mode = \"light\";\n\n    // Only invoke Rust command if an explicit theme override is detected\n    if (mode && window.__TAURI__?.core) {\n      window.__TAURI__.core.invoke(\"update_theme_mode\", { mode });\n    }\n  };\n\n  const debouncedUpdateTheme = debounce(updateTheme, 200);\n\n  // Initial check with delay to allow site to render\n  setTimeout(updateTheme, 500);\n\n  // Watch for DOM changes\n  const observer = new MutationObserver(debouncedUpdateTheme);\n  const config = {\n    attributes: true,\n    attributeFilter: [\"class\", \"data-theme\", \"style\"],\n    subtree: false,\n  };\n\n  observer.observe(document.documentElement, config);\n  observer.observe(document.body, config);\n\n  // Watch for system theme changes (though window should handle this natively now)\n  window\n    .matchMedia(\"(prefers-color-scheme: dark)\")\n    .addEventListener(\"change\", updateTheme);\n});\n"
  },
  {
    "path": "src-tauri/src/lib.rs",
    "content": "#[cfg_attr(mobile, tauri::mobile_entry_point)]\nmod app;\nmod util;\n\nuse tauri::Manager;\nuse tauri_plugin_window_state::Builder as WindowStatePlugin;\nuse tauri_plugin_window_state::StateFlags;\n\n#[cfg(target_os = \"macos\")]\nuse std::time::Duration;\n\nconst WINDOW_SHOW_DELAY: u64 = 50;\n\nuse app::{\n    invoke::{\n        clear_cache_and_restart, download_file, download_file_by_binary, send_notification,\n        update_theme_mode,\n    },\n    setup::{set_global_shortcut, set_system_tray},\n    window::{open_additional_window_safe, set_window, MultiWindowState},\n};\nuse util::get_pake_config;\n\npub fn run_app() {\n    #[cfg(target_os = \"linux\")]\n    {\n        if std::env::var(\"WEBKIT_DISABLE_DMABUF_RENDERER\").is_err() {\n            std::env::set_var(\"WEBKIT_DISABLE_DMABUF_RENDERER\", \"1\");\n        }\n    }\n\n    let (pake_config, tauri_config) = get_pake_config();\n    let tauri_app = tauri::Builder::default();\n\n    let show_system_tray = pake_config.show_system_tray();\n    let hide_on_close = pake_config.windows[0].hide_on_close;\n    let activation_shortcut = pake_config.windows[0].activation_shortcut.clone();\n    let init_fullscreen = pake_config.windows[0].fullscreen;\n    let start_to_tray = pake_config.windows[0].start_to_tray && show_system_tray; // Only valid when tray is enabled\n    let multi_instance = pake_config.multi_instance;\n    let multi_window = pake_config.multi_window;\n\n    let window_state_plugin = WindowStatePlugin::default()\n        .with_state_flags(if init_fullscreen {\n            StateFlags::FULLSCREEN\n        } else {\n            // Prevent flickering on the first open.\n            StateFlags::all() & !StateFlags::VISIBLE\n        })\n        .build();\n\n    #[allow(deprecated)]\n    let mut app_builder = tauri_app\n        .plugin(window_state_plugin)\n        .plugin(tauri_plugin_oauth::init())\n        .plugin(tauri_plugin_http::init())\n        .plugin(tauri_plugin_shell::init())\n        .plugin(tauri_plugin_notification::init())\n        .plugin(tauri_plugin_opener::init()); // Add this\n\n    // Only add single instance plugin if multiple instances are not allowed\n    if !multi_instance {\n        app_builder = app_builder.plugin(tauri_plugin_single_instance::init(\n            move |app, _args, _cwd| {\n                if multi_window {\n                    open_additional_window_safe(app);\n                } else if let Some(window) = app.get_webview_window(\"pake\") {\n                    let _ = window.unminimize();\n                    let _ = window.show();\n                    let _ = window.set_focus();\n                }\n            },\n        ));\n    }\n\n    app_builder\n        .invoke_handler(tauri::generate_handler![\n            download_file,\n            download_file_by_binary,\n            send_notification,\n            update_theme_mode,\n            clear_cache_and_restart,\n        ])\n        .setup(move |app| {\n            app.manage(MultiWindowState::new(\n                pake_config.clone(),\n                tauri_config.clone(),\n            ));\n\n            // --- Menu Construction Start ---\n            #[cfg(target_os = \"macos\")]\n            {\n                let menu = app::menu::get_menu(app.app_handle(), multi_window)?;\n                app.set_menu(menu)?;\n\n                // Event Handling for Custom Menu Item\n                app.on_menu_event(move |app_handle, event| {\n                    app::menu::handle_menu_click(app_handle, event.id().as_ref());\n                });\n            }\n            // --- Menu Construction End ---\n\n            let window = set_window(app.app_handle(), &pake_config, &tauri_config);\n            set_system_tray(\n                app.app_handle(),\n                show_system_tray,\n                &pake_config.system_tray_path,\n                init_fullscreen,\n                multi_window,\n            )\n            .unwrap();\n            set_global_shortcut(app.app_handle(), activation_shortcut, init_fullscreen).unwrap();\n\n            // Show window after state restoration to prevent position flashing\n            // Unless start_to_tray is enabled, then keep it hidden\n            if !start_to_tray {\n                let window_clone = window.clone();\n                tauri::async_runtime::spawn(async move {\n                    tokio::time::sleep(tokio::time::Duration::from_millis(WINDOW_SHOW_DELAY)).await;\n                    window_clone.show().unwrap();\n\n                    // Fixed: Linux fullscreen issue with virtual keyboard\n                    #[cfg(target_os = \"linux\")]\n                    {\n                        if init_fullscreen {\n                            window_clone.set_fullscreen(true).unwrap();\n                            // Ensure webview maintains focus for input after fullscreen\n                            let _ = window_clone.set_focus();\n                        } else {\n                            // Fix: Ubuntu 24.04/GNOME window buttons non-functional until resize (#1122)\n                            // The window manager needs time to process the MapWindow event before\n                            // accepting focus requests. Without this, decorations remain non-interactive.\n                            tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;\n                            let _ = window_clone.set_focus();\n                        }\n                    }\n                });\n            }\n\n            Ok(())\n        })\n        .on_window_event(move |_window, _event| {\n            if let tauri::WindowEvent::CloseRequested { api, .. } = _event {\n                if hide_on_close && _window.label() == \"pake\" {\n                    // Hide window when hide_on_close is enabled (regardless of tray status)\n                    let window = _window.clone();\n                    tauri::async_runtime::spawn(async move {\n                        #[cfg(target_os = \"macos\")]\n                        {\n                            if window.is_fullscreen().unwrap_or(false) {\n                                window.set_fullscreen(false).unwrap();\n                                tokio::time::sleep(Duration::from_millis(900)).await;\n                            }\n                        }\n                        #[cfg(target_os = \"linux\")]\n                        {\n                            if window.is_fullscreen().unwrap_or(false) {\n                                window.set_fullscreen(false).unwrap();\n                                // Restore focus after exiting fullscreen to fix input issues\n                                let _ = window.set_focus();\n                            }\n                        }\n                        // On macOS, directly hide without minimize to avoid duplicate Dock icons\n                        #[cfg(not(target_os = \"macos\"))]\n                        window.minimize().unwrap();\n                        window.hide().unwrap();\n                    });\n                    api.prevent_close();\n                }\n                // If hide_on_close is false, allow normal close behavior\n                // This lets tauri-plugin-window-state save the window position and size\n            }\n        })\n        .build(tauri::generate_context!())\n        .expect(\"error while building tauri application\")\n        .run(|_app, _event| {\n            // Handle macOS dock icon click to reopen hidden window\n            #[cfg(target_os = \"macos\")]\n            if let tauri::RunEvent::Reopen {\n                has_visible_windows,\n                ..\n            } = _event\n            {\n                if !has_visible_windows {\n                    if let Some(window) = _app.get_webview_window(\"pake\") {\n                        let _ = window.show();\n                        let _ = window.set_focus();\n                    }\n                }\n            }\n        });\n}\n\npub fn run() {\n    run_app()\n}\n"
  },
  {
    "path": "src-tauri/src/main.rs",
    "content": "#![cfg_attr(\n    all(not(debug_assertions), target_os = \"windows\"),\n    windows_subsystem = \"windows\"\n)]\n\nfn main() {\n    app_lib::run()\n}\n"
  },
  {
    "path": "src-tauri/src/util.rs",
    "content": "use crate::app::config::PakeConfig;\nuse std::env;\nuse std::path::PathBuf;\nuse tauri::{AppHandle, Config, Manager, WebviewWindow};\n\npub fn get_pake_config() -> (PakeConfig, Config) {\n    #[cfg(feature = \"cli-build\")]\n    let pake_config: PakeConfig = serde_json::from_str(include_str!(\"../.pake/pake.json\"))\n        .expect(\"Failed to parse pake config\");\n\n    #[cfg(not(feature = \"cli-build\"))]\n    let pake_config: PakeConfig =\n        serde_json::from_str(include_str!(\"../pake.json\")).expect(\"Failed to parse pake config\");\n\n    #[cfg(feature = \"cli-build\")]\n    let tauri_config: Config = serde_json::from_str(include_str!(\"../.pake/tauri.conf.json\"))\n        .expect(\"Failed to parse tauri config\");\n\n    #[cfg(not(feature = \"cli-build\"))]\n    let tauri_config: Config = serde_json::from_str(include_str!(\"../tauri.conf.json\"))\n        .expect(\"Failed to parse tauri config\");\n\n    (pake_config, tauri_config)\n}\n\npub fn get_data_dir(app: &AppHandle, package_name: String) -> PathBuf {\n    {\n        let data_dir = app\n            .path()\n            .config_dir()\n            .expect(\"Failed to get data dirname\")\n            .join(package_name);\n\n        if !data_dir.exists() {\n            std::fs::create_dir(&data_dir)\n                .unwrap_or_else(|_| panic!(\"Can't create dir {}\", data_dir.display()));\n        }\n        data_dir\n    }\n}\n\npub fn show_toast(window: &WebviewWindow, message: &str) {\n    let script = format!(r#\"pakeToast(\"{message}\");\"#);\n    window.eval(&script).unwrap();\n}\n\npub enum MessageType {\n    Start,\n    Success,\n    Failure,\n}\n\npub fn get_download_message_with_lang(\n    message_type: MessageType,\n    language: Option<String>,\n) -> String {\n    let default_start_message = \"Start downloading~\";\n    let chinese_start_message = \"开始下载中~\";\n\n    let default_success_message = \"Download successful, saved to download directory~\";\n    let chinese_success_message = \"下载成功，已保存到下载目录~\";\n\n    let default_failure_message = \"Download failed, please check your network connection~\";\n    let chinese_failure_message = \"下载失败，请检查你的网络连接~\";\n\n    let is_chinese = language\n        .as_ref()\n        .map(|lang| {\n            lang.starts_with(\"zh\")\n                || lang.contains(\"CN\")\n                || lang.contains(\"TW\")\n                || lang.contains(\"HK\")\n        })\n        .unwrap_or_else(|| {\n            // Try multiple environment variables for better system detection\n            [\"LANG\", \"LC_ALL\", \"LC_MESSAGES\", \"LANGUAGE\"]\n                .iter()\n                .find_map(|var| env::var(var).ok())\n                .map(|lang| {\n                    lang.starts_with(\"zh\")\n                        || lang.contains(\"CN\")\n                        || lang.contains(\"TW\")\n                        || lang.contains(\"HK\")\n                })\n                .unwrap_or(false)\n        });\n\n    if is_chinese {\n        match message_type {\n            MessageType::Start => chinese_start_message,\n            MessageType::Success => chinese_success_message,\n            MessageType::Failure => chinese_failure_message,\n        }\n    } else {\n        match message_type {\n            MessageType::Start => default_start_message,\n            MessageType::Success => default_success_message,\n            MessageType::Failure => default_failure_message,\n        }\n    }\n    .to_string()\n}\n\n// Check if the file exists, if it exists, add a number to file name\npub fn check_file_or_append(file_path: &str) -> String {\n    let mut new_path = PathBuf::from(file_path);\n    let mut counter = 0;\n\n    while new_path.exists() {\n        let file_stem = new_path.file_stem().unwrap().to_string_lossy().to_string();\n        let extension = new_path.extension().unwrap().to_string_lossy().to_string();\n        let parent_dir = new_path.parent().unwrap();\n\n        let new_file_stem = match file_stem.rfind('-') {\n            Some(index) if file_stem[index + 1..].parse::<u32>().is_ok() => {\n                let base_name = &file_stem[..index];\n                counter = file_stem[index + 1..].parse::<u32>().unwrap() + 1;\n                format!(\"{base_name}-{counter}\")\n            }\n            _ => {\n                counter += 1;\n                format!(\"{file_stem}-{counter}\")\n            }\n        };\n\n        new_path = parent_dir.join(format!(\"{new_file_stem}.{extension}\"));\n    }\n\n    new_path.to_string_lossy().into_owned()\n}\n"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "content": "{\n  \"productName\": \"Weekly\",\n  \"identifier\": \"com.pake.weekly\",\n  \"version\": \"3.10.1\",\n  \"app\": {\n    \"withGlobalTauri\": true,\n    \"trayIcon\": {\n      \"iconPath\": \"png/weekly_512.png\",\n      \"iconAsTemplate\": false,\n      \"id\": \"pake-tray\"\n    },\n    \"security\": {\n      \"headers\": {},\n      \"csp\": null\n    }\n  },\n  \"build\": {\n    \"frontendDist\": \"../dist\"\n  }\n}\n"
  },
  {
    "path": "src-tauri/tauri.linux.conf.json",
    "content": "{\n  \"bundle\": {\n    \"icon\": [\"png/weekly_512.png\"],\n    \"active\": true,\n    \"linux\": {\n      \"deb\": {\n        \"depends\": [\"curl\", \"wget\"]\n      }\n    },\n    \"targets\": [\"deb\", \"appimage\"]\n  }\n}\n"
  },
  {
    "path": "src-tauri/tauri.macos.conf.json",
    "content": "{\n  \"bundle\": {\n    \"icon\": [\"icons/weekly.icns\"],\n    \"active\": true,\n    \"targets\": [\"dmg\"],\n    \"macOS\": {\n      \"signingIdentity\": \"-\",\n      \"hardenedRuntime\": true,\n      \"entitlements\": \"entitlements.plist\",\n      \"infoPlist\": \"Info.plist\",\n      \"dmg\": {\n        \"background\": \"assets/macos/dmg/background.png\",\n        \"windowSize\": {\n          \"width\": 680,\n          \"height\": 420\n        },\n        \"appPosition\": {\n          \"x\": 190,\n          \"y\": 250\n        },\n        \"applicationFolderPosition\": {\n          \"x\": 500,\n          \"y\": 250\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src-tauri/tauri.windows.conf.json",
    "content": "{\n  \"bundle\": {\n    \"icon\": [\"png/weekly_256.ico\", \"png/weekly_32.ico\"],\n    \"active\": true,\n    \"resources\": [\"png/weekly_32.ico\"],\n    \"targets\": [\"msi\"],\n    \"windows\": {\n      \"digestAlgorithm\": \"sha256\",\n      \"wix\": {\n        \"language\": [\"en-US\"],\n        \"template\": \"assets/main.wxs\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/config.js",
    "content": "/**\n * Test Configuration for Pake CLI\n *\n * This file contains test configuration and utilities\n * shared across different test files.\n */\n\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nexport const PROJECT_ROOT = path.dirname(__dirname);\nexport const CLI_PATH = path.join(PROJECT_ROOT, \"dist/cli.js\");\n\n// Test timeouts (in milliseconds)\nexport const TIMEOUTS = {\n  QUICK: 10000, // For version, help commands\n  MEDIUM: 20000, // For validation tests\n  LONG: 300000, // For build tests (5 minutes)\n};\n\n// Test URLs for different scenarios\nexport const TEST_URLS = {\n  WEEKLY: \"https://weekly.tw93.fun\",\n  VALID: \"https://example.com\",\n  GITHUB: \"https://github.com\",\n  GOOGLE: \"https://www.google.com\",\n  INVALID: \"not://a/valid[url]\",\n  LOCAL: \"./test-file.html\",\n};\n\n// Test assets for different scenarios\nexport const TEST_ASSETS = {\n  WEEKLY_ICNS: \"https://cdn.tw93.fun/pake/weekly.icns\",\n  INVALID_ICON: \"https://example.com/nonexistent.icns\",\n};\n\n// Test app names\nexport const TEST_NAMES = {\n  WEEKLY: \"Weekly\",\n  BASIC: \"TestApp\",\n  DEBUG: \"DebugApp\",\n  FULL: \"FullscreenApp\",\n  GOOGLE_TRANSLATE: \"Google Translate\",\n  MAC: \"MacApp\",\n};\n\n// Expected file extensions by platform\nexport const PLATFORM_EXTENSIONS = {\n  darwin: \"dmg\",\n  win32: \"msi\",\n  linux: \"deb\",\n};\n\n// Helper functions\nexport const testHelpers = {\n  /**\n   * Clean test name for filesystem\n   */\n  sanitizeName: (name) => name.replace(/[^a-zA-Z0-9]/g, \"\"),\n\n  /**\n   * Get expected output file for current platform\n   */\n  getExpectedOutput: (appName) => {\n    const ext = PLATFORM_EXTENSIONS[process.platform] || \"bin\";\n    return `${appName}.${ext}`;\n  },\n\n  /**\n   * Create test command with common options\n   */\n  createCommand: (url, options = {}) => {\n    const baseCmd = `node \"${CLI_PATH}\" \"${url}\"`;\n    const optionsStr = Object.entries(options)\n      .map(([key, value]) => {\n        if (value === true) return `--${key}`;\n        if (value === false) return \"\";\n        return `--${key} \"${value}\"`;\n      })\n      .filter(Boolean)\n      .join(\" \");\n\n    return `${baseCmd} ${optionsStr}`.trim();\n  },\n};\n\nexport default {\n  PROJECT_ROOT,\n  CLI_PATH,\n  TIMEOUTS,\n  TEST_URLS,\n  TEST_NAMES,\n  PLATFORM_EXTENSIONS,\n  testHelpers,\n};\n"
  },
  {
    "path": "tests/index.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Unified Test Runner for Pake CLI\n *\n * This is a simplified, unified test runner that replaces the scattered\n * test files with a single, easy-to-use interface.\n */\n\nimport { execSync, spawn } from \"child_process\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport ora from \"ora\";\nimport config, { TIMEOUTS, TEST_URLS } from \"./config.js\";\n\nclass PakeTestRunner {\n  constructor() {\n    this.results = [];\n    this.tempFiles = [];\n    this.tempDirs = [];\n  }\n\n  async runAll(options = {}) {\n    const {\n      unit = true,\n      integration = true,\n      builder = true,\n      pakeCliTests = false,\n      e2e = false,\n      quick = false,\n      realBuild = false, // Add option for real build test\n    } = options;\n\n    console.log(\"Pake CLI Test Suite\");\n    console.log(\"======================\\n\");\n\n    this.validateEnvironment();\n\n    // Clean up any leftover files from previous test runs\n    console.log(\"[Clean] Removing any leftover test artifacts...\");\n    this.cleanupTempIcons();\n\n    let testCount = 0;\n\n    if (unit && !quick) {\n      console.log(\"Running CLI Health Checks...\");\n      await this.runCliHealthChecks();\n      testCount++;\n\n      console.log(\"\\nRunning Project Unit Tests (Vitest)...\");\n      try {\n        execSync(\"npx vitest run\", {\n          stdio: \"inherit\",\n          cwd: config.PROJECT_ROOT,\n        });\n        this.results.push({ name: \"Vitest Unit Tests\", passed: true });\n        testCount++;\n      } catch (e) {\n        console.log(\"[FAIL] Vitest unit tests failed\");\n        this.results.push({\n          name: \"Vitest Unit Tests\",\n          passed: false,\n          error: e.message,\n        });\n      }\n    }\n\n    if (integration && !quick) {\n      console.log(\"\\n[Integration] Running Integration Tests...\");\n      await this.runIntegrationTests();\n      testCount++;\n    }\n\n    if (builder && !quick) {\n      console.log(\"\\n[Build] Running Builder Tests...\");\n      await this.runBuilderTests();\n      testCount++;\n    }\n\n    if (pakeCliTests) {\n      console.log(\"\\n[Package] Running Pake-CLI GitHub Actions Tests...\");\n      await this.runPakeCliTests();\n      testCount++;\n    }\n\n    if (e2e && !quick) {\n      console.log(\"\\n[Run] Running End-to-End Tests...\");\n      await this.runE2ETests();\n      testCount++;\n\n      console.log(\"\\n[Network] Running Proxy Configuration Test...\");\n      await this.runProxyTest();\n      testCount++;\n    }\n\n    if (builder && !quick) {\n      console.log(\"\\n[Build] Running Local File Build Test...\");\n      await this.runLocalFileTest();\n      testCount++;\n    }\n\n    if (realBuild && !quick) {\n      // On macOS, prefer multi-arch test as it's more likely to catch issues\n      if (process.platform === \"darwin\") {\n        console.log(\"\\n[Build] Running Real Build Test (Multi-Arch)...\");\n        await this.runMultiArchBuildTest();\n        testCount++;\n      } else {\n        console.log(\"\\n[Build] Running Real Build Test...\");\n        await this.runRealBuildTest();\n        testCount++;\n      }\n    }\n\n    this.cleanup();\n    this.displayFinalResults();\n\n    const passed = this.results.filter((r) => r.passed).length;\n    const total = this.results.length;\n\n    return passed === total;\n  }\n\n  validateEnvironment() {\n    console.log(\"Environment Validation:\");\n    console.log(\"-----------------------\");\n\n    // Check if CLI file exists\n    if (!fs.existsSync(config.CLI_PATH)) {\n      console.log(\"[FAIL] CLI file not found. Run: pnpm run cli:build\");\n      process.exit(1);\n    }\n    console.log(\"[PASS] CLI file exists\");\n\n    // Check if CLI is executable\n    try {\n      execSync(`node \"${config.CLI_PATH}\" --version`, {\n        encoding: \"utf8\",\n        timeout: 3000,\n      });\n      console.log(\"[PASS] CLI is executable\");\n    } catch (error) {\n      console.log(\"[FAIL] CLI is not executable\");\n      process.exit(1);\n    }\n\n    // Platform info\n    console.log(`[PASS] Platform: ${process.platform} (${process.arch})`);\n    console.log(`[PASS] Node.js: ${process.version}`);\n\n    const isCI = process.env.CI || process.env.GITHUB_ACTIONS;\n    console.log(`[INFO] CI Environment: ${isCI ? \"Yes\" : \"No\"}`);\n\n    console.log();\n  }\n\n  async runTest(name, testFn, timeout = TIMEOUTS.MEDIUM) {\n    const spinner = ora(`Running ${name}...`).start();\n\n    try {\n      const result = await Promise.race([\n        testFn(),\n        new Promise((_, reject) =>\n          setTimeout(() => reject(new Error(\"Test timeout\")), timeout),\n        ),\n      ]);\n\n      if (result) {\n        spinner.succeed(`${name}: PASS`);\n        this.results.push({ name, passed: true });\n      } else {\n        spinner.fail(`${name}: FAIL`);\n        this.results.push({ name, passed: false });\n      }\n    } catch (error) {\n      spinner.fail(`${name}: ERROR - ${error.message.slice(0, 100)}...`);\n      this.results.push({\n        name,\n        passed: false,\n        error: error.message,\n      });\n    }\n  }\n\n  async runCliHealthChecks() {\n    // Version command test\n    await this.runTest(\n      \"Version Command\",\n      () => {\n        const output = execSync(`node \"${config.CLI_PATH}\" --version`, {\n          encoding: \"utf8\",\n          timeout: TIMEOUTS.QUICK,\n        });\n        return /^\\d+\\.\\d+\\.\\d+/.test(output.trim());\n      },\n      TIMEOUTS.QUICK,\n    );\n\n    // Help command test\n    await this.runTest(\n      \"Help Command\",\n      () => {\n        const output = execSync(`node \"${config.CLI_PATH}\"`, {\n          encoding: \"utf8\",\n          timeout: TIMEOUTS.QUICK,\n        });\n        return output.includes(\"Usage: cli [url] [options]\");\n      },\n      TIMEOUTS.QUICK,\n    );\n\n    // URL validation test\n    await this.runTest(\"URL Validation\", () => {\n      try {\n        execSync(`node \"${config.CLI_PATH}\" \"invalid-url\" --name TestApp`, {\n          encoding: \"utf8\",\n          timeout: TIMEOUTS.QUICK,\n        });\n        return false; // Should have failed\n      } catch (error) {\n        return error.status !== 0;\n      }\n    });\n\n    // Number validation test\n    await this.runTest(\"Number Validation\", () => {\n      try {\n        execSync(`node \"${config.CLI_PATH}\" https://example.com --width abc`, {\n          encoding: \"utf8\",\n          timeout: TIMEOUTS.QUICK,\n        });\n        return false; // Should throw error\n      } catch (error) {\n        return error.message.includes(\"Not a number\");\n      }\n    });\n\n    // CLI response time test\n    await this.runTest(\"CLI Response Time\", () => {\n      const start = Date.now();\n      execSync(`node \"${config.CLI_PATH}\" --version`, {\n        encoding: \"utf8\",\n        timeout: TIMEOUTS.QUICK,\n      });\n      const elapsed = Date.now() - start;\n      return elapsed < 5000;\n    });\n\n    // Weekly URL accessibility test\n    await this.runTest(\"Weekly URL Accessibility\", () => {\n      try {\n        const testCommand = `node \"${config.CLI_PATH}\" ${TEST_URLS.WEEKLY} --name \"URLTest\" --debug`;\n        execSync(`echo \"n\" | timeout 5s ${testCommand} || true`, {\n          encoding: \"utf8\",\n          timeout: 8000,\n        });\n        return true; // If we get here, URL was parsed successfully\n      } catch (error) {\n        return (\n          !error.message.includes(\"Invalid URL\") &&\n          !error.message.includes(\"invalid\")\n        );\n      }\n    });\n  }\n\n  async runIntegrationTests() {\n    // Process spawning test\n    await this.runTest(\"CLI Process Spawning\", () => {\n      return new Promise((resolve) => {\n        const child = spawn(\"node\", [config.CLI_PATH, \"--version\"], {\n          stdio: [\"pipe\", \"pipe\", \"pipe\"],\n        });\n\n        let output = \"\";\n        child.stdout.on(\"data\", (data) => {\n          output += data.toString();\n        });\n\n        child.on(\"close\", (code) => {\n          resolve(code === 0 && /\\d+\\.\\d+\\.\\d+/.test(output));\n        });\n\n        setTimeout(() => {\n          child.kill();\n          resolve(false);\n        }, TIMEOUTS.QUICK);\n      });\n    });\n\n    // File system permissions test\n    await this.runTest(\"File System Permissions\", () => {\n      try {\n        const testFile = \"test-write-permission.tmp\";\n        fs.writeFileSync(testFile, \"test\");\n        this.trackTempFile(testFile);\n\n        const cliStats = fs.statSync(config.CLI_PATH);\n        return cliStats.isFile();\n      } catch {\n        return false;\n      }\n    });\n\n    // Dependency resolution test\n    await this.runTest(\"Dependency Resolution\", () => {\n      try {\n        const packageJsonPath = path.join(config.PROJECT_ROOT, \"package.json\");\n        const packageJson = JSON.parse(\n          fs.readFileSync(packageJsonPath, \"utf8\"),\n        );\n\n        const essentialDeps = [\"commander\", \"chalk\", \"fs-extra\", \"execa\"];\n        return essentialDeps.every(\n          (dep) => packageJson.dependencies && packageJson.dependencies[dep],\n        );\n      } catch {\n        return false;\n      }\n    });\n  }\n\n  async runBuilderTests() {\n    // Platform detection test\n    await this.runTest(\"Platform Detection\", () => {\n      const platform = process.platform;\n      const platformConfigs = {\n        darwin: { ext: \".dmg\", multiArch: true },\n        win32: { ext: \".msi\", multiArch: false },\n        linux: { ext: \".deb\", multiArch: false },\n      };\n\n      const config = platformConfigs[platform];\n      return config && typeof config.ext === \"string\";\n    });\n\n    // Architecture detection test\n    await this.runTest(\"Architecture Detection\", () => {\n      const currentArch = process.arch;\n      const macArch = currentArch === \"arm64\" ? \"aarch64\" : currentArch;\n      const linuxArch = currentArch === \"x64\" ? \"amd64\" : currentArch;\n\n      return typeof macArch === \"string\" && typeof linuxArch === \"string\";\n    });\n\n    // File naming pattern test\n    await this.runTest(\"File Naming Patterns\", () => {\n      const testNames = [\"Simple App\", \"App-With_Symbols\", \"CamelCaseApp\"];\n      return testNames.every((name) => {\n        const processed = name.toLowerCase().replace(/\\s+/g, \"\");\n        return processed.length > 0;\n      });\n    });\n  }\n\n  async runPakeCliTests() {\n    // Package installation test\n    await this.runTest(\n      \"pake-cli Package Installation\",\n      async () => {\n        try {\n          execSync(\"pnpm install pake-cli@latest\", {\n            encoding: \"utf8\",\n            timeout: 60000,\n            cwd: \"/tmp\",\n          });\n\n          const pakeCliPath = \"/tmp/node_modules/.bin/pake\";\n          return fs.existsSync(pakeCliPath);\n        } catch (error) {\n          console.error(\"Package installation failed:\", error.message);\n          return false;\n        }\n      },\n      TIMEOUTS.LONG,\n    );\n\n    // Version command test\n    await this.runTest(\"pake-cli Version Command\", async () => {\n      try {\n        const version = execSync(\"npx pake --version\", {\n          encoding: \"utf8\",\n          timeout: 10000,\n        });\n        return /^\\d+\\.\\d+\\.\\d+/.test(version.trim());\n      } catch {\n        return false;\n      }\n    });\n\n    // Configuration validation test\n    await this.runTest(\"Configuration Validation\", async () => {\n      try {\n        const validateConfig = (config) => {\n          const required = [\"url\", \"name\", \"width\", \"height\"];\n          const hasRequired = required.every((field) =>\n            config.hasOwnProperty(field),\n          );\n\n          const validTypes =\n            typeof config.url === \"string\" &&\n            typeof config.name === \"string\" &&\n            typeof config.width === \"number\" &&\n            typeof config.height === \"number\";\n\n          let validUrl = false;\n          try {\n            new URL(config.url);\n            validUrl = true;\n          } catch {}\n\n          const validName = config.name.length > 0;\n          return hasRequired && validTypes && validUrl && validName;\n        };\n\n        const testConfig = {\n          url: \"https://github.com\",\n          name: \"github\",\n          width: 1200,\n          height: 780,\n        };\n\n        return validateConfig(testConfig);\n      } catch {\n        return false;\n      }\n    });\n  }\n\n  async runE2ETests() {\n    // GitHub.com CLI build test\n    await this.runTest(\n      \"GitHub.com CLI Build Test\",\n      async () => {\n        return new Promise((resolve, reject) => {\n          const testName = \"GitHubApp\";\n          const command = `node \"${config.CLI_PATH}\" \"https://github.com\" --name \"${testName}\" --debug --width 1200 --height 780`;\n\n          const child = spawn(command, {\n            shell: true,\n            cwd: config.PROJECT_ROOT,\n            stdio: [\"pipe\", \"pipe\", \"pipe\"],\n            env: {\n              ...process.env,\n              PAKE_E2E_TEST: \"1\",\n              PAKE_CREATE_APP: \"1\",\n            },\n          });\n\n          let buildStarted = false;\n          let configGenerated = false;\n\n          child.stdout.on(\"data\", (data) => {\n            const output = data.toString();\n            if (\n              output.includes(\"Building app\") ||\n              output.includes(\"Compiling\") ||\n              output.includes(\"Installing package\") ||\n              output.includes(\"Bundling\")\n            ) {\n              buildStarted = true;\n            }\n            if (\n              output.includes(\"GitHub\") &&\n              (output.includes(\"config\") || output.includes(\"name\"))\n            ) {\n              configGenerated = true;\n            }\n          });\n\n          child.stderr.on(\"data\", (data) => {\n            const output = data.toString();\n            if (\n              output.includes(\"Building app\") ||\n              output.includes(\"Compiling\") ||\n              output.includes(\"Installing package\") ||\n              output.includes(\"Bundling\") ||\n              output.includes(\"Finished\") ||\n              output.includes(\"Built application at:\")\n            ) {\n              buildStarted = true;\n            }\n          });\n\n          // Kill process after 60 seconds if build started\n          const timeout = setTimeout(() => {\n            child.kill(\"SIGTERM\");\n\n            const appFile = path.join(config.PROJECT_ROOT, `${testName}.app`);\n            const dmgFile = path.join(config.PROJECT_ROOT, `${testName}.dmg`);\n            this.trackTempFile(appFile);\n            this.trackTempFile(dmgFile);\n\n            if (buildStarted) {\n              console.log(\n                `✓ GitHub.com CLI build started successfully (${testName})`,\n              );\n              resolve(true);\n            } else {\n              reject(\n                new Error(\"GitHub.com CLI build did not start within timeout\"),\n              );\n            }\n          }, 60000);\n\n          child.on(\"close\", () => {\n            clearTimeout(timeout);\n            const appFile = path.join(config.PROJECT_ROOT, `${testName}.app`);\n            const dmgFile = path.join(config.PROJECT_ROOT, `${testName}.dmg`);\n            this.trackTempFile(appFile);\n            this.trackTempFile(dmgFile);\n\n            if (buildStarted) {\n              resolve(true);\n            } else {\n              reject(\n                new Error(\"GitHub.com CLI build process ended before starting\"),\n              );\n            }\n          });\n\n          child.on(\"error\", (error) => {\n            reject(\n              new Error(`GitHub.com CLI build process error: ${error.message}`),\n            );\n          });\n\n          child.stdin.end();\n        });\n      },\n      70000, // 70 seconds timeout\n    );\n\n    // Configuration verification test\n    await this.runTest(\n      \"Configuration File Verification\",\n      async () => {\n        const pakeDir = path.join(config.PROJECT_ROOT, \"src-tauri\", \".pake\");\n\n        return new Promise((resolve, reject) => {\n          const testName = \"GitHubConfigTest\";\n          const command = `node \"${config.CLI_PATH}\" \"https://github.com\" --name \"${testName}\" --debug --width 1200 --height 780`;\n\n          const child = spawn(command, {\n            shell: true,\n            cwd: config.PROJECT_ROOT,\n            stdio: [\"pipe\", \"pipe\", \"pipe\"],\n            env: {\n              ...process.env,\n              PAKE_E2E_TEST: \"1\",\n              PAKE_CREATE_APP: \"1\",\n            },\n          });\n\n          const checkConfigFiles = () => {\n            if (fs.existsSync(pakeDir)) {\n              const configFile = path.join(pakeDir, \"tauri.conf.json\");\n              const pakeConfigFile = path.join(pakeDir, \"pake.json\");\n\n              if (fs.existsSync(configFile) && fs.existsSync(pakeConfigFile)) {\n                try {\n                  const config = JSON.parse(\n                    fs.readFileSync(configFile, \"utf8\"),\n                  );\n                  const pakeConfig = JSON.parse(\n                    fs.readFileSync(pakeConfigFile, \"utf8\"),\n                  );\n\n                  if (\n                    config.productName === testName &&\n                    pakeConfig.windows[0].url === \"https://github.com/\"\n                  ) {\n                    child.kill(\"SIGTERM\");\n                    this.trackTempDir(pakeDir);\n                    console.log(\n                      \"✓ GitHub.com configuration files verified correctly\",\n                    );\n                    resolve(true);\n                    return true;\n                  }\n                } catch (error) {\n                  // Continue if config parsing fails\n                }\n              }\n            }\n            return false;\n          };\n\n          child.stdout.on(\"data\", (data) => {\n            const output = data.toString();\n            if (\n              output.includes(\"Installing package\") ||\n              output.includes(\"Building app\")\n            ) {\n              setTimeout(checkConfigFiles, 1000);\n            }\n          });\n\n          child.stderr.on(\"data\", (data) => {\n            const output = data.toString();\n            if (\n              output.includes(\"Installing package\") ||\n              output.includes(\"Building app\") ||\n              output.includes(\"Package installed\")\n            ) {\n              setTimeout(checkConfigFiles, 1000);\n            }\n          });\n\n          // Timeout after 20 seconds\n          setTimeout(() => {\n            child.kill(\"SIGTERM\");\n            this.trackTempDir(pakeDir);\n            reject(new Error(\"GitHub.com configuration verification timeout\"));\n          }, 40000);\n\n          child.on(\"error\", (error) => {\n            reject(\n              new Error(\n                `GitHub.com config verification error: ${error.message}`,\n              ),\n            );\n          });\n\n          child.stdin.end();\n        });\n      },\n      45000,\n    );\n  }\n\n  async runProxyTest() {\n    await this.runTest(\"Proxy Configuration\", async () => {\n      const command = `node \"${config.CLI_PATH}\" \"https://google.com\" --name \"ProxyTest\" --proxy-url \"http://127.0.0.1:7890\" --debug`;\n      // We just want to check if the command parses the proxy argument correctly\n      // It might fail to connect if no proxy is running, but that's expected\n      try {\n        execSync(`echo \"n\" | timeout 5s ${command} || true`, {\n          encoding: \"utf8\",\n          timeout: 8000,\n        });\n        return true;\n      } catch (error) {\n        // If it fails with \"connection refused\" or similar, it means it TRIED to use the proxy\n        return true;\n      }\n    });\n  }\n\n  async runLocalFileTest() {\n    await this.runTest(\"Local File Build Handling\", async () => {\n      const testFile = path.join(config.PROJECT_ROOT, \"test-local.html\");\n      fs.writeFileSync(\n        testFile,\n        \"<html><body><h1>Hello Pake</h1></body></html>\",\n      );\n      this.trackTempFile(testFile);\n\n      try {\n        const command = `node \"${config.CLI_PATH}\" \"${testFile}\" --name \"LocalApp\" --debug`;\n        // We just verify it accepts the local file path\n        execSync(`echo \"n\" | timeout 5s ${command} || true`, {\n          encoding: \"utf8\",\n          timeout: 8000,\n        });\n        return true;\n      } catch (error) {\n        // Validation failure is what we want to catch (if it rejected local files)\n        return !error.message.includes(\"Invalid URL\");\n      }\n    });\n  }\n\n  async runRealBuildTest() {\n    // Real build test that actually creates a complete app\n    await this.runTest(\n      \"Complete GitHub.com App Build\",\n      async () => {\n        return new Promise((resolve, reject) => {\n          const testName = \"GitHubRealBuild\";\n          // Platform-specific output files\n          const outputFiles = {\n            darwin: {\n              app: path.join(config.PROJECT_ROOT, `${testName}.app`),\n              installer: path.join(config.PROJECT_ROOT, `${testName}.dmg`),\n              bundleDir: path.join(\n                config.PROJECT_ROOT,\n                \"src-tauri/target/release/bundle\",\n              ),\n            },\n            linux: {\n              app: path.join(\n                config.PROJECT_ROOT,\n                `src-tauri/target/release/pake`,\n              ),\n              installer: path.join(\n                config.PROJECT_ROOT,\n                \"src-tauri/target/release/bundle/deb\",\n              ),\n              bundleDir: path.join(\n                config.PROJECT_ROOT,\n                \"src-tauri/target/release/bundle\",\n              ),\n            },\n            win32: {\n              app: path.join(\n                config.PROJECT_ROOT,\n                \"src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi\",\n              ),\n              installer: path.join(\n                config.PROJECT_ROOT,\n                \"src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi\",\n              ),\n              bundleDir: path.join(\n                config.PROJECT_ROOT,\n                \"src-tauri/target/x86_64-pc-windows-msvc/release/bundle\",\n              ),\n              // Alternative directories to check\n              altDirs: [\n                path.join(\n                  config.PROJECT_ROOT,\n                  \"src-tauri/target/release/bundle/msi\",\n                ),\n                path.join(\n                  config.PROJECT_ROOT,\n                  \"src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis\",\n                ),\n                path.join(\n                  config.PROJECT_ROOT,\n                  \"src-tauri/target/release/bundle/nsis\",\n                ),\n              ],\n            },\n          };\n          const platform = process.platform;\n          const expectedFiles = outputFiles[platform] || outputFiles.darwin;\n\n          console.log(\n            `[Integration] Starting real build test for GitHub.com...`,\n          );\n          console.log(`[Note] Platform: ${platform}`);\n          console.log(`[Note] Expected app directory: ${expectedFiles.app}`);\n          console.log(\n            `[Note] Expected installer directory: ${expectedFiles.installer}`,\n          );\n          if (expectedFiles.bundleDir) {\n            console.log(`[Note] Bundle directory: ${expectedFiles.bundleDir}`);\n          }\n          if (expectedFiles.altDirs) {\n            console.log(`[Note] Alternative directories to check:`);\n            expectedFiles.altDirs.forEach((dir, i) => {\n              console.log(`     ${i + 1}. ${dir}`);\n            });\n          }\n\n          const command = `node \"${config.CLI_PATH}\" \"https://github.com\" --name \"${testName}\" --width 1200 --height 800 --hide-title-bar`;\n\n          const child = spawn(command, {\n            shell: true,\n            cwd: config.PROJECT_ROOT,\n            stdio: [\"pipe\", \"pipe\", \"pipe\"],\n            env: {\n              ...process.env,\n              PAKE_CREATE_APP: \"1\",\n            },\n          });\n\n          let buildStarted = false;\n          let compilationStarted = false;\n\n          // Track progress without too much noise\n          child.stdout.on(\"data\", (data) => {\n            const output = data.toString();\n            if (output.includes(\"Installing package\")) {\n              console.log(\"   [Package] Installing dependencies...\");\n            }\n            if (output.includes(\"Building app\")) {\n              buildStarted = true;\n              console.log(\"   [Build]  Build started...\");\n            }\n            if (output.includes(\"Compiling\")) {\n              compilationStarted = true;\n              console.log(\"   ⚙️  Compiling...\");\n            }\n            if (output.includes(\"Bundling\")) {\n              console.log(\"   [Package] Bundling...\");\n            }\n            if (output.includes(\"Built application at:\")) {\n              console.log(\"   [PASS] Build completed!\");\n            }\n          });\n\n          let errorOutput = \"\";\n          child.stderr.on(\"data\", (data) => {\n            const output = data.toString();\n            if (output.includes(\"Building app\")) buildStarted = true;\n            if (output.includes(\"Compiling\")) compilationStarted = true;\n            if (output.includes(\"Finished\"))\n              console.log(\"   [PASS] Compilation finished!\");\n\n            // Capture error output for debugging\n            if (\n              output.includes(\"error:\") ||\n              output.includes(\"Error:\") ||\n              output.includes(\"ERROR\")\n            ) {\n              errorOutput += output;\n            }\n          });\n\n          // Real timeout - 8 minutes for actual build\n          const timeout = setTimeout(() => {\n            console.log(\n              \"   [Check] Build timeout reached, checking for output files...\",\n            );\n\n            const foundFiles = this.findBuildOutputFiles(testName, platform);\n\n            if (foundFiles.length > 0) {\n              console.log(\n                \"   [Success] Build completed successfully - found output files!\",\n              );\n              foundFiles.forEach((file) => {\n                console.log(`   [App] Found: ${file.path} (${file.type})`);\n              });\n              console.log(\"   [Success] Build artifacts tracked for cleanup\");\n              child.kill(\"SIGTERM\");\n              resolve(true);\n            } else {\n              console.log(\n                \"   [Warn]  Build process completed but no output files found\",\n              );\n              this.debugBuildDirectories();\n              child.kill(\"SIGTERM\");\n              reject(\n                new Error(\"Real build test timeout - no output files found\"),\n              );\n            }\n          }, 480000); // 8 minutes\n\n          child.on(\"close\", (code) => {\n            clearTimeout(timeout);\n\n            console.log(\n              `   [Status] Build process finished with exit code: ${code}`,\n            );\n\n            const foundFiles = this.findBuildOutputFiles(testName, platform);\n\n            if (foundFiles.length > 0) {\n              console.log(\n                \"   [Success] Real build test SUCCESS: Build file(s) generated!\",\n              );\n              foundFiles.forEach((file) => {\n                console.log(`   [App] ${file.type}: ${file.path}`);\n                try {\n                  const stats = fs.statSync(file.path);\n                  const size = (stats.size / 1024 / 1024).toFixed(1);\n                  console.log(`      Size: ${size}MB`);\n                } catch (error) {\n                  console.log(`      (Could not get file size)`);\n                }\n              });\n              console.log(\"   [Success] Build artifacts tracked for cleanup\");\n              // Track files for cleanup\n              foundFiles.forEach((f) => this.trackTempFile(f.path));\n              resolve(true);\n            } else if (code === 0 && buildStarted && compilationStarted) {\n              console.log(\n                \"   [Warn] Build process completed but no output files found\",\n              );\n              this.debugBuildDirectories();\n              resolve(false);\n            } else {\n              console.log(\n                `   [FAIL] Build process failed with exit code: ${code}`,\n              );\n              if (buildStarted) {\n                console.log(\n                  \"   [Status] Build was started but failed during execution\",\n                );\n                if (errorOutput.trim()) {\n                  console.log(\"   [Check] Error details:\");\n                  errorOutput.split(\"\\n\").forEach((line) => {\n                    if (line.trim()) console.log(`     ${line.trim()}`);\n                  });\n                }\n                this.debugBuildDirectories();\n              } else {\n                console.log(\n                  \"   [Status] Build failed before starting compilation\",\n                );\n                if (errorOutput.trim()) {\n                  console.log(\"   [Check] Error details:\");\n                  errorOutput.split(\"\\n\").forEach((line) => {\n                    if (line.trim()) console.log(`     ${line.trim()}`);\n                  });\n                }\n              }\n              reject(new Error(`Real build test failed with code ${code}`));\n            }\n          });\n\n          child.on(\"error\", (error) => {\n            clearTimeout(timeout);\n            reject(\n              new Error(`Real build test process error: ${error.message}`),\n            );\n          });\n\n          child.stdin.end();\n        });\n      },\n      500000, // 8+ minutes timeout\n    );\n  }\n\n  async runMultiArchBuildTest() {\n    // Multi-arch build test specifically for macOS\n    await this.runTest(\n      \"Multi-Arch GitHub.com Build (Universal Binary)\",\n      async () => {\n        return new Promise((resolve, reject) => {\n          const testName = \"GitHubMultiArch\";\n          const appFile = path.join(config.PROJECT_ROOT, `${testName}.app`);\n          const dmgFile = path.join(config.PROJECT_ROOT, `${testName}.dmg`);\n\n          console.log(\n            `[Integration] Starting multi-arch build test for GitHub.com...`,\n          );\n          console.log(`[Note] Expected output: ${appFile}`);\n          console.log(\n            `[Build]  Building Universal Binary (Intel + Apple Silicon)`,\n          );\n\n          const command = `node \"${config.CLI_PATH}\" \"https://github.com\" --name \"${testName}\" --width 1200 --height 800 --hide-title-bar --multi-arch`;\n\n          const child = spawn(command, {\n            shell: true,\n            cwd: config.PROJECT_ROOT,\n            stdio: [\"pipe\", \"pipe\", \"pipe\"],\n            env: {\n              ...process.env,\n              PAKE_CREATE_APP: \"1\",\n              HDIUTIL_QUIET: \"1\",\n              HDIUTIL_NO_AUTOOPEN: \"1\",\n            },\n          });\n\n          let buildStarted = false;\n          let compilationStarted = false;\n\n          // Track progress\n          child.stdout.on(\"data\", (data) => {\n            const output = data.toString();\n            if (output.includes(\"Installing package\")) {\n              console.log(\"   [Package] Installing dependencies...\");\n            }\n            if (output.includes(\"Building app\")) {\n              buildStarted = true;\n              console.log(\"   [Build]  Multi-arch build started...\");\n            }\n            if (output.includes(\"Compiling\")) {\n              compilationStarted = true;\n              console.log(\"   ⚙️  Compiling for multiple architectures...\");\n            }\n            if (\n              output.includes(\"universal-apple-darwin\") ||\n              output.includes(\"Universal\")\n            ) {\n              console.log(\"   [Multi] Universal binary target detected\");\n            }\n            if (output.includes(\"Bundling\")) {\n              console.log(\"   [Package] Bundling universal binary...\");\n            }\n            if (output.includes(\"Built application at:\")) {\n              console.log(\"   [PASS] Multi-arch build completed!\");\n            }\n          });\n\n          child.stderr.on(\"data\", (data) => {\n            const output = data.toString();\n            if (output.includes(\"Building app\")) buildStarted = true;\n            if (output.includes(\"Compiling\")) compilationStarted = true;\n            if (output.includes(\"Finished\"))\n              console.log(\"   [PASS] Multi-arch compilation finished!\");\n          });\n\n          // Multi-arch builds take longer - 20 minutes timeout\n          const timeout = setTimeout(() => {\n            console.log(\n              \"   [Check] Multi-arch build timeout reached, checking for output files...\",\n            );\n\n            const foundFiles = this.findBuildOutputFiles(testName, \"darwin\");\n\n            if (foundFiles.length > 0) {\n              console.log(\n                \"   [Success] Multi-arch build completed successfully!\",\n              );\n              foundFiles.forEach((file) => {\n                console.log(`   [App] Found: ${file.path} (${file.type})`);\n              });\n              console.log(\n                \"   [Multi] Universal binary preserved for inspection\",\n              );\n              child.kill(\"SIGTERM\");\n              resolve(true);\n            } else {\n              console.log(\n                \"   [FAIL] Multi-arch build timeout - no output files generated\",\n              );\n              this.debugBuildDirectories(\n                {\n                  app: appFile,\n                  installer: dmgFile,\n                  bundleDir: path.join(\n                    config.PROJECT_ROOT,\n                    \"src-tauri/target/universal-apple-darwin/release/bundle\",\n                  ),\n                },\n                \"darwin\",\n              );\n              child.kill(\"SIGTERM\");\n              reject(new Error(\"Multi-arch build test timeout\"));\n            }\n          }, 1200000); // 20 minutes for multi-arch\n\n          child.on(\"close\", (code) => {\n            clearTimeout(timeout);\n\n            console.log(\n              `   [Status] Multi-arch build process finished with exit code: ${code}`,\n            );\n\n            const foundFiles = this.findBuildOutputFiles(testName, \"darwin\");\n\n            if (foundFiles.length > 0) {\n              console.log(\n                \"   [Success] Multi-arch build test SUCCESS: Universal binary generated!\",\n              );\n              foundFiles.forEach((file) => {\n                console.log(`   [App] ${file.type}: ${file.path}`);\n              });\n              console.log(\n                \"   [Multi] Universal binary preserved for inspection\",\n              );\n\n              // Verify it's actually a universal binary\n              const appFile = foundFiles.find((f) => f.type.includes(\"App\"));\n              if (appFile) {\n                try {\n                  const binaryPath = path.join(\n                    appFile.path,\n                    \"Contents/MacOS/pake\",\n                  );\n                  const fileOutput = execSync(`file \"${binaryPath}\"`, {\n                    encoding: \"utf8\",\n                  });\n                  if (fileOutput.includes(\"universal binary\")) {\n                    console.log(\n                      \"   [PASS] Verified: Universal binary created successfully\",\n                    );\n                  } else {\n                    console.log(\n                      \"   [Warn]  Note: Binary architecture:\",\n                      fileOutput.trim(),\n                    );\n                  }\n                } catch (error) {\n                  console.log(\n                    \"   [Warn]  Could not verify binary architecture\",\n                  );\n                }\n              }\n\n              resolve(true);\n            } else if (buildStarted && compilationStarted) {\n              // If build started and compilation happened, but no output files found\n              console.log(\n                \"   [Warn]  Multi-arch build process completed but no output files found\",\n              );\n              this.debugBuildDirectories(\n                {\n                  app: appFile,\n                  installer: dmgFile,\n                  bundleDir: path.join(\n                    config.PROJECT_ROOT,\n                    \"src-tauri/target/universal-apple-darwin/release/bundle\",\n                  ),\n                },\n                \"darwin\",\n              );\n              resolve(false);\n            } else {\n              // Only reject if the build never started or failed early\n              reject(\n                new Error(`Multi-arch build test failed with code ${code}`),\n              );\n            }\n          });\n\n          child.on(\"error\", (error) => {\n            clearTimeout(timeout);\n            reject(\n              new Error(\n                `Multi-arch build test process error: ${error.message}`,\n              ),\n            );\n          });\n\n          child.stdin.end();\n        });\n      },\n      1250000, // 20+ minutes timeout\n    );\n  }\n\n  // Simplified build output detection - if build succeeds, check for any output files\n  findBuildOutputFiles(testName, platform) {\n    const foundFiles = [];\n    console.log(`   [Check] Checking for ${platform} build outputs...`);\n\n    // Simple approach: look for common build artifacts in project root and common locations\n    const searchLocations = [\n      // Always check project root first (most builds output there)\n      config.PROJECT_ROOT,\n      // Platform-specific bundle directories\n      ...(platform === \"linux\"\n        ? [\n            path.join(config.PROJECT_ROOT, \"src-tauri/target/release\"),\n            path.join(\n              config.PROJECT_ROOT,\n              \"src-tauri/target/release/bundle/deb\",\n            ),\n          ]\n        : []),\n      ...(platform === \"win32\"\n        ? [\n            path.join(\n              config.PROJECT_ROOT,\n              \"src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi\",\n            ),\n            path.join(\n              config.PROJECT_ROOT,\n              \"src-tauri/target/release/bundle/msi\",\n            ),\n          ]\n        : []),\n      ...(platform === \"darwin\"\n        ? [\n            path.join(\n              config.PROJECT_ROOT,\n              \"src-tauri/target/release/bundle/macos\",\n            ),\n            path.join(\n              config.PROJECT_ROOT,\n              \"src-tauri/target/release/bundle/dmg\",\n            ),\n            path.join(\n              config.PROJECT_ROOT,\n              \"src-tauri/target/universal-apple-darwin/release/bundle\",\n            ),\n          ]\n        : []),\n    ];\n\n    // Define what we're looking for based on platform\n    const buildPatterns = {\n      win32: [\".msi\", \".exe\"],\n      linux: [\".deb\", \".appimage\"],\n      darwin: [\".dmg\", \".app\"],\n    };\n\n    const patterns = buildPatterns[platform] || buildPatterns.darwin;\n\n    for (const location of searchLocations) {\n      if (!fs.existsSync(location)) {\n        continue;\n      }\n\n      console.log(\n        `      [Dir] Checking: ${path.relative(config.PROJECT_ROOT, location)}`,\n      );\n\n      try {\n        const items = fs.readdirSync(location);\n        const buildFiles = items.filter((item) => {\n          const itemPath = path.join(location, item);\n          const stats = fs.statSync(itemPath);\n\n          // Skip common non-build directories\n          if (\n            stats.isDirectory() &&\n            [\".git\", \".github\", \"node_modules\", \"src\", \"bin\", \"tests\"].includes(\n              item,\n            )\n          ) {\n            return false;\n          }\n\n          // Check if it's a build artifact we care about\n          const lowerItem = item.toLowerCase();\n          return (\n            patterns.some((pattern) => lowerItem.endsWith(pattern)) ||\n            lowerItem.includes(testName.toLowerCase()) ||\n            (lowerItem.includes(\"github\") && !item.startsWith(\".\")) || // Avoid .github directory\n            (platform === \"linux\" && item === \"pake\")\n          ); // Linux binary\n        });\n\n        buildFiles.forEach((file) => {\n          const fullPath = path.join(location, file);\n          const stats = fs.statSync(fullPath);\n\n          let fileType = \"Build Artifact\";\n          if (file.endsWith(\".msi\")) fileType = \"MSI Installer\";\n          else if (file.endsWith(\".exe\")) fileType = \"Windows Executable\";\n          else if (file.endsWith(\".deb\")) fileType = \"DEB Package\";\n          else if (file.endsWith(\".appimage\")) fileType = \"AppImage\";\n          else if (file.endsWith(\".dmg\")) fileType = \"DMG Image\";\n          else if (file.endsWith(\".app\"))\n            fileType = stats.isDirectory() ? \"macOS App Bundle\" : \"macOS App\";\n          else if (file === \"pake\") fileType = \"Linux Binary\";\n\n          foundFiles.push({\n            path: fullPath,\n            type: fileType,\n            size: stats.isFile() ? stats.size : 0,\n          });\n\n          const size =\n            stats.isFile() && stats.size > 0\n              ? ` (${(stats.size / 1024 / 1024).toFixed(1)}MB)`\n              : \"\";\n          console.log(`      [PASS] Found ${fileType}: ${file}${size}`);\n        });\n\n        // For Linux, also check inside architecture directories\n        if (platform === \"linux\") {\n          const archDirs = items.filter(\n            (item) => item.includes(\"amd64\") || item.includes(\"x86_64\"),\n          );\n\n          for (const archDir of archDirs) {\n            const archPath = path.join(location, archDir);\n            if (fs.statSync(archPath).isDirectory()) {\n              console.log(`      [Check] Checking arch directory: ${archDir}`);\n              try {\n                const archFiles = fs.readdirSync(archPath);\n                archFiles\n                  .filter((f) => f.endsWith(\".deb\"))\n                  .forEach((debFile) => {\n                    const debPath = path.join(archPath, debFile);\n                    const debStats = fs.statSync(debPath);\n                    foundFiles.push({\n                      path: debPath,\n                      type: \"DEB Package\",\n                      size: debStats.size,\n                    });\n                    const size = `(${(debStats.size / 1024 / 1024).toFixed(1)}MB)`;\n                    console.log(\n                      `      [PASS] Found DEB Package: ${debFile} ${size}`,\n                    );\n                  });\n              } catch (error) {\n                console.log(\n                  `      [Warn]  Could not check ${archDir}: ${error.message}`,\n                );\n              }\n            }\n          }\n        }\n      } catch (error) {\n        console.log(\n          `      [Warn]  Could not read ${location}: ${error.message}`,\n        );\n      }\n    }\n\n    console.log(`   [Status] Found ${foundFiles.length} build artifact(s)`);\n    return foundFiles;\n  }\n\n  // Debug function to show directory structure\n  debugBuildDirectories() {\n    console.log(\"   [Check] Debug: Analyzing build directories...\");\n\n    const targetDir = path.join(config.PROJECT_ROOT, \"src-tauri/target\");\n    if (fs.existsSync(targetDir)) {\n      console.log(\"   [Check] Target directory structure:\");\n      try {\n        this.listTargetContents(targetDir);\n      } catch (error) {\n        console.log(\n          `   [Warn]  Could not list target contents: ${error.message}`,\n        );\n      }\n    } else {\n      console.log(`   [FAIL] Target directory does not exist: ${targetDir}`);\n    }\n\n    // Check project root for direct outputs\n    console.log(\"   [Check] Project root files:\");\n    try {\n      const rootFiles = fs\n        .readdirSync(config.PROJECT_ROOT)\n        .filter(\n          (file) =>\n            file.endsWith(\".app\") ||\n            file.endsWith(\".dmg\") ||\n            file.endsWith(\".msi\") ||\n            file.endsWith(\".deb\") ||\n            file.endsWith(\".exe\"),\n        );\n      if (rootFiles.length > 0) {\n        rootFiles.forEach((file) => {\n          console.log(`      [App] ${file}`);\n        });\n      } else {\n        console.log(`      (No build artifacts in project root)`);\n      }\n    } catch (error) {\n      console.log(`      [FAIL] Error reading project root: ${error.message}`);\n    }\n  }\n\n  listTargetContents(targetDir, maxDepth = 3, currentDepth = 0) {\n    if (currentDepth >= maxDepth) return;\n\n    try {\n      const items = fs.readdirSync(targetDir);\n      items.forEach((item) => {\n        const fullPath = path.join(targetDir, item);\n        const relativePath = path.relative(config.PROJECT_ROOT, fullPath);\n        const indent = \"     \".repeat(currentDepth + 1);\n\n        try {\n          const stats = fs.statSync(fullPath);\n          if (stats.isDirectory()) {\n            console.log(`${indent}[Dir] ${relativePath}/`);\n            // Show more directories for Windows debugging\n            if (\n              item === \"bundle\" ||\n              item === \"release\" ||\n              item === \"msi\" ||\n              item === \"nsis\" ||\n              item.includes(\"windows\") ||\n              item.includes(\"msvc\")\n            ) {\n              this.listTargetContents(fullPath, maxDepth, currentDepth + 1);\n            }\n          } else {\n            const size =\n              stats.size > 0\n                ? ` (${(stats.size / 1024 / 1024).toFixed(1)}MB)`\n                : \"\";\n            console.log(`${indent}[File] ${relativePath}${size}`);\n          }\n        } catch (statError) {\n          console.log(`${indent}❓ ${relativePath} (cannot stat)`);\n        }\n      });\n    } catch (error) {\n      console.log(\n        `     [Warn]  Could not list contents of ${targetDir}: ${error.message}`,\n      );\n    }\n  }\n\n  trackTempFile(filepath) {\n    this.tempFiles.push(filepath);\n  }\n\n  trackTempDir(dirpath) {\n    this.tempDirs.push(dirpath);\n  }\n\n  cleanupTempIcons() {\n    // Clean up temporary icon files generated during tests\n    const iconsDir = path.join(config.PROJECT_ROOT, \"src-tauri/icons\");\n    const testNames = [\n      \"urltest\",\n      \"githubapp\",\n      \"githubmultiarch\",\n      \"githubconfigtest\",\n      \"localapp\",\n      \"proxytest\",\n    ];\n\n    testNames.forEach((name) => {\n      const iconPath = path.join(iconsDir, `${name}.icns`);\n      try {\n        if (fs.existsSync(iconPath)) {\n          fs.unlinkSync(iconPath);\n          console.log(`   [Clean] Cleaned up temporary icon: ${name}.icns`);\n        }\n      } catch (error) {\n        console.warn(`Warning: Could not clean up icon ${iconPath}`);\n      }\n    });\n  }\n\n  cleanup() {\n    console.log(\"\\nCleaning up test artifacts...\");\n\n    // Clean up temporary icon files generated during tests\n    this.cleanupTempIcons();\n\n    // Clean up tracked files\n    this.tempFiles.forEach((file) => {\n      try {\n        if (fs.existsSync(file)) {\n          if (fs.statSync(file).isDirectory()) {\n            fs.rmSync(file, { recursive: true, force: true });\n          } else {\n            fs.unlinkSync(file);\n          }\n        }\n      } catch (error) {\n        // Ignore errors during cleanup\n      }\n    });\n\n    this.tempDirs.forEach((dir) => {\n      try {\n        if (fs.existsSync(dir)) {\n          fs.rmSync(dir, { recursive: true, force: true });\n        }\n      } catch (error) {\n        // Ignore errors\n      }\n    });\n\n    // Aggressive cleanup of known test artifacts in project root\n    const testPatterns = [\n      \"GitHubRealBuild\",\n      \"GitHubApp\",\n      \"GitHubMultiArch\",\n      \"GitHubConfigTest\",\n      \"LocalApp\",\n      \"ProxyTest\",\n      \"URLTest\",\n    ];\n\n    const extensions = [\".app\", \".dmg\", \".msi\", \".deb\", \".exe\", \".AppImage\"];\n\n    try {\n      const files = fs.readdirSync(config.PROJECT_ROOT);\n      files.forEach((file) => {\n        // Check if file matches any test name pattern and extension\n        const isTestArtifact =\n          testPatterns.some((pattern) => file.includes(pattern)) &&\n          (extensions.some((ext) => file.endsWith(ext)) ||\n            (!file.includes(\".\") &&\n              !fs\n                .statSync(path.join(config.PROJECT_ROOT, file))\n                .isDirectory())); // Linux binary often has no extension\n\n        if (isTestArtifact) {\n          const fullPath = path.join(config.PROJECT_ROOT, file);\n          console.log(`   [Clean] Removing artifact: ${file}`);\n          fs.rmSync(fullPath, { recursive: true, force: true });\n        }\n      });\n\n      // Also clean src-tauri/.pake directory if it exists\n      const pakeDir = path.join(config.PROJECT_ROOT, \"src-tauri\", \".pake\");\n      if (fs.existsSync(pakeDir)) {\n        fs.rmSync(pakeDir, { recursive: true, force: true });\n      }\n    } catch (e) {\n      console.warn(\"   [Warn]  Cleanup warning:\", e.message);\n    }\n  }\n\n  displayFinalResults() {\n    const passed = this.results.filter((r) => r.passed).length;\n    const total = this.results.length;\n\n    console.log(\"\\nOverall Test Summary\");\n    console.log(\"====================\");\n    console.log(`Total: ${passed}/${total} tests passed`);\n\n    if (passed === total) {\n      console.log(\"All tests passed! CLI is ready for use.\\n\");\n    } else {\n      console.log(\n        `[FAIL] ${total - passed} test(s) failed. Please check the issues above.\\n`,\n      );\n\n      // Show failed tests\n      const failed = this.results.filter((r) => !r.passed);\n      if (failed.length > 0) {\n        console.log(\"Failed tests:\");\n        failed.forEach((result) => {\n          const error = result.error ? ` (${result.error})` : \"\";\n          console.log(`  [FAIL] ${result.name}${error}`);\n        });\n        console.log();\n      }\n    }\n  }\n}\n\nimport ReleaseBuildTest from \"./release.js\";\n\n// Command line interface\nconst args = process.argv.slice(2);\n\n// Complete test suite by default - no more smart modes\nconst options = {\n  unit: !args.includes(\"--no-unit\"),\n  integration: !args.includes(\"--no-integration\"),\n  builder: !args.includes(\"--no-builder\"),\n  pakeCliTests: args.includes(\"--pake-cli\"),\n  e2e: args.includes(\"--e2e\"),\n  realBuild: !args.includes(\"--no-build\"), // Always include real build test\n  quick: false,\n};\n\n// Help message\nif (args.includes(\"--help\") || args.includes(\"-h\")) {\n  console.log(`\n[Run] Pake CLI Test Suite\n\nUsage: npm test [-- options]\n\nComplete Test Suite (Default):\n  pnpm test                   # Run complete test suite with real build (8-12 minutes)\n\nTest Components:\n  [PASS] Unit Tests               # CLI commands, validation, response time\n  [PASS] Integration Tests        # Process spawning, file permissions, dependencies\n  [PASS] Builder Tests           # Platform detection, architecture, file naming\n  [PASS] Real Build Test         # Complete GitHub.com app build with packaging\n\nOptional Components:\n  --e2e          Add end-to-end configuration tests\n  --pake-cli     Add pake-cli GitHub Actions tests\n  --release      Run release workflow tests (Twitter/WeRead) - Slow!\n\nSkip Components (if needed):\n  --no-unit      Skip unit tests\n  --no-integration  Skip integration tests\n  --no-builder   Skip builder tests\n  --no-build     Skip real build test\n\nExamples:\n  npm test                         # Complete test suite (recommended)\n  npm test -- --release           # Run everything including release workflow\n  pnpm test -- --no-build         # Skip real build (faster for development)\n\nEnvironment:\n  CI=1              # Enable CI mode\n  DEBUG=1           # Enable debug output\n  PAKE_CREATE_APP=1 # Allow app creation in tests\n`);\n  process.exit(0);\n}\n\n// Run tests\nconst runner = new PakeTestRunner();\nrunner\n  .runAll(options)\n  .then(async (success) => {\n    // Run release workflow tests as part of the standard suite\n    // We skip this if builder tests are explicitly disabled (often used for quick checks)\n    if (success && options.realBuild) {\n      console.log(\"\\n[Package] Running Release Workflow Test...\");\n      console.log(\n        \"   (This mimics the GitHub Actions release process for popular apps)\",\n      );\n\n      // Pass skipCliBuild=true since \"npm test\" already builds the CLI\n      const releaseTester = new ReleaseBuildTest();\n      const releaseSuccess = await releaseTester.run({ skipCliBuild: true });\n\n      if (!releaseSuccess) {\n        console.error(\"\\n[FAIL] Release workflow tests failed\");\n        process.exit(1);\n      }\n    }\n\n    process.exit(success ? 0 : 1);\n  })\n  .catch((error) => {\n    console.error(\"Test runner failed:\", error);\n    process.exit(1);\n  });\n\nexport default runner;\n"
  },
  {
    "path": "tests/integration/workflow-paths.test.js",
    "content": "/**\n * Workflow Path Integration Tests\n *\n * These tests verify that the paths used in GitHub Actions workflows\n * match the actual output paths from the CLI builders.\n */\n\nimport { describe, it, expect } from \"vitest\";\nimport path from \"path\";\n\ndescribe(\"Workflow path integration\", () => {\n  describe(\"Platform-specific output paths\", () => {\n    it(\"should match Linux output paths\", () => {\n      // Expected paths based on LinuxBuilder behavior\n      const linuxPaths = {\n        deb: {\n          // CLI copies to project root\n          primary: \"appname.deb\",\n          // Fallback location in bundle directory\n          fallback: \"src-tauri/target/release/bundle/deb\",\n        },\n        appimage: {\n          primary: \"appname.AppImage\",\n          fallback: \"src-tauri/target/release/bundle/appimage\",\n        },\n        rpm: {\n          primary: \"appname.rpm\",\n          fallback: \"src-tauri/target/release/bundle/rpm\",\n        },\n      };\n\n      // Verify paths are defined\n      expect(linuxPaths.deb.primary).toBeTruthy();\n      expect(linuxPaths.deb.fallback).toBeTruthy();\n      expect(linuxPaths.appimage.primary).toBeTruthy();\n      expect(linuxPaths.appimage.fallback).toBeTruthy();\n    });\n\n    it(\"should match Windows output paths\", () => {\n      // Expected paths based on WinBuilder behavior\n      const windowsPaths = {\n        msi: {\n          // For x64 builds, files are in architecture-specific directory\n          architectureSpecific:\n            \"src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi\",\n          // Fallback to generic path\n          generic: \"src-tauri/target/release/bundle/msi\",\n        },\n      };\n\n      expect(windowsPaths.msi.architectureSpecific).toBeTruthy();\n      expect(windowsPaths.msi.generic).toBeTruthy();\n    });\n\n    it(\"should match macOS output paths\", () => {\n      // Expected paths based on MacBuilder behavior\n      const macosPaths = {\n        dmg: {\n          // CLI copies to project root\n          primary: \"appname.dmg\",\n          // Universal builds use universal-apple-darwin target\n          universalBundle:\n            \"src-tauri/target/universal-apple-darwin/release/bundle/dmg\",\n          // Regular builds\n          genericBundle: \"src-tauri/target/release/bundle/dmg\",\n        },\n        app: {\n          primary: \"appname.app\",\n          universalBundle:\n            \"src-tauri/target/universal-apple-darwin/release/bundle/macos\",\n          genericBundle: \"src-tauri/target/release/bundle/macos\",\n        },\n      };\n\n      expect(macosPaths.dmg.primary).toBeTruthy();\n      expect(macosPaths.dmg.universalBundle).toBeTruthy();\n      expect(macosPaths.app.primary).toBeTruthy();\n      expect(macosPaths.app.universalBundle).toBeTruthy();\n    });\n  });\n\n  describe(\"Multi-target scenarios\", () => {\n    it(\"should handle Linux multi-target builds\", () => {\n      const targets = \"deb,appimage\";\n      const parsedTargets = targets.split(\",\").map((t) => t.trim());\n\n      expect(parsedTargets).toEqual([\"deb\", \"appimage\"]);\n      expect(parsedTargets).toHaveLength(2);\n    });\n\n    it(\"should handle targets with spaces\", () => {\n      const targets = \"deb, appimage, rpm\";\n      const parsedTargets = targets.split(\",\").map((t) => t.trim());\n\n      expect(parsedTargets).toEqual([\"deb\", \"appimage\", \"rpm\"]);\n      expect(parsedTargets).toHaveLength(3);\n    });\n\n    it(\"should filter valid targets\", () => {\n      const targets = \"deb,invalid,appimage\";\n      const parsedTargets = targets.split(\",\").map((t) => t.trim());\n      const validTargets = [\"deb\", \"appimage\", \"rpm\"];\n      const filtered = parsedTargets.filter((t) => validTargets.includes(t));\n\n      expect(filtered).toEqual([\"deb\", \"appimage\"]);\n      expect(filtered).not.toContain(\"invalid\");\n    });\n  });\n\n  describe(\"Architecture-specific paths\", () => {\n    it(\"should construct correct Windows x64 path\", () => {\n      const basePath = \"src-tauri/target\";\n      const arch = \"x86_64-pc-windows-msvc\";\n      const mode = \"release\";\n      const bundleType = \"msi\";\n\n      const fullPath = path.join(basePath, arch, mode, \"bundle\", bundleType);\n\n      expect(fullPath).toContain(\"x86_64-pc-windows-msvc\");\n      expect(fullPath).toContain(\"release\");\n      expect(fullPath).toContain(\"msi\");\n    });\n\n    it(\"should construct correct macOS universal path\", () => {\n      const basePath = \"src-tauri/target\";\n      const arch = \"universal-apple-darwin\";\n      const mode = \"release\";\n      const bundleType = \"dmg\";\n\n      const fullPath = path.join(basePath, arch, mode, \"bundle\", bundleType);\n\n      expect(fullPath).toContain(\"universal-apple-darwin\");\n      expect(fullPath).toContain(\"release\");\n      expect(fullPath).toContain(\"dmg\");\n    });\n\n    it(\"should construct correct Linux arm64 path\", () => {\n      const basePath = \"src-tauri/target\";\n      const arch = \"aarch64-unknown-linux-gnu\";\n      const mode = \"release\";\n      const bundleType = \"deb\";\n\n      const fullPath = path.join(basePath, arch, mode, \"bundle\", bundleType);\n\n      expect(fullPath).toContain(\"aarch64-unknown-linux-gnu\");\n      expect(fullPath).toContain(\"release\");\n      expect(fullPath).toContain(\"deb\");\n    });\n  });\n\n  describe(\"File naming patterns\", () => {\n    it(\"should match Linux DEB naming pattern\", () => {\n      // Format: {name}_{version}_{arch}.deb\n      const pattern = /^[\\w-]+_\\d+\\.\\d+\\.\\d+_(amd64|arm64)\\.deb$/;\n\n      expect(\"myapp_1.0.0_amd64.deb\").toMatch(pattern);\n      expect(\"my-app_2.5.1_arm64.deb\").toMatch(pattern);\n      expect(\"invalid.deb\").not.toMatch(pattern);\n    });\n\n    it(\"should match Windows MSI naming pattern\", () => {\n      // Format: {name}_{version}_{arch}_{language}.msi\n      const pattern = /^[\\w-]+_\\d+\\.\\d+\\.\\d+_(x64|arm64)_[\\w-]+\\.msi$/;\n\n      expect(\"myapp_1.0.0_x64_en-US.msi\").toMatch(pattern);\n      expect(\"my-app_2.5.1_arm64_zh-CN.msi\").toMatch(pattern);\n      expect(\"invalid.msi\").not.toMatch(pattern);\n    });\n\n    it(\"should match macOS DMG naming pattern\", () => {\n      // Format: {name}_{version}_{arch}.dmg\n      const pattern = /^[\\w-]+_\\d+\\.\\d+\\.\\d+_(universal|x64|aarch64)\\.dmg$/;\n\n      expect(\"myapp_1.0.0_universal.dmg\").toMatch(pattern);\n      expect(\"my-app_2.5.1_x64.dmg\").toMatch(pattern);\n      expect(\"my-app_3.0.0_aarch64.dmg\").toMatch(pattern);\n      expect(\"invalid.dmg\").not.toMatch(pattern);\n    });\n  });\n\n  describe(\"Path traversal safety\", () => {\n    it(\"should handle paths without directory traversal\", () => {\n      const safePaths = [\n        \"src-tauri/target/release/bundle/msi\",\n        \"output/windows\",\n        \"dist/cli.js\",\n      ];\n\n      safePaths.forEach((p) => {\n        expect(p).not.toContain(\"..\");\n        expect(p).not.toMatch(/\\.\\.[/\\\\]/);\n      });\n    });\n\n    it(\"should normalize paths correctly\", () => {\n      const inputPath = \"src-tauri/target/../target/release/bundle\";\n      const normalized = path.normalize(inputPath);\n\n      // path.normalize should resolve the .. reference\n      expect(normalized).not.toContain(\"..\");\n      expect(normalized).toContain(\"target\");\n      expect(normalized).toContain(\"release\");\n    });\n  });\n\n  describe(\"Cross-platform path handling\", () => {\n    it(\"should use correct path separator\", () => {\n      const joined = path.join(\"src-tauri\", \"target\", \"release\");\n\n      // Should not contain wrong separators\n      if (path.sep === \"/\") {\n        expect(joined).not.toContain(\"\\\\\");\n      } else {\n        expect(joined).not.toContain(\"/\");\n      }\n    });\n\n    it(\"should handle paths with both separators\", () => {\n      // This can happen when paths come from different sources\n      const mixedPath = \"src-tauri\\\\target/release\";\n      const normalized = path.normalize(mixedPath);\n\n      // After normalization, should use consistent separator\n      const parts = normalized.split(path.sep);\n      expect(parts.length).toBeGreaterThan(1);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/release.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Release Build Test\n *\n * Tests the actual release workflow by building 2 sample apps.\n * Validates the complete packaging process.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { execSync } from \"child_process\";\nimport { PROJECT_ROOT } from \"./config.js\";\n\nconst GREEN = \"\\x1b[32m\";\nconst YELLOW = \"\\x1b[33m\";\nconst BLUE = \"\\x1b[34m\";\nconst RED = \"\\x1b[31m\";\nconst NC = \"\\x1b[0m\";\n\n// Fixed test apps for consistent testing\nconst TEST_APPS = [\"weread\", \"twitter\"];\n\nclass ReleaseBuildTest {\n  constructor() {\n    this.startTime = Date.now();\n  }\n\n  log(level, message) {\n    const colors = { INFO: GREEN, WARN: YELLOW, ERROR: RED, DEBUG: BLUE };\n    const timestamp = new Date().toLocaleTimeString();\n    console.log(`${colors[level] || NC}[${timestamp}] ${message}${NC}`);\n  }\n\n  async getAppConfig(appName) {\n    const configPath = path.join(PROJECT_ROOT, \"default_app_list.json\");\n    const apps = JSON.parse(fs.readFileSync(configPath, \"utf8\"));\n\n    let config = apps.find((app) => app.name === appName);\n\n    // All test apps should be in default_app_list.json\n    if (!config) {\n      throw new Error(`App \"${appName}\" not found in default_app_list.json`);\n    }\n\n    return config;\n  }\n\n  async buildApp(appName) {\n    this.log(\"INFO\", `🔨 Building ${appName}...`);\n\n    const config = await this.getAppConfig(appName);\n    if (!config) {\n      throw new Error(`App config not found: ${appName}`);\n    }\n\n    // Set environment variables\n    process.env.NAME = config.name;\n    process.env.TITLE = config.title;\n    process.env.NAME_ZH = config.name_zh;\n    process.env.URL = config.url;\n\n    try {\n      // Build config\n      this.log(\"INFO\", `\\n📦 Building ${appName}...\\n`);\n\n      // Build the app using CLI directly\n      this.log(\"DEBUG\", \"Building app package...\");\n      const commonArgs = `${config.new_window ? \"--new-window \" : \"\"}--iterative-build --debug`;\n      const cmd = `node dist/cli.js ${config.url} --name ${config.name} --icon ${config.icon} ${commonArgs}`;\n\n      try {\n        execSync(cmd, {\n          stdio: \"pipe\",\n          timeout: 480000, // 8 minutes\n          env: { ...process.env, PAKE_CREATE_APP: \"1\" },\n        });\n\n        // Check files immediately after build\n        const outputFiles = this.findOutputFiles(config.name);\n        if (outputFiles.length === 0) {\n          throw new Error(\"No output files generated\");\n        }\n      } catch (buildError) {\n        throw new Error(`Build failed: ${buildError.message}`);\n      }\n\n      // Always return true - release test just needs to verify the process works\n      this.log(\"INFO\", `✅ Successfully built ${config.title}`);\n      return true;\n    } catch (error) {\n      this.log(\"ERROR\", `❌ Failed to build ${config.title}: ${error.message}`);\n      return false;\n    }\n  }\n\n  findOutputFiles(appName) {\n    const files = [];\n\n    // Check for direct output files (created by PAKE_CREATE_APP=1)\n    const directPatterns = [\n      `${appName}.dmg`,\n      `${appName}.app`,\n      `${appName}.msi`,\n      `${appName}.deb`,\n      `${appName}.AppImage`,\n    ];\n\n    // Use Node.js fs instead of Unix find command for cross-platform compatibility\n    for (const pattern of directPatterns) {\n      try {\n        const rootPath = path.join(PROJECT_ROOT);\n        if (fs.existsSync(rootPath)) {\n          const items = fs.readdirSync(rootPath);\n          const matching = items.filter((item) => item === pattern);\n          matching.forEach((item) => {\n            files.push(path.join(rootPath, item));\n          });\n        }\n      } catch (error) {\n        // Ignore errors\n      }\n    }\n\n    // Also check bundle directories for app and dmg files\n    const bundleLocations = [\n      `src-tauri/target/release/bundle/macos/${appName}.app`,\n      `src-tauri/target/release/bundle/dmg/${appName}.dmg`,\n      `src-tauri/target/universal-apple-darwin/release/bundle/macos/${appName}.app`,\n      `src-tauri/target/universal-apple-darwin/release/bundle/dmg/${appName}.dmg`,\n      `src-tauri/target/release/bundle/deb/${appName}_*.deb`,\n      `src-tauri/target/release/bundle/msi/${appName}_*.msi`,\n      `src-tauri/target/release/bundle/appimage/${appName}_*.AppImage`,\n    ];\n\n    for (const location of bundleLocations) {\n      try {\n        if (location.includes(\"*\")) {\n          // Handle wildcard patterns using Node.js\n          const dir = path.dirname(location);\n          const pattern = path.basename(location);\n          const fullDir = path.join(PROJECT_ROOT, dir);\n\n          if (fs.existsSync(fullDir)) {\n            const items = fs.readdirSync(fullDir);\n            // Convert glob pattern to regex\n            const regex = new RegExp(\n              \"^\" + pattern.replace(/\\*/g, \".*\").replace(/\\?/g, \".\") + \"$\",\n            );\n            const matching = items.filter((item) => regex.test(item));\n            matching.forEach((item) => {\n              const fullPath = path.join(fullDir, item);\n              if (fs.statSync(fullPath).isFile() || item.endsWith(\".app\")) {\n                files.push(fullPath);\n              }\n            });\n          }\n        } else {\n          // Direct path check\n          const fullPath = path.join(PROJECT_ROOT, location);\n          if (fs.existsSync(fullPath)) {\n            files.push(fullPath);\n          }\n        }\n      } catch (error) {\n        // Ignore errors\n      }\n    }\n\n    return files.filter((f) => f && f.length > 0);\n  }\n\n  async run(options = {}) {\n    console.log(`${BLUE}🚀 Release Build Test${NC}`);\n    console.log(`${BLUE}===================${NC}`);\n\n    // Build CLI first (unless skipped)\n    if (!options.skipCliBuild) {\n      this.log(\"INFO\", \"🔨 Building CLI...\");\n      try {\n        execSync(`pnpm run cli:build`, { stdio: \"pipe\" });\n      } catch (e) {\n        this.log(\"ERROR\", \"Failed to build CLI\");\n        return false;\n      }\n    }\n\n    console.log(`Testing apps: ${TEST_APPS.join(\", \")}`);\n    console.log(\"\");\n\n    let successCount = 0;\n    const results = [];\n\n    for (const appName of TEST_APPS) {\n      try {\n        const success = await this.buildApp(appName);\n\n        if (success) {\n          successCount++;\n          // Optional: Show generated files if found\n          const outputFiles = this.findOutputFiles(appName);\n          if (outputFiles.length > 0) {\n            this.log(\"INFO\", `📦 Generated files for ${appName}:`);\n            outputFiles.forEach((file) => {\n              try {\n                const stats = fs.statSync(file);\n                const size = (stats.size / 1024 / 1024).toFixed(1);\n                this.log(\"INFO\", `   - ${file} (${size}MB)`);\n              } catch (error) {\n                this.log(\"INFO\", `   - ${file}`);\n              }\n            });\n          }\n        }\n\n        results.push({\n          app: appName,\n          success,\n          outputFiles: this.findOutputFiles(appName),\n        });\n      } catch (error) {\n        this.log(\"ERROR\", `Failed to build ${appName}: ${error.message}`);\n        results.push({ app: appName, success: false, error: error.message });\n      }\n\n      console.log(\"\"); // Add spacing between apps\n    }\n\n    // Summary\n    const duration = Math.round((Date.now() - this.startTime) / 1000);\n\n    console.log(`${BLUE}📊 Test Summary${NC}`);\n    console.log(`==================`);\n    console.log(`✅ Successful builds: ${successCount}/${TEST_APPS.length}`);\n    console.log(`⏱️  Total time: ${duration}s`);\n\n    if (successCount === TEST_APPS.length) {\n      this.log(\"INFO\", \"🎉 All test builds completed successfully!\");\n      this.log(\"INFO\", \"Release workflow logic is working correctly.\");\n    } else {\n      this.log(\n        \"ERROR\",\n        `⚠️  ${TEST_APPS.length - successCount} builds failed.`,\n      );\n    }\n\n    return successCount === TEST_APPS.length;\n  }\n}\n\n// Run if called directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n  const tester = new ReleaseBuildTest();\n  const success = await tester.run();\n  process.exit(success ? 0 : 1);\n}\n\nexport default ReleaseBuildTest;\n"
  },
  {
    "path": "tests/unit/builders.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\n\n/**\n * Tests for multi-target build parsing logic\n * These tests verify the core logic used in LinuxBuilder without needing to instantiate the class\n */\ndescribe('Multi-target build parsing', () => {\n  /**\n   * Simulates the logic from LinuxBuilder.build()\n   */\n  function parseAndFilterTargets(targetsString: string): string[] {\n    const validTargets = ['deb', 'appimage', 'rpm'];\n    const requestedTargets = targetsString\n      .split(',')\n      .map((t: string) => t.trim());\n\n    return validTargets.filter((target) => requestedTargets.includes(target));\n  }\n\n  describe('Target parsing', () => {\n    it('should parse single target', () => {\n      const result = parseAndFilterTargets('deb');\n\n      expect(result).toEqual(['deb']);\n      expect(result).toHaveLength(1);\n    });\n\n    it('should parse comma-separated targets', () => {\n      const result = parseAndFilterTargets('deb,appimage');\n\n      expect(result).toEqual(['deb', 'appimage']);\n      expect(result).toHaveLength(2);\n    });\n\n    it('should handle targets with spaces', () => {\n      const result = parseAndFilterTargets('deb, appimage, rpm');\n\n      expect(result).toEqual(['deb', 'appimage', 'rpm']);\n      expect(result).toHaveLength(3);\n    });\n\n    it('should filter out invalid targets', () => {\n      const result = parseAndFilterTargets('deb,invalid,appimage');\n\n      expect(result).toEqual(['deb', 'appimage']);\n      expect(result).not.toContain('invalid');\n      expect(result).toHaveLength(2);\n    });\n\n    it('should handle all valid targets', () => {\n      const result = parseAndFilterTargets('deb,appimage,rpm');\n\n      expect(result).toEqual(['deb', 'appimage', 'rpm']);\n      expect(result).toHaveLength(3);\n    });\n\n    it('should return empty array for all invalid targets', () => {\n      const result = parseAndFilterTargets('invalid1,invalid2');\n\n      expect(result).toEqual([]);\n      expect(result).toHaveLength(0);\n    });\n\n    it('should handle excessive whitespace', () => {\n      const result = parseAndFilterTargets('  deb  ,  appimage  ,  rpm  ');\n\n      expect(result).toEqual(['deb', 'appimage', 'rpm']);\n      expect(result).toHaveLength(3);\n    });\n\n    it('should be case-sensitive', () => {\n      const result = parseAndFilterTargets('DEB,APPIMAGE');\n\n      // Should not match uppercase\n      expect(result).toEqual([]);\n    });\n\n    it('should handle single target with comma', () => {\n      const result = parseAndFilterTargets('deb,');\n\n      expect(result).toEqual(['deb']);\n      expect(result).toHaveLength(1);\n    });\n  });\n\n  describe('Target validation', () => {\n    it('should validate against Linux target types', () => {\n      const validTargets = ['deb', 'appimage', 'rpm'];\n\n      expect(validTargets).toContain('deb');\n      expect(validTargets).toContain('appimage');\n      expect(validTargets).toContain('rpm');\n      expect(validTargets).not.toContain('msi');\n      expect(validTargets).not.toContain('dmg');\n    });\n\n    it('should check if target is valid', () => {\n      const validTargets = ['deb', 'appimage', 'rpm'];\n      const testTargets = ['deb', 'invalid', 'appimage', 'msi'];\n\n      const valid = testTargets.filter((t) => validTargets.includes(t));\n      const invalid = testTargets.filter((t) => !validTargets.includes(t));\n\n      expect(valid).toEqual(['deb', 'appimage']);\n      expect(invalid).toEqual(['invalid', 'msi']);\n    });\n  });\n\n  describe('Architecture suffix handling', () => {\n    it('should extract format from arm64 target', () => {\n      const target = 'deb-arm64';\n      const format = target.replace('-arm64', '');\n\n      expect(format).toBe('deb');\n    });\n\n    it('should keep format without suffix', () => {\n      const target = 'deb';\n      const format = target.replace('-arm64', '');\n\n      expect(format).toBe('deb');\n    });\n\n    it('should handle appimage-arm64', () => {\n      const target = 'appimage-arm64';\n      const format = target.replace('-arm64', '');\n\n      expect(format).toBe('appimage');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/unit/cli-options.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { getCliProgram } from '../../bin/helpers/cli-program.js';\n\ndescribe('CLI options', () => {\n  const program = getCliProgram();\n\n  it('registers hidden --multi-window option', () => {\n    const option = program.options.find(\n      (item) => item.long === '--multi-window',\n    );\n\n    expect(option).toBeDefined();\n    expect(option?.defaultValue).toBe(false);\n  });\n\n  it('registers hidden --internal-url-regex option', () => {\n    const option = program.options.find(\n      (item) => item.long === '--internal-url-regex',\n    );\n\n    expect(option).toBeDefined();\n    expect(option?.defaultValue).toBe('');\n  });\n\n  it('registers hidden --identifier option', () => {\n    const option = program.options.find((item) => item.long === '--identifier');\n\n    expect(option).toBeDefined();\n    expect(option?.hidden).toBe(true);\n  });\n\n  it('registers visible --install option', () => {\n    const option = program.options.find((item) => item.long === '--install');\n\n    expect(option).toBeDefined();\n    expect(option?.defaultValue).toBe(false);\n    expect(option?.hidden).toBe(false);\n  });\n});\n"
  },
  {
    "path": "tests/unit/file-finding.test.js",
    "content": "/**\n * Cross-platform file finding tests\n *\n * These tests verify that file finding logic works correctly\n * across different platforms (Windows, macOS, Linux).\n */\n\nimport { describe, it, expect, beforeEach, afterEach } from \"vitest\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport os from \"os\";\n\ndescribe(\"Cross-platform file finding\", () => {\n  let tempDir;\n\n  beforeEach(() => {\n    // Create a temporary directory for testing\n    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), \"pake-test-\"));\n  });\n\n  afterEach(() => {\n    // Clean up temporary directory\n    if (fs.existsSync(tempDir)) {\n      fs.rmSync(tempDir, { recursive: true, force: true });\n    }\n  });\n\n  describe(\"findFilesByPattern\", () => {\n    /**\n     * Simulates the fixed findOutputFiles logic from tests/release.js\n     */\n    function findFilesByPattern(dir, pattern) {\n      const files = [];\n\n      if (!fs.existsSync(dir)) {\n        return files;\n      }\n\n      const items = fs.readdirSync(dir);\n\n      // Convert glob pattern to regex\n      const regex = new RegExp(\n        \"^\" + pattern.replace(/\\*/g, \".*\").replace(/\\?/g, \".\") + \"$\",\n      );\n\n      const matching = items.filter((item) => regex.test(item));\n\n      matching.forEach((item) => {\n        const fullPath = path.join(dir, item);\n        try {\n          const stat = fs.statSync(fullPath);\n          if (stat.isFile() || item.endsWith(\".app\")) {\n            files.push(fullPath);\n          }\n        } catch (error) {\n          // Skip files we can't stat\n        }\n      });\n\n      return files;\n    }\n\n    it(\"should find exact filename matches\", () => {\n      const testFile = path.join(tempDir, \"test.deb\");\n      fs.writeFileSync(testFile, \"test content\");\n\n      const found = findFilesByPattern(tempDir, \"test.deb\");\n\n      expect(found).toHaveLength(1);\n      expect(found[0]).toBe(testFile);\n    });\n\n    it(\"should find files with wildcard patterns\", () => {\n      const files = [\n        \"myapp_1.0.0_amd64.deb\",\n        \"myapp_1.0.0_arm64.deb\",\n        \"other.txt\",\n      ];\n\n      files.forEach((file) => {\n        fs.writeFileSync(path.join(tempDir, file), \"test\");\n      });\n\n      const found = findFilesByPattern(tempDir, \"myapp_*.deb\");\n\n      expect(found).toHaveLength(2);\n      expect(found.map((f) => path.basename(f)).sort()).toEqual([\n        \"myapp_1.0.0_amd64.deb\",\n        \"myapp_1.0.0_arm64.deb\",\n      ]);\n    });\n\n    it(\"should handle question mark wildcards\", () => {\n      const files = [\"app1.msi\", \"app2.msi\", \"app10.msi\"];\n\n      files.forEach((file) => {\n        fs.writeFileSync(path.join(tempDir, file), \"test\");\n      });\n\n      const found = findFilesByPattern(tempDir, \"app?.msi\");\n\n      expect(found).toHaveLength(2);\n      expect(found.map((f) => path.basename(f)).sort()).toEqual([\n        \"app1.msi\",\n        \"app2.msi\",\n      ]);\n    });\n\n    it(\"should return empty array for non-existent directory\", () => {\n      const nonExistent = path.join(tempDir, \"does-not-exist\");\n      const found = findFilesByPattern(nonExistent, \"*.deb\");\n\n      expect(found).toHaveLength(0);\n    });\n\n    it(\"should work on Windows paths\", () => {\n      // Test with backslashes (Windows-style paths)\n      const testFile = path.join(tempDir, \"windows-test.msi\");\n      fs.writeFileSync(testFile, \"test content\");\n\n      const found = findFilesByPattern(tempDir, \"*.msi\");\n\n      expect(found).toHaveLength(1);\n      expect(path.basename(found[0])).toBe(\"windows-test.msi\");\n    });\n\n    it(\"should handle .app bundles on macOS\", () => {\n      const appBundle = path.join(tempDir, \"MyApp.app\");\n      fs.mkdirSync(appBundle, { recursive: true });\n\n      const found = findFilesByPattern(tempDir, \"MyApp.app\");\n\n      expect(found).toHaveLength(1);\n      expect(found[0]).toBe(appBundle);\n    });\n\n    it(\"should ignore subdirectories when matching files\", () => {\n      const subdir = path.join(tempDir, \"subdir.deb\");\n      fs.mkdirSync(subdir);\n\n      const file = path.join(tempDir, \"app.deb\");\n      fs.writeFileSync(file, \"test\");\n\n      const found = findFilesByPattern(tempDir, \"*.deb\");\n\n      // Should only find the file, not the directory\n      expect(found).toHaveLength(1);\n      expect(found[0]).toBe(file);\n    });\n  });\n\n  describe(\"findInMultipleLocations\", () => {\n    /**\n     * Simulates the pattern used in workflow files:\n     * Check project root first, then fallback to bundle directory\n     */\n    function findInMultipleLocations(locations) {\n      for (const location of locations) {\n        const fullPath = path.join(tempDir, location);\n        if (fs.existsSync(fullPath)) {\n          return fullPath;\n        }\n      }\n      return null;\n    }\n\n    it(\"should find file in first location\", () => {\n      const file1 = path.join(tempDir, \"test.dmg\");\n      fs.writeFileSync(file1, \"test\");\n\n      const found = findInMultipleLocations([\n        \"test.dmg\",\n        \"src-tauri/target/release/bundle/dmg/test.dmg\",\n      ]);\n\n      expect(found).toBe(file1);\n    });\n\n    it(\"should fallback to second location\", () => {\n      const bundleDir = path.join(\n        tempDir,\n        \"src-tauri/target/release/bundle/dmg\",\n      );\n      fs.mkdirSync(bundleDir, { recursive: true });\n\n      const file2 = path.join(bundleDir, \"test.dmg\");\n      fs.writeFileSync(file2, \"test\");\n\n      const found = findInMultipleLocations([\n        \"test.dmg\",\n        \"src-tauri/target/release/bundle/dmg/test.dmg\",\n      ]);\n\n      expect(found).toBe(file2);\n    });\n\n    it(\"should return null when file not found\", () => {\n      const found = findInMultipleLocations([\n        \"test.dmg\",\n        \"other/path/test.dmg\",\n      ]);\n\n      expect(found).toBeNull();\n    });\n\n    it(\"should work with Windows paths\", () => {\n      const msiDir = path.join(\n        tempDir,\n        \"src-tauri\",\n        \"target\",\n        \"x86_64-pc-windows-msvc\",\n        \"release\",\n        \"bundle\",\n        \"msi\",\n      );\n      fs.mkdirSync(msiDir, { recursive: true });\n\n      const msiFile = path.join(msiDir, \"test.msi\");\n      fs.writeFileSync(msiFile, \"test\");\n\n      const found = findInMultipleLocations([\n        \"test.msi\",\n        \"src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/test.msi\",\n      ]);\n\n      expect(found).toBe(msiFile);\n    });\n  });\n\n  describe(\"Path normalization\", () => {\n    it(\"should handle mixed slashes\", () => {\n      // This is important for cross-platform compatibility\n      const mixedPath = \"src-tauri\\\\target/release\\\\bundle/msi\";\n      const normalized = path.normalize(mixedPath);\n\n      expect(normalized).toBeTruthy();\n      // Should work regardless of platform\n    });\n\n    it(\"should handle path.join correctly on all platforms\", () => {\n      const joined = path.join(\n        \"src-tauri\",\n        \"target\",\n        \"release\",\n        \"bundle\",\n        \"msi\",\n      );\n\n      // Should use platform-specific separator\n      const parts = joined.split(path.sep);\n      expect(parts).toHaveLength(5);\n      expect(parts[0]).toBe(\"src-tauri\");\n      expect(parts[4]).toBe(\"msi\");\n    });\n  });\n});\n"
  },
  {
    "path": "tests/unit/identifier.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { getIdentifier, resolveIdentifier } from '@/utils/info';\n\ndescribe('identifier generation', () => {\n  const url = 'https://gmail.com';\n\n  it('generates different identifiers for the same URL when app names differ', () => {\n    expect(getIdentifier(url, 'Work Gmail')).not.toBe(\n      getIdentifier(url, 'Personal Gmail'),\n    );\n  });\n\n  it('generates stable identifiers for the same URL and app name', () => {\n    expect(getIdentifier(url, 'Work Gmail')).toBe(\n      getIdentifier(url, 'Work Gmail'),\n    );\n  });\n\n  it('prefers a custom identifier when provided', () => {\n    expect(resolveIdentifier(url, 'Work Gmail', 'com.example.work-gmail')).toBe(\n      'com.example.work-gmail',\n    );\n  });\n});\n"
  },
  {
    "path": "tests/unit/name.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport {\n  getSafeAppName,\n  generateLinuxPackageName,\n  generateIdentifierSafeName,\n} from '@/utils/name';\n\ndescribe('getSafeAppName', () => {\n  it('should handle simple names', () => {\n    expect(getSafeAppName('MyApp')).toBe('myapp');\n  });\n\n  it('should handle names with spaces', () => {\n    expect(getSafeAppName('My App')).toBe('my_app');\n  });\n\n  it('should handle names with hyphens', () => {\n    expect(getSafeAppName('my-app')).toBe('my-app');\n  });\n\n  it('should handle Chinese names', () => {\n    expect(getSafeAppName('我的应用')).toBe('我的应用');\n  });\n\n  it('should handle mixed Chinese and English', () => {\n    expect(getSafeAppName('我的 App')).toBe('我的_app');\n  });\n\n  it('should preserve special characters like @', () => {\n    expect(getSafeAppName('App@2024')).toBe('app@2024');\n  });\n\n  it('should replace forward slashes', () => {\n    expect(getSafeAppName('My/App')).toBe('my_app');\n  });\n\n  it('should replace backslashes', () => {\n    expect(getSafeAppName('My\\\\App')).toBe('my_app');\n  });\n\n  it('should replace colons', () => {\n    expect(getSafeAppName('App:Name')).toBe('app_name');\n  });\n\n  it('should replace asterisks', () => {\n    expect(getSafeAppName('App*Name')).toBe('app_name');\n  });\n\n  it('should replace question marks', () => {\n    expect(getSafeAppName('App?Name')).toBe('app_name');\n  });\n\n  it('should replace double quotes', () => {\n    expect(getSafeAppName('App\"Name')).toBe('app_name');\n  });\n\n  it('should replace angle brackets', () => {\n    expect(getSafeAppName('App<Name>')).toBe('app_name_');\n  });\n\n  it('should replace pipes', () => {\n    expect(getSafeAppName('App|Name')).toBe('app_name');\n  });\n\n  it('should handle all uppercase names', () => {\n    expect(getSafeAppName('APP')).toBe('app');\n  });\n\n  it('should handle single character names', () => {\n    expect(getSafeAppName('a')).toBe('a');\n  });\n\n  it('should handle numeric names', () => {\n    expect(getSafeAppName('123')).toBe('123');\n  });\n\n  it('should handle leading/trailing spaces', () => {\n    expect(getSafeAppName('  App  ')).toBe('_app_');\n  });\n\n  it('should handle trailing dots', () => {\n    expect(getSafeAppName('App...')).toBe('app');\n  });\n\n  it('should truncate very long names', () => {\n    const longName = 'A'.repeat(300);\n    const expected = 'a'.repeat(255);\n    expect(getSafeAppName(longName)).toBe(expected);\n  });\n});\n\ndescribe('generateLinuxPackageName', () => {\n  it('should handle simple names', () => {\n    expect(generateLinuxPackageName('MyApp')).toBe('myapp');\n  });\n\n  it('should replace spaces and special characters with hyphens', () => {\n    expect(generateLinuxPackageName('My App! @123')).toBe('my-app-123');\n  });\n\n  it('should handle multiple hyphens', () => {\n    expect(generateLinuxPackageName('my--app')).toBe('my-app');\n  });\n\n  it('should handle Chinese characters', () => {\n    expect(generateLinuxPackageName('我的应用')).toBe('我的应用');\n  });\n\n  it('should trim leading/trailing hyphens', () => {\n    expect(generateLinuxPackageName('--my-app--')).toBe('my-app');\n  });\n});\n\ndescribe('generateIdentifierSafeName', () => {\n  it('should handle alphanumeric names', () => {\n    expect(generateIdentifierSafeName('MyApp123')).toBe('myapp123');\n  });\n\n  it('should remove special characters', () => {\n    expect(generateIdentifierSafeName('My-App! @#')).toBe('myapp');\n  });\n\n  it('should handle Chinese characters', () => {\n    expect(generateIdentifierSafeName('我的应用App')).toBe('我的应用app');\n  });\n\n  it('should provide fallback for names without alphanumeric/Chinese', () => {\n    expect(generateIdentifierSafeName('!@#$')).not.toBe('');\n    expect(generateIdentifierSafeName('!@#$')).not.toBe('!@#$');\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"target\": \"es2020\",\n    \"types\": [\"node\"],\n    \"lib\": [\"es2020\", \"dom\"],\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noImplicitAny\": true,\n    \"moduleResolution\": \"node\",\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"bin/*\"]\n    }\n  },\n  \"include\": [\"bin/**/*\"]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\nimport path from 'path';\n\nexport default defineConfig({\n  test: {\n    environment: 'node',\n    include: [\n      'bin/**/*.{test,spec}.ts',\n      'tests/unit/**/*.{test,spec}.{ts,js}',\n      'tests/integration/**/*.{test,spec}.{ts,js}',\n    ],\n  },\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, './bin'),\n    },\n  },\n});\n"
  }
]