Full Code of tw93/Pake for AI

main 1a34b5f912c2 cached
123 files
466.5 KB
125.3k tokens
262 symbols
1 requests
Download .txt
Showing preview only (495K chars total). Download the full file or copy to clipboard to get everything.
Repository: tw93/Pake
Branch: main
Commit: 1a34b5f912c2
Files: 123
Total size: 466.5 KB

Directory structure:
gitextract_0ul1epmu/

├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yml
│   │   ├── config.yml
│   │   └── feature.yml
│   ├── actions/
│   │   └── setup-env/
│   │       └── action.yml
│   └── workflows/
│       ├── pake-cli.yaml
│       ├── quality-and-test.yml
│       ├── release.yml
│       ├── single-app.yaml
│       └── update-contributors.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .pnpmrc
├── .prettierignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── README_CN.md
├── action.yml
├── bin/
│   ├── builders/
│   │   ├── BaseBuilder.ts
│   │   ├── BuilderProvider.ts
│   │   ├── LinuxBuilder.ts
│   │   ├── MacBuilder.ts
│   │   └── WinBuilder.ts
│   ├── cli.ts
│   ├── defaults.ts
│   ├── dev.ts
│   ├── helpers/
│   │   ├── cli-program.ts
│   │   ├── merge.ts
│   │   ├── rust.ts
│   │   └── tauriConfig.ts
│   ├── options/
│   │   ├── icon.ts
│   │   ├── index.ts
│   │   └── logger.ts
│   ├── types.ts
│   └── utils/
│       ├── combine.ts
│       ├── dir.ts
│       ├── ico.ts
│       ├── info.ts
│       ├── ip.ts
│       ├── name.ts
│       ├── platform.ts
│       ├── shell.ts
│       ├── url.ts
│       └── validate.ts
├── default_app_list.json
├── docs/
│   ├── README.md
│   ├── README_CN.md
│   ├── advanced-usage.md
│   ├── advanced-usage_CN.md
│   ├── cli-usage.md
│   ├── cli-usage_CN.md
│   ├── faq.md
│   ├── faq_CN.md
│   ├── github-actions-usage.md
│   ├── github-actions-usage_CN.md
│   └── pake-action.md
├── icns2png.py
├── package.json
├── rollup.config.js
├── rust-toolchain.toml
├── src-tauri/
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── Info.plist
│   ├── assets/
│   │   └── main.wxs
│   ├── build.rs
│   ├── capabilities/
│   │   └── default.json
│   ├── entitlements.plist
│   ├── icons/
│   │   ├── chatgpt.icns
│   │   ├── deepseek.icns
│   │   ├── excalidraw.icns
│   │   ├── flomo.icns
│   │   ├── gemini.icns
│   │   ├── grok.icns
│   │   ├── icon.icns
│   │   ├── lizhi.icns
│   │   ├── programmusic.icns
│   │   ├── qwerty.icns
│   │   ├── twitter.icns
│   │   ├── wechat.icns
│   │   ├── weekly.icns
│   │   ├── weread.icns
│   │   ├── xiaohongshu.icns
│   │   ├── youtube.icns
│   │   └── youtubemusic.icns
│   ├── pake.json
│   ├── rust_proxy.toml
│   ├── src/
│   │   ├── app/
│   │   │   ├── config.rs
│   │   │   ├── invoke.rs
│   │   │   ├── menu.rs
│   │   │   ├── mod.rs
│   │   │   ├── setup.rs
│   │   │   └── window.rs
│   │   ├── inject/
│   │   │   ├── auth.js
│   │   │   ├── component.js
│   │   │   ├── custom.js
│   │   │   ├── event.js
│   │   │   ├── style.js
│   │   │   └── theme_refresh.js
│   │   ├── lib.rs
│   │   ├── main.rs
│   │   └── util.rs
│   ├── tauri.conf.json
│   ├── tauri.linux.conf.json
│   ├── tauri.macos.conf.json
│   └── tauri.windows.conf.json
├── tests/
│   ├── config.js
│   ├── index.js
│   ├── integration/
│   │   └── workflow-paths.test.js
│   ├── release.js
│   └── unit/
│       ├── builders.test.ts
│       ├── cli-options.test.ts
│       ├── file-finding.test.js
│       ├── identifier.test.ts
│       └── name.test.ts
├── tsconfig.json
└── vitest.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.git
.gitignore

**/target
**/node_modules

**/*.log
**/*.md
**/tmp

Dockerfile


================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

# Use 4 spaces for Python, Rust and Bash files
[*.{py,rs,sh}]
indent_size = 4

# Makefiles always use tabs for indentation
[Makefile]
indent_style = tab

[*.bat]
indent_size = 2

[*.md]
trim_trailing_whitespace = false

[*.ts]
quote_type= "single"


================================================
FILE: .gitattributes
================================================
# Exclude all non-source directories from language detection
bin/**/*                  linguist-vendored
dist/**/*                 linguist-vendored
scripts/**/*              linguist-vendored
tests/**/*                linguist-vendored
docs/**/*                 linguist-vendored
.github/**/*              linguist-vendored
node_modules/**/*         linguist-vendored

# Exclude build artifacts and config files
/cli.js                   linguist-vendored
/rollup.config.js         linguist-vendored
/icns2png.py              linguist-vendored
*.json                    linguist-vendored

# Exclude Tauri generated and vendor code
src-tauri/target/**/*     linguist-vendored
src-tauri/gen/**/*        linguist-vendored
src-tauri/capabilities/** linguist-vendored
src-tauri/icons/**/*      linguist-vendored
src-tauri/assets/**/*     linguist-vendored
src-tauri/png/**/*        linguist-vendored
src-tauri/.pake/**/*      linguist-vendored
src-tauri/.cargo/**/*     linguist-vendored

# Exclude injection system (since it's mostly JS/CSS)
src-tauri/src/inject/**/* linguist-vendored


================================================
FILE: .github/FUNDING.yml
================================================
github: ["tw93"]
custom: ["https://miaoyan.app/cats.html?name=Pake"]


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: Bug report
description: Problems with the software
title: "[Bug] "
labels: ["bug"]
body:
  - type: markdown
    attributes:
      value: |
        Thank you very much for your feedback!

        For suggestions or help, please consider using [Github Discussion](https://github.com/tw93/Pake/discussions) instead.
  - type: checkboxes
    attributes:
      label: Search before asking
      description: >
        🙊 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).
      options:
        - label: >
            I searched in the [issues](https://github.com/tw93/Pake/issues?q=) and found nothing similar.
          required: true
  - type: textarea
    attributes:
      label: Pake version
      description: >
        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.
    validations:
      required: true
  - type: textarea
    attributes:
      label: Rust version
      description: >
        Please provide the Rust version (e.g., `rustc --version`). This is critical for build issues.
    validations:
      required: true
  - type: dropdown
    attributes:
      label: Package Manager
      description: Which package manager are you using?
      options:
        - npm
        - pnpm
        - yarn
        - bun
        - other
    validations:
      required: true
  - type: textarea
    attributes:
      label: System version
      description: >
        Please provide the version of System you are using (e.g., macOS 14.2, Windows 11, Ubuntu 24.04).
    validations:
      required: true
  - type: textarea
    attributes:
      label: Node.js version
      description: >
        Please provide the Node.js version.
    validations:
      required: true
  - type: textarea
    attributes:
      label: Build Command
      description: >
        Please provide the exact command you used to build the app (e.g., `pake https://github.com --name GitHub`).
    validations:
      required: true
  - type: textarea
    attributes:
      label: Minimal reproduce step
      description: Please try to give reproducing steps to facilitate quick location of the problem.
    validations:
      required: true
  - type: textarea
    attributes:
      label: What did you expect to see?
    validations:
      required: true
  - type: textarea
    attributes:
      label: What did you see instead?
    validations:
      required: true
  - type: textarea
    attributes:
      label: Anything else?
  - type: checkboxes
    attributes:
      label: Are you willing to submit a PR?
      description: >
        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.
      options:
        - label: I'm willing to submit a PR!
  - type: markdown
    attributes:
      value: "Thanks for completing our form!"


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Ask a question or get support
    url: https://github.com/tw93/Pake/discussions/categories/q-a
    about: Ask a question or request support for Pake


================================================
FILE: .github/ISSUE_TEMPLATE/feature.yml
================================================
name: Feature
description: Add new feature, improve code, and more
labels: ["enhancement"]
body:
  - type: markdown
    attributes:
      value: |
        Thank you very much for your feature proposal!
  - type: checkboxes
    attributes:
      label: Search before asking
      description: >
        Please search [issues](https://github.com/tw93/Pake/issues?q=) to check if your issue has already been reported.
      options:
        - label: >
            I searched in the [issues](https://github.com/tw93/Pake/issues?q=) and found nothing similar.
          required: true
  - type: textarea
    attributes:
      label: Motivation
      description: Describe the motivations for this feature, like how it fixes the problem you meet.
    validations:
      required: true
  - type: textarea
    attributes:
      label: Solution
      description: Describe the proposed solution and add related materials like links if any.
  - type: textarea
    attributes:
      label: Alternatives
      description: Describe other alternative solutions or features you considered, but rejected.
  - type: textarea
    attributes:
      label: Anything else?
  - type: checkboxes
    attributes:
      label: Are you willing to submit a PR?
      description: >
        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.
      options:
        - label: I'm willing to submit a PR!
  - type: markdown
    attributes:
      value: "Thanks for completing our form!"


================================================
FILE: .github/actions/setup-env/action.yml
================================================
name: Setup Development Environment
description: Unified environment setup with Node.js, Rust, and system dependencies

inputs:
  mode:
    description: |
      Setup mode:
      - build: Complete environment (Node + Rust + System deps + Cache)
      - node: Node.js only (pnpm + Node 22)
      - rust: Rust only (toolchain + targets)
    required: false
    default: "build"

outputs:
  setup-complete:
    description: Setup completion status
    value: "true"

runs:
  using: composite
  steps:
    # Parse mode and set environment flags
    - name: Setup environment flags
      shell: bash
      run: |
        MODE="${{ inputs.mode }}"

        # Validate and set flags in one pass
        case "$MODE" in
          build|full)
            echo "SETUP_NODE=true" >> $GITHUB_ENV
            echo "SETUP_RUST=true" >> $GITHUB_ENV
            echo "SETUP_SYSTEM=true" >> $GITHUB_ENV
            ;;
          node|node-only)
            echo "SETUP_NODE=true" >> $GITHUB_ENV
            echo "SETUP_RUST=false" >> $GITHUB_ENV
            echo "SETUP_SYSTEM=false" >> $GITHUB_ENV
            ;;
          rust|rust-only)
            echo "SETUP_NODE=false" >> $GITHUB_ENV
            echo "SETUP_RUST=true" >> $GITHUB_ENV
            echo "SETUP_SYSTEM=false" >> $GITHUB_ENV
            ;;
          *)
            echo "❌ Invalid mode: '$MODE'. Valid modes: build, node, rust"
            exit 1
            ;;
        esac

    # Node.js Environment Setup
    - name: Install pnpm
      if: env.SETUP_NODE == 'true'
      uses: pnpm/action-setup@v4
      with:
        version: "10.26.2"
        run_install: false

    - name: Setup Node.js
      if: env.SETUP_NODE == 'true'
      uses: actions/setup-node@v4
      with:
        node-version: "22"
        cache: pnpm

    - name: Install dependencies
      if: env.SETUP_NODE == 'true'
      shell: bash
      run: pnpm install --frozen-lockfile

    # Rust Environment Setup
    - name: Setup Rust for Linux
      if: env.SETUP_RUST == 'true' && runner.os == 'Linux'
      uses: dtolnay/rust-toolchain@stable
      with:
        toolchain: stable
        target: x86_64-unknown-linux-gnu

    - name: Setup Rust for Windows
      if: env.SETUP_RUST == 'true' && runner.os == 'Windows'
      uses: dtolnay/rust-toolchain@stable
      with:
        toolchain: stable-x86_64-msvc
        target: x86_64-pc-windows-msvc

    - name: Setup Rust for macOS
      if: env.SETUP_RUST == 'true' && runner.os == 'macOS'
      uses: dtolnay/rust-toolchain@stable
      with:
        toolchain: stable

    - name: Add macOS universal targets
      if: env.SETUP_RUST == 'true' && runner.os == 'macOS'
      shell: bash
      run: |
        rustup target add x86_64-apple-darwin
        rustup target add aarch64-apple-darwin

    # System Dependencies
    - name: Install Ubuntu dependencies
      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Linux'
      uses: awalsh128/cache-apt-pkgs-action@v1.4.3
      with:
        packages: >
          libdbus-1-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev
          build-essential curl wget file libxdo-dev libssl-dev libgtk-3-dev
          libayatana-appindicator3-dev librsvg2-dev gnome-video-effects
          libglib2.0-dev libgirepository1.0-dev
          pkg-config
        version: 1.1

    - name: Set PKG_CONFIG_PATH for Linux
      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Linux'
      shell: bash
      run: |
        echo "PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV

    - name: Cache WIX Toolset
      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Windows'
      uses: actions/cache@v4
      id: wix-cache
      with:
        path: C:\Program Files (x86)\WiX Toolset v3.11
        key: windows-wix-3.11.2

    - name: Install WIX Toolset
      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Windows' && steps.wix-cache.outputs.cache-hit != 'true'
      shell: powershell
      run: |
        try {
          # Download and install WIX Toolset v3.11
          Invoke-WebRequest -Uri "https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311.exe" -OutFile "wix311.exe"
          Start-Process -FilePath "wix311.exe" -ArgumentList "/quiet" -Wait
          Write-Host "✅ WIX Toolset installed successfully"
        } catch {
          Write-Error "Failed to install WIX Toolset: $($_.Exception.Message)"
          exit 1
        }

    - name: Add WIX to PATH
      if: env.SETUP_SYSTEM == 'true' && runner.os == 'Windows'
      shell: powershell
      run: |
        $wixPath = "${env:ProgramFiles(x86)}\WiX Toolset v3.11\bin"
        if (Test-Path $wixPath) {
          echo $wixPath >> $env:GITHUB_PATH
        }

    # Build optimizations (caching)
    - name: Setup sccache
      if: inputs.mode == 'build'
      uses: mozilla-actions/sccache-action@v0.0.9

    - name: Enable sccache
      if: inputs.mode == 'build'
      shell: bash
      run: |
        echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
        echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV

    - name: Setup Rust cache
      if: inputs.mode == 'build'
      uses: swatinem/rust-cache@v2
      with:
        workspaces: "src-tauri -> target"
        shared-key: "pake-${{ runner.os }}"


================================================
FILE: .github/workflows/pake-cli.yaml
================================================
name: Build App With Pake CLI

env:
  NODE_VERSION: "22"
  PNPM_VERSION: "10.26.2"

on:
  workflow_dispatch:
    inputs:
      platform:
        description: "Platform"
        required: true
        default: "macos-latest"
        type: choice
        options:
          - "windows-latest"
          - "macos-latest"
          - "ubuntu-24.04"
      url:
        description: "Website URL"
        required: true
      name:
        description: "App name (lowercase for Linux)"
        required: true
      icon:
        description: "Icon URL, auto-fetch if empty"
        required: false
      width:
        description: "Window width (px)"
        required: false
        default: "1200"
      height:
        description: "Window height (px)"
        required: false
        default: "780"
      fullscreen:
        description: "Start in fullscreen mode"
        required: false
        type: boolean
        default: false
      hide_title_bar:
        description: "Hide title bar (macOS only)"
        required: false
        type: boolean
        default: false
      multi_arch:
        description: "Universal binary (macOS only)"
        required: false
        type: boolean
        default: false
      targets:
        description: "Package formats (comma-separated: deb,appimage,rpm)"
        required: false
        default: "deb"

jobs:
  build:
    name: ${{ inputs.platform }}
    runs-on: ${{ inputs.platform }}
    strategy:
      fail-fast: false

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable

      - name: Setup Node.js Environment
        uses: ./.github/actions/setup-env
        with:
          mode: build

      - name: Build CLI
        run: pnpm run cli:build

      - name: Setup mold linker
        if: runner.os == 'Linux'
        uses: rui314/setup-mold@v1

      - name: Rust cache restore
        uses: actions/cache/restore@v4.2.0
        id: cache_store
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            src-tauri/target/
          key: ${{ runner.os }}-cargo-pake-${{ hashFiles('**/Cargo.lock') }}

      - name: Build App (Linux/macOS)
        if: runner.os != 'Windows'
        timeout-minutes: 25
        shell: bash
        run: |
          ARGS=("${{ inputs.url }}" "--name" "${{ inputs.name }}")

          if [ -n "${{ inputs.icon }}" ]; then
            ARGS+=("--icon" "${{ inputs.icon }}")
          fi

          if [ -n "${{ inputs.width }}" ]; then
            ARGS+=("--width" "${{ inputs.width }}")
          fi

          if [ -n "${{ inputs.height }}" ]; then
            ARGS+=("--height" "${{ inputs.height }}")
          fi

          if [ "${{ inputs.fullscreen }}" == "true" ]; then
            ARGS+=("--fullscreen")
          fi

          if [ "${{ inputs.hide_title_bar }}" == "true" ]; then
            ARGS+=("--hide-title-bar")
          fi

          if [ "${{ inputs.multi_arch }}" == "true" ]; then
            ARGS+=("--multi-arch")
          fi

          if [ -n "${{ inputs.targets }}" ] && [ "${{ runner.os }}" == "Linux" ]; then
            ARGS+=("--targets" "${{ inputs.targets }}")
          fi

          echo "Running: node dist/cli.js ${ARGS[@]}"
          node dist/cli.js "${ARGS[@]}"

      - name: Build App (Windows)
        if: runner.os == 'Windows'
        timeout-minutes: 25
        shell: pwsh
        run: |
          $args = "${{ inputs.url }}", "--name", "${{ inputs.name }}"

          if ("${{ inputs.icon }}" -ne "") {
            $args += "--icon", "${{ inputs.icon }}"
          }

          if ("${{ inputs.width }}" -ne "") {
            $args += "--width", "${{ inputs.width }}"
          }

          if ("${{ inputs.height }}" -ne "") {
            $args += "--height", "${{ inputs.height }}"
          }

          if ("${{ inputs.fullscreen }}" -eq "true") {
            $args += "--fullscreen"
          }

          if ("${{ inputs.hide_title_bar }}" -eq "true") {
            $args += "--hide-title-bar"
          }

          Write-Host "Running: node dist/cli.js $($args -join ' ')"
          node dist/cli.js $args

          git checkout -- src-tauri/Cargo.lock

      - name: Upload DMG (macOS)
        if: runner.os == 'macOS'
        uses: actions/upload-artifact@v6
        with:
          name: ${{ inputs.name }}-macOS
          path: ${{ inputs.name }}.dmg
          retention-days: 3

      - name: Upload DEB (Linux)
        if: runner.os == 'Linux'
        uses: actions/upload-artifact@v6
        with:
          name: ${{ inputs.name }}-Linux-deb
          path: ${{ inputs.name }}.deb
          retention-days: 3
          if-no-files-found: ignore

      - name: Upload AppImage (Linux)
        if: runner.os == 'Linux'
        uses: actions/upload-artifact@v6
        with:
          name: ${{ inputs.name }}-Linux-AppImage
          path: ${{ inputs.name }}.AppImage
          retention-days: 3
          if-no-files-found: ignore

      - name: Upload MSI (Windows)
        if: runner.os == 'Windows'
        uses: actions/upload-artifact@v6
        with:
          name: ${{ inputs.name }}-Windows
          path: ${{ inputs.name }}.msi
          retention-days: 3

      - name: Rust cache store
        uses: actions/cache/save@v4.2.0
        if: steps.cache_store.outputs.cache-hit != 'true'
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            src-tauri/target/
          key: ${{ runner.os }}-cargo-pake-${{ hashFiles('**/Cargo.lock') }}


================================================
FILE: .github/workflows/quality-and-test.yml
================================================
name: Quality & Testing

on:
  push:
    branches: [main, dev]
  pull_request:
    branches: [main, dev]
  workflow_dispatch:

env:
  NODE_VERSION: "22"
  PNPM_VERSION: "10.26.2"

permissions:
  actions: write
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  auto-format:
    name: Auto-fix Formatting
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup Development Environment
        uses: ./.github/actions/setup-env
        with:
          mode: node

      - name: Auto-fix Prettier formatting
        run: npx prettier --write . --ignore-unknown --cache

      - name: Auto-fix Rust formatting
        run: cargo fmt --all --manifest-path src-tauri/Cargo.toml

      - name: Commit formatting fixes
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add .
          if ! git diff --staged --quiet; then
            git commit -m "Auto-fix formatting issues"
            git push
          else
            echo "No formatting changes to commit"
          fi


  rust-quality:
    name: Rust Code Quality
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false

    defaults:
      run:
        shell: bash
        working-directory: src-tauri
    steps:
      - uses: actions/checkout@v6

      - name: Setup Rust Environment
        uses: ./.github/actions/setup-env
        with:
          mode: build

      - name: Install Rust components
        shell: bash
        run: rustup component add rustfmt clippy

      - uses: rui314/setup-mold@v1

      - name: Cache cargo-hack
        uses: actions/cache@v5
        id: cargo-hack-cache
        with:
          path: ~/.cargo/bin/cargo-hack
          key: ${{ runner.os }}-cargo-hack-${{ hashFiles('~/.cargo/bin/cargo-hack') }}
          restore-keys: |
            ${{ runner.os }}-cargo-hack-

      - name: Install cargo-hack
        if: steps.cargo-hack-cache.outputs.cache-hit != 'true'
        run: cargo install cargo-hack --force

      - name: Check Rust formatting
        run: cargo fmt --all -- --color=always --check

      - name: Run Clippy lints
        run: cargo hack --feature-powerset --exclude-features cli-build --no-dev-deps clippy # cspell:disable-line

  validation:
    name: CLI & Build Validation (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
      fail-fast: false
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup Build Environment
        uses: ./.github/actions/setup-env
        with:
          mode: build

      - name: Build CLI
        run: pnpm run cli:build

      - name: Run CI Test Suite
        run: pnpm test
        timeout-minutes: 30
        env:
          CI: true
          NODE_ENV: test

      - name: Test CLI Integration
        shell: bash
        run: |
          echo "Testing CLI integration..."
          if [[ "$RUNNER_OS" == "Windows" ]]; then
            timeout 60s PAKE_CREATE_APP=1 node dist/cli.js https://weekly.tw93.fun --name "CITest" --debug --iterative-build || true
          else
            timeout 30s PAKE_CREATE_APP=1 node dist/cli.js https://weekly.tw93.fun --name "CITest" --debug --iterative-build || true
          fi

  summary:
    name: Quality Summary
    runs-on: ubuntu-latest
    needs: [auto-format, rust-quality, validation]
    if: always()
    steps:
      - name: Generate Summary
        run: |
          {
            echo "# Quality & Testing Summary"
            echo ""
            echo "| Check | Status |"
            echo "|-------|--------|"
            echo "| Auto Formatting | ${{ needs.auto-format.result == 'success' && 'PASSED' || needs.auto-format.result == 'skipped' && 'SKIPPED' || 'FAILED' }} |"
            echo "| Rust Quality | ${{ needs.rust-quality.result == 'success' && 'PASSED' || 'FAILED' }} |"
            echo "| CLI & Build Validation | ${{ needs.validation.result == 'success' && 'PASSED' || 'FAILED' }} |"
          } >> $GITHUB_STEP_SUMMARY


================================================
FILE: .github/workflows/release.yml
================================================
name: Release & Publish

on:
  push:
    tags:
      - "V*"
  workflow_dispatch:
    inputs:
      release_apps:
        description: "Build popular apps"
        type: boolean
        default: false
      publish_docker:
        description: "Publish Docker image"
        type: boolean
        default: false

env:
  NODE_VERSION: "22"
  PNPM_VERSION: "10.26.2"
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # Build and release popular apps
  release-apps:
    if: |
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||
      (github.event_name == 'workflow_dispatch' && inputs.release_apps)
    runs-on: ubuntu-latest
    outputs:
      apps_name: ${{ steps.read-apps-config.outputs.apps_name }}
      apps_config: ${{ steps.read-apps-config.outputs.apps_config }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Get Apps Config
        id: read-apps-config
        run: |
          echo "apps_name=$(jq -c '[.[] | .name]' default_app_list.json)" >> $GITHUB_OUTPUT
          echo "apps_config=$(jq -c '.' default_app_list.json)" >> $GITHUB_OUTPUT

  create-release:
    name: Create GitHub Release
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    permissions:
      contents: write
    steps:
      - name: Create release placeholder
        uses: ncipollo/release-action@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          skipIfReleaseExists: true

  build-cli:
    name: Build CLI
    needs: release-apps
    if: needs.release-apps.result == 'success'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup Node.js Environment
        uses: ./.github/actions/setup-env
        with:
          mode: 'node'

      - name: Build CLI
        run: pnpm run cli:build

      - name: Upload CLI Artifact
        uses: actions/upload-artifact@v6
        with:
          name: pake-cli-dist
          path: dist/
          retention-days: 1

  build-popular-apps:
    name: ${{ matrix.config.title }}
    needs: [release-apps, build-cli, create-release]
    if: |
      needs.release-apps.result == 'success' &&
      needs.build-cli.result == 'success' &&
      (needs.create-release.result == 'success' || needs.create-release.result == 'skipped')
    strategy:
      matrix:
        config: ${{ fromJSON(needs.release-apps.outputs.apps_config) }}
    uses: ./.github/workflows/single-app.yaml
    secrets: inherit
    with:
      name: ${{ matrix.config.name }}
      title: ${{ matrix.config.title }}
      name_zh: ${{ matrix.config.name_zh }}
      url: ${{ matrix.config.url }}
      icon: ${{ matrix.config.icon }}
      new_window: ${{ matrix.config.new_window || false }}

  # Publish Docker image (runs in parallel with app builds)
  publish-docker:
    if: |
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||
      (github.event_name == 'workflow_dispatch' && inputs.publish_docker)
    runs-on: ubuntu-22.04
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=raw,value=latest,enable={{is_default_branch}}
            type=ref,event=tag
            type=sha

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          platforms: linux/amd64
          cache-from: type=gha
          cache-to: type=gha,mode=max


================================================
FILE: .github/workflows/single-app.yaml
================================================
name: Build Single Popular App

env:
  NODE_VERSION: "22"
  PNPM_VERSION: "10.26.2"

on:
  workflow_dispatch:
    inputs:
      name:
        description: "App Name"
        required: true
        default: "twitter"
      title:
        description: "App Title"
        required: true
        default: "Twitter"
      name_zh:
        description: "App Name in Chinese"
        required: true
        default: "推特"
      url:
        description: "App URL"
        required: true
        default: "https://twitter.com/"
      icon:
        description: "App Icon"
        required: false
      new_window:
        description: "Allow sites to open new windows"
        required: false
        default: false
  workflow_call:
    inputs:
      name:
        description: "App Name"
        type: string
        required: true
        default: "twitter"
      title:
        description: "App Title"
        required: true
        type: string
        default: "Twitter"
      name_zh:
        description: "App Name in Chinese"
        required: true
        type: string
        default: "推特"
      url:
        description: "App URL"
        required: true
        type: string
        default: "https://twitter.com/"
      icon:
        description: "App Icon"
        type: string
        required: false
      new_window:
        description: "Allow sites to open new windows"
        type: boolean
        required: false
        default: false
    secrets:
      PAKE_SIGNING_IDENTITY:
        required: false
      PAKE_CERTIFICATE_P12:
        required: false
      PAKE_CERTIFICATE_PASSWORD:
        required: false
      PAKE_NOTARIZE_APPLE_ID:
        required: false
      PAKE_NOTARIZE_TEAM_ID:
        required: false
      PAKE_NOTARIZE_PASSWORD:
        required: false

jobs:
  build:
    name: ${{ inputs.title }} (${{ matrix.build }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        build: [linux, macos, windows]
        include:
          - build: linux
            os: ubuntu-latest
            rust: stable
          - build: windows
            os: windows-latest
            rust: stable-x86_64-msvc
          - build: macos
            os: macos-latest
            rust: stable
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: ${{ matrix.rust }}

      - name: Setup Node.js Environment
        uses: ./.github/actions/setup-env
        with:
          mode: build

      - name: Download CLI Artifact
        if: github.event_name != 'workflow_dispatch'
        uses: actions/download-artifact@v7
        with:
          name: pake-cli-dist
          path: dist

      - name: Build CLI (workflow_dispatch)
        if: github.event_name == 'workflow_dispatch'
        run: pnpm run cli:build

      - name: Setup mold linker
        if: matrix.os == 'ubuntu-latest'
        uses: rui314/setup-mold@v1

      - name: Prepare macOS signing
        if: matrix.os == 'macos-latest'
        env:
          PAKE_SIGNING_IDENTITY: ${{ secrets.PAKE_SIGNING_IDENTITY }}
          PAKE_CERTIFICATE_P12: ${{ secrets.PAKE_CERTIFICATE_P12 }}
          PAKE_CERTIFICATE_PASSWORD: ${{ secrets.PAKE_CERTIFICATE_PASSWORD }}
        run: |
          set -euo pipefail

          SIGNING_IDENTITY="${PAKE_SIGNING_IDENTITY:-}"

          if [ -n "${PAKE_CERTIFICATE_P12:-}" ] && [ -n "${PAKE_CERTIFICATE_PASSWORD:-}" ]; then
            echo "Importing macOS signing certificate..."
            KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
            KEYCHAIN_PASSWORD="$(openssl rand -base64 16)"

            security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
            security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
            security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

            echo "$PAKE_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
            security import "$RUNNER_TEMP/certificate.p12" -P "$PAKE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
            security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
            security list-keychain -d user -s "$KEYCHAIN_PATH"

            if [ -z "$SIGNING_IDENTITY" ]; then
              SIGNING_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | sed -n 's/.*"\(Developer ID Application:.*\)".*/\1/p' | head -n 1)"
            fi
          else
            echo "No certificate secret configured, fallback to ad-hoc signing."
          fi

          if [ -z "$SIGNING_IDENTITY" ]; then
            SIGNING_IDENTITY="-"
          fi

          SIGNING_IDENTITY="$SIGNING_IDENTITY" node <<'NODE'
          const fs = require('fs');
          const file = 'src-tauri/tauri.macos.conf.json';
          const config = JSON.parse(fs.readFileSync(file, 'utf8'));
          config.bundle ??= {};
          config.bundle.macOS ??= {};
          config.bundle.macOS.signingIdentity = process.env.SIGNING_IDENTITY || '-';
          fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
          NODE

          echo "Using signing identity: $SIGNING_IDENTITY"

      - name: Build for Linux
        if: matrix.os == 'ubuntu-latest'
        timeout-minutes: 20
        run: |
          ARGS=("${{ inputs.url }}" "--name" "${{ inputs.name }}")

          # Auto-detect local icon or use provided icon
          if [ -n "${{ inputs.icon }}" ]; then
            ARGS+=("--icon" "${{ inputs.icon }}")
          elif [ -f "src-tauri/png/${{ inputs.name }}_512.png" ]; then
            ARGS+=("--icon" "src-tauri/png/${{ inputs.name }}_512.png")
          fi

          if [ "${{ inputs.new_window }}" = "true" ]; then
            ARGS+=("--new-window")
          fi

          # Build once with multiple targets (faster than separate builds)
          node dist/cli.js "${ARGS[@]}" --targets deb,appimage

          mkdir -p output/linux

          # The CLI copies files to project root and removes them from bundle directory
          # Check project root first, then fallback to bundle directory
          if [ -f "${{ inputs.name }}.deb" ]; then
            mv "${{ inputs.name }}.deb" output/linux/${{inputs.title}}_`arch`.deb
          elif [ -n "$(ls src-tauri/target/release/bundle/deb/*.deb 2>/dev/null)" ]; then
            mv src-tauri/target/release/bundle/deb/*.deb output/linux/${{inputs.title}}_`arch`.deb
          else
            echo "Error: No DEB file found"
            find . -name "*.deb" -type f
            exit 1
          fi

          if [ -f "${{ inputs.name }}.AppImage" ]; then
            mv "${{ inputs.name }}.AppImage" output/linux/"${{inputs.title}}"_`arch`.AppImage
          elif [ -n "$(ls src-tauri/target/release/bundle/appimage/*.AppImage 2>/dev/null)" ]; then
            mv src-tauri/target/release/bundle/appimage/*.AppImage output/linux/"${{inputs.title}}"_`arch`.AppImage
          else
            echo "Error: No AppImage file found"
            find . -name "*.AppImage" -type f
            exit 1
          fi

      - name: Build for macOS
        if: matrix.os == 'macos-latest'
        timeout-minutes: 25
        env:
          TAURI_BUNDLER_DMG_IGNORE_CI: "true"
        run: |
          # Use title as app product name on macOS to preserve display casing in .app
          ARGS=("${{ inputs.url }}" "--name" "${{ inputs.title }}" "--hide-title-bar")

          # Auto-detect local icon or use provided icon
          if [ -n "${{ inputs.icon }}" ]; then
            ARGS+=("--icon" "${{ inputs.icon }}")
          elif [ -f "src-tauri/icons/${{ inputs.name }}.icns" ]; then
            ARGS+=("--icon" "src-tauri/icons/${{ inputs.name }}.icns")
          fi

          if [ "${{ inputs.new_window }}" = "true" ]; then
            ARGS+=("--new-window")
          fi

          node dist/cli.js "${ARGS[@]}" --targets universal --multi-arch

          mkdir -p output/macos

          # The CLI copies the DMG to project root and removes it from bundle directory
          # Check project root first, then fallback to bundle directory
          if [ -f "${{ inputs.title }}.dmg" ]; then
            mv "${{ inputs.title }}.dmg" output/macos/"${{inputs.title}}".dmg
          elif [ -f "${{ inputs.name }}.dmg" ]; then
            mv "${{ inputs.name }}.dmg" output/macos/"${{inputs.title}}".dmg
          elif [ -n "$(ls src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg 2>/dev/null)" ]; then
            mv src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg output/macos/"${{inputs.title}}".dmg
          else
            echo "Error: No DMG file found"
            echo "Searched locations:"
            echo "  - ${{ inputs.name }}.dmg (project root)"
            echo "  - src-tauri/target/universal-apple-darwin/release/bundle/dmg/"
            find . -name "*.dmg" -type f
            exit 1
          fi

      - name: Notarize macOS DMG
        if: matrix.os == 'macos-latest'
        env:
          PAKE_NOTARIZE_APPLE_ID: ${{ secrets.PAKE_NOTARIZE_APPLE_ID }}
          PAKE_NOTARIZE_TEAM_ID: ${{ secrets.PAKE_NOTARIZE_TEAM_ID }}
          PAKE_NOTARIZE_PASSWORD: ${{ secrets.PAKE_NOTARIZE_PASSWORD }}
        run: |
          set -euo pipefail

          if [ -z "${PAKE_NOTARIZE_APPLE_ID:-}" ] || [ -z "${PAKE_NOTARIZE_TEAM_ID:-}" ] || [ -z "${PAKE_NOTARIZE_PASSWORD:-}" ]; then
            echo "Notarization secrets not configured, skipping notarization."
            exit 0
          fi

          DMG_PATH="output/macos/${{inputs.title}}.dmg"
          if [ ! -f "$DMG_PATH" ]; then
            echo "Error: DMG file not found at $DMG_PATH"
            exit 1
          fi

          echo "Submitting DMG to Apple notarization service..."
          xcrun notarytool submit "$DMG_PATH" \
            --apple-id "$PAKE_NOTARIZE_APPLE_ID" \
            --team-id "$PAKE_NOTARIZE_TEAM_ID" \
            --password "$PAKE_NOTARIZE_PASSWORD" \
            --wait

          echo "Stapling notarization ticket..."
          xcrun stapler staple "$DMG_PATH"

      - name: Build for Windows
        if: matrix.os == 'windows-latest'
        timeout-minutes: 20
        run: |
          # Use title as app product name on Windows to preserve display casing
          $args = "${{ inputs.url }}", "--name", "${{ inputs.title }}"

          # Auto-detect local icon or use provided icon
          if ("${{ inputs.icon }}" -ne "") {
            $args += "--icon", "${{ inputs.icon }}"
          } elseif (Test-Path "src-tauri\png\${{ inputs.name }}_256.ico") {
            $args += "--icon", "src-tauri\png\${{ inputs.name }}_256.ico"
          }

          if ("${{ inputs.new_window }}" -eq "true") {
            $args += "--new-window"
          }

          $args += "--targets", "x64"

          node dist/cli.js $args

          New-Item -Path "output\windows" -ItemType Directory -Force

          # The CLI copies the MSI to project root and removes it from bundle directory
          # Check project root first, then fallback to bundle directories
          $projectRootMsi = "${{ inputs.title }}.msi"
          $legacyProjectRootMsi = "${{ inputs.name }}.msi"

          if (Test-Path $projectRootMsi) {
            Move-Item -Path $projectRootMsi -Destination "output\windows\${{inputs.title}}_x64.msi"
          } elseif (Test-Path $legacyProjectRootMsi) {
            Move-Item -Path $legacyProjectRootMsi -Destination "output\windows\${{inputs.title}}_x64.msi"
          } else {
            # Check architecture-specific path (x64 builds)
            $msiFiles = Get-ChildItem -Path "src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue

            # Fallback to generic path
            if (-not $msiFiles) {
              $msiFiles = Get-ChildItem -Path "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue
            }

            if ($msiFiles) {
              Move-Item -Path $msiFiles[0].FullName -Destination "output\windows\${{inputs.title}}_x64.msi"
            } else {
              Write-Error "No MSI files found in expected locations"
              Write-Host "Searched paths:"
              Write-Host "  - $projectRootMsi (project root)"
              Write-Host "  - $legacyProjectRootMsi (project root)"
              Write-Host "  - src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\"
              Write-Host "  - src-tauri\target\release\bundle\msi\"
              Write-Host "`nAll MSI files in target directory:"
              Get-ChildItem -Path "src-tauri\target\" -Recurse -Name "*.msi" | Write-Host
              exit 1
            }
          }

          git checkout -- src-tauri/Cargo.lock

      - name: Upload artifacts
        uses: actions/upload-artifact@v6
        with:
          name: ${{ inputs.title }}-${{ matrix.build }}
          path: output/*/*.*
          retention-days: 3

      - name: Upload to release (Linux)
        uses: ncipollo/release-action@v1 # cspell:disable-line
        if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/')
        with:
          allowUpdates: true
          omitBody: true
          omitName: true
          artifacts: "output/linux/*.deb,output/linux/*.AppImage"
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload to release (macOS)
        uses: ncipollo/release-action@v1 # cspell:disable-line
        if: matrix.os == 'macos-latest' && startsWith(github.ref, 'refs/tags/')
        with:
          allowUpdates: true
          omitBody: true
          omitName: true
          artifacts: "output/macos/*.dmg"
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload to release (Windows)
        uses: ncipollo/release-action@v1 # cspell:disable-line
        if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
        with:
          allowUpdates: true
          omitBody: true
          omitName: true
          artifacts: "output/windows/*.msi"
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/update-contributors.yml
================================================
name: Update Contributors

on:
  push:
    branches: [main, dev]
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * 0" # Every Sunday at midnight UTC

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  update-contributors:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          fetch-depth: 0

      - name: Generate contributors SVG
        uses: tw93/contributors-list@master
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          svgPath: CONTRIBUTORS.svg
          svgWidth: 1000
          avatarSize: 72
          avatarMargin: 45
          userNameHeight: 20
          noFetch: false
          noCommit: true
          truncate: 0
          includeBots: false
          excludeUsers: "github-actions web-flow dependabot claude"
          itemTemplate: |
            <g transform="translate({{ x }}, {{ y }})">
              <defs>
                <clipPath id="cp-{{ login }}">
                  <circle cx="36" cy="36" r="36" />
                </clipPath>
              </defs>
              <a xlink:href="{{{ url }}}" href="{{{ url }}}" class="contributor-link" target="_blank" rel="nofollow sponsored" title="{{{ name }}}">
                <image width="72" height="72" xlink:href="{{{ avatar }}}" href="{{{ avatar }}}" clip-path="url(#cp-{{ login }})" />
                <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>
              </a>
            </g>

      - name: Commit & Push
        uses: stefanzweifel/git-auto-commit-action@v7
        with:
          commit_message: "chore: update contributors [skip ci]"
          file_pattern: CONTRIBUTORS.svg
          commit_user_name: github-actions[bot]
          commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com
          commit_author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
          push_options: '--force'


================================================
FILE: .gitignore
================================================

!dist/.gitkeep
!dist/about_pake.html
!dist/cli.js
.DS_Store
.idea
.next
.vscode
*.app
*.AppImage
*.deb
*.desktop
*.dmg
*.local
*.log
*.msi
*.njsproj
*.ntvs*
*.sln
*.suo
*.sw?
*.tmp
# AI assistant docs (do not commit)
# AI Assistant files
# Editor directories and files
# Logs
.claude/
AGENT.md
AGENTS.md
CLAUDE.md
dist
dist-ssr
journal/
lerna-debug.log*
logs
node_modules
npm-debug.log*
output
pnpm-debug.log*
src-tauri/.cargo/
src-tauri/.cargo/config.toml
src-tauri/.pake/
src-tauri/gen
yarn-debug.log*
yarn-error.log*


================================================
FILE: .npmignore
================================================
# Development files
pnpm-lock.yaml
package-lock.json
yarn.lock

# Development directories
node_modules/
.vscode/
.idea/

# Build artifacts
dist-ssr/
*.local

# Development configs
.env
.env.local
.env.development
.env.test
.env.production

# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*

# OS files
.DS_Store
Thumbs.db

# Testing
coverage/
.nyc_output/

# Documentation source
docs/
*.md
!README.md

# Development scripts
script/
rollup.config.js
tsconfig.json
.prettierrc*
.eslintrc*

# Tauri development files
src-tauri/target/
src-tauri/.cargo/config.toml
src-tauri/.pake/
src-tauri/gen/
output/


================================================
FILE: .npmrc
================================================
# Suppress npm funding and audit messages during installation
fund=false
audit=false

# Resolve sharp version conflicts
prefer-dedupe=true


================================================
FILE: .pnpmrc
================================================
strict-peer-dependencies=false
node-linker=hoisted
auto-install-peers=true


================================================
FILE: .prettierignore
================================================
src-tauri/target
node_modules
dist/**/*
*.ico
*.icns
*.png
*.jpg
*.jpeg
*.gif
*.svg
*.bin
*.exe
*.dll
*.so
*.dylib
Cargo.lock
src-tauri/Cargo.lock
pnpm-lock.yaml
cli.js
*.desktop
*.wxs
*.plist
*.toml
.github/workflows/


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community

Examples of unacceptable behavior include:

- The use of erotic language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
  address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
tw93@qq.com.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.


================================================
FILE: CONTRIBUTING.md
================================================
## How to contribute to Pake

**Welcome to create [pull requests](https://github.com/tw93/Pake/compare/) for bugfix, new component, doc, example, suggestion and anything.**

## Branch Management

All development happens directly on `main`. Submit pull requests to `main`.

## Development Setup

### Prerequisites

- Node.js ≥22.0.0 (recommended LTS, older versions ≥18.0.0 may work)
- Rust ≥1.85.0 (required for edition2024 support in dependencies)
- Platform-specific build tools:
  - **macOS**: Xcode Command Line Tools (`xcode-select --install`)
  - **Windows**: Visual Studio Build Tools with MSVC
  - **Linux**: `build-essential`, `libwebkit2gtk`, system dependencies

### Installation

```bash
# Clone the repository
git clone https://github.com/tw93/Pake.git
cd Pake

# Install dependencies
pnpm install

# Start development (Tauri only)
pnpm run dev

# Start development (CLI Wrapper + Tauri) - Recommended for CLI changes
pnpm run cli:dev -- https://web.telegram.org/k/
```

### Testing

```bash
# Run all tests (unit + integration + builder)
pnpm test

# Build CLI for testing
pnpm run cli:build
```

### Tips

- Use `--iterative-build` flag during development to skip some hefty checks and use app bundle format for faster debugging:

  ```bash
  pnpm run cli:dev --iterative-build
  ```

## Continuous Integration

The project uses streamlined GitHub Actions workflows:

- **Quality & Testing**: Automatic code quality checks and comprehensive testing on all platforms
- **Claude AI Integration**: Automated code review and interactive assistance
- **Release Management**: Coordinated releases with app building and Docker publishing

## Troubleshooting

### macOS 26 Beta Compilation Issues

If 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:

```toml
[env]
# Fix for macOS 26 Beta compatibility issues
# Forces use of compatible SDK when building on macOS 26 Beta
MACOSX_DEPLOYMENT_TARGET = "15.0"
SDKROOT = "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"
```

This file is already in `.gitignore` and should not be committed to the repository.

**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.

### Common Build Issues

- **Rust compilation errors**: Run `cargo clean` in `src-tauri/` directory
- **`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
- **Node dependency issues**: Delete `node_modules` and run `pnpm install`
- **Permission errors on macOS**: Run `sudo xcode-select --reset`

See the [Advanced Usage Guide](docs/advanced-usage.md) for project structure and customization techniques.

## More

It 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.


================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1.4
# Cargo build stage - Updated to latest Rust for edition2024 support
FROM rust:latest AS cargo-builder

# Update Rust to ensure we have the latest version with edition2024 support
RUN rustup update stable && rustup default stable

# Verify Rust version supports edition2024
RUN rustc --version && cargo --version

# Install Rust dependencies
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock && \
    apt-get update && apt-get install -y --no-install-recommends \
    libdbus-1-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev \
    libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev \
    libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev \
    gnome-video-effects && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

# Set PKG_CONFIG_PATH for GLib detection
ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig

# Verify Rust version
RUN rustc --version && echo "Rust version verified"

COPY . /pake
WORKDIR /pake/src-tauri
# Build cargo packages and store cache
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    cargo fetch && \
    cargo build --release && \
    mkdir -p /cargo-cache && \
    cp -R /usr/local/cargo/registry /cargo-cache/ && \
    ([ -d "/usr/local/cargo/git" ] && cp -R /usr/local/cargo/git /cargo-cache/ || mkdir -p /usr/local/cargo/git) && \
    cp -R /usr/local/cargo/git /cargo-cache/
# Verify the content of /cargo-cache && clean unnecessary files
RUN ls -la /cargo-cache/registry && ls -la /cargo-cache/git && rm -rfd /cargo-cache/registry/src

# Main build stage
FROM rust:latest AS builder

# Update Rust to ensure we have the latest version with edition2024 support
RUN rustup update stable && rustup default stable

# Install Rust dependencies
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock && \
    apt-get update && apt-get install -y --no-install-recommends \
    libdbus-1-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev \
    libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev \
    libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev \
    gnome-video-effects && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

# Set PKG_CONFIG_PATH for GLib detection
ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig

# Verify Rust version in builder stage
RUN rustc --version && echo "Builder stage Rust version verified"

# Install Node.js 22.x and pnpm
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock && \
    curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
    apt-get update && apt-get install -y nodejs && \
    npm install -g pnpm && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

# Copy project files
COPY . /pake
WORKDIR /pake

# Copy Rust build artifacts
COPY --from=cargo-builder /pake/src-tauri /pake/src-tauri
COPY --from=cargo-builder /cargo-cache/git /usr/local/cargo/git
COPY --from=cargo-builder /cargo-cache/registry /usr/local/cargo/registry

# Install dependencies and build pake-cli
RUN --mount=type=cache,target=/root/.local/share/pnpm \
    pnpm install --frozen-lockfile && \
    pnpm run cli:build

# Set up the entrypoint
WORKDIR /output
ENTRYPOINT ["node", "/pake/dist/cli.js"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Tw93

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<h4 align="right"><strong>English</strong> | <a href="README_CN.md">简体中文</a></h4>
<p align="center">
    <img src=https://gw.alipayobjects.com/zos/k/fa/logo-modified.png width=138/>
</p>
<h1 align="center">Pake</h1>
<p align="center"><strong>Turn any webpage into a desktop app with one command, supports macOS, Windows, and Linux</strong></p>
<div align="center">
    <a href="https://twitter.com/HiTw93" target="_blank">
    <img alt="twitter" src="https://img.shields.io/badge/follow-Tw93-red?style=flat-square&logo=Twitter"></a>
    <a href="https://t.me/+GclQS9ZnxyI2ODQ1" target="_blank">
    <img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat-square&logo=Telegram"></a>
    <a href="https://github.com/tw93/Pake/releases" target="_blank">
    <img alt="GitHub downloads" src="https://img.shields.io/github/downloads/tw93/Pake/total.svg?style=flat-square"></a>
    <a href="https://github.com/tw93/Pake/commits" target="_blank">
    <img alt="GitHub commit" src="https://img.shields.io/github/commit-activity/m/tw93/Pake?style=flat-square"></a>
    <a href="https://github.com/tw93/Pake/issues?q=is%3Aissue+is%3Aclosed" target="_blank">
    <img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/tw93/Pake.svg?style=flat-square"></a>
</div>

## Features

- 🎐 **Lightweight**: Nearly 20 times smaller than Electron packages, typically around 5M
- 🚀 **Fast**: Built with Rust Tauri, much faster than traditional JS frameworks with lower memory usage
- ⚡ **Easy to use**: One-command packaging via CLI or online building, no complex configuration needed
- 📦 **Feature-rich**: Supports shortcuts, immersive windows, drag & drop, style customization, ad removal

## Getting Started

- **Beginners**: Download ready-made [Popular Packages](#popular-packages) or use [Online Building](docs/github-actions-usage.md) with no environment setup required
- **Developers**: Install [CLI Tool](docs/cli-usage.md) for one-command packaging of any website with customizable icons, window settings, and more
- **Advanced Users**: Clone the project locally for [Custom Development](#development), or check [Advanced Usage](docs/advanced-usage.md) for style customization and feature enhancement
- **Troubleshooting**: Check [FAQ](docs/faq.md) for common issues and solutions

## Popular Packages

<table>
    <tr>
        <td>WeRead
            <a href="https://github.com/tw93/Pake/releases/latest/download/WeRead.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/WeRead_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/WeRead_x86_64.deb">Linux</a>
        </td>
        <td>Twitter
            <a href="https://github.com/tw93/Pake/releases/latest/download/Twitter.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Twitter_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Twitter_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/WeRead.jpg width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Twitter.jpg width=600/></td>
    </tr>
    <tr>
        <td>Grok
            <a href="https://github.com/tw93/Pake/releases/latest/download/Grok.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Grok_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Grok_x86_64.deb">Linux</a>
        </td>
        <td>DeepSeek
            <a href="https://github.com/tw93/Pake/releases/latest/download/DeepSeek.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/DeepSeek_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/DeepSeek_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Grok.png width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/DeepSeek.png width=600/></td>
    </tr>
    <tr>
        <td>ChatGPT
            <a href="https://github.com/tw93/Pake/releases/latest/download/ChatGPT.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/ChatGPT_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/ChatGPT_x86_64.deb">Linux</a>
        </td>
        <td>Gemini
            <a href="https://github.com/tw93/Pake/releases/latest/download/Gemini.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Gemini_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Gemini_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/ChatGPT.png width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Gemini.png width=600/></td>
    </tr>
    <tr>
      <td>YouTube Music
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic_x86_64.deb">Linux</a>
      </td>
      <td>YouTube
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTube.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTube_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTube_x86_64.deb">Linux</a>
      </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/YouTubeMusic.png width=600 /></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/YouTube.jpg width=600 /></td>
    </tr>
    <tr>
        <td>LiZhi
            <a href="https://github.com/tw93/Pake/releases/latest/download/LiZhi.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/LiZhi_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/LiZhi_x86_64.deb">Linux</a>
        </td>
        <td>ProgramMusic
            <a href="https://github.com/tw93/Pake/releases/latest/download/ProgramMusic.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/ProgramMusic_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/ProgramMusic_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/LiZhi.jpg width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/ProgramMusic.jpg width=600/></td>
    </tr>
    <tr>
        <td>Excalidraw
            <a href="https://github.com/tw93/Pake/releases/latest/download/Excalidraw.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Excalidraw_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Excalidraw_x86_64.deb">Linux</a>
        </td>
        <td>XiaoHongShu
            <a href="https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Excalidraw.png width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/XiaoHongShu.png width=600/></td>
    </tr>
</table>

<details>
<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>

<br/>

| Mac                                                       | Windows/Linux                                       | Function                            |
| --------------------------------------------------------- | --------------------------------------------------- | ----------------------------------- |
| <kbd>⌘</kbd> + <kbd>[</kbd>                               | <kbd>Ctrl</kbd> + <kbd>←</kbd>                      | Return to the previous page         |
| <kbd>⌘</kbd> + <kbd>]</kbd>                               | <kbd>Ctrl</kbd> + <kbd>→</kbd>                      | Go to the next page                 |
| <kbd>⌘</kbd> + <kbd>↑</kbd>                               | <kbd>Ctrl</kbd> + <kbd>↑</kbd>                      | Auto scroll to top of page          |
| <kbd>⌘</kbd> + <kbd>↓</kbd>                               | <kbd>Ctrl</kbd> + <kbd>↓</kbd>                      | Auto scroll to bottom of page       |
| <kbd>⌘</kbd> + <kbd>r</kbd>                               | <kbd>Ctrl</kbd> + <kbd>r</kbd>                      | Refresh Page                        |
| <kbd>⌘</kbd> + <kbd>w</kbd>                               | <kbd>Ctrl</kbd> + <kbd>w</kbd>                      | Hide window, not quit               |
| <kbd>⌘</kbd> + <kbd>-</kbd>                               | <kbd>Ctrl</kbd> + <kbd>-</kbd>                      | Zoom out the page                   |
| <kbd>⌘</kbd> + <kbd>=</kbd>                               | <kbd>Ctrl</kbd> + <kbd>=</kbd>                      | Zoom in the Page                    |
| <kbd>⌘</kbd> + <kbd>0</kbd>                               | <kbd>Ctrl</kbd> + <kbd>0</kbd>                      | Reset the page zoom                 |
| <kbd>⌘</kbd> + <kbd>L</kbd>                               | <kbd>Ctrl</kbd> + <kbd>L</kbd>                      | Copy Current Page URL               |
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌥</kbd> + <kbd>V</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd>   | Paste and Match Style               |
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>H</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd>   | Go to Home Page                     |
| <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>   | Toggle Developer Tools (Debug Only) |
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌫</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Del</kbd> | Clear Cache & Restart               |

In 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.

</details>

## Command-Line Packaging

![Pake](https://raw.githubusercontent.com/tw93/static/main/pake/pake1.gif)

```bash
# Install Pake CLI
pnpm install -g pake-cli

# Basic usage - automatically fetches website icon
pake https://github.com --name GitHub

# Advanced usage with custom options
pake https://weekly.tw93.fun --name Weekly --icon https://cdn.tw93.fun/pake/weekly.icns --width 1200 --height 800 --hide-title-bar
```

First-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).

## Development

Requires 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.

```bash
# Install dependencies
pnpm i

# Local development [right-click to open debug mode]
pnpm run dev

# Build application
pnpm run build
```

For style customization, feature enhancement, container communication and other advanced features, see [Advanced Usage Documentation](docs/advanced-usage.md).

## Developers

Pake's development can not be without these Hackers. They contributed a lot of capabilities for Pake. Also, welcome to follow them! ❤️

<a href="https://github.com/tw93/Pake/graphs/contributors">
  <img src="./CONTRIBUTORS.svg?v=2" alt="Contributors" width="1000" />
</a>

## Support

<a href="https://miaoyan.app/cats.html?name=Pake"><img src="https://miaoyan.app/assets/sponsors.svg" width="1000px" /></a>

1. 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>.
2. 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.
3. 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.
4. I hope that you enjoy playing with it. Let us know if you find a website that would be great for a Mac App!


================================================
FILE: README_CN.md
================================================
<h4 align="right"><a href="README.md">English</a> | <strong>简体中文</strong></h4>
<p align="center">
    <img src=https://gw.alipayobjects.com/zos/k/fa/logo-modified.png width=138/>
</p>
<h1 align="center">Pake</h1>
<p align="center"><strong>一键打包网页生成轻量桌面应用,支持 macOS、Windows 和 Linux</strong></p>
<div align="center">
    <a href="https://twitter.com/HiTw93" target="_blank">
    <img alt="twitter" src="https://img.shields.io/badge/follow-Tw93-red?style=flat-square&logo=Twitter"></a>
    <a href="https://t.me/+GclQS9ZnxyI2ODQ1" target="_blank">
    <img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat-square&logo=Telegram"></a>
    <a href="https://github.com/tw93/Pake/releases" target="_blank">
    <img alt="GitHub downloads" src="https://img.shields.io/github/downloads/tw93/Pake/total.svg?style=flat-square"></a>
    <a href="https://github.com/tw93/Pake/commits" target="_blank">
    <img alt="GitHub commit" src="https://img.shields.io/github/commit-activity/m/tw93/Pake?style=flat-square"></a>
    <a href="https://github.com/tw93/Pake/issues?q=is%3Aissue+is%3Aclosed" target="_blank">
    <img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/tw93/Pake.svg?style=flat-square"></a>
</div>

## 特征

- 🎐 **体积小巧**:相比 Electron 应用小近 20 倍,通常只有 5M 左右
- 🚀 **性能优异**:基于 Rust Tauri,比传统 JS 框架更快,内存占用更少
- ⚡ **使用简单**:命令行一键打包,或在线构建,无需复杂配置
- 📦 **功能丰富**:支持快捷键透传、沉浸式窗口、拖拽、样式定制、去广告

## 快速开始

- **新手用户**:直接下载现成的 [常用包](#常用包下载),或通过 [在线构建](docs/github-actions-usage_CN.md) 无需环境配置即可打包
- **开发者**:安装 [CLI 工具](docs/cli-usage_CN.md) 后一行命令打包任意网站,支持自定义图标、窗口等参数
- **高级用户**:本地克隆项目进行 [定制开发](#定制开发),或查看 [高级用法](docs/advanced-usage_CN.md) 实现样式定制、功能增强
- **遇到问题**:查看 [常见问题](docs/faq_CN.md) 获取常见问题的解决方案

## 常用包下载

<table>
    <tr>
        <td>WeRead
            <a href="https://github.com/tw93/Pake/releases/latest/download/WeRead.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/WeRead_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/WeRead_x86_64.deb">Linux</a>
        </td>
        <td>Twitter
            <a href="https://github.com/tw93/Pake/releases/latest/download/Twitter.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Twitter_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Twitter_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/WeRead.jpg width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Twitter.jpg width=600/></td>
    </tr>
    <tr>
        <td>Grok
            <a href="https://github.com/tw93/Pake/releases/latest/download/Grok.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Grok_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Grok_x86_64.deb">Linux</a>
        </td>
        <td>DeepSeek
            <a href="https://github.com/tw93/Pake/releases/latest/download/DeepSeek.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/DeepSeek_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/DeepSeek_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Grok.png width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/DeepSeek.png width=600/></td>
    </tr>
    <tr>
        <td>ChatGPT
            <a href="https://github.com/tw93/Pake/releases/latest/download/ChatGPT.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/ChatGPT_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/ChatGPT_x86_64.deb">Linux</a>
        </td>
        <td>Gemini
            <a href="https://github.com/tw93/Pake/releases/latest/download/Gemini.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Gemini_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Gemini_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/ChatGPT.png width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Gemini.png width=600/></td>
    </tr>
    <tr>
      <td>YouTube Music
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTubeMusic_x86_64.deb">Linux</a>
      </td>
      <td>YouTube
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTube.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTube_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/YouTube_x86_64.deb">Linux</a>
      </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/YouTubeMusic.png width=600 /></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/YouTube.jpg width=600 /></td>
    </tr>
    <tr>
        <td>LiZhi
            <a href="https://github.com/tw93/Pake/releases/latest/download/LiZhi.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/LiZhi_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/LiZhi_x86_64.deb">Linux</a>
        </td>
        <td>ProgramMusic
            <a href="https://github.com/tw93/Pake/releases/latest/download/ProgramMusic.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/ProgramMusic_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/ProgramMusic_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/LiZhi.jpg width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/ProgramMusic.jpg width=600/></td>
    </tr>
    <tr>
        <td>Excalidraw
            <a href="https://github.com/tw93/Pake/releases/latest/download/Excalidraw.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Excalidraw_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/Excalidraw_x86_64.deb">Linux</a>
        </td>
        <td>XiaoHongShu
            <a href="https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu.dmg">Mac</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu_x64.msi">Windows</a>
            <a href="https://github.com/tw93/Pake/releases/latest/download/XiaoHongShu_x86_64.deb">Linux</a>
        </td>
    </tr>
    <tr>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Excalidraw.png width=600/></td>
        <td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/XiaoHongShu.png width=600/></td>
    </tr>
</table>

<details>

<summary>🏂 更多应用可去 <a href="https://github.com/tw93/Pake/releases">Release</a>下载,<b>此外点击可展开快捷键说明</b></summary>

<br/>

| Mac                                                       | Windows/Linux                                       | 功能                |
| --------------------------------------------------------- | --------------------------------------------------- | ------------------- |
| <kbd>⌘</kbd> + <kbd>[</kbd>                               | <kbd>Ctrl</kbd> + <kbd>←</kbd>                      | 返回上一个页面      |
| <kbd>⌘</kbd> + <kbd>]</kbd>                               | <kbd>Ctrl</kbd> + <kbd>→</kbd>                      | 去下一个页面        |
| <kbd>⌘</kbd> + <kbd>↑</kbd>                               | <kbd>Ctrl</kbd> + <kbd>↑</kbd>                      | 自动滚动到页面顶部  |
| <kbd>⌘</kbd> + <kbd>↓</kbd>                               | <kbd>Ctrl</kbd> + <kbd>↓</kbd>                      | 自动滚动到页面底部  |
| <kbd>⌘</kbd> + <kbd>r</kbd>                               | <kbd>Ctrl</kbd> + <kbd>r</kbd>                      | 刷新页面            |
| <kbd>⌘</kbd> + <kbd>w</kbd>                               | <kbd>Ctrl</kbd> + <kbd>w</kbd>                      | 隐藏窗口,非退出     |
| <kbd>⌘</kbd> + <kbd>-</kbd>                               | <kbd>Ctrl</kbd> + <kbd>-</kbd>                      | 缩小页面            |
| <kbd>⌘</kbd> + <kbd>=</kbd>                               | <kbd>Ctrl</kbd> + <kbd>=</kbd>                      | 放大页面            |
| <kbd>⌘</kbd> + <kbd>0</kbd>                               | <kbd>Ctrl</kbd> + <kbd>0</kbd>                      | 重置页面缩放        |
| <kbd>⌘</kbd> + <kbd>L</kbd>                               | <kbd>Ctrl</kbd> + <kbd>L</kbd>                      | 复制当前页面网址    |
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌥</kbd> + <kbd>V</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd>   | 粘贴并匹配样式      |
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>H</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd>   | 回到首页            |
| <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>   | 开启调试 (仅开发版) |
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌫</kbd>                | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Del</kbd> | 清除缓存并重启      |

此外还支持双击头部全屏切换,拖拽头部移动窗口,Mac 用户支持手势返回和前进,新菜单也提供了导航、缩放和窗口控制等选项。

</details>

## 命令行一键打包

![Pake](https://raw.githubusercontent.com/tw93/static/main/pake/pake1.gif)

```bash
# 安装 Pake CLI
pnpm install -g pake-cli

# 基础用法 - 自动获取网站图标
pake https://github.com --name GitHub

# 高级用法:自定义选项
pake https://weekly.tw93.fun --name Weekly --icon https://cdn.tw93.fun/pake/weekly.icns --width 1200 --height 800 --hide-title-bar
```

首次打包需要安装环境会比较慢,后续很快。完整参数说明查看 [CLI 使用指南](docs/cli-usage_CN.md),不想用命令行可以试试 [GitHub Actions 在线构建](docs/github-actions-usage_CN.md)。

## 定制开发

需要 Rust `>=1.85` 和 Node `>=22`,详细安装指南参考 [Tauri 文档](https://tauri.app/start/prerequisites/)。不熟悉开发环境建议直接使用命令行工具。

```bash
# 安装依赖
pnpm i

# 本地开发[右键可打开调试模式]
pnpm run dev

# 打包应用
pnpm run build
```

想要样式定制、功能增强、容器通信等高级玩法,查看 [高级用法文档](docs/advanced-usage_CN.md)。

## 开发者

Pake 的发展离不开这些优秀的贡献者 ❤️

<a href="https://github.com/tw93/Pake/graphs/contributors">
  <img src="https://raw.githubusercontent.com/tw93/Pake/main/CONTRIBUTORS.svg?sanitize=true" alt="Contributors" width="1000" />
</a>

## 支持

<a href="https://miaoyan.app/cats.html?name=Pake"><img src="https://miaoyan.app/assets/sponsors.svg" width="1000px" /></a>

1. 我有两只猫,一只叫汤圆,一只可乐,假如 Pake 让你生活更美好,可以给她们 <a href="https://miaoyan.app/cats.html?name=Pake" target="_blank">喂罐头 🥩</a>。
2. 如果你喜欢 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) 给志同道合的朋友使用。
3. 可以关注我的 [Twitter](https://twitter.com/HiTw93) 获取最新的 Pake 更新消息,也欢迎加入 [Telegram](https://t.me/+GclQS9ZnxyI2ODQ1) 聊天群。
4. 希望大伙玩的过程中有一种学习新技术的喜悦感,发现适合做成桌面 App 的网页也欢迎告诉我。


================================================
FILE: action.yml
================================================
name: "Pake Web App Builder"
description: "Transform any webpage into a lightweight desktop app using Rust and Tauri"
author: "tw93"
branding:
  icon: "package"
  color: "blue"

inputs:
  url:
    description: "Target URL to package"
    required: true

  name:
    description: "Application name"
    required: true

  output-dir:
    description: "Output directory for packages"
    required: false
    default: "dist"

  icon:
    description: "Custom app icon URL or path"
    required: false

  width:
    description: "Window width"
    required: false
    default: "1200"

  height:
    description: "Window height"
    required: false
    default: "780"

  debug:
    description: "Enable debug mode"
    required: false
    default: "false"

outputs:
  package-path:
    description: "Path to the generated package"

runs:
  using: "composite"
  steps:
    - name: Setup Environment
      shell: bash
      run: |
        # Install Node.js dependencies
        npm install

        # Build Pake CLI if not exists
        if [ ! -f "dist/cli.js" ]; then
          npm run cli:build
        fi

        # Ensure node is accessible in subsequent steps
        echo "$(npm bin)" >> $GITHUB_PATH

        # Install Rust/Cargo if needed
        if ! command -v cargo &> /dev/null; then
          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
          source ~/.cargo/env
          echo "$HOME/.cargo/bin" >> $GITHUB_PATH
        fi

    - name: Build Pake App
      shell: bash
      run: |
        # Build arguments
        ARGS=("${{ inputs.url }}")

        ARGS+=("--name" "${{ inputs.name }}")

        if [ -n "${{ inputs.icon }}" ]; then
          ARGS+=("--icon" "${{ inputs.icon }}")
        fi

        ARGS+=("--width" "${{ inputs.width }}")
        ARGS+=("--height" "${{ inputs.height }}")

        if [ "${{ inputs.debug }}" == "true" ]; then
          ARGS+=("--debug")
        fi

        # Create output directory
        mkdir -p "${{ inputs.output-dir }}"
        export PAKE_CREATE_APP=1

        # Run Pake CLI
        echo "🔧 Running: node dist/cli.js ${ARGS[*]}"
        node dist/cli.js "${ARGS[@]}"

        # Find generated package and set output
        PACKAGE=$(find src-tauri/target -type f \( -name "*.deb" -o -name "*.exe" -o -name "*.msi" -o -name "*.dmg" \) 2>/dev/null | head -1)

        # If no file packages found, look for .app directory (macOS)
        if [ -z "$PACKAGE" ]; then
          PACKAGE=$(find src-tauri/target -type d -name "*.app" 2>/dev/null | head -1)
        fi
        if [ -n "$PACKAGE" ]; then
          # Move to output directory
          BASENAME=$(basename "$PACKAGE")
          mv "$PACKAGE" "${{ inputs.output-dir }}/$BASENAME" 2>/dev/null || cp -r "$PACKAGE" "${{ inputs.output-dir }}/$BASENAME"
          echo "package-path=${{ inputs.output-dir }}/$BASENAME" >> $GITHUB_OUTPUT
          echo "✅ Package created: ${{ inputs.output-dir }}/$BASENAME"
        else
          echo "❌ No package found"
          exit 1
        fi


================================================
FILE: bin/builders/BaseBuilder.ts
================================================
import path from 'path';
import fsExtra from 'fs-extra';
import chalk from 'chalk';
import prompts from 'prompts';

import { PakeAppOptions } from '@/types';
import { checkRustInstalled, ensureRustEnv, installRust } from '@/helpers/rust';
import { mergeConfig } from '@/helpers/merge';
import tauriConfig from '@/helpers/tauriConfig';
import {
  generateIdentifierSafeName,
  generateLinuxPackageName,
} from '@/utils/name';
import { npmDirectory } from '@/utils/dir';
import { getSpinner } from '@/utils/info';
import { shellExec } from '@/utils/shell';
import { isChinaDomain } from '@/utils/ip';
import { IS_MAC } from '@/utils/platform';
import logger from '@/options/logger';

export default abstract class BaseBuilder {
  protected options: PakeAppOptions;
  private static packageManagerCache: string | null = null;

  protected constructor(options: PakeAppOptions) {
    this.options = options;
  }

  private getBuildEnvironment() {
    return IS_MAC
      ? {
          CFLAGS: '-fno-modules',
          CXXFLAGS: '-fno-modules',
          MACOSX_DEPLOYMENT_TARGET: '14.0',
        }
      : undefined;
  }

  private getInstallTimeout(): number {
    // Windows needs more time due to native compilation and antivirus scanning
    return process.platform === 'win32' ? 900000 : 600000;
  }

  private getBuildTimeout(): number {
    return 900000;
  }

  private async detectPackageManager(): Promise<string> {
    if (BaseBuilder.packageManagerCache) {
      return BaseBuilder.packageManagerCache;
    }

    const { execa } = await import('execa');

    try {
      await execa('pnpm', ['--version'], { stdio: 'ignore' });
      logger.info('✺ Using pnpm for package management.');
      BaseBuilder.packageManagerCache = 'pnpm';
      return 'pnpm';
    } catch {
      try {
        await execa('npm', ['--version'], { stdio: 'ignore' });
        logger.info('✺ pnpm not available, using npm for package management.');
        BaseBuilder.packageManagerCache = 'npm';
        return 'npm';
      } catch {
        throw new Error(
          'Neither pnpm nor npm is available. Please install a package manager.',
        );
      }
    }
  }

  async prepare() {
    const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
    const tauriTargetPath = path.join(tauriSrcPath, 'target');
    const tauriTargetPathExists = await fsExtra.pathExists(tauriTargetPath);

    if (!IS_MAC && !tauriTargetPathExists) {
      logger.warn('✼ The first use requires installing system dependencies.');
      logger.warn('✼ See more in https://tauri.app/start/prerequisites/.');
    }

    ensureRustEnv();

    if (!checkRustInstalled()) {
      const res = await prompts({
        type: 'confirm',
        message: 'Rust not detected. Install now?',
        name: 'value',
      });

      if (res.value) {
        await installRust();
      } else {
        logger.error('✕ Rust required to package your webapp.');
        process.exit(0);
      }
    }

    const isChina = await isChinaDomain('www.npmjs.com');
    const spinner = getSpinner('Installing package...');
    const rustProjectDir = path.join(tauriSrcPath, '.cargo');
    const projectConf = path.join(rustProjectDir, 'config.toml');
    await fsExtra.ensureDir(rustProjectDir);

    // Detect available package manager
    const packageManager = await this.detectPackageManager();
    const registryOption = ' --registry=https://registry.npmmirror.com';
    const peerDepsOption =
      packageManager === 'npm' ? ' --legacy-peer-deps' : '';

    const timeout = this.getInstallTimeout();
    const buildEnv = this.getBuildEnvironment();

    // Show helpful message for first-time users
    if (!tauriTargetPathExists) {
      logger.info(
        process.platform === 'win32'
          ? '✺ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'
          : '✺ First-time setup may take 5-10 minutes (installing dependencies)...',
      );
    }

    let usedMirror = isChina;

    try {
      if (isChina) {
        logger.info(
          `✺ Located in China, using ${packageManager}/rsProxy CN mirror.`,
        );
        const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
        await fsExtra.copy(projectCnConf, projectConf);
        await shellExec(
          `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`,
          timeout,
          { ...buildEnv, CI: 'true' },
        );
      } else {
        await shellExec(
          `cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`,
          timeout,
          { ...buildEnv, CI: 'true' },
        );
      }
      spinner.succeed(chalk.green('Package installed!'));
    } catch (error: unknown) {
      // If installation times out and we haven't tried the mirror yet, retry with mirror
      if (
        error instanceof Error &&
        error.message.includes('timed out') &&
        !usedMirror
      ) {
        spinner.fail(
          chalk.yellow('Installation timed out, retrying with CN mirror...'),
        );
        logger.info(
          '✺ Retrying installation with CN mirror for better speed...',
        );

        const retrySpinner = getSpinner('Retrying installation...');
        usedMirror = true;

        try {
          const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
          await fsExtra.copy(projectCnConf, projectConf);
          await shellExec(
            `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`,
            timeout,
            { ...buildEnv, CI: 'true' },
          );
          retrySpinner.succeed(
            chalk.green('Package installed with CN mirror!'),
          );
        } catch (retryError) {
          retrySpinner.fail(chalk.red('Installation failed'));
          throw retryError;
        }
      } else {
        spinner.fail(chalk.red('Installation failed'));
        throw error;
      }
    }

    if (!tauriTargetPathExists) {
      logger.warn(
        '✼ The first packaging may be slow, please be patient and wait, it will be faster afterwards.',
      );
    }
  }

  async build(url: string) {
    await this.buildAndCopy(url, this.options.targets);
  }

  async start(url: string) {
    logger.info('Pake dev server starting...');
    await mergeConfig(url, this.options, tauriConfig);

    const packageManager = await this.detectPackageManager();
    const configPath = path.join(
      npmDirectory,
      'src-tauri',
      '.pake',
      'tauri.conf.json',
    );

    const features = this.getBuildFeatures();
    const featureArgs =
      features.length > 0 ? `--features ${features.join(',')}` : '';

    const argSeparator = packageManager === 'npm' ? ' --' : '';
    const command = `cd "${npmDirectory}" && ${packageManager} run tauri${argSeparator} dev --config "${configPath}" ${featureArgs}`;

    await shellExec(command);
  }

  async buildAndCopy(url: string, target: string) {
    const { name = 'pake-app' } = this.options;
    await mergeConfig(url, this.options, tauriConfig);

    // Detect available package manager
    const packageManager = await this.detectPackageManager();

    // Build app
    const buildSpinner = getSpinner('Building app...');
    // Let spinner run for a moment so user can see it, then stop before package manager command
    await new Promise((resolve) => setTimeout(resolve, 500));
    buildSpinner.stop();
    // Show static message to keep the status visible
    logger.warn('✸ Building app...');

    const baseEnv = this.getBuildEnvironment();
    let buildEnv: Record<string, string> = {
      ...(baseEnv ?? {}),
      ...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
    };

    const resolveExecEnv = () =>
      Object.keys(buildEnv).length > 0 ? buildEnv : undefined;

    // Warn users about potential AppImage build failures on modern Linux systems.
    // The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't
    // recognize the .relr.dyn section introduced in glibc 2.38+.
    if (process.platform === 'linux' && target === 'appimage') {
      if (!buildEnv.NO_STRIP) {
        logger.warn(
          '⚠ Building AppImage on Linux may fail due to strip incompatibility with glibc 2.38+',
        );
        logger.warn(
          '⚠ If build fails, retry with: NO_STRIP=1 pake <url> --targets appimage',
        );
      }
    }

    const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
    const buildTimeout = this.getBuildTimeout();

    try {
      await shellExec(buildCommand, buildTimeout, resolveExecEnv());
    } catch (error) {
      const shouldRetryWithoutStrip =
        process.platform === 'linux' &&
        target === 'appimage' &&
        !buildEnv.NO_STRIP &&
        this.isLinuxDeployStripError(error);

      if (shouldRetryWithoutStrip) {
        logger.warn(
          '⚠ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.',
        );
        buildEnv = {
          ...buildEnv,
          NO_STRIP: '1',
        };
        await shellExec(buildCommand, buildTimeout, resolveExecEnv());
      } else {
        throw error;
      }
    }

    // Copy app
    const fileName = this.getFileName();
    const fileType = this.getFileType(target);
    const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
    const distPath = path.resolve(`${name}.${fileType}`);
    await fsExtra.copy(appPath, distPath);

    // Copy raw binary if requested
    if (this.options.keepBinary) {
      await this.copyRawBinary(npmDirectory, name);
    }

    await fsExtra.remove(appPath);
    logger.success('✔ Build success!');
    logger.success('✔ App installer located in', distPath);

    // Log binary location if preserved
    if (this.options.keepBinary) {
      const binaryPath = this.getRawBinaryPath(name);
      logger.success('✔ Raw binary located in', path.resolve(binaryPath));
    }

    if (IS_MAC && fileType === 'app' && this.options.install) {
      await this.installAppToApplications(distPath, name);
    }
  }

  private async installAppToApplications(
    appBundlePath: string,
    appName: string,
  ): Promise<void> {
    try {
      logger.info(`- Installing ${appName} to /Applications...`);

      const appBundleName = path.basename(appBundlePath);
      const appDest = path.join('/Applications', appBundleName);

      // fsExtra.move uses fs.rename (atomic on same filesystem) and falls back
      // to copy+remove only when moving across volumes.
      await fsExtra.move(appBundlePath, appDest, { overwrite: true });

      logger.success(
        `✔ ${appBundleName.replace(/\.app$/, '')} installed to /Applications`,
      );
    } catch (error) {
      logger.error(`✕ Failed to install ${appName}: ${error}`);
      logger.info(`  App bundle still available at: ${appBundlePath}`);
    }
  }

  protected getFileType(target: string): string {
    return target;
  }

  abstract getFileName(): string;

  private isLinuxDeployStripError(error: unknown): boolean {
    if (!(error instanceof Error) || !error.message) {
      return false;
    }
    const message = error.message.toLowerCase();
    return (
      message.includes('linuxdeploy') ||
      message.includes('failed to run linuxdeploy') ||
      message.includes('strip:') ||
      message.includes('unable to recognise the format of the input file') ||
      message.includes('appimage tool failed') ||
      message.includes('strip tool')
    );
  }

  // 架构映射配置
  protected static readonly ARCH_MAPPINGS: Record<
    string,
    Record<string, string>
  > = {
    darwin: {
      arm64: 'aarch64-apple-darwin',
      x64: 'x86_64-apple-darwin',
      universal: 'universal-apple-darwin',
    },
    win32: {
      arm64: 'aarch64-pc-windows-msvc',
      x64: 'x86_64-pc-windows-msvc',
    },
    linux: {
      arm64: 'aarch64-unknown-linux-gnu',
      x64: 'x86_64-unknown-linux-gnu',
    },
  };

  // 架构名称映射(用于文件名生成)
  protected static readonly ARCH_DISPLAY_NAMES: Record<string, string> = {
    arm64: 'aarch64',
    x64: 'x64',
    universal: 'universal',
  };

  /**
   * 解析目标架构
   */
  protected resolveTargetArch(requestedArch?: string): string {
    if (requestedArch === 'auto' || !requestedArch) {
      return process.arch;
    }
    return requestedArch;
  }

  /**
   * 获取Tauri构建目标
   */
  protected getTauriTarget(
    arch: string,
    platform: NodeJS.Platform = process.platform,
  ): string | null {
    const platformMappings = BaseBuilder.ARCH_MAPPINGS[platform];
    if (!platformMappings) return null;
    return platformMappings[arch] || null;
  }

  /**
   * 获取架构显示名称(用于文件名)
   */
  protected getArchDisplayName(arch: string): string {
    return BaseBuilder.ARCH_DISPLAY_NAMES[arch] || arch;
  }

  /**
   * 构建基础构建命令
   */
  protected buildBaseCommand(
    packageManager: string,
    configPath: string,
    target?: string,
  ): string {
    const baseCommand = this.options.debug
      ? `${packageManager} run build:debug`
      : `${packageManager} run build`;

    const argSeparator = packageManager === 'npm' ? ' --' : '';
    let fullCommand = `${baseCommand}${argSeparator} -c "${configPath}"`;

    if (target) {
      fullCommand += ` --target ${target}`;
    }

    // Enable verbose output in debug mode to help diagnose build issues.
    // This provides detailed logs from Tauri CLI and bundler tools.
    if (this.options.debug) {
      fullCommand += ' --verbose';
    }

    return fullCommand;
  }

  /**
   * 获取构建特性列表
   */
  protected getBuildFeatures(): string[] {
    const features = ['cli-build'];

    // Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
    if (IS_MAC) {
      const macOSVersion = this.getMacOSMajorVersion();
      if (macOSVersion >= 23) {
        features.push('macos-proxy');
      }
    }

    return features;
  }

  protected getBuildCommand(packageManager: string = 'pnpm'): string {
    // Use temporary config directory to avoid modifying source files
    const configPath = path.join(
      npmDirectory,
      'src-tauri',
      '.pake',
      'tauri.conf.json',
    );

    let fullCommand = this.buildBaseCommand(packageManager, configPath);

    // For macOS, use app bundles by default unless DMG is explicitly requested
    if (IS_MAC && this.options.targets === 'app') {
      fullCommand += ' --bundles app';
    }

    // Add features
    const features = this.getBuildFeatures();
    if (features.length > 0) {
      fullCommand += ` --features ${features.join(',')}`;
    }

    return fullCommand;
  }

  protected getMacOSMajorVersion(): number {
    try {
      const os = require('os');
      const release = os.release();
      const majorVersion = parseInt(release.split('.')[0], 10);
      return majorVersion;
    } catch (error) {
      return 0; // Disable proxy feature if version detection fails
    }
  }

  protected getBasePath(): string {
    const basePath = this.options.debug ? 'debug' : 'release';
    return `src-tauri/target/${basePath}/bundle/`;
  }

  protected getBuildAppPath(
    npmDirectory: string,
    fileName: string,
    fileType: string,
  ): string {
    // For app bundles on macOS, the directory is 'macos', not 'app'
    const bundleDir =
      fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
    return path.join(
      npmDirectory,
      this.getBasePath(),
      bundleDir,
      `${fileName}.${fileType}`,
    );
  }

  /**
   * Copy raw binary file to output directory
   */
  protected async copyRawBinary(
    npmDirectory: string,
    appName: string,
  ): Promise<void> {
    const binaryPath = this.getRawBinarySourcePath(npmDirectory, appName);
    const outputPath = this.getRawBinaryPath(appName);

    if (await fsExtra.pathExists(binaryPath)) {
      await fsExtra.copy(binaryPath, outputPath);
      // Make binary executable on Unix-like systems
      if (process.platform !== 'win32') {
        await fsExtra.chmod(outputPath, 0o755);
      }
    } else {
      logger.warn(`✼ Raw binary not found at ${binaryPath}, skipping...`);
    }
  }

  /**
   * Get the source path of the raw binary file in the build directory
   */
  protected getRawBinarySourcePath(
    npmDirectory: string,
    appName: string,
  ): string {
    const basePath = this.options.debug ? 'debug' : 'release';
    const binaryName = this.getBinaryName(appName);

    // Handle cross-platform builds
    if (this.options.multiArch || this.hasArchSpecificTarget()) {
      return path.join(
        npmDirectory,
        this.getArchSpecificPath(),
        basePath,
        binaryName,
      );
    }

    return path.join(npmDirectory, 'src-tauri/target', basePath, binaryName);
  }

  /**
   * Get the output path for the raw binary file
   */
  protected getRawBinaryPath(appName: string): string {
    const extension = process.platform === 'win32' ? '.exe' : '';
    const suffix = process.platform === 'win32' ? '' : '-binary';
    return `${appName}${suffix}${extension}`;
  }

  /**
   * Get the binary name based on app name and platform
   */
  protected getBinaryName(appName: string): string {
    const extension = process.platform === 'win32' ? '.exe' : '';

    // Use unique binary name for all platforms to avoid conflicts
    const nameToUse =
      process.platform === 'linux'
        ? generateLinuxPackageName(appName)
        : generateIdentifierSafeName(appName);
    return `pake-${nameToUse}${extension}`;
  }

  /**
   * Check if this build has architecture-specific target
   */
  protected hasArchSpecificTarget(): boolean {
    return false; // Override in subclasses if needed
  }

  /**
   * Get architecture-specific path for binary
   */
  protected getArchSpecificPath(): string {
    return 'src-tauri/target'; // Override in subclasses if needed
  }
}


================================================
FILE: bin/builders/BuilderProvider.ts
================================================
import BaseBuilder from './BaseBuilder';
import MacBuilder from './MacBuilder';
import WinBuilder from './WinBuilder';
import LinuxBuilder from './LinuxBuilder';
import { PakeAppOptions } from '@/types';

const { platform } = process;

const buildersMap: Record<
  string,
  new (options: PakeAppOptions) => BaseBuilder
> = {
  darwin: MacBuilder,
  win32: WinBuilder,
  linux: LinuxBuilder,
};

export default class BuilderProvider {
  static create(options: PakeAppOptions): BaseBuilder {
    const Builder = buildersMap[platform];
    if (!Builder) {
      throw new Error('The current system is not supported!');
    }
    return new Builder(options);
  }
}


================================================
FILE: bin/builders/LinuxBuilder.ts
================================================
import path from 'path';
import BaseBuilder from './BaseBuilder';
import { PakeAppOptions } from '@/types';
import tauriConfig from '@/helpers/tauriConfig';

export default class LinuxBuilder extends BaseBuilder {
  private buildFormat: string;
  private buildArch: string;
  private currentBuildType: string = '';

  constructor(options: PakeAppOptions) {
    super(options);

    const target = options.targets || 'deb';
    if (target.includes('-arm64')) {
      this.buildFormat = target.replace('-arm64', '');
      this.buildArch = 'arm64';
    } else {
      this.buildFormat = target;
      this.buildArch = this.resolveTargetArch('auto');
    }

    this.options.targets = this.buildFormat;
  }

  getFileName() {
    const { name = 'pake-app', targets } = this.options;
    const version = tauriConfig.version;
    const buildType =
      this.currentBuildType || targets.split(',').map((t) => t.trim())[0];

    let arch: string;
    if (this.buildArch === 'arm64') {
      arch =
        buildType === 'rpm' || buildType === 'appimage' ? 'aarch64' : 'arm64';
    } else {
      if (this.buildArch === 'x64') {
        arch = buildType === 'rpm' ? 'x86_64' : 'amd64';
      } else {
        arch = this.buildArch;
        if (
          this.buildArch === 'arm64' &&
          (buildType === 'rpm' || buildType === 'appimage')
        ) {
          arch = 'aarch64';
        }
      }
    }

    if (this.currentBuildType === 'rpm') {
      return `${name}-${version}-1.${arch}`;
    }

    return `${name}_${version}_${arch}`;
  }

  async build(url: string) {
    const targetTypes = ['deb', 'appimage', 'rpm'];
    const requestedTargets = this.options.targets
      .split(',')
      .map((t: string) => t.trim());

    for (const target of targetTypes) {
      if (requestedTargets.includes(target)) {
        this.currentBuildType = target;
        await this.buildAndCopy(url, target);
      }
    }
  }

  // Override buildAndCopy to ensure currentBuildType is synced if called directly, though the loop above handles it most of the time.
  async buildAndCopy(url: string, target: string) {
    this.currentBuildType = target;
    await super.buildAndCopy(url, target);
  }

  protected getBuildCommand(packageManager: string = 'pnpm'): string {
    const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');

    const buildTarget =
      this.buildArch === 'arm64'
        ? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
        : undefined;

    let fullCommand = this.buildBaseCommand(
      packageManager,
      configPath,
      buildTarget,
    );

    const features = this.getBuildFeatures();
    if (features.length > 0) {
      fullCommand += ` --features ${features.join(',')}`;
    }

    if (this.currentBuildType) {
      fullCommand += ` --bundles ${this.currentBuildType}`;
    }

    // Enable verbose output for AppImage builds when debugging or PAKE_VERBOSE is set.
    // AppImage builds often fail with minimal error messages from linuxdeploy,
    // so verbose mode helps diagnose issues like strip failures and missing dependencies.
    if (
      this.currentBuildType === 'appimage' &&
      (this.options.targets.includes('appimage') ||
        this.options.debug ||
        process.env.PAKE_VERBOSE)
    ) {
      fullCommand += ' --verbose';
    }

    return fullCommand;
  }

  protected getBasePath(): string {
    const basePath = this.options.debug ? 'debug' : 'release';

    if (this.buildArch === 'arm64') {
      const target = this.getTauriTarget(this.buildArch, 'linux');
      return `src-tauri/target/${target}/${basePath}/bundle/`;
    }

    return super.getBasePath();
  }

  protected getFileType(target: string): string {
    if (target === 'appimage') {
      return 'AppImage';
    }
    return super.getFileType(target);
  }

  protected hasArchSpecificTarget(): boolean {
    return this.buildArch === 'arm64';
  }

  protected getArchSpecificPath(): string {
    if (this.buildArch === 'arm64') {
      const target = this.getTauriTarget(this.buildArch, 'linux');
      return `src-tauri/target/${target}`;
    }
    return super.getArchSpecificPath();
  }
}


================================================
FILE: bin/builders/MacBuilder.ts
================================================
import path from 'path';
import tauriConfig from '@/helpers/tauriConfig';
import { PakeAppOptions } from '@/types';
import BaseBuilder from './BaseBuilder';

export default class MacBuilder extends BaseBuilder {
  private buildFormat: string;
  private buildArch: string;

  constructor(options: PakeAppOptions) {
    super(options);

    const validArchs = ['intel', 'apple', 'universal', 'auto', 'x64', 'arm64'];
    this.buildArch = validArchs.includes(options.targets || '')
      ? options.targets
      : 'auto';

    if (
      options.iterativeBuild ||
      options.install ||
      process.env.PAKE_CREATE_APP === '1'
    ) {
      this.buildFormat = 'app';
    } else {
      this.buildFormat = 'dmg';
    }

    this.options.targets = this.buildFormat;
  }

  getFileName(): string {
    const { name = 'pake-app' } = this.options;

    if (this.buildFormat === 'app') {
      return name;
    }

    let arch: string;
    if (this.buildArch === 'universal' || this.options.multiArch) {
      arch = 'universal';
    } else if (this.buildArch === 'apple') {
      arch = 'aarch64';
    } else if (this.buildArch === 'intel') {
      arch = 'x64';
    } else {
      arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));
    }
    return `${name}_${tauriConfig.version}_${arch}`;
  }

  private getActualArch(): string {
    if (this.buildArch === 'universal' || this.options.multiArch) {
      return 'universal';
    } else if (this.buildArch === 'apple') {
      return 'arm64';
    } else if (this.buildArch === 'intel') {
      return 'x64';
    }
    return this.resolveTargetArch(this.buildArch);
  }

  protected getBuildCommand(packageManager: string = 'pnpm'): string {
    const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
    const actualArch = this.getActualArch();

    const buildTarget = this.getTauriTarget(actualArch, 'darwin');
    if (!buildTarget) {
      throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
    }

    let fullCommand = this.buildBaseCommand(
      packageManager,
      configPath,
      buildTarget,
    );

    const features = this.getBuildFeatures();
    if (features.length > 0) {
      fullCommand += ` --features ${features.join(',')}`;
    }

    return fullCommand;
  }

  protected getBasePath(): string {
    const basePath = this.options.debug ? 'debug' : 'release';
    const actualArch = this.getActualArch();
    const target = this.getTauriTarget(actualArch, 'darwin');

    return `src-tauri/target/${target}/${basePath}/bundle`;
  }

  protected hasArchSpecificTarget(): boolean {
    return true;
  }

  protected getArchSpecificPath(): string {
    const actualArch = this.getActualArch();
    const target = this.getTauriTarget(actualArch, 'darwin');
    return `src-tauri/target/${target}`;
  }
}


================================================
FILE: bin/builders/WinBuilder.ts
================================================
import path from 'path';
import BaseBuilder from './BaseBuilder';
import { PakeAppOptions } from '@/types';
import tauriConfig from '@/helpers/tauriConfig';

export default class WinBuilder extends BaseBuilder {
  private buildFormat: string = 'msi';
  private buildArch: string;

  constructor(options: PakeAppOptions) {
    super(options);
    const validArchs = ['x64', 'arm64', 'auto'];
    this.buildArch = validArchs.includes(options.targets || '')
      ? this.resolveTargetArch(options.targets)
      : this.resolveTargetArch('auto');
    this.options.targets = this.buildFormat;
  }

  getFileName(): string {
    const { name } = this.options;
    const language = tauriConfig.bundle.windows.wix.language[0];
    const targetArch = this.getArchDisplayName(this.buildArch);
    return `${name}_${tauriConfig.version}_${targetArch}_${language}`;
  }

  protected getBuildCommand(packageManager: string = 'pnpm'): string {
    const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
    const buildTarget = this.getTauriTarget(this.buildArch, 'win32');

    if (!buildTarget) {
      throw new Error(
        `Unsupported architecture: ${this.buildArch} for Windows`,
      );
    }

    let fullCommand = this.buildBaseCommand(
      packageManager,
      configPath,
      buildTarget,
    );

    const features = this.getBuildFeatures();
    if (features.length > 0) {
      fullCommand += ` --features ${features.join(',')}`;
    }

    return fullCommand;
  }

  protected getBasePath(): string {
    const basePath = this.options.debug ? 'debug' : 'release';
    const target = this.getTauriTarget(this.buildArch, 'win32');
    return `src-tauri/target/${target}/${basePath}/bundle/`;
  }

  protected hasArchSpecificTarget(): boolean {
    return true;
  }

  protected getArchSpecificPath(): string {
    const target = this.getTauriTarget(this.buildArch, 'win32');
    return `src-tauri/target/${target}`;
  }
}


================================================
FILE: bin/cli.ts
================================================
import log from 'loglevel';
import updateNotifier from 'update-notifier';
import packageJson from '../package.json';
import BuilderProvider from './builders/BuilderProvider';
import handleInputOptions from './options/index';
import { getCliProgram } from './helpers/cli-program';
import { PakeCliOptions } from './types';

const program = getCliProgram();

async function checkUpdateTips() {
  updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({
    isGlobal: true,
  });
}

program.action(async (url: string, options: PakeCliOptions) => {
  await checkUpdateTips();

  if (!url) {
    program.help({
      error: false,
    });
    return;
  }

  log.setDefaultLevel('info');
  log.setLevel('info');
  if (options.debug) {
    log.setLevel('debug');
  }

  const appOptions = await handleInputOptions(options, url);

  const builder = BuilderProvider.create(appOptions);
  await builder.prepare();
  await builder.build(url);
});

program.parse();


================================================
FILE: bin/defaults.ts
================================================
import { PakeCliOptions } from './types.js';

export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
  icon: '',
  height: 780,
  width: 1200,
  fullscreen: false,
  maximize: false,
  resizable: true,
  hideTitleBar: false,
  alwaysOnTop: false,
  appVersion: '1.0.0',
  darkMode: false,
  disabledWebShortcuts: false,
  activationShortcut: '',
  userAgent: '',
  showSystemTray: false,
  multiArch: false,
  targets: (() => {
    switch (process.platform) {
      case 'linux':
        return 'deb,appimage';
      case 'darwin':
        return 'dmg';
      case 'win32':
        return 'msi';
      default:
        return 'deb';
    }
  })(),
  useLocalFile: false,
  systemTrayIcon: '',
  proxyUrl: '',
  debug: false,
  inject: [],
  installerLanguage: 'en-US',
  hideOnClose: undefined, // Platform-specific: true for macOS, false for others
  incognito: false,
  wasm: false,
  enableDragDrop: false,
  keepBinary: false,
  multiInstance: false,
  multiWindow: false,
  startToTray: false,
  forceInternalNavigation: false,
  internalUrlRegex: '',
  iterativeBuild: false,
  zoom: 100,
  minWidth: 0,
  minHeight: 0,
  ignoreCertificateErrors: false,
  newWindow: false,
  install: false,
  camera: false,
  microphone: false,
};

// Just for cli development
export const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = {
  ...DEFAULT_PAKE_OPTIONS,
  url: 'https://weekly.tw93.fun/en',
  name: 'Weekly',
  hideTitleBar: true,
};


================================================
FILE: bin/dev.ts
================================================
import log from 'loglevel';
import { PakeCliOptions } from './types';
import handleInputOptions from './options/index';
import BuilderProvider from './builders/BuilderProvider';
import { getCliProgram } from './helpers/cli-program';

const program = getCliProgram();

program.action(async (url: string, options: PakeCliOptions) => {
  log.setDefaultLevel('debug');

  const appOptions = await handleInputOptions(options, url);
  log.debug('PakeAppOptions', appOptions);

  const builder = BuilderProvider.create(appOptions);
  await builder.prepare();
  await builder.start(url);
});

program.parse();


================================================
FILE: bin/helpers/cli-program.ts
================================================
import chalk from 'chalk';
import { program, Option } from 'commander';
import packageJson from '../../package.json';
import {
  DEFAULT_PAKE_OPTIONS as DEFAULT,
  DEFAULT_PAKE_OPTIONS,
} from '../defaults';
import { validateNumberInput, validateUrlInput } from '../utils/validate';

export function getCliProgram() {
  const { green, yellow } = chalk;
  const logo = `${chalk.green(' ____       _')}
${green('|  _ \\ __ _| | _____')}
${green('| |_) / _` | |/ / _ \\')}
${green('|  __/ (_| |   <  __/')}  ${yellow('https://github.com/tw93/pake')}
${green('|_|   \\__,_|_|\\_\\___|  can turn any webpage into a desktop app with Rust.')}
`;

  return program
    .addHelpText('beforeAll', logo)
    .usage(`[url] [options]`)
    .showHelpAfterError()
    .argument('[url]', 'The web URL you want to package', validateUrlInput)
    .option('--name <string>', 'Application name')
    .addOption(
      new Option(
        '--identifier <string>',
        'Application identifier / bundle ID',
      ).hideHelp(),
    )
    .option('--icon <string>', 'Application icon', DEFAULT.icon)
    .option(
      '--width <number>',
      'Window width',
      validateNumberInput,
      DEFAULT.width,
    )
    .option(
      '--height <number>',
      'Window height',
      validateNumberInput,
      DEFAULT.height,
    )
    .option(
      '--use-local-file',
      'Use local file packaging',
      DEFAULT.useLocalFile,
    )
    .option('--fullscreen', 'Start in full screen', DEFAULT.fullscreen)
    .option('--hide-title-bar', 'For Mac, hide title bar', DEFAULT.hideTitleBar)
    .option('--multi-arch', 'For Mac, both Intel and M1', DEFAULT.multiArch)
    .option(
      '--inject <files>',
      'Inject local CSS/JS files into the page',
      (val, previous) => {
        if (!val) return DEFAULT.inject;

        // Split by comma and trim whitespace, filter out empty strings
        const files = val
          .split(',')
          .map((item) => item.trim())
          .filter((item) => item.length > 0);

        // If previous values exist (from multiple --inject options), merge them
        return previous ? [...previous, ...files] : files;
      },
      DEFAULT.inject,
    )
    .option('--debug', 'Debug build and more output', DEFAULT.debug)
    .addOption(
      new Option(
        '--proxy-url <url>',
        'Proxy URL for all network requests (http://, https://, socks5://)',
      )
        .default(DEFAULT_PAKE_OPTIONS.proxyUrl)
        .hideHelp(),
    )
    .addOption(
      new Option('--user-agent <string>', 'Custom user agent')
        .default(DEFAULT.userAgent)
        .hideHelp(),
    )
    .addOption(
      new Option(
        '--targets <string>',
        'Build target format for your system',
      ).default(DEFAULT.targets),
    )
    .addOption(
      new Option(
        '--app-version <string>',
        'App version, the same as package.json version',
      )
        .default(DEFAULT.appVersion)
        .hideHelp(),
    )
    .addOption(
      new Option('--always-on-top', 'Always on the top level')
        .default(DEFAULT.alwaysOnTop)
        .hideHelp(),
    )
    .addOption(
      new Option('--maximize', 'Start window maximized')
        .default(DEFAULT.maximize)
        .hideHelp(),
    )
    .addOption(
      new Option('--dark-mode', 'Force Mac app to use dark mode')
        .default(DEFAULT.darkMode)
        .hideHelp(),
    )
    .addOption(
      new Option('--disabled-web-shortcuts', 'Disabled webPage shortcuts')
        .default(DEFAULT.disabledWebShortcuts)
        .hideHelp(),
    )
    .addOption(
      new Option('--activation-shortcut <string>', 'Shortcut key to active App')
        .default(DEFAULT_PAKE_OPTIONS.activationShortcut)
        .hideHelp(),
    )
    .addOption(
      new Option('--show-system-tray', 'Show system tray in app')
        .default(DEFAULT.showSystemTray)
        .hideHelp(),
    )
    .addOption(
      new Option('--system-tray-icon <string>', 'Custom system tray icon')
        .default(DEFAULT.systemTrayIcon)
        .hideHelp(),
    )
    .addOption(
      new Option(
        '--hide-on-close [boolean]',
        'Hide window on close instead of exiting (default: true for macOS, false for others)',
      )
        .default(DEFAULT.hideOnClose)
        .argParser((value) => {
          if (value === undefined) return true; // --hide-on-close without value
          if (value === 'true') return true;
          if (value === 'false') return false;
          throw new Error('--hide-on-close must be true or false');
        })
        .hideHelp(),
    )
    .addOption(new Option('--title <string>', 'Window title').hideHelp())
    .addOption(
      new Option('--incognito', 'Launch app in incognito/private mode')
        .default(DEFAULT.incognito)
        .hideHelp(),
    )
    .addOption(
      new Option('--wasm', 'Enable WebAssembly support (Flutter Web, etc.)')
        .default(DEFAULT.wasm)
        .hideHelp(),
    )
    .addOption(
      new Option('--enable-drag-drop', 'Enable drag and drop functionality')
        .default(DEFAULT.enableDragDrop)
        .hideHelp(),
    )
    .addOption(
      new Option('--keep-binary', 'Keep raw binary file alongside installer')
        .default(DEFAULT.keepBinary)
        .hideHelp(),
    )
    .addOption(
      new Option('--multi-instance', 'Allow multiple app instances')
        .default(DEFAULT.multiInstance)
        .hideHelp(),
    )
    .addOption(
      new Option(
        '--multi-window',
        'Allow opening multiple windows within one app instance',
      )
        .default(DEFAULT.multiWindow)
        .hideHelp(),
    )
    .addOption(
      new Option('--start-to-tray', 'Start app minimized to tray')
        .default(DEFAULT.startToTray)
        .hideHelp(),
    )
    .addOption(
      new Option(
        '--force-internal-navigation',
        'Keep every link inside the Pake window instead of opening external handlers',
      )
        .default(DEFAULT.forceInternalNavigation)
        .hideHelp(),
    )
    .addOption(
      new Option(
        '--internal-url-regex <string>',
        'Regex pattern to match URLs that should be considered internal',
      )
        .default(DEFAULT.internalUrlRegex)
        .hideHelp(),
    )
    .addOption(
      new Option('--installer-language <string>', 'Installer language')
        .default(DEFAULT.installerLanguage)
        .hideHelp(),
    )
    .addOption(
      new Option('--zoom <number>', 'Initial page zoom level (50-200)')
        .default(DEFAULT.zoom)
        .argParser((value) => {
          const zoom = parseInt(value);
          if (isNaN(zoom) || zoom < 50 || zoom > 200) {
            throw new Error('--zoom must be a number between 50 and 200');
          }
          return zoom;
        })
        .hideHelp(),
    )
    .addOption(
      new Option('--min-width <number>', 'Minimum window width')
        .default(DEFAULT.minWidth)
        .argParser(validateNumberInput)
        .hideHelp(),
    )
    .addOption(
      new Option('--min-height <number>', 'Minimum window height')
        .default(DEFAULT.minHeight)
        .argParser(validateNumberInput)
        .hideHelp(),
    )
    .addOption(
      new Option(
        '--ignore-certificate-errors',
        'Ignore certificate errors (for self-signed certificates)',
      )
        .default(DEFAULT.ignoreCertificateErrors)
        .hideHelp(),
    )
    .addOption(
      new Option(
        '--iterative-build',
        'Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging',
      )
        .default(DEFAULT.iterativeBuild)
        .hideHelp(),
    )
    .addOption(
      new Option(
        '--new-window',
        'Allow sites to open new windows (for auth flows, tabs, branches)',
      )
        .default(DEFAULT.newWindow)
        .hideHelp(),
    )
    .option(
      '--install',
      'Auto-install app to /Applications (macOS) after build and remove local bundle',
      DEFAULT.install,
    )
    .addOption(
      new Option('--camera', 'Request camera permission on macOS')
        .default(DEFAULT.camera)
        .hideHelp(),
    )
    .addOption(
      new Option('--microphone', 'Request microphone permission on macOS')
        .default(DEFAULT.microphone)
        .hideHelp(),
    )
    .version(packageJson.version, '-v, --version')
    .configureHelp({
      sortSubcommands: true,
      optionTerm: (option) => {
        if (option.flags === '-v, --version' || option.flags === '-h, --help')
          return '';
        return option.flags;
      },
      optionDescription: (option) => {
        if (option.flags === '-v, --version' || option.flags === '-h, --help')
          return '';
        return option.description;
      },
    });
}


================================================
FILE: bin/helpers/merge.ts
================================================
import path from 'path';
import fsExtra from 'fs-extra';

import combineFiles from '@/utils/combine';
import logger from '@/options/logger';
import {
  generateSafeFilename,
  generateIdentifierSafeName,
  getSafeAppName,
  generateLinuxPackageName,
} from '@/utils/name';
import { PakeAppOptions, PlatformMap, WindowConfig } from '@/types';
import { tauriConfigDirectory, npmDirectory } from '@/utils/dir';

export async function mergeConfig(
  url: string,
  options: PakeAppOptions,
  tauriConf: any,
) {
  // Ensure .pake directory exists and copy source templates if needed
  const srcTauriDir = path.join(npmDirectory, 'src-tauri');
  await fsExtra.ensureDir(tauriConfigDirectory);

  // Copy source config files to .pake directory (as templates)
  const sourceFiles = [
    'tauri.conf.json',
    'tauri.macos.conf.json',
    'tauri.windows.conf.json',
    'tauri.linux.conf.json',
    'pake.json',
  ];

  await Promise.all(
    sourceFiles.map(async (file) => {
      const sourcePath = path.join(srcTauriDir, file);
      const destPath = path.join(tauriConfigDirectory, file);

      if (
        (await fsExtra.pathExists(sourcePath)) &&
        !(await fsExtra.pathExists(destPath))
      ) {
        await fsExtra.copy(sourcePath, destPath);
      }
    }),
  );
  const {
    width,
    height,
    fullscreen,
    maximize,
    hideTitleBar,
    alwaysOnTop,
    appVersion,
    darkMode,
    disabledWebShortcuts,
    activationShortcut,
    userAgent,
    showSystemTray,
    systemTrayIcon,
    useLocalFile,
    identifier,
    name = 'pake-app',
    resizable = true,
    inject,
    proxyUrl,
    installerLanguage,
    hideOnClose,
    incognito,
    title,
    wasm,
    enableDragDrop,
    multiInstance,
    multiWindow,
    startToTray,
    forceInternalNavigation,
    internalUrlRegex,
    zoom,
    minWidth,
    minHeight,
    ignoreCertificateErrors,
    newWindow,
    camera,
    microphone,
  } = options;

  const { platform } = process;

  const platformHideOnClose = hideOnClose ?? platform === 'darwin';

  const tauriConfWindowOptions: Partial<WindowConfig> = {
    width,
    height,
    fullscreen,
    maximize,
    resizable,
    hide_title_bar: hideTitleBar,
    activation_shortcut: activationShortcut,
    always_on_top: alwaysOnTop,
    dark_mode: darkMode,
    disabled_web_shortcuts: disabledWebShortcuts,
    hide_on_close: platformHideOnClose,
    incognito: incognito,
    title: title,
    enable_wasm: wasm,
    enable_drag_drop: enableDragDrop,
    start_to_tray: startToTray && showSystemTray,
    force_internal_navigation: forceInternalNavigation,
    internal_url_regex: internalUrlRegex,
    zoom,
    min_width: minWidth,
    min_height: minHeight,
    ignore_certificate_errors: ignoreCertificateErrors,
    new_window: newWindow,
  };
  Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });

  tauriConf.productName = name;
  tauriConf.identifier = identifier;
  tauriConf.version = appVersion;

  // Always set mainBinaryName to ensure binary uniqueness
  const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;
  tauriConf.mainBinaryName =
    platform === 'linux'
      ? linuxBinaryName
      : `pake-${generateIdentifierSafeName(name)}`;

  if (platform == 'win32') {
    tauriConf.bundle.windows.wix.language[0] = installerLanguage;
  }

  const pathExists = await fsExtra.pathExists(url);
  if (pathExists) {
    logger.warn('✼ Your input might be a local file.');
    tauriConf.pake.windows[0].url_type = 'local';

    const fileName = path.basename(url);
    const dirName = path.dirname(url);

    const distDir = path.join(npmDirectory, 'dist');
    const distBakDir = path.join(npmDirectory, 'dist_bak');

    if (!useLocalFile) {
      const urlPath = path.join(distDir, fileName);
      await fsExtra.copy(url, urlPath);
    } else {
      fsExtra.moveSync(distDir, distBakDir, { overwrite: true });
      fsExtra.copySync(dirName, distDir, { overwrite: true });

      // ignore it, because about_pake.html have be erased.
      // const filesToCopyBack = ['cli.js', 'about_pake.html'];
      const filesToCopyBack = ['cli.js'];
      await Promise.all(
        filesToCopyBack.map((file) =>
          fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file)),
        ),
      );
    }

    tauriConf.pake.windows[0].url = fileName;
    tauriConf.pake.windows[0].url_type = 'local';
  } else {
    tauriConf.pake.windows[0].url_type = 'web';
  }

  const platformMap: PlatformMap = {
    win32: 'windows',
    linux: 'linux',
    darwin: 'macos',
  };
  const currentPlatform = platformMap[platform];

  if (userAgent.length > 0) {
    tauriConf.pake.user_agent[currentPlatform] = userAgent;
  }

  tauriConf.pake.system_tray[currentPlatform] = showSystemTray;

  // Processing targets are currently only open to Linux.
  if (platform === 'linux') {
    // Remove hardcoded desktop files and regenerate with correct app name
    delete tauriConf.bundle.linux.deb.files;

    // Generate correct desktop file configuration
    const linuxName = generateLinuxPackageName(name);
    const desktopFileName = `com.pake.${linuxName}.desktop`;
    const iconName = `${linuxName}_512`;

    // Create desktop file content
    // Determine if title contains Chinese characters for Name[zh_CN]
    const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;

    const desktopContent = `[Desktop Entry]
Version=1.0
Type=Application
Name=${name}
${chineseName ? `Name[zh_CN]=${chineseName}` : ''}
Comment=${name}
Exec=${linuxBinaryName}
Icon=${iconName}
Categories=Network;WebBrowser;Utility;
MimeType=text/html;text/xml;application/xhtml_xml;
StartupNotify=true
Terminal=false
`;

    // Write desktop file to src-tauri/assets directory where Tauri expects it
    const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
    const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
    await fsExtra.ensureDir(srcAssetsDir);
    await fsExtra.writeFile(srcDesktopFilePath, desktopContent);

    // Set up desktop file in bundle configuration
    // Use absolute path from src-tauri directory to assets
    const desktopInstallPath = `/usr/share/applications/${desktopFileName}`;
    tauriConf.bundle.linux.deb.files = {
      [desktopInstallPath]: `assets/${desktopFileName}`,
    };

    // Add desktop file support for RPM
    if (!tauriConf.bundle.linux.rpm) {
      tauriConf.bundle.linux.rpm = {};
    }
    tauriConf.bundle.linux.rpm.files = {
      [desktopInstallPath]: `assets/${desktopFileName}`,
    };

    const validTargets = [
      'deb',
      'appimage',
      'rpm',
      'deb-arm64',
      'appimage-arm64',
      'rpm-arm64',
    ];
    const baseTarget = options.targets.includes('-arm64')
      ? options.targets.replace('-arm64', '')
      : options.targets;

    if (validTargets.includes(options.targets)) {
      tauriConf.bundle.targets = [baseTarget];
    } else {
      logger.warn(
        `✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`,
      );
    }
  }

  // Set macOS bundle targets (for app vs dmg)
  if (platform === 'darwin') {
    const validMacTargets = ['app', 'dmg'];
    if (validMacTargets.includes(options.targets)) {
      tauriConf.bundle.targets = [options.targets];
    }
  }

  // Set icon.
  const safeAppName = getSafeAppName(name);
  const platformIconMap: PlatformMap = {
    win32: {
      fileExt: '.ico',
      path: `png/${safeAppName}_256.ico`,
      defaultIcon: 'png/icon_256.ico',
      message: 'Windows icon must be .ico and 256x256px.',
    },
    linux: {
      fileExt: '.png',
      path: `png/${generateLinuxPackageName(name)}_512.png`,
      defaultIcon: 'png/icon_512.png',
      message: 'Linux icon must be .png and 512x512px.',
    },
    darwin: {
      fileExt: '.icns',
      path: `icons/${safeAppName}.icns`,
      defaultIcon: 'icons/icon.icns',
      message: 'macOS icon must be .icns type.',
    },
  };
  const iconInfo = platformIconMap[platform];
  const resolvedIconPath = options.icon ? path.resolve(options.icon) : null;
  const exists =
    resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
  if (exists) {
    let updateIconPath = true;
    let customIconExt = path.extname(resolvedIconPath).toLowerCase();

    if (customIconExt !== iconInfo.fileExt) {
      updateIconPath = false;
      logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);
      tauriConf.bundle.icon = [iconInfo.defaultIcon];
    } else {
      const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
      tauriConf.bundle.resources = [iconInfo.path];

      // Avoid copying if source and destination are the same
      const absoluteDestPath = path.resolve(iconPath);
      if (resolvedIconPath !== absoluteDestPath) {
        await fsExtra.copy(resolvedIconPath, iconPath);
      }
    }

    if (updateIconPath) {
      tauriConf.bundle.icon = [iconInfo.path];
    } else {
      logger.warn(`✼ Icon will remain as default.`);
    }
  } else {
    logger.warn(
      '✼ Custom icon path may be invalid, default icon will be used instead.',
    );
    tauriConf.bundle.icon = [iconInfo.defaultIcon];
  }

  // Set tray icon path.
  let trayIconPath =
    platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];
  if (systemTrayIcon.length > 0) {
    try {
      await fsExtra.pathExists(systemTrayIcon);
      // 需要判断图标格式,默认只支持ico和png两种
      let iconExt = path.extname(systemTrayIcon).toLowerCase();
      if (iconExt == '.png' || iconExt == '.ico') {
        const trayIcoPath = path.join(
          npmDirectory,
          `src-tauri/png/${safeAppName}${iconExt}`,
        );
        trayIconPath = `png/${safeAppName}${iconExt}`;
        await fsExtra.copy(systemTrayIcon, trayIcoPath);
      } else {
        logger.warn(
          `✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`,
        );
        logger.warn(`✼ Default system tray icon will be used.`);
      }
    } catch {
      logger.warn(`✼ ${systemTrayIcon} not exists!`);
      logger.warn(`✼ Default system tray icon will remain unchanged.`);
    }
  }

  // Ensure trayIcon object exists before setting iconPath
  if (!tauriConf.app.trayIcon) {
    tauriConf.app.trayIcon = {};
  }
  tauriConf.app.trayIcon.iconPath = trayIconPath;
  tauriConf.pake.system_tray_path = trayIconPath;

  delete tauriConf.app.trayIcon;

  const injectFilePath = path.join(
    npmDirectory,
    `src-tauri/src/inject/custom.js`,
  );

  // inject js or css files
  if (inject?.length > 0) {
    // Ensure inject is an array before calling .every()
    const injectArray = Array.isArray(inject) ? inject : [inject];
    if (
      !injectArray.every(
        (item) => item.endsWith('.css') || item.endsWith('.js'),
      )
    ) {
      logger.error('The injected file must be in either CSS or JS format.');
      return;
    }
    const files = injectArray.map((filepath) =>
      path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath),
    );
    tauriConf.pake.inject = files;
    await combineFiles(files, injectFilePath);
  } else {
    tauriConf.pake.inject = [];
    await fsExtra.writeFile(injectFilePath, '');
  }
  tauriConf.pake.proxy_url = proxyUrl || '';
  tauriConf.pake.multi_instance = multiInstance;
  tauriConf.pake.multi_window = multiWindow;

  // Configure WASM support with required HTTP headers
  if (wasm) {
    tauriConf.app.security = {
      headers: {
        'Cross-Origin-Opener-Policy': 'same-origin',
        'Cross-Origin-Embedder-Policy': 'require-corp',
      },
    };
  }

  // Write entitlements dynamically on macOS so camera/microphone are opt-in
  if (platform === 'darwin') {
    const entitlementEntries: string[] = [];
    if (camera) {
      entitlementEntries.push(
        '    <key>com.apple.security.device.camera</key>\n    <true/>',
      );
    }
    if (microphone) {
      entitlementEntries.push(
        '    <key>com.apple.security.device.audio-input</key>\n    <true/>',
      );
    }
    const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
${entitlementEntries.join('\n')}
  </dict>
</plist>
`;
    const entitlementsPath = path.join(
      npmDirectory,
      'src-tauri',
      'entitlements.plist',
    );
    await fsExtra.writeFile(entitlementsPath, entitlementsContent);
  }

  // Save config file.
  const platformConfigPaths: PlatformMap = {
    win32: 'tauri.windows.conf.json',
    darwin: 'tauri.macos.conf.json',
    linux: 'tauri.linux.conf.json',
  };

  const configPath = path.join(
    tauriConfigDirectory,
    platformConfigPaths[platform],
  );

  const bundleConf = { bundle: tauriConf.bundle };
  await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
  const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
  await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });

  let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
  delete tauriConf2.pake;

  // delete tauriConf2.bundle;
  if (process.env.NODE_ENV === 'development') {
    tauriConf2.bundle = bundleConf.bundle;
  }
  const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
  await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
}


================================================
FILE: bin/helpers/rust.ts
================================================
import os from 'os';
import path from 'path';
import fsExtra from 'fs-extra';
import chalk from 'chalk';
import { execaSync } from 'execa';

import { getSpinner } from '@/utils/info';
import { IS_WIN } from '@/utils/platform';
import { shellExec } from '@/utils/shell';
import { isChinaDomain } from '@/utils/ip';

function normalizePathForComparison(targetPath: string) {
  const normalized = path.normalize(targetPath);
  return IS_WIN ? normalized.toLowerCase() : normalized;
}

function getCargoHomeCandidates(): string[] {
  const candidates = new Set<string>();
  if (process.env.CARGO_HOME) {
    candidates.add(process.env.CARGO_HOME);
  }
  const homeDir = os.homedir();
  if (homeDir) {
    candidates.add(path.join(homeDir, '.cargo'));
  }
  if (IS_WIN && process.env.USERPROFILE) {
    candidates.add(path.join(process.env.USERPROFILE, '.cargo'));
  }
  return Array.from(candidates).filter(Boolean);
}

function ensureCargoBinOnPath() {
  const currentPath = process.env.PATH || '';
  const segments = currentPath.split(path.delimiter).filter(Boolean);
  const normalizedSegments = new Set(
    segments.map((segment) => normalizePathForComparison(segment)),
  );

  const additions: string[] = [];
  let cargoHomeSet = Boolean(process.env.CARGO_HOME);

  for (const cargoHome of getCargoHomeCandidates()) {
    const binDir = path.join(cargoHome, 'bin');
    if (
      fsExtra.pathExistsSync(binDir) &&
      !normalizedSegments.has(normalizePathForComparison(binDir))
    ) {
      additions.push(binDir);
      normalizedSegments.add(normalizePathForComparison(binDir));
    }

    if (!cargoHomeSet && fsExtra.pathExistsSync(cargoHome)) {
      process.env.CARGO_HOME = cargoHome;
      cargoHomeSet = true;
    }
  }

  if (additions.length) {
    const prefix = additions.join(path.delimiter);
    process.env.PATH = segments.length
      ? `${prefix}${path.delimiter}${segments.join(path.delimiter)}`
      : prefix;
  }
}

export function ensureRustEnv() {
  ensureCargoBinOnPath();
}

export async function installRust() {
  const isActions = process.env.GITHUB_ACTIONS;
  const isInChina = await isChinaDomain('sh.rustup.rs');
  const rustInstallScriptForMac =
    isInChina && !isActions
      ? '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'
      : "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
  const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';

  const spinner = getSpinner('Downloading Rust...');

  try {
    await shellExec(
      IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac,
      300000,
      undefined,
    );
    spinner.succeed(chalk.green('✔ Rust installed successfully!'));
    ensureRustEnv();
  } catch (error) {
    spinner.fail(chalk.red('✕ Rust installation failed!'));
    if (error instanceof Error) {
      console.error(error.message);
    } else {
      console.error(error);
    }
    process.exit(1);
  }
}

export function checkRustInstalled() {
  ensureCargoBinOnPath();
  try {
    execaSync('rustc', ['--version']);
    return true;
  } catch {
    return false;
  }
}


================================================
FILE: bin/helpers/tauriConfig.ts
================================================
import path from 'path';
import fsExtra from 'fs-extra';
import { npmDirectory } from '@/utils/dir';

// Load configs from npm package directory, not from project source
const tauriSrcDir = path.join(npmDirectory, 'src-tauri');
const pakeConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'pake.json'));
const CommonConf = fsExtra.readJSONSync(
  path.join(tauriSrcDir, 'tauri.conf.json'),
);
const WinConf = fsExtra.readJSONSync(
  path.join(tauriSrcDir, 'tauri.windows.conf.json'),
);
const MacConf = fsExtra.readJSONSync(
  path.join(tauriSrcDir, 'tauri.macos.conf.json'),
);
const LinuxConf = fsExtra.readJSONSync(
  path.join(tauriSrcDir, 'tauri.linux.conf.json'),
);

const platformConfigs = {
  win32: WinConf,
  darwin: MacConf,
  linux: LinuxConf,
};

const { platform } = process;
// @ts-ignore
const platformConfig = platformConfigs[platform];

let tauriConfig = {
  ...CommonConf,
  bundle: platformConfig.bundle,
  app: {
    ...CommonConf.app,
    trayIcon: {
      ...(platformConfig?.app?.trayIcon ?? {}),
    },
  },
  build: CommonConf.build,
  pake: pakeConf,
};

export default tauriConfig;


================================================
FILE: bin/options/icon.ts
================================================
import path from 'path';
import fsExtra from 'fs-extra';
import chalk from 'chalk';
import { dir } from 'tmp-promise';
import { fileTypeFromBuffer } from 'file-type';
import icongen from 'icon-gen';
import sharp from 'sharp';

import logger from './logger';
import { getSpinner } from '@/utils/info';
import { npmDirectory } from '@/utils/dir';
import { IS_LINUX, IS_WIN, IS_MAC } from '@/utils/platform';
import { PakeAppOptions } from '@/types';
import { writeIcoWithPreferredSize } from '@/utils/ico';

type PlatformIconConfig = {
  format: string;
  sizes?: number[];
  size?: number;
};
const ICON_CONFIG = {
  minFileSize: 100,
  supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp', 'icns'] as const,
  whiteBackground: { r: 255, g: 255, b: 255 },
  transparentBackground: { r: 255, g: 255, b: 255, alpha: 0 },
  downloadTimeout: {
    ci: 5000,
    default: 15000,
  },
} as const;

const PLATFORM_CONFIG: Record<'win' | 'linux' | 'macos', PlatformIconConfig> = {
  win: { format: '.ico', sizes: [16, 32, 48, 64, 128, 256] },
  linux: { format: '.png', size: 512 },
  macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
};

const API_KEYS = {
  logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
  brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
};

/**
 * Generates platform-specific icon paths and handles copying for Windows
 */
import { generateLinuxPackageName, generateSafeFilename } from '@/utils/name';

function generateIconPath(appName: string, isDefault = false): string {
  const safeName = isDefault ? 'icon' : getIconBaseName(appName);
  const baseName = safeName;

  if (IS_WIN) {
    return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_256.ico`);
  }
  if (IS_LINUX) {
    return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_512.png`);
  }
  return path.join(npmDirectory, 'src-tauri', 'icons', `${baseName}.icns`);
}

function getIconBaseName(appName: string): string {
  const baseName = IS_LINUX
    ? generateLinuxPackageName(appName)
    : generateSafeFilename(appName).toLowerCase();
  return baseName || 'pake-app';
}

async function copyWindowsIconIfNeeded(
  convertedPath: string,
  appName: string,
): Promise<string> {
  if (!IS_WIN || !convertedPath.endsWith('.ico')) {
    return convertedPath;
  }

  try {
    const finalIconPath = generateIconPath(appName);
    await fsExtra.ensureDir(path.dirname(finalIconPath));
    // Reorder ICO to prioritize 256px icons for better Windows display
    const reordered = await writeIcoWithPreferredSize(
      convertedPath,
      finalIconPath,
      256,
    );
    if (!reordered) {
      await fsExtra.copy(convertedPath, finalIconPath);
    }
    return finalIconPath;
  } catch (error) {
    logger.warn(
      `Failed to copy Windows icon: ${error instanceof Error ? error.message : 'Unknown error'}`,
    );
    return convertedPath;
  }
}

/**
 * Adds white background to transparent icons only
 */
async function preprocessIcon(inputPath: string): Promise<string> {
  try {
    const metadata = await sharp(inputPath).metadata();
    if (metadata.channels !== 4) return inputPath; // No transparency

    const { path: tempDir } = await dir();
    const outputPath = path.join(tempDir, 'icon-with-background.png');

    await sharp({
      create: {
        width: metadata.width || 512,
        height: metadata.height || 512,
        channels: 4,
        background: { ...ICON_CONFIG.whiteBackground, alpha: 1 },
      },
    })
      .composite([{ input: inputPath }])
      .png()
      .toFile(outputPath);

    return outputPath;
  } catch (error) {
    if (error instanceof Error) {
      logger.warn(`Failed to add background to icon: ${error.message}`);
    }
    return inputPath;
  }
}

/**
 * Applies macOS squircle mask to icon
 */
async function applyMacOSMask(inputPath: string): Promise<string> {
  try {
    const { path: tempDir } = await dir();
    const outputPath = path.join(tempDir, 'icon-macos-rounded.png');

    // 1. Create a 1024x1024 rounded rect mask
    // rx="224" is closer to the smooth Apple squircle look for 1024px
    const mask = Buffer.from(
      '<svg width="1024" height="1024"><rect x="0" y="0" width="1024" height="1024" rx="224" ry="224" fill="white"/></svg>',
    );

    // 2. Load input, resize to 1024, apply mask
    const maskedBuffer = await sharp(inputPath)
      .resize(1024, 1024, {
        fit: 'contain',
        background: { r: 0, g: 0, b: 0, alpha: 0 },
      })
      .composite([
        {
          input: mask,
          blend: 'dest-in',
        },
      ])
      .png()
      .toBuffer();

    // 3. Resize to 840x840 (~18% padding) to solve "too big" visual issue
    // Native MacOS icons often leave some breathing room
    await sharp(maskedBuffer)
      .resize(840, 840, {
        fit: 'contain',
        background: { r: 0, g: 0, b: 0, alpha: 0 },
      })
      .extend({
        top: 92,
        bottom: 92,
        left: 92,
        right: 92,
        background: { r: 0, g: 0, b: 0, alpha: 0 },
      })
      .toFile(outputPath);

    return outputPath;
  } catch (error) {
    if (error instanceof Error) {
      logger.warn(`Failed to apply macOS mask: ${error.message}`);
    }
    return inputPath;
  }
}

/**
 * Converts icon to platform-specific format
 */
async function convertIconFormat(
  inputPath: string,
  appName: string,
): Promise<string | null> {
  try {
    if (!(await fsExtra.pathExists(inputPath))) return null;

    const { path: outputDir } = await dir();
    const platformOutputDir = path.join(outputDir, 'converted-icons');
    await fsExtra.ensureDir(platformOutputDir);

    const processedInputPath = await preprocessIcon(inputPath);
    const iconName = getIconBaseName(appName);

    // Generate platform-specific format
    if (IS_WIN) {
      // Support multiple sizes for better Windows compatibility
      await icongen(processedInputPath, platformOutputDir, {
        report: false,
        ico: {
          name: `${iconName}_256`,
          sizes: PLATFORM_CONFIG.win.sizes,
        },
      });
      return path.join(
        platformOutputDir,
        `${iconName}_256${PLATFORM_CONFIG.win.format}`,
      );
    }

    if (IS_LINUX) {
      const outputPath = path.join(
        platformOutputDir,
        `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`,
      );

      // Ensure we convert to proper PNG format with correct size
      await sharp(processedInputPath)
        .resize(PLATFORM_CONFIG.linux.size, PLATFORM_CONFIG.linux.size, {
          fit: 'contain',
          background: ICON_CONFIG.transparentBackground,
        })
        .ensureAlpha()
        .png()
        .toFile(outputPath);

      return outputPath;
    }

    // macOS
    const macIconPath = await applyMacOSMask(processedInputPath);
    await icongen(macIconPath, platformOutputDir, {
      report: false,
      icns: { name: iconName, sizes: PLATFORM_CONFIG.macos.sizes },
    });
    const outputPath = path.join(
      platformOutputDir,
      `${iconName}${PLATFORM_CONFIG.macos.format}`,
    );
    return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
  } catch (error) {
    if (error instanceof Error) {
      logger.warn(`Icon format conversion failed: ${error.message}`);
    }
    return null;
  }
}

/**
 * Processes downloaded or local icon for platform-specific format
 */
async function processIcon(
  iconPath: string,
  appName: string,
): Promise<string | null> {
  if (!iconPath || !appName) return iconPath;

  // Check if already in correct platform format
  const ext = path.extname(iconPath).toLowerCase();
  const isCorrectFormat =
    (IS_WIN && ext === '.ico') ||
    (IS_LINUX && ext === '.png') ||
    (!IS_WIN && !IS_LINUX && ext === '.icns');

  if (isCorrectFormat) {
    return await copyWindowsIconIfNeeded(iconPath, appName);
  }

  // Convert to platform format
  const convertedPath = await convertIconFormat(iconPath, appName);
  if (convertedPath) {
    return await copyWindowsIconIfNeeded(convertedPath, appName);
  }

  return iconPath;
}

/**
 * Gets default icon with platform-specific fallback logic
 */
async function getDefaultIcon(): Promise<string> {
  logger.info('✼ No icon provided, using default icon.');

  if (IS_WIN) {
    const defaultIcoPath = generateIconPath('icon', true);
    const defaultPngPath = path.join(
      npmDirectory,
      'src-tauri/png/icon_512.png',
    );

    // Try default ico first
    if (await fsExtra.pathExists(defaultIcoPath)) {
      return defaultIcoPath;
    }

    // Convert from png if ico doesn't exist
    if (await fsExtra.pathExists(defaultPngPath)) {
      logger.info('✼ Default ico not found, converting from png...');
      try {
        const convertedPath = await convertIconFormat(defaultPngPath, 'icon');
        if (convertedPath && (await fsExtra.pathExists(convertedPath))) {
          return await copyWindowsIconIfNeeded(convertedPath, 'icon');
        }
      } catch (error) {
        logger.warn(
          `Failed to convert default png to ico: ${error instanceof Error ? error.message : 'Unknown error'}`,
        );
      }
    }

    // Fallback to png or empty
    if (await fsExtra.pathExists(defaultPngPath)) {
      logger.warn('✼ Using png as fallback for Windows (may cause issues).');
      return defaultPngPath;
    }

    logger.warn('✼ No default icon found, will use pake default.');
    return '';
  }

  // Linux and macOS defaults
  const iconPath = IS_LINUX
    ? 'src-tauri/png/icon_512.png'
    : 'src-tauri/icons/icon.icns';
  return path.join(npmDirectory, iconPath);
}

/**
 * Main icon handling function with simplified logic flow
 */
export async function handleIcon(
  options: PakeAppOptions,
  url?: string,
): Promise<string> {
  // Handle custom icon (local file or remote URL)
  if (options.icon) {
    if (options.icon.startsWith('http')) {
      const downloadedPath = await downloadIcon(options.icon);
      if (downloadedPath) {
        const result = await processIcon(downloadedPath, options.name || '');
        if (result) return result;
      }
      return '';
    }
    // Local file path
    const resolvedPath = path.resolve(options.icon);
    const result = await processIcon(resolvedPath, options.name || '');
    return result || resolvedPath;
  }

  // Check for existing local icon before downloading
  if (options.name) {
    const localIconPath = generateIconPath(options.name);
    if (await fsExtra.pathExists(localIconPath)) {
      logger.info(`✼ Using existing local icon: ${localIconPath}`);
      return localIconPath;
    }
  }

  // Try favicon from website
  if (url && options.name) {
    const faviconPath = await tryGetFavicon(url, options.name);
    if (faviconPath) return faviconPath;
  }

  // Use default icon
  return await getDefaultIcon();
}

/**
 * Generates icon service URLs for a domain
 */
function generateIconServiceUrls(domain: string): string[] {
  const logoDevUrls = API_KEYS.logoDev
    .sort(() => Math.random() - 0.5)
    .map(
      (token) =>
        `https://img.logo.dev/${domain}?token=${token}&format=png&size=256`,
    );

  const brandfetchUrls = API_KEYS.brandfetch
    .sort(() => Math.random() - 0.5)
    .map((key) => `https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`);

  return [
    ...logoDevUrls,
    ...brandfetchUrls,
    `https://logo.clearbit.com/${domain}?size=256`,
    `https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
    `https://favicon.is/${domain}`,
    `https://${domain}/favicon.ico`,
    `https://www.${domain}/favicon.ico`,
  ];
}

/**
 * Attempts to fetch favicon from website
 */
async function tryGetFavicon(
  url: string,
  appName: string,
): Promise<string | null> {
  try {
    const domain = new URL(url).hostname;
    const spinner = getSpinner(`Fetching icon from ${domain}...`);

    const serviceUrls = generateIconServiceUrls(domain);

    const isCI =
      process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
    const downloadTimeout = isCI
      ? ICON_CONFIG.downloadTimeout.ci
      : ICON_CONFIG.downloadTimeout.default;

    for (const serviceUrl of serviceUrls) {
      try {
        const faviconPath = await downloadIcon(
          serviceUrl,
          false,
          downloadTimeout,
        );
        if (!faviconPath) continue;

        const convertedPath = await convertIconFormat(faviconPath, appName);
        if (convertedPath) {
          const finalPath = await copyWindowsIconIfNeeded(
            convertedPath,
            appName,
          );
          spinner.succeed(
            chalk.green('Icon fetched and converted successfully!'),
          );
          return finalPath;
        }
      } catch (error: unknown) {
        if (error instanceof Error) {
          logger.debug(`Icon service ${serviceUrl} failed: ${error.message}`);
        }
        continue;
      }
    }

    spinner.warn(`No favicon found for ${domain}. Using default.`);
    return null;
  } catch (error) {
    if (error instanceof Error) {
      logger.warn(`Failed to fetch favicon: ${error.message}`);
    }
    return null;
  }
}

/**
 * Downloads icon from URL
 */
export async function downloadIcon(
  iconUrl: string,
  showSpinner = true,
  customTimeout?: number,
): Promise<string | null> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, customTimeout || 10000);

  try {
    const response = await fetch(iconUrl, {
      signal: controller.signal,
    });

    clearTimeout(timeoutId);

    if (!response.ok) {
      if (response.status === 404 && !showSpinner) {
        return null;
      }
      throw new Error(`HTTP ${response.status} ${response.statusText}`);
    }

    const arrayBuffer = await response.arrayBuffer();

    if (!arrayBuffer || arrayBuffer.byteLength < ICON_CONFIG.minFileSize)
      return null;

    const fileDetails = await fileTypeFromBuffer(arrayBuffer);
    if (
      !fileDetails ||
      !ICON_CONFIG.supportedFormats.includes(fileDetails.ext as any)
    ) {
      return null;
    }

    return await saveIconFile(arrayBuffer, fileDetails.ext);
  } catch (error: unknown) {
    clearTimeout(timeoutId);
    if (showSpinner) {
      if (error instanceof Error && error.name === 'AbortError') {
        logger.error('Icon download timed out!');
      } else {
        logger.error(
          'Icon download failed!',
          error instanceof Error ? error.message : String(error),
        );
      }
    }
    return null;
  }
}

/**
 * Saves icon file to temporary location
 */
async function saveIconFile(
  iconData: ArrayBuffer,
  extension: string,
): Promise<string> {
  const buffer = Buffer.from(iconData);
  const { path: tempPath } = await dir();

  // Always save with the original extension first
  const originalIconPath = path.join(tempPath, `icon.${extension}`);
  await fsExtra.outputFile(originalIconPath, buffer);

  return originalIconPath;
}


================================================
FILE: bin/options/index.ts
================================================
import path from 'path';
import fsExtra from 'fs-extra';
import logger from '@/options/logger';

import { handleIcon } from './icon';
import { getDomain } from '@/utils/url';
import {
  promptText,
  capitalizeFirstLetter,
  resolveIdentifier,
} from '@/utils/info';
import { generateLinuxPackageName } from '@/utils/name';
import { PakeAppOptions, PakeCliOptions, PlatformMap } from '@/types';

function resolveAppName(name: string, platform: NodeJS.Platform): string {
  const domain = getDomain(name) || 'pake';
  return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
}

function resolveLocalAppName(
  filePath: string,
  platform: NodeJS.Platform,
): string {
  const baseName = path.parse(filePath).name || 'pake-app';
  if (platform === 'linux') {
    return generateLinuxPackageName(baseName) || 'pake-app';
  }
  const normalized = baseName
    .replace(/[^a-zA-Z0-9\u4e00-\u9fff -]/g, '')
    .replace(/^[ -]+/, '')
    .replace(/\s+/g, ' ')
    .trim();
  return normalized || 'pake-app';
}

function isValidName(name: string, platform: NodeJS.Platform): boolean {
  const platformRegexMapping: PlatformMap = {
    linux: /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/,
    default: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/,
  };
  const reg = platformRegexMapping[platform] || platformRegexMapping.default;
  return !!name && reg.test(name);
}

export default async function handleOptions(
  options: PakeCliOptions,
  url: string,
): Promise<PakeAppOptions> {
  const { platform } = process;
  const isActions = process.env.GITHUB_ACTIONS;
  let name = options.name;

  const pathExists = await fsExtra.pathExists(url);
  if (!options.name) {
    const defaultName = pathExists
      ? resolveLocalAppName(url, platform)
      : resolveAppName(url, platform);
    const promptMessage = 'Enter your application name';
    const namePrompt = await promptText(promptMessage, defaultName);
    name = namePrompt?.trim() || defaultName;
  }

  if (name && platform === 'linux') {
    name = generateLinuxPackageName(name);
  }

  if (name && !isValidName(name, platform)) {
    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.`;
    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.`;
    const errorMsg =
      platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
    logger.error(errorMsg);
    if (isActions) {
      name = resolveAppName(url, platform);
      logger.warn(`✼ Inside github actions, use the default name: ${name}`);
    } else {
      process.exit(1);
    }
  }

  const resolvedName = name || 'pake-app';

  const appOptions: PakeAppOptions = {
    ...options,
    name: resolvedName,
    identifier: resolveIdentifier(url, options.name, options.identifier),
  };

  const iconPath = await handleIcon(appOptions, url);
  appOptions.icon = iconPath || '';

  return appOptions;
}


================================================
FILE: bin/options/logger.ts
================================================
import chalk from 'chalk';
import log from 'loglevel';

const logger = {
  info(...msg: any[]) {
    log.info(...msg.map((m) => chalk.white(m)));
  },
  debug(...msg: any[]) {
    log.debug(...msg);
  },
  error(...msg: any[]) {
    log.error(...msg.map((m) => chalk.red(m)));
  },
  warn(...msg: any[]) {
    log.info(...msg.map((m) => chalk.yellow(m)));
  },
  success(...msg: any[]) {
    log.info(...msg.map((m) => chalk.green(m)));
  },
};

export default logger;


================================================
FILE: bin/types.ts
================================================
export interface PlatformMap {
  [key: string]: any;
}

export interface PakeCliOptions {
  // Application name
  name?: string;

  // Explicit app identifier / bundle id
  identifier?: string;

  // Window title (supports Chinese characters)
  title?: string;

  // Application icon
  icon: string;

  // Application window width, default 1200px
  width: number;

  // Application window height, default 780px
  height: number;

  // Whether the window is resizable, default true
  resizable: boolean;

  // Whether the window can be fullscreen, default false
  fullscreen: boolean;

  // Start window maximized, default false
  maximize: boolean;

  // Enable immersive header, default false.
  hideTitleBar: boolean;

  // Enable windows always on top, default false
  alwaysOnTop: boolean;

  // App version, the same as package.json version, default 1.0.0
  appVersion: string;

  // Force Mac to use dark mode, default false
  darkMode: boolean;

  // Disable web shortcuts, default false
  disabledWebShortcuts: boolean;

  // Set a shortcut key to wake up the app, default empty
  activationShortcut: string;

  // Custom User-Agent, default off
  userAgent: string;

  // Enable system tray, default off for macOS, on for Windows and Linux
  showSystemTray: boolean;

  // Tray icon, default same as app icon for Windows and Linux, macOS requires separate png or ico
  systemTrayIcon: string;

  // 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
  useLocalFile: false;

  // Multi arch, supports both Intel and M1 chips, only for Mac
  multiArch: boolean;

  // Build target architecture/format:
  // Linux: "deb", "appimage", "deb-arm64", "appimage-arm64"; Windows: "x64", "arm64"; macOS: "intel", "apple", "universal"
  targets: string;

  // Debug mode, outputs more logs
  debug: boolean;

  /** External scripts that need to be injected into the page. */
  inject: string[];

  // Set Api Proxy
  proxyUrl: string;

  // Installer language, valid for Windows users, default is en-US
  installerLanguage: string;

  // Hide window on close instead of exiting, platform-specific: true for macOS, false for others
  hideOnClose: boolean | undefined;

  // Launch app in incognito/private mode, default false
  incognito: boolean;

  // Enable WebAssembly support (Flutter Web, etc.), default false
  wasm: boolean;

  // Enable drag and drop functionality, default false
  enableDragDrop: boolean;

  // Keep raw binary file alongside installer, default false
  keepBinary: boolean;

  // Allow multiple instances, default false (single instance)
  multiInstance: boolean;

  // Allow opening multiple windows in one app instance, default false
  multiWindow: boolean;

  // Start app minimized to tray, default false
  startToTray: boolean;

  // Force navigation to stay inside the Pake window even for external links
  forceInternalNavigation: boolean;

  // Regex pattern to match URLs that should be considered internal
  internalUrlRegex: string;

  // Initial page zoom level (50-200), default 100
  zoom: number;

  // Minimum window width, default 0 (no limit)
  minWidth: number;

  // Minimum window height, default 0 (no limit)
  minHeight: number;

  // Ignore certificate errors (for self-signed certs), default false
  ignoreCertificateErrors: boolean;

  // Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging
  iterativeBuild: boolean;

  // Allow sites to open new windows, default false
  newWindow: boolean;

  // Auto-install app to /Applications (macOS) after build, default false
  install: boolean;

  // Request camera entitlement on macOS, default false
  camera: boolean;

  // Request microphone entitlement on macOS, default false
  microphone: boolean;
}

export interface PakeAppOptions extends PakeCliOptions {
  identifier: string;
}

export interface PlatformSpecific<T> {
  macos: T;
  linux: T;
  windows: T;
}

export interface WindowConfig {
  url: string;
  hide_title_bar: boolean;
  fullscreen: boolean;
  maximize: boolean;
  width: number;
  height: number;
  resizable: boolean;
  url_type: string;
  always_on_top: boolean;
  dark_mode: boolean;
  disabled_web_shortcuts: boolean;
  activation_shortcut: string;
  hide_on_close: boolean;
  incognito: boolean;
  title?: string;
  enable_wasm: boolean;
  enable_drag_drop: boolean;
  start_to_tray: boolean;
  force_internal_navigation: boolean;
  internal_url_regex: string;
  zoom: number;
  min_width: number;
  min_height: number;
  ignore_certificate_errors: boolean;
  new_window: boolean;
}

export interface PakeConfig {
  windows: WindowConfig[];
  user_agent: PlatformSpecific<string>;
  system_tray: PlatformSpecific<boolean>;
  system_tray_path: string;
  proxy_url: string;
  multi_instance: boolean;
  multi_window: boolean;
}


================================================
FILE: bin/utils/combine.ts
================================================
import fs from 'fs';

export default async function combineFiles(files: string[], output: string) {
  const contents = files.map((file) => {
    if (file.endsWith('.css')) {
      const fileContent = fs.readFileSync(file, 'utf-8');
      return `window.addEventListener('DOMContentLoaded', (_event) => {
        const css = ${JSON.stringify(fileContent)};
        const style = document.createElement('style');
        style.innerHTML = css;
        document.head.appendChild(style);
      });`;
    }

    const fileContent = fs.readFileSync(file);
    return (
      "window.addEventListener('DOMContentLoaded', (_event) => { " +
      fileContent +
      ' });'
    );
  });
  fs.writeFileSync(output, contents.join('\n'));
  return files;
}


================================================
FILE: bin/utils/dir.ts
================================================
import path from 'path';
import { fileURLToPath } from 'url';

// Convert the current module URL to a file path
const currentModulePath = fileURLToPath(import.meta.url);

// Resolve the parent directory of the current module
export const npmDirectory = path.join(path.dirname(currentModulePath), '..');

export const tauriConfigDirectory = path.join(
  npmDirectory,
  'src-tauri',
  '.pake',
);


================================================
FILE: bin/utils/ico.ts
================================================
import path from 'path';
import fsExtra from 'fs-extra';

const ICO_HEADER_SIZE = 6;
const ICO_DIR_ENTRY_SIZE = 16;
const ICO_TYPE_ICON = 1;

export type IcoEntry = {
  index: number;
  width: number;
  height: number;
  bitCount: number;
  bytesInRes: number;
  imageOffset: number;
  directory: Buffer;
  data: Buffer;
};

function decodeDimension(value: number): number {
  return value === 0 ? 256 : value;
}

function compareByPreferredSize(
  preferredSize: number,
): (a: IcoEntry, b: IcoEntry) => number {
  return (a, b) => {
    const aSize = Math.max(a.width, a.height);
    const bSize = Math.max(b.width, b.height);

    const aExact = aSize === preferredSize ? 0 : 1;
    const bExact = bSize === preferredSize ? 0 : 1;
    if (aExact !== bExact) return aExact - bExact;

    const aDistance = Math.abs(aSize - preferredSize);
    const bDistance = Math.abs(bSize - preferredSize);
    if (aDistance !== bDistance) return aDistance - bDistance;

    const aSmaller = aSize < preferredSize ? 1 : 0;
    const bSmaller = bSize < preferredSize ? 1 : 0;
    if (aSmaller !== bSmaller) return aSmaller - bSmaller;

    if (a.bitCount !== b.bitCount) return b.bitCount - a.bitCount;
    if (aSize !== bSize) return bSize - aSize;

    return a.index - b.index;
  };
}

export function parseIcoBuffer(buffer: Buffer): IcoEntry[] {
  if (buffer.length < ICO_HEADER_SIZE) {
    throw new Error('Invalid ICO: header too short.');
  }

  const reserved = buffer.readUInt16LE(0);
  const type = buffer.readUInt16LE(2);
  const count = buffer.readUInt16LE(4);

  if (reserved !== 0 || type !== ICO_TYPE_ICON || count < 1) {
    throw new Error('Invalid ICO: invalid header.');
  }

  const tableSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;
  if (buffer.length < tableSize) {
    throw new Error('Invalid ICO: directory table too short.');
  }

  const entries: IcoEntry[] = [];

  for (let i = 0; i < count; i++) {
    const offset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;
    const widthByte = buffer.readUInt8(offset);
    const heightByte = buffer.readUInt8(offset + 1);
    const bitCount = buffer.readUInt16LE(offset + 6);
    const bytesInRes = buffer.readUInt32LE(offset + 8);
    const imageOffset = buffer.readUInt32LE(offset + 12);

    if (bytesInRes < 1 || imageOffset + bytesInRes > buffer.length) {
      throw new Error('Invalid ICO: frame out of bounds.');
    }

    entries.push({
      index: i,
      width: decodeDimension(widthByte),
      height: decodeDimension(heightByte),
      bitCount,
      bytesInRes,
      imageOffset,
      directory: buffer.subarray(offset, offset + ICO_DIR_ENTRY_SIZE),
      data: buffer.subarray(imageOffset, imageOffset + bytesInRes),
    });
  }

  return entries;
}

export function buildReorderedIcoBuffer(
  buffer: Buffer,
  preferredSize: number,
): Buffer {
  const entries = parseIcoBuffer(buffer);
  const ordered = [...entries].sort(compareByPreferredSize(preferredSize));
  const count = ordered.length;
  const tableSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;
  const payloadSize = ordered.reduce(
    (acc, entry) => acc + entry.data.length,
    0,
  );
  const output = Buffer.alloc(tableSize + payloadSize);

  output.writeUInt16LE(0, 0);
  output.writeUInt16LE(ICO_TYPE_ICON, 2);
  output.writeUInt16LE(count, 4);

  let currentOffset = tableSize;
  for (let i = 0; i < count; i++) {
    const entry = ordered[i];
    const entryOffset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;

    entry.directory.copy(output, entryOffset, 0, 8);
    output.writeUInt32LE(entry.data.length, entryOffset + 8);
    output.writeUInt32LE(currentOffset, entryOffset + 12);
    entry.data.copy(output, currentOffset);
    currentOffset += entry.data.length;
  }

  return output;
}

export async function writeIcoWithPreferredSize(
  sourcePath: string,
  outputPath: string,
  preferredSize: number,
): Promise<boolean> {
  try {
    const sourceBuffer = await fsExtra.readFile(sourcePath);
    const reordered = buildReorderedIcoBuffer(sourceBuffer, preferredSize);
    await fsExtra.ensureDir(path.dirname(outputPath));
    await fsExtra.outputFile(outputPath, reordered);
    return true;
  } catch {
    return false;
  }
}


================================================
FILE: bin/utils/info.ts
================================================
import crypto from 'crypto';
import prompts from 'prompts';
import ora from 'ora';
import chalk from 'chalk';

// Generates a stable identifier based on the app URL (and optionally name).
// When name is provided it is included in the hash so two apps wrapping
// the same URL can coexist. Omitting name preserves backward compatibility
// with identifiers generated before V3.10.1.
export function getIdentifier(url: string, name?: string) {
  const hashInput = name ? `${url}::${name}` : url;
  const postFixHash = crypto
    .createHash('md5')
    .update(hashInput)
    .digest('hex')
    .substring(0, 6);
  return `com.pake.${postFixHash}`;
}

export function resolveIdentifier(
  url: string,
  explicitName: string | undefined,
  customIdentifier?: string,
) {
  const trimmedIdentifier = customIdentifier?.trim();
  if (trimmedIdentifier) {
    return trimmedIdentifier;
  }

  return getIdentifier(url, explicitName);
}

export async function promptText(
  message: string,
  initial?: string,
): Promise<string> {
  const response = await prompts({
    type: 'text',
    name: 'content',
    message,
    initial,
  });
  return response.content;
}

export function capitalizeFirstLetter(string: string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

export function getSpinner(text: string) {
  const loadingType = {
    interval: 80,
    frames: ['✦', '✶', '✺', '✵', '✸', '✹', '✺'],
  };
  return ora({
    text: `${chalk.cyan(text)}\n`,
    spinner: loadingType,
    color: 'cyan',
  }).start();
}


================================================
FILE: bin/utils/ip.ts
================================================
import dns from 'dns';
import http from 'http';
import { promisify } from 'util';

import logger from '@/options/logger';

const resolve = promisify(dns.resolve);

const ping = async (host: string) => {
  const lookup = promisify(dns.lookup);
  const ip = await lookup(host);
  const start = new Date();

  // Prevent timeouts from affecting user experience.
  const requestPromise = new Promise<number>((resolve, reject) => {
    const req = http.get(`http://${ip.address}`, (res) => {
      const delay = new Date().getTime() - start.getTime();
      res.resume();
      resolve(delay);
    });

    req.on('error', (err) => {
      reject(err);
    });
  });

  const timeoutPromise = new Promise<number>((_, reject) => {
    setTimeout(() => {
      reject(new Error('Request timed out after 3 seconds'));
    }, 1000);
  });

  return Promise.race([requestPromise, timeoutPromise]);
};

async function isChinaDomain(domain: string): Promise<boolean> {
  try {
    const [ip] = await resolve(domain);
    return await isChinaIP(ip, domain);
  } catch (error) {
    logger.debug(`${domain} can't be parse!`);
    return true;
  }
}

async function isChinaIP(ip: string, domain: string): Promise<boolean> {
  try {
    const delay = await ping(ip);
    logger.debug(`${domain} latency is ${delay} ms`);
    return delay > 1000;
  } catch (error) {
    logger.debug(`ping ${domain} failed!`);
    return true;
  }
}

export { isChinaDomain, isChinaIP };


================================================
FILE: bin/utils/name.ts
================================================
export function generateSafeFilename(name: string): string {
  return name
    .replace(/[<>:"/\\|?*]/g, '_')
    .replace(/\s+/g, '_')
    .replace(/\.+$/g, '')
    .slice(0, 255);
}

export function getSafeAppName(name: string): string {
  return generateSafeFilename(name).toLowerCase();
}

export function generateLinuxPackageName(name: string): string {
  return name
    .toLowerCase()
    .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .replace(/-+/g, '-');
}

export function generateIdentifierSafeName(name: string): string {
  const cleaned = name.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '').toLowerCase();

  if (cleaned === '') {
    const fallback = Array.from(name)
      .map((char) => {
        const code = char.charCodeAt(0);
        if (
          (code >= 48 && code <= 57) ||
          (code >= 65 && code <= 90) ||
          (code >= 97 && code <= 122)
        ) {
          return char.toLowerCase();
        }
        return code.toString(16);
      })
      .join('')
      .slice(0, 50);

    return fallback || 'pake-app';
  }

  return cleaned;
}

export function generateWindowsFilename(name: string): string {
  return name
    .replace(/[<>:"/\\|?*]/g, '_')
    .replace(/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i, '$&_')
    .slice(0, 255);
}

export function generateMacOSFilename(name: string): string {
  return name.replace(/[:]/g, '_').slice(0, 255);
}


================================================
FILE: bin/utils/platform.ts
================================================
const { platform } = process;

export const IS_MAC = platform === 'darwin';
export const IS_WIN = platform === 'win32';
export const IS_LINUX = platform === 'linux';


================================================
FILE: bin/utils/shell.ts
================================================
import { execa } from 'execa';
import { npmDirectory } from './dir';

export async function shellExec(
  command: string,
  timeout: number = 300000,
  env?: Record<string, string>,
) {
  try {
    const { exitCode } = await execa(command, {
      cwd: npmDirectory,
      // Use 'inherit' to show all output directly to user in real-time.
      // This ensures linuxdeploy and other tool outputs are visible during builds.
      stdio: 'inherit',
      shell: true,
      timeout,
      env: env ? { ...process.env, ...env } : process.env,
    });
    return exitCode;
  } catch (error: any) {
    const exitCode = error.exitCode ?? 'unknown';
    const errorMessage = error.message || 'Unknown error occurred';

    if (error.timedOut) {
      throw new Error(
        `Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`,
      );
    }

    let errorMsg = `Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`;

    // Provide helpful guidance for common Linux AppImage build failures
    // caused by strip tool incompatibility with modern glibc (2.38+)
    const lowerError = errorMessage.toLowerCase();

    if (
      process.platform === 'linux' &&
      (lowerError.includes('linuxdeploy') ||
        lowerError.includes('appimage') ||
        lowerError.includes('strip'))
    ) {
      errorMsg +=
        '\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
        'Linux AppImage Build Failed\n' +
        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' +
        'Cause: Strip tool incompatibility with glibc 2.38+\n' +
        '       (affects Debian Trixie, Arch Linux, and other modern distros)\n\n' +
        'Quick fix:\n' +
        '  NO_STRIP=1 pake <url> --targets appimage --debug\n\n' +
        'Alternatives:\n' +
        '  • Use DEB format: pake <url> --targets deb\n' +
        '  • Update binutils: sudo apt install binutils (or pacman -S binutils)\n' +
        '  • Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' +
        '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';

      if (
        lowerError.includes('fuse') ||
        lowerError.includes('operation not permitted') ||
        lowerError.includes('/dev/fuse')
      ) {
        errorMsg +=
          '\n\nDocker / Container hint:\n' +
          '  AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\n' +
          '    --privileged --device /dev/fuse --security-opt apparmor=unconfined\n' +
          '  or run on the host directly.';
      }
    }

    throw new Error(errorMsg);
  }
}


================================================
FILE: bin/utils/url.ts
================================================
import * as psl from 'psl';

// Extracts the domain from a given URL.
export function getDomain(inputUrl: string): string | null {
  try {
    const url = new URL(inputUrl);
    // Use PSL to parse domain names.
    const parsed = psl.parse(url.hostname);

    // If domain is available, split it and return the SLD.
    if ('domain' in parsed && parsed.domain) {
      return parsed.domain.split('.')[0];
    } else {
      return null;
    }
  } catch (error) {
    return null;
  }
}

// Appends 'https://' protocol to the URL if not present.
export function appendProtocol(inputUrl: string): string {
  try {
    new URL(inputUrl);
    return inputUrl;
  } catch {
    return `https://${inputUrl}`;
  }
}

// Normalizes the URL by ensuring it has a protocol and is valid.
export function normalizeUrl(urlToNormalize: string): string {
  const urlWithProtocol = appendProtocol(urlToNormalize);
  try {
    new URL(urlWithProtocol);
    return urlWithProtocol;
  } catch (err) {
    throw new Error(
      `Your url "${urlWithProtocol}" is invalid: ${(err as Error).message}`,
    );
  }
}


================================================
FILE: bin/utils/validate.ts
================================================
import fs from 'fs';
import { InvalidArgumentError } from 'commander';
import { normalizeUrl } from './url';

export function validateNumberInput(value: string) {
  const parsedValue = Number(value);
  if (isNaN(parsedValue)) {
    throw new InvalidArgumentError('Not a number.');
  }
  return parsedValue;
}

export function validateUrlInput(url: string) {
  const isFile = fs.existsSync(url);

  if (!isFile) {
    try {
      return normalizeUrl(url);
    } catch (error) {
      if (error instanceof Error) {
        throw new InvalidArgumentError(error.message);
      }
      throw error;
    }
  }

  return url;
}


================================================
FILE: default_app_list.json
================================================
[
  {
    "name": "deepseek",
    "title": "DeepSeek",
    "name_zh": "DeepSeek",
    "url": "https://chat.deepseek.com/"
  },
  {
    "name": "grok",
    "title": "Grok",
    "name_zh": "Grok",
    "url": "https://grok.com/"
  },
  {
    "name": "gemini",
    "title": "Gemini",
    "name_zh": "Gemini",
    "url": "https://gemini.google.com/"
  },
  {
    "name": "excalidraw",
    "title": "Excalidraw",
    "name_zh": "Excalidraw",
    "url": "https://excalidraw.com/"
  },
  {
    "name": "programmusic",
    "title": "ProgramMusic",
    "name_zh": "ProgramMusic",
    "url": "https://musicforprogramming.net/"
  },
  {
    "name": "twitter",
    "title": "Twitter",
    "name_zh": "推特",
    "url": "https://twitter.com/"
  },
  {
    "name": "youtube",
    "title": "YouTube",
    "name_zh": "YouTube",
    "url": "https://www.youtube.com"
  },
  {
    "name": "chatgpt",
    "title": "ChatGPT",
    "name_zh": "ChatGPT",
    "url": "https://chatgpt.com/",
    "new_window": true
  },
  {
    "name": "flomo",
    "title": "Flomo",
    "name_zh": "浮墨",
    "url": "https://v.flomoapp.com/mine"
  },
  {
    "name": "qwerty",
    "title": "Qwerty",
    "name_zh": "Qwerty",
    "url": "https://qwerty.kaiyi.cool/"
  },
  {
    "name": "lizhi",
    "title": "LiZhi",
    "name_zh": "李志",
    "url": "https://lizhi.dengdengju.com/?from=pake"
  },
  {
    "name": "xiaohongshu",
    "title": "XiaoHongShu",
    "name_zh": "小红书",
    "url": "https://www.xiaohongshu.com/explore"
  },
  {
    "name": "youtubemusic",
    "title": "YouTubeMusic",
    "name_zh": "YouTubeMusic",
    "url": "https://music.youtube.com/"
  },
  {
    "name": "weread",
    "title": "WeRead",
    "name_zh": "微信阅读",
    "url": "https://weread.qq.com/"
  }
]


================================================
FILE: docs/README.md
================================================
# Pake Documentation

<h4 align="right"><strong>English</strong> | <a href="README_CN.md">简体中文</a></h4>

Welcome to Pake documentation! Here you'll find comprehensive guides and documentation to help you start working with Pake as quickly as possible.

## User Guides

- **[CLI Command Reference](cli-usage.md)** - Complete command-line parameters and basic usage
- **[GitHub Actions Online Build](github-actions-usage.md)** - Online build without local environment setup
- **[Pake Action Integration](pake-action.md)** - Use Pake as a GitHub Action in your projects

## Developer Guides

- **[Advanced Usage & Development](advanced-usage.md)** - Code customization, project structure, development environment setup and testing guides
- **[Contributing Guide](../CONTRIBUTING.md)** - How to contribute to Pake development

## Quick Links

- [Main Repository](https://github.com/tw93/Pake)
- [Releases](https://github.com/tw93/Pake/releases)
- [Discussions](https://github.com/tw93/Pake/discussions)
- [Issues](https://github.com/tw93/Pake/issues)


================================================
FILE: docs/README_CN.md
================================================
# Pake 文档

<h4 align="right"><a href="README.md">English</a> | <strong>简体中文</strong></h4>

欢迎使用 Pake 文档!在这里您可以找到全面的指南和文档,帮助您快速开始使用 Pake。

## 使用指南

- **[CLI命令参考](cli-usage_CN.md)** - 完整的命令行参数说明和基础用法
- **[GitHub Actions在线构建](github-actions-usage_CN.md)** - 无需本地环境的在线构建方式
- **[Pake Action集成](pake-action.md)** - 在你的项目中使用 Pake 作为 GitHub Action

## 开发指南

- **[高级用法与开发](advanced-usage_CN.md)** - 代码自定义、项目结构、开发环境配置和测试指南
- **[贡献指南](../CONTRIBUTING.md)** - 如何为 Pake 开发做贡献

## 快捷链接

- [主仓库](https://github.com/tw93/Pake)
- [发布页面](https://github.com/tw93/Pake/releases)
- [讨论区](https://github.com/tw93/Pake/discussions)
- [问题反馈](https://github.com/tw93/Pake/issues)


================================================
FILE: docs/advanced-usage.md
================================================
# Advanced Usage

<h4 align="right"><strong>English</strong> | <a href="advanced-usage_CN.md">简体中文</a></h4>

Customize Pake apps with style modifications, JavaScript injection, and container communication.

## Style Customization

Remove ads or customize appearance by modifying CSS.

**Quick Process:**

1. Run `pnpm run dev` for development
2. Use DevTools to find elements to modify
3. Edit `src-tauri/src/inject/style.js`:

```javascript
const css = `
  .ads-banner { display: none !important; }
  .header { background: #1a1a1a !important; }
`;
```

## JavaScript Injection

Add custom functionality like keyboard shortcuts.

**Implementation:**

1. Edit `src-tauri/src/inject/event.js`
2. Add event listeners:

```javascript
document.addEventListener("keydown", (e) => {
  if (e.ctrlKey && e.key === "k") {
    // Custom action
  }
});
```

## Built-in Features

### Download Error Notifications

Pake automatically provides user-friendly download error notifications:

**Features:**

- **Bilingual Support**: Automatically detects browser language (Chinese/English)
- **System Notifications**: Uses native OS notifications when permission is granted
- **Graceful Fallback**: Falls back to console logging if notifications are unavailable
- **Comprehensive Coverage**: Handles all download types (HTTP, Data URI, Blob)

**User Experience:**

When a download fails, users will see a notification:

- English: "Download Error - Download failed: filename.pdf"
- Chinese: "下载错误 - 下载失败: filename.pdf"

**Requesting Notification Permission:**

To enable notifications, add this to your injected JavaScript:

```javascript
// Request notification permission on app start
if (window.Notification && Notification.permission === "default") {
  Notification.requestPermission();
}
```

The download system automatically handles:

- Regular HTTP(S) downloads
- Data URI downloads (base64 encoded files)
- Blob URL downloads (dynamically generated files)
- Context menu initiated downloads

## Container Communication

Send messages between web content and Pake container.

**Web Side (JavaScript):**

```javascript
window.__TAURI__.invoke("handle_scroll", {
  scrollY: window.scrollY,
  scrollX: window.scrollX,
});
```

**Container Side (Rust):**

```rust
#[tauri::command]
fn handle_scroll(scroll_y: f64, scroll_x: f64) {
  println!("Scroll: {}, {}", scroll_x, scroll_y);
}
```

## Window Configuration

Configure window properties in `pake.json`:

```json
{
  "windows": {
    "width": 1200,
    "height": 780,
    "fullscreen": false,
    "resizable": true
  },
  "hideTitleBar": true
}
```

## Static File Packaging

Package local HTML/CSS/JS files:

```bash
pake ./my-app/index.html --name my-static-app --use-local-file
```

Requirements: Pake CLI >= 3.0.0

## macOS Media Permissions

By 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:

```bash
pake https://chatgpt.com --name ChatGPT --microphone
pake https://meet.google.com --name GoogleMeet --camera --microphone
```

- `--microphone` — grants microphone access (`com.apple.security.device.audio-input`)
- `--camera` — grants camera access (`com.apple.security.device.camera`)

macOS will prompt the user for permission on first use. Only add these flags for sites that actually need them.

## Multiple Apps For The Same Site

If you need separate apps for the same site, for example two Gmail accounts with different login state, build them with different app names:

```bash
pake https://gmail.com --name "Gmail Work"
pake https://gmail.com --name "Gmail Personal"
```

Pake 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.

For advanced cases, Pake also supports a hidden `--identifier` option if you need to pin the bundle identifier explicitly:

```bash
pake https://gmail.com --name "Gmail Work" --identifier com.example.gmail.work
```

`--multi-instance` is different. It only allows multiple processes for the same packaged app, it does not create separate app identities.

## Project Structure

Understanding Pake's codebase structure will help you navigate and contribute effectively:

```tree
├── bin/                    # CLI source code (TypeScript)
│   ├── builders/          # Platform-specific builders
│   ├── helpers/           # Utility functions
│   └── options/           # CLI option processing
├── docs/                  # Project documentation
├── src-tauri/             # Tauri application core
│   ├── src/
│   │   ├── app/           # Core modules (window, tray, shortcuts)
│   │   ├── inject/        # Web page injection logic
│   │   └── lib.rs         # Application entry point
│   ├── icons/             # macOS icons (.icns)
│   ├── png/               # Windows/Linux icons (.ico, .png)
│   ├── pake.json          # App configuration
│   └── tauri.*.conf.json  # Platform-specific configs
├── scripts/               # Build and utility scripts
└── tests/                 # Test suites
```

### Key Components

- **CLI Tool** (`bin/`): TypeScript-based command interface for packaging apps
- **Tauri App** (`src-tauri/`): Rust-based desktop framework
- **Injection System** (`src-tauri/src/inject/`): Custom CSS/JS injection for webpages
- **Configuration**: Multi-platform app settings and build configurations

## Development Workflow

### Prerequisites

- Node.js ≥22.0.0 (recommended LTS, older versions ≥18.0.0 may work)
- Rust ≥1.85.0 (recommended stable)

#### Platform-Specific Requirements

**macOS:**

- Xcode Command Line Tools: `xcode-select --install`

**Windows:**

- **CRITICAL**: Consult [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) before proceeding
- Windows 10 SDK (10.0.19041.0) and Visual Studio Build Tools 2022 (≥17.2)
- Required redistributables:
  1. Microsoft Visual C++ 2015-2022 Redistributable (x64)
  2. Microsoft Visual C++ 2015-2022 Redistributable (x86)
  3. Microsoft Visual C++ 2012 Redistributable (x86) (optional)
  4. Microsoft Visual C++ 2013 Redistributable (x86) (optional)
  5. Microsoft Visual C++ 2008 Redistributable (x86) (optional)

- **Windows ARM (ARM64) support**: Install C++ ARM64 build tools in Visual Studio Installer under "Individual Components" → "MSVC v143 - VS 2022 C++ ARM64 build tools"

**Linux (Ubuntu):**

```bash
sudo apt install libdbus-1-dev \
    libsoup-3.0-dev \
    libjavascriptcoregtk-4.1-dev \
    libwebkit2gtk-4.1-dev \
    build-essential \
    curl \
    wget \
    file \
    libxdo-dev \
    libssl-dev \
    libgtk-3-dev \
    libayatana-appindicator3-dev \
    librsvg2-dev \
    gnome-video-effects \
    gnome-video-effects-extra \
    libglib2.0-dev \
    pkg-config
```

### Installation

```bash
# Clone the repository
git clone https://github.com/tw93/Pake.git
cd Pake

# Install dependencies
pnpm install

# Start development
pnpm run dev
```

### Development Commands

1. **CLI Changes**: Edit files in `bin/`, then run `pnpm run cli:build`
2. **Core App Changes**: Edit files in `src-tauri/src/`, then run `pnpm run dev`
3. **Injection Logic**: Modify files in `src-tauri/src/inject/` for web customizations
4. **Testing**: Run `pnpm test` for comprehensive validation

#### Command Reference

- **Dev mode**: `pnpm run dev` (hot reload)
- **Build**: `pnpm run build`
- **Debug build**: `pnpm run build:debug`
- **CLI build**: `pnpm run cli:build`

#### CLI Development

For CLI development with hot reloading, modify the `DEFAULT_DEV_PAKE_OPTIONS` configuration in `bin/defaults.ts`:

```typescript
export const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = {
  ...DEFAULT_PAKE_OPTIONS,
  url: "https://weekly.tw93.fun/en",
  name: "Weekly",
};
```

Then run:

```bash
pnpm run cli:dev
```

This script reads the configuration and packages the specified app in watch mode, with hot updates for `pake-cli` code changes.

### Testing Guide

Comprehensive CLI build and release validation guidance for multi-platform packaging.

#### Running Tests

```bash
# Complete test suite (recommended)
pnpm test                   # Build the CLI, run the Vitest suite, then run real build + release workflow smoke tests

# Skip the real build and release workflow smoke tests
pnpm test -- --no-build

# Run the fast Vitest suite only
npx vitest run

# Build the CLI explicitly
pnpm run cli:build

# Run the release workflow smoke test directly
node ./tests/release.js
```

#### 🚀 Complete Test Suite Includes

- ✅ **Vitest suite**: unit, integration, builder, and CLI option coverage
- ✅ **Real build smoke test**: platform-aware packaging validation
- ✅ **Release workflow smoke test**: verifies the release build path used for popular apps

#### Test Details

- `pnpm test` runs the main CLI test runner in [`tests/index.js`](../tests/index.js), which:
- builds the CLI,
- runs the Vitest suite,
- runs the real build smoke test unless `--no-build` is passed,
- and then runs the release workflow smoke test when the real build phase succeeds.

Useful optional flags:

- `--no-unit`: skip unit tests
- `--no-integration`: skip integration tests
- `--no-builder`: skip builder tests
- `--no-build`: skip the real build smoke test and the follow-up release workflow smoke test
- `--e2e`: add end-to-end configuration tests
- `--pake-cli`: add GitHub Actions related checks

If you only want the release workflow smoke test, run `node ./tests/release.js` directly.

#### Troubleshooting

- **CLI file not found**: Run `pnpm run cli:build`
- **Test timeout**: Build tests require extended time to complete
- **Build failures**: Check Rust toolchain with `rustup update`
- **Permission errors**: Ensure write permissions are available

### Common Build Issues

- **Rust compilation errors**: Run `cargo clean` in `src-tauri/` directory
- **Node dependency iss
Download .txt
gitextract_0ul1epmu/

├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yml
│   │   ├── config.yml
│   │   └── feature.yml
│   ├── actions/
│   │   └── setup-env/
│   │       └── action.yml
│   └── workflows/
│       ├── pake-cli.yaml
│       ├── quality-and-test.yml
│       ├── release.yml
│       ├── single-app.yaml
│       └── update-contributors.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .pnpmrc
├── .prettierignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── README_CN.md
├── action.yml
├── bin/
│   ├── builders/
│   │   ├── BaseBuilder.ts
│   │   ├── BuilderProvider.ts
│   │   ├── LinuxBuilder.ts
│   │   ├── MacBuilder.ts
│   │   └── WinBuilder.ts
│   ├── cli.ts
│   ├── defaults.ts
│   ├── dev.ts
│   ├── helpers/
│   │   ├── cli-program.ts
│   │   ├── merge.ts
│   │   ├── rust.ts
│   │   └── tauriConfig.ts
│   ├── options/
│   │   ├── icon.ts
│   │   ├── index.ts
│   │   └── logger.ts
│   ├── types.ts
│   └── utils/
│       ├── combine.ts
│       ├── dir.ts
│       ├── ico.ts
│       ├── info.ts
│       ├── ip.ts
│       ├── name.ts
│       ├── platform.ts
│       ├── shell.ts
│       ├── url.ts
│       └── validate.ts
├── default_app_list.json
├── docs/
│   ├── README.md
│   ├── README_CN.md
│   ├── advanced-usage.md
│   ├── advanced-usage_CN.md
│   ├── cli-usage.md
│   ├── cli-usage_CN.md
│   ├── faq.md
│   ├── faq_CN.md
│   ├── github-actions-usage.md
│   ├── github-actions-usage_CN.md
│   └── pake-action.md
├── icns2png.py
├── package.json
├── rollup.config.js
├── rust-toolchain.toml
├── src-tauri/
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── Info.plist
│   ├── assets/
│   │   └── main.wxs
│   ├── build.rs
│   ├── capabilities/
│   │   └── default.json
│   ├── entitlements.plist
│   ├── icons/
│   │   ├── chatgpt.icns
│   │   ├── deepseek.icns
│   │   ├── excalidraw.icns
│   │   ├── flomo.icns
│   │   ├── gemini.icns
│   │   ├── grok.icns
│   │   ├── icon.icns
│   │   ├── lizhi.icns
│   │   ├── programmusic.icns
│   │   ├── qwerty.icns
│   │   ├── twitter.icns
│   │   ├── wechat.icns
│   │   ├── weekly.icns
│   │   ├── weread.icns
│   │   ├── xiaohongshu.icns
│   │   ├── youtube.icns
│   │   └── youtubemusic.icns
│   ├── pake.json
│   ├── rust_proxy.toml
│   ├── src/
│   │   ├── app/
│   │   │   ├── config.rs
│   │   │   ├── invoke.rs
│   │   │   ├── menu.rs
│   │   │   ├── mod.rs
│   │   │   ├── setup.rs
│   │   │   └── window.rs
│   │   ├── inject/
│   │   │   ├── auth.js
│   │   │   ├── component.js
│   │   │   ├── custom.js
│   │   │   ├── event.js
│   │   │   ├── style.js
│   │   │   └── theme_refresh.js
│   │   ├── lib.rs
│   │   ├── main.rs
│   │   └── util.rs
│   ├── tauri.conf.json
│   ├── tauri.linux.conf.json
│   ├── tauri.macos.conf.json
│   └── tauri.windows.conf.json
├── tests/
│   ├── config.js
│   ├── index.js
│   ├── integration/
│   │   └── workflow-paths.test.js
│   ├── release.js
│   └── unit/
│       ├── builders.test.ts
│       ├── cli-options.test.ts
│       ├── file-finding.test.js
│       ├── identifier.test.ts
│       └── name.test.ts
├── tsconfig.json
└── vitest.config.ts
Download .txt
SYMBOL INDEX (262 symbols across 41 files)

FILE: bin/builders/BaseBuilder.ts
  method constructor (line 25) | protected constructor(options: PakeAppOptions) {
  method getBuildEnvironment (line 29) | private getBuildEnvironment() {
  method getInstallTimeout (line 39) | private getInstallTimeout(): number {
  method getBuildTimeout (line 44) | private getBuildTimeout(): number {
  method detectPackageManager (line 48) | private async detectPackageManager(): Promise<string> {
  method prepare (line 74) | async prepare() {
  method build (line 192) | async build(url: string) {
  method start (line 196) | async start(url: string) {
  method buildAndCopy (line 218) | async buildAndCopy(url: string, target: string) {
  method installAppToApplications (line 309) | private async installAppToApplications(
  method getFileType (line 332) | protected getFileType(target: string): string {
  method isLinuxDeployStripError (line 338) | private isLinuxDeployStripError(error: unknown): boolean {
  method resolveTargetArch (line 383) | protected resolveTargetArch(requestedArch?: string): string {
  method getTauriTarget (line 393) | protected getTauriTarget(
  method getArchDisplayName (line 405) | protected getArchDisplayName(arch: string): string {
  method buildBaseCommand (line 412) | protected buildBaseCommand(
  method getBuildFeatures (line 440) | protected getBuildFeatures(): string[] {
  method getBuildCommand (line 454) | protected getBuildCommand(packageManager: string = 'pnpm'): string {
  method getMacOSMajorVersion (line 479) | protected getMacOSMajorVersion(): number {
  method getBasePath (line 490) | protected getBasePath(): string {
  method getBuildAppPath (line 495) | protected getBuildAppPath(
  method copyRawBinary (line 514) | protected async copyRawBinary(
  method getRawBinarySourcePath (line 535) | protected getRawBinarySourcePath(
  method getRawBinaryPath (line 558) | protected getRawBinaryPath(appName: string): string {
  method getBinaryName (line 567) | protected getBinaryName(appName: string): string {
  method hasArchSpecificTarget (line 581) | protected hasArchSpecificTarget(): boolean {
  method getArchSpecificPath (line 588) | protected getArchSpecificPath(): string {

FILE: bin/builders/BuilderProvider.ts
  class BuilderProvider (line 18) | class BuilderProvider {
    method create (line 19) | static create(options: PakeAppOptions): BaseBuilder {

FILE: bin/builders/LinuxBuilder.ts
  class LinuxBuilder (line 6) | class LinuxBuilder extends BaseBuilder {
    method constructor (line 11) | constructor(options: PakeAppOptions) {
    method getFileName (line 26) | getFileName() {
    method build (line 57) | async build(url: string) {
    method buildAndCopy (line 72) | async buildAndCopy(url: string, target: string) {
    method getBuildCommand (line 77) | protected getBuildCommand(packageManager: string = 'pnpm'): string {
    method getBasePath (line 115) | protected getBasePath(): string {
    method getFileType (line 126) | protected getFileType(target: string): string {
    method hasArchSpecificTarget (line 133) | protected hasArchSpecificTarget(): boolean {
    method getArchSpecificPath (line 137) | protected getArchSpecificPath(): string {

FILE: bin/builders/MacBuilder.ts
  class MacBuilder (line 6) | class MacBuilder extends BaseBuilder {
    method constructor (line 10) | constructor(options: PakeAppOptions) {
    method getFileName (line 31) | getFileName(): string {
    method getActualArch (line 51) | private getActualArch(): string {
    method getBuildCommand (line 62) | protected getBuildCommand(packageManager: string = 'pnpm'): string {
    method getBasePath (line 85) | protected getBasePath(): string {
    method hasArchSpecificTarget (line 93) | protected hasArchSpecificTarget(): boolean {
    method getArchSpecificPath (line 97) | protected getArchSpecificPath(): string {

FILE: bin/builders/WinBuilder.ts
  class WinBuilder (line 6) | class WinBuilder extends BaseBuilder {
    method constructor (line 10) | constructor(options: PakeAppOptions) {
    method getFileName (line 19) | getFileName(): string {
    method getBuildCommand (line 26) | protected getBuildCommand(packageManager: string = 'pnpm'): string {
    method getBasePath (line 50) | protected getBasePath(): string {
    method hasArchSpecificTarget (line 56) | protected hasArchSpecificTarget(): boolean {
    method getArchSpecificPath (line 60) | protected getArchSpecificPath(): string {

FILE: bin/cli.ts
  function checkUpdateTips (line 11) | async function checkUpdateTips() {

FILE: bin/defaults.ts
  constant DEFAULT_PAKE_OPTIONS (line 3) | const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
  constant DEFAULT_DEV_PAKE_OPTIONS (line 59) | const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = {

FILE: bin/helpers/cli-program.ts
  function getCliProgram (line 10) | function getCliProgram() {

FILE: bin/helpers/merge.ts
  function mergeConfig (line 15) | async function mergeConfig(

FILE: bin/helpers/rust.ts
  function normalizePathForComparison (line 12) | function normalizePathForComparison(targetPath: string) {
  function getCargoHomeCandidates (line 17) | function getCargoHomeCandidates(): string[] {
  function ensureCargoBinOnPath (line 32) | function ensureCargoBinOnPath() {
  function ensureRustEnv (line 66) | function ensureRustEnv() {
  function installRust (line 70) | async function installRust() {
  function checkRustInstalled (line 100) | function checkRustInstalled() {

FILE: bin/options/icon.ts
  type PlatformIconConfig (line 16) | type PlatformIconConfig = {
  constant ICON_CONFIG (line 21) | const ICON_CONFIG = {
  constant PLATFORM_CONFIG (line 32) | const PLATFORM_CONFIG: Record<'win' | 'linux' | 'macos', PlatformIconCon...
  constant API_KEYS (line 38) | const API_KEYS = {
  function generateIconPath (line 48) | function generateIconPath(appName: string, isDefault = false): string {
  function getIconBaseName (line 61) | function getIconBaseName(appName: string): string {
  function copyWindowsIconIfNeeded (line 68) | async function copyWindowsIconIfNeeded(
  function preprocessIcon (line 100) | async function preprocessIcon(inputPath: string): Promise<string> {
  function applyMacOSMask (line 132) | async function applyMacOSMask(inputPath: string): Promise<string> {
  function convertIconFormat (line 186) | async function convertIconFormat(
  function processIcon (line 257) | async function processIcon(
  function getDefaultIcon (line 286) | async function getDefaultIcon(): Promise<string> {
  function handleIcon (line 336) | async function handleIcon(
  function generateIconServiceUrls (line 378) | function generateIconServiceUrls(domain: string): string[] {
  function tryGetFavicon (line 404) | async function tryGetFavicon(
  function downloadIcon (line 461) | async function downloadIcon(
  function saveIconFile (line 518) | async function saveIconFile(

FILE: bin/options/index.ts
  function resolveAppName (line 15) | function resolveAppName(name: string, platform: NodeJS.Platform): string {
  function resolveLocalAppName (line 20) | function resolveLocalAppName(
  function isValidName (line 36) | function isValidName(name: string, platform: NodeJS.Platform): boolean {
  function handleOptions (line 45) | async function handleOptions(

FILE: bin/options/logger.ts
  method info (line 5) | info(...msg: any[]) {
  method debug (line 8) | debug(...msg: any[]) {
  method error (line 11) | error(...msg: any[]) {
  method warn (line 14) | warn(...msg: any[]) {
  method success (line 17) | success(...msg: any[]) {

FILE: bin/types.ts
  type PlatformMap (line 1) | interface PlatformMap {
  type PakeCliOptions (line 5) | interface PakeCliOptions {
  type PakeAppOptions (line 140) | interface PakeAppOptions extends PakeCliOptions {
  type PlatformSpecific (line 144) | interface PlatformSpecific<T> {
  type WindowConfig (line 150) | interface WindowConfig {
  type PakeConfig (line 178) | interface PakeConfig {

FILE: bin/utils/combine.ts
  function combineFiles (line 3) | async function combineFiles(files: string[], output: string) {

FILE: bin/utils/ico.ts
  constant ICO_HEADER_SIZE (line 4) | const ICO_HEADER_SIZE = 6;
  constant ICO_DIR_ENTRY_SIZE (line 5) | const ICO_DIR_ENTRY_SIZE = 16;
  constant ICO_TYPE_ICON (line 6) | const ICO_TYPE_ICON = 1;
  type IcoEntry (line 8) | type IcoEntry = {
  function decodeDimension (line 19) | function decodeDimension(value: number): number {
  function compareByPreferredSize (line 23) | function compareByPreferredSize(
  function parseIcoBuffer (line 49) | function parseIcoBuffer(buffer: Buffer): IcoEntry[] {
  function buildReorderedIcoBuffer (line 96) | function buildReorderedIcoBuffer(
  function writeIcoWithPreferredSize (line 129) | async function writeIcoWithPreferredSize(

FILE: bin/utils/info.ts
  function getIdentifier (line 10) | function getIdentifier(url: string, name?: string) {
  function resolveIdentifier (line 20) | function resolveIdentifier(
  function promptText (line 33) | async function promptText(
  function capitalizeFirstLetter (line 46) | function capitalizeFirstLetter(string: string) {
  function getSpinner (line 50) | function getSpinner(text: string) {

FILE: bin/utils/ip.ts
  function isChinaDomain (line 36) | async function isChinaDomain(domain: string): Promise<boolean> {
  function isChinaIP (line 46) | async function isChinaIP(ip: string, domain: string): Promise<boolean> {

FILE: bin/utils/name.ts
  function generateSafeFilename (line 1) | function generateSafeFilename(name: string): string {
  function getSafeAppName (line 9) | function getSafeAppName(name: string): string {
  function generateLinuxPackageName (line 13) | function generateLinuxPackageName(name: string): string {
  function generateIdentifierSafeName (line 21) | function generateIdentifierSafeName(name: string): string {
  function generateWindowsFilename (line 46) | function generateWindowsFilename(name: string): string {
  function generateMacOSFilename (line 53) | function generateMacOSFilename(name: string): string {

FILE: bin/utils/platform.ts
  constant IS_MAC (line 3) | const IS_MAC = platform === 'darwin';
  constant IS_WIN (line 4) | const IS_WIN = platform === 'win32';
  constant IS_LINUX (line 5) | const IS_LINUX = platform === 'linux';

FILE: bin/utils/shell.ts
  function shellExec (line 4) | async function shellExec(

FILE: bin/utils/url.ts
  function getDomain (line 4) | function getDomain(inputUrl: string): string | null {
  function appendProtocol (line 22) | function appendProtocol(inputUrl: string): string {
  function normalizeUrl (line 32) | function normalizeUrl(urlToNormalize: string): string {

FILE: bin/utils/validate.ts
  function validateNumberInput (line 5) | function validateNumberInput(value: string) {
  function validateUrlInput (line 13) | function validateUrlInput(url: string) {

FILE: rollup.config.js
  method onwarn (line 41) | onwarn(warning, warn) {
  function pakeCliDevPlugin (line 74) | function pakeCliDevPlugin() {

FILE: src-tauri/build.rs
  function main (line 1) | fn main() {

FILE: src-tauri/src/app/config.rs
  type WindowConfig (line 4) | pub struct WindowConfig {
  function default_zoom (line 39) | fn default_zoom() -> u32 {
  type PlatformSpecific (line 44) | pub struct PlatformSpecific<T> {
  function get (line 51) | pub const fn get(&self) -> &T {
  function copied (line 67) | pub const fn copied(&self) -> T {
  type UserAgent (line 72) | pub type UserAgent = PlatformSpecific<String>;
  type FunctionON (line 73) | pub type FunctionON = PlatformSpecific<bool>;
  type PakeConfig (line 76) | pub struct PakeConfig {
    method show_system_tray (line 89) | pub fn show_system_tray(&self) -> bool {

FILE: src-tauri/src/app/invoke.rs
  type DownloadFileParams (line 13) | pub struct DownloadFileParams {
  type BinaryDownloadParams (line 20) | pub struct BinaryDownloadParams {
  type NotificationParams (line 27) | pub struct NotificationParams {
  function download_file (line 34) | pub async fn download_file(app: AppHandle, params: DownloadFileParams) -...
  function download_file_by_binary (line 94) | pub async fn download_file_by_binary(
  function send_notification (line 135) | pub fn send_notification(app: AppHandle, params: NotificationParams) -> ...
  function update_theme_mode (line 148) | pub async fn update_theme_mode(app: AppHandle, mode: String) {
  function clear_cache_and_restart (line 169) | pub fn clear_cache_and_restart(app: AppHandle) -> Result<(), String> {

FILE: src-tauri/src/app/menu.rs
  function get_menu (line 9) | pub fn get_menu(app: &AppHandle<Wry>, allow_multi_window: bool) -> tauri...
  function app_menu (line 29) | fn app_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {
  function file_menu (line 48) | fn file_menu(app: &AppHandle<Wry>, allow_multi_window: bool) -> tauri::R...
  function edit_menu (line 72) | fn edit_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {
  function view_menu (line 99) | fn view_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {
  function navigation_menu (line 143) | fn navigation_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {
  function window_menu (line 169) | fn window_menu(app: &AppHandle<Wry>) -> tauri::Result<Submenu<Wry>> {
  function help_menu (line 186) | fn help_menu(app: &AppHandle<Wry>, title: &str) -> tauri::Result<Submenu...
  function handle_menu_click (line 193) | pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {

FILE: src-tauri/src/app/setup.rs
  function set_system_tray (line 13) | pub fn set_system_tray(
  function set_global_shortcut (line 107) | pub fn set_global_shortcut(

FILE: src-tauri/src/app/window.rs
  function build_proxy_browser_arg (line 13) | fn build_proxy_browser_arg(url: &Url) -> Option<String> {
  type MultiWindowState (line 28) | pub struct MultiWindowState {
    method new (line 35) | pub fn new(pake_config: PakeConfig, tauri_config: Config) -> Self {
    method next_window_label (line 43) | fn next_window_label(&self) -> String {
  function set_window (line 50) | pub fn set_window(app: &AppHandle, config: &PakeConfig, tauri_config: &C...
  function open_additional_window (line 54) | pub fn open_additional_window(app: &AppHandle) -> tauri::Result<WebviewW...
  type WindowBuildOptions (line 60) | struct WindowBuildOptions<'a> {
  function open_requested_window (line 67) | fn open_requested_window(
  function open_additional_window_safe (line 95) | pub fn open_additional_window_safe(app: &AppHandle) {
  function build_window_with_label (line 116) | fn build_window_with_label(
  function build_window (line 145) | fn build_window(

FILE: src-tauri/src/inject/auth.js
  function matchesAuthUrl (line 4) | function matchesAuthUrl(url, baseUrl = window.location.href) {
  function isAuthLink (line 48) | function isAuthLink(url) {
  function isAuthPopup (line 53) | function isAuthPopup(url, name) {

FILE: src-tauri/src/inject/component.js
  function pakeToast (line 3) | function pakeToast(msg) {
  function initFullscreenPolyfill (line 30) | function initFullscreenPolyfill() {

FILE: src-tauri/src/inject/event.js
  function setZoom (line 13) | function setZoom(zoom) {
  function zoomCommon (line 31) | function zoomCommon(zoomChange) {
  function zoomIn (line 36) | function zoomIn() {
  function zoomOut (line 40) | function zoomOut() {
  function triggerPasteAsPlainText (line 46) | function triggerPasteAsPlainText() {
  function handleShortcut (line 54) | function handleShortcut(event) {
  constant DOWNLOADABLE_FILE_EXTENSIONS (line 61) | const DOWNLOADABLE_FILE_EXTENSIONS = {
  constant ALL_DOWNLOADABLE_EXTENSIONS (line 144) | const ALL_DOWNLOADABLE_EXTENSIONS = Object.values(
  constant PREVIEWABLE_MEDIA_EXTENSIONS (line 148) | const PREVIEWABLE_MEDIA_EXTENSIONS = [
  constant DOWNLOAD_PATH_PATTERNS (line 176) | const DOWNLOAD_PATH_PATTERNS = [
  function getUserLanguage (line 186) | function getUserLanguage() {
  function isChineseLanguage (line 190) | function isChineseLanguage(language = getUserLanguage()) {
  function showDownloadError (line 201) | function showDownloadError(filename) {
  function getExtension (line 216) | function getExtension(url) {
  function isPreviewableMedia (line 226) | function isPreviewableMedia(url) {
  function isDownloadableFile (line 232) | function isDownloadableFile(url) {
  function collectUrlToBlobs (line 328) | function collectUrlToBlobs() {
  function convertBlobUrlToBinary (line 338) | function convertBlobUrlToBinary(blobUrl) {
  function downloadFromDataUri (line 350) | function downloadFromDataUri(dataURI, filename) {
  function downloadFromBlobUrl (line 382) | function downloadFromBlobUrl(blobUrl, filename) {
  function detectDownloadByCreateAnchor (line 404) | function detectDownloadByCreateAnchor() {
  function getTheme (line 700) | function getTheme() {
  function getMenuStyles (line 707) | function getMenuStyles(theme = getTheme()) {
  function createContextMenu (line 728) | function createContextMenu() {
  function createMenuItem (line 752) | function createMenuItem(text, onClick, divider = false) {
  function showContextMenu (line 788) | function showContextMenu(x, y, items) {
  function hideContextMenu (line 815) | function hideContextMenu() {
  function downloadImage (line 822) | function downloadImage(imageUrl) {
  function getMediaInfo (line 863) | function getMediaInfo(target) {
  function buildMenuItems (line 904) | function buildMenuItems(type, data) {
  function setDefaultZoom (line 1034) | function setDefaultZoom() {
  function getFilenameFromUrl (line 1043) | function getFilenameFromUrl(url) {

FILE: src-tauri/src/lib.rs
  constant WINDOW_SHOW_DELAY (line 12) | const WINDOW_SHOW_DELAY: u64 = 50;
  function run_app (line 24) | pub fn run_app() {
  function run (line 194) | pub fn run() {

FILE: src-tauri/src/main.rs
  function main (line 6) | fn main() {

FILE: src-tauri/src/util.rs
  function get_pake_config (line 6) | pub fn get_pake_config() -> (PakeConfig, Config) {
  function get_data_dir (line 26) | pub fn get_data_dir(app: &AppHandle, package_name: String) -> PathBuf {
  function show_toast (line 42) | pub fn show_toast(window: &WebviewWindow, message: &str) {
  type MessageType (line 47) | pub enum MessageType {
  function get_download_message_with_lang (line 53) | pub fn get_download_message_with_lang(
  function check_file_or_append (line 105) | pub fn check_file_or_append(file_path: &str) -> String {

FILE: tests/config.js
  constant PROJECT_ROOT (line 12) | const PROJECT_ROOT = path.dirname(__dirname);
  constant CLI_PATH (line 13) | const CLI_PATH = path.join(PROJECT_ROOT, "dist/cli.js");
  constant TIMEOUTS (line 16) | const TIMEOUTS = {
  constant TEST_URLS (line 23) | const TEST_URLS = {
  constant TEST_ASSETS (line 33) | const TEST_ASSETS = {
  constant TEST_NAMES (line 39) | const TEST_NAMES = {
  constant PLATFORM_EXTENSIONS (line 49) | const PLATFORM_EXTENSIONS = {

FILE: tests/index.js
  class PakeTestRunner (line 16) | class PakeTestRunner {
    method constructor (line 17) | constructor() {
    method runAll (line 23) | async runAll(options = {}) {
    method validateEnvironment (line 124) | validateEnvironment() {
    method runTest (line 157) | async runTest(name, testFn, timeout = TIMEOUTS.MEDIUM) {
    method runCliHealthChecks (line 185) | async runCliHealthChecks() {
    method runIntegrationTests (line 267) | async runIntegrationTests() {
    method runBuilderTests (line 323) | async runBuilderTests() {
    method runPakeCliTests (line 356) | async runPakeCliTests() {
    method runE2ETests (line 430) | async runE2ETests() {
    method runProxyTest (line 632) | async runProxyTest() {
    method runLocalFileTest (line 650) | async runLocalFileTest() {
    method runRealBuildTest (line 674) | async runRealBuildTest() {
    method runMultiArchBuildTest (line 918) | async runMultiArchBuildTest() {
    method findBuildOutputFiles (line 1120) | findBuildOutputFiles(testName, platform) {
    method debugBuildDirectories (line 1286) | debugBuildDirectories() {
    method listTargetContents (line 1328) | listTargetContents(targetDir, maxDepth = 3, currentDepth = 0) {
    method trackTempFile (line 1371) | trackTempFile(filepath) {
    method trackTempDir (line 1375) | trackTempDir(dirpath) {
    method cleanupTempIcons (line 1379) | cleanupTempIcons() {
    method cleanup (line 1404) | cleanup() {
    method displayFinalResults (line 1477) | displayFinalResults() {

FILE: tests/release.js
  constant GREEN (line 15) | const GREEN = "\x1b[32m";
  constant YELLOW (line 16) | const YELLOW = "\x1b[33m";
  constant BLUE (line 17) | const BLUE = "\x1b[34m";
  constant RED (line 18) | const RED = "\x1b[31m";
  constant TEST_APPS (line 22) | const TEST_APPS = ["weread", "twitter"];
  class ReleaseBuildTest (line 24) | class ReleaseBuildTest {
    method constructor (line 25) | constructor() {
    method log (line 29) | log(level, message) {
    method getAppConfig (line 35) | async getAppConfig(appName) {
    method buildApp (line 49) | async buildApp(appName) {
    method findOutputFiles (line 97) | findOutputFiles(appName) {
    method run (line 173) | async run(options = {}) {

FILE: tests/unit/builders.test.ts
  function parseAndFilterTargets (line 11) | function parseAndFilterTargets(targetsString: string): string[] {

FILE: tests/unit/file-finding.test.js
  function findFilesByPattern (line 32) | function findFilesByPattern(dir, pattern) {
  function findInMultipleLocations (line 157) | function findInMultipleLocations(locations) {
Condensed preview — 123 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (505K chars).
[
  {
    "path": ".dockerignore",
    "chars": 80,
    "preview": ".git\n.gitignore\n\n**/target\n**/node_modules\n\n**/*.log\n**/*.md\n**/tmp\n\nDockerfile\n"
  },
  {
    "path": ".editorconfig",
    "chars": 396,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
  },
  {
    "path": ".gitattributes",
    "chars": 1083,
    "preview": "# Exclude all non-source directories from language detection\nbin/**/*                  linguist-vendored\ndist/**/*      "
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 69,
    "preview": "github: [\"tw93\"]\ncustom: [\"https://miaoyan.app/cats.html?name=Pake\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "chars": 3240,
    "preview": "name: Bug report\ndescription: Problems with the software\ntitle: \"[Bug] \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    at"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 202,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a question or get support\n    url: https://github.com/tw93/Pake"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "chars": 1584,
    "preview": "name: Feature\ndescription: Add new feature, improve code, and more\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    "
  },
  {
    "path": ".github/actions/setup-env/action.yml",
    "chars": 5272,
    "preview": "name: Setup Development Environment\ndescription: Unified environment setup with Node.js, Rust, and system dependencies\n\n"
  },
  {
    "path": ".github/workflows/pake-cli.yaml",
    "chars": 5773,
    "preview": "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:"
  },
  {
    "path": ".github/workflows/quality-and-test.yml",
    "chars": 4309,
    "preview": "name: Quality & Testing\n\non:\n  push:\n    branches: [main, dev]\n  pull_request:\n    branches: [main, dev]\n  workflow_disp"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 4140,
    "preview": "name: Release & Publish\n\non:\n  push:\n    tags:\n      - \"V*\"\n  workflow_dispatch:\n    inputs:\n      release_apps:\n       "
  },
  {
    "path": ".github/workflows/single-app.yaml",
    "chars": 14133,
    "preview": "name: Build Single Popular App\n\nenv:\n  NODE_VERSION: \"22\"\n  PNPM_VERSION: \"10.26.2\"\n\non:\n  workflow_dispatch:\n    inputs"
  },
  {
    "path": ".github/workflows/update-contributors.yml",
    "chars": 2236,
    "preview": "name: Update Contributors\n\non:\n  push:\n    branches: [main, dev]\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * 0"
  },
  {
    "path": ".gitignore",
    "chars": 521,
    "preview": "\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"
  },
  {
    "path": ".npmignore",
    "chars": 618,
    "preview": "# Development files\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n\n# Development directories\nnode_modules/\n.vscode/\n.idea/\n"
  },
  {
    "path": ".npmrc",
    "chars": 139,
    "preview": "# Suppress npm funding and audit messages during installation\nfund=false\naudit=false\n\n# Resolve sharp version conflicts\n"
  },
  {
    "path": ".pnpmrc",
    "chars": 75,
    "preview": "strict-peer-dependencies=false\nnode-linker=hoisted\nauto-install-peers=true\n"
  },
  {
    "path": ".prettierignore",
    "chars": 219,
    "preview": "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"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5200,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3415,
    "preview": "## How to contribute to Pake\n\n**Welcome to create [pull requests](https://github.com/tw93/Pake/compare/) for bugfix, new"
  },
  {
    "path": "Dockerfile",
    "chars": 3543,
    "preview": "# syntax=docker/dockerfile:1.4\n# Cargo build stage - Updated to latest Rust for edition2024 support\nFROM rust:latest AS "
  },
  {
    "path": "LICENSE",
    "chars": 1061,
    "preview": "MIT License\n\nCopyright (c) 2024 Tw93\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof th"
  },
  {
    "path": "README.md",
    "chars": 13360,
    "preview": "<h4 align=\"right\"><strong>English</strong> | <a href=\"README_CN.md\">简体中文</a></h4>\n<p align=\"center\">\n    <img src=https:"
  },
  {
    "path": "README_CN.md",
    "chars": 11286,
    "preview": "<h4 align=\"right\"><a href=\"README.md\">English</a> | <strong>简体中文</strong></h4>\n<p align=\"center\">\n    <img src=https://g"
  },
  {
    "path": "action.yml",
    "chars": 3023,
    "preview": "name: \"Pake Web App Builder\"\ndescription: \"Transform any webpage into a lightweight desktop app using Rust and Tauri\"\nau"
  },
  {
    "path": "bin/builders/BaseBuilder.ts",
    "chars": 17823,
    "preview": "import path from 'path';\nimport fsExtra from 'fs-extra';\nimport chalk from 'chalk';\nimport prompts from 'prompts';\n\nimpo"
  },
  {
    "path": "bin/builders/BuilderProvider.ts",
    "chars": 662,
    "preview": "import BaseBuilder from './BaseBuilder';\nimport MacBuilder from './MacBuilder';\nimport WinBuilder from './WinBuilder';\ni"
  },
  {
    "path": "bin/builders/LinuxBuilder.ts",
    "chars": 4157,
    "preview": "import path from 'path';\nimport BaseBuilder from './BaseBuilder';\nimport { PakeAppOptions } from '@/types';\nimport tauri"
  },
  {
    "path": "bin/builders/MacBuilder.ts",
    "chars": 2826,
    "preview": "import path from 'path';\nimport tauriConfig from '@/helpers/tauriConfig';\nimport { PakeAppOptions } from '@/types';\nimpo"
  },
  {
    "path": "bin/builders/WinBuilder.ts",
    "chars": 1944,
    "preview": "import path from 'path';\nimport BaseBuilder from './BaseBuilder';\nimport { PakeAppOptions } from '@/types';\nimport tauri"
  },
  {
    "path": "bin/cli.ts",
    "chars": 975,
    "preview": "import log from 'loglevel';\nimport updateNotifier from 'update-notifier';\nimport packageJson from '../package.json';\nimp"
  },
  {
    "path": "bin/defaults.ts",
    "chars": 1449,
    "preview": "import { PakeCliOptions } from './types.js';\n\nexport const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {\n  icon: '',\n  height"
  },
  {
    "path": "bin/dev.ts",
    "chars": 602,
    "preview": "import log from 'loglevel';\nimport { PakeCliOptions } from './types';\nimport handleInputOptions from './options/index';\n"
  },
  {
    "path": "bin/helpers/cli-program.ts",
    "chars": 8725,
    "preview": "import chalk from 'chalk';\nimport { program, Option } from 'commander';\nimport packageJson from '../../package.json';\nim"
  },
  {
    "path": "bin/helpers/merge.ts",
    "chars": 13416,
    "preview": "import path from 'path';\nimport fsExtra from 'fs-extra';\n\nimport combineFiles from '@/utils/combine';\nimport logger from"
  },
  {
    "path": "bin/helpers/rust.ts",
    "chars": 3237,
    "preview": "import os from 'os';\nimport path from 'path';\nimport fsExtra from 'fs-extra';\nimport chalk from 'chalk';\nimport { execaS"
  },
  {
    "path": "bin/helpers/tauriConfig.ts",
    "chars": 1111,
    "preview": "import path from 'path';\nimport fsExtra from 'fs-extra';\nimport { npmDirectory } from '@/utils/dir';\n\n// Load configs fr"
  },
  {
    "path": "bin/options/icon.ts",
    "chars": 14979,
    "preview": "import path from 'path';\nimport fsExtra from 'fs-extra';\nimport chalk from 'chalk';\nimport { dir } from 'tmp-promise';\ni"
  },
  {
    "path": "bin/options/index.ts",
    "chars": 3131,
    "preview": "import path from 'path';\nimport fsExtra from 'fs-extra';\nimport logger from '@/options/logger';\n\nimport { handleIcon } f"
  },
  {
    "path": "bin/options/logger.ts",
    "chars": 469,
    "preview": "import chalk from 'chalk';\nimport log from 'loglevel';\n\nconst logger = {\n  info(...msg: any[]) {\n    log.info(...msg.map"
  },
  {
    "path": "bin/types.ts",
    "chars": 4891,
    "preview": "export interface PlatformMap {\n  [key: string]: any;\n}\n\nexport interface PakeCliOptions {\n  // Application name\n  name?:"
  },
  {
    "path": "bin/utils/combine.ts",
    "chars": 745,
    "preview": "import fs from 'fs';\n\nexport default async function combineFiles(files: string[], output: string) {\n  const contents = f"
  },
  {
    "path": "bin/utils/dir.ts",
    "chars": 396,
    "preview": "import path from 'path';\nimport { fileURLToPath } from 'url';\n\n// Convert the current module URL to a file path\nconst cu"
  },
  {
    "path": "bin/utils/ico.ts",
    "chars": 4203,
    "preview": "import path from 'path';\nimport fsExtra from 'fs-extra';\n\nconst ICO_HEADER_SIZE = 6;\nconst ICO_DIR_ENTRY_SIZE = 16;\ncons"
  },
  {
    "path": "bin/utils/info.ts",
    "chars": 1529,
    "preview": "import crypto from 'crypto';\nimport prompts from 'prompts';\nimport ora from 'ora';\nimport chalk from 'chalk';\n\n// Genera"
  },
  {
    "path": "bin/utils/ip.ts",
    "chars": 1455,
    "preview": "import dns from 'dns';\nimport http from 'http';\nimport { promisify } from 'util';\n\nimport logger from '@/options/logger'"
  },
  {
    "path": "bin/utils/name.ts",
    "chars": 1414,
    "preview": "export function generateSafeFilename(name: string): string {\n  return name\n    .replace(/[<>:\"/\\\\|?*]/g, '_')\n    .repla"
  },
  {
    "path": "bin/utils/platform.ts",
    "chars": 166,
    "preview": "const { platform } = process;\n\nexport const IS_MAC = platform === 'darwin';\nexport const IS_WIN = platform === 'win32';\n"
  },
  {
    "path": "bin/utils/shell.ts",
    "chars": 2681,
    "preview": "import { execa } from 'execa';\nimport { npmDirectory } from './dir';\n\nexport async function shellExec(\n  command: string"
  },
  {
    "path": "bin/utils/url.ts",
    "chars": 1092,
    "preview": "import * as psl from 'psl';\n\n// Extracts the domain from a given URL.\nexport function getDomain(inputUrl: string): strin"
  },
  {
    "path": "bin/utils/validate.ts",
    "chars": 622,
    "preview": "import fs from 'fs';\nimport { InvalidArgumentError } from 'commander';\nimport { normalizeUrl } from './url';\n\nexport fun"
  },
  {
    "path": "default_app_list.json",
    "chars": 1736,
    "preview": "[\n  {\n    \"name\": \"deepseek\",\n    \"title\": \"DeepSeek\",\n    \"name_zh\": \"DeepSeek\",\n    \"url\": \"https://chat.deepseek.com/"
  },
  {
    "path": "docs/README.md",
    "chars": 1047,
    "preview": "# Pake Documentation\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"README_CN.md\">简体中文</a></h4>\n\nWelcome to Pake"
  },
  {
    "path": "docs/README_CN.md",
    "chars": 655,
    "preview": "# Pake 文档\n\n<h4 align=\"right\"><a href=\"README.md\">English</a> | <strong>简体中文</strong></h4>\n\n欢迎使用 Pake 文档!在这里您可以找到全面的指南和文档"
  },
  {
    "path": "docs/advanced-usage.md",
    "chars": 10145,
    "preview": "# Advanced Usage\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"advanced-usage_CN.md\">简体中文</a></h4>\n\nCustomize P"
  },
  {
    "path": "docs/advanced-usage_CN.md",
    "chars": 6776,
    "preview": "# 高级用法\n\n<h4 align=\"right\"><strong><a href=\"advanced-usage.md\">English</a></strong> | 简体中文</h4>\n\n通过样式修改、JavaScript 注入和容器通"
  },
  {
    "path": "docs/cli-usage.md",
    "chars": 17785,
    "preview": "# CLI Usage Guide\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"cli-usage_CN.md\">简体中文</a></h4>\n\nComplete comman"
  },
  {
    "path": "docs/cli-usage_CN.md",
    "chars": 11152,
    "preview": "# CLI 使用指南\n\n<h4 align=\"right\"><strong><a href=\"cli-usage.md\">English</a></strong> | 简体中文</h4>\n\n完整的命令行参数说明和基础用法指南。\n\n## 安装"
  },
  {
    "path": "docs/faq.md",
    "chars": 11991,
    "preview": "# Frequently Asked Questions (FAQ)\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"faq_CN.md\">简体中文</a></h4>\n\nComm"
  },
  {
    "path": "docs/faq_CN.md",
    "chars": 8573,
    "preview": "# 常见问题 (FAQ)\n\n<h4 align=\"right\"><a href=\"faq.md\">English</a> | <strong>简体中文</strong></h4>\n\n使用 Pake 时的常见问题和解决方案。\n\n## 目录\n\n"
  },
  {
    "path": "docs/github-actions-usage.md",
    "chars": 1204,
    "preview": "# GitHub Actions Usage Guide\n\n<h4 align=\"right\"><strong>English</strong> | <a href=\"github-actions-usage_CN.md\">简体中文</a>"
  },
  {
    "path": "docs/github-actions-usage_CN.md",
    "chars": 795,
    "preview": "# GitHub Actions 使用指南\n\n<h4 align=\"right\"><strong><a href=\"github-actions-usage.md\">English</a></strong> | 简体中文</h4>\n\n无需本"
  },
  {
    "path": "docs/pake-action.md",
    "chars": 2698,
    "preview": "# Pake Action\n\nTransform any webpage into a lightweight desktop app with a single GitHub Actions step.\n\n> This guide sho"
  },
  {
    "path": "icns2png.py",
    "chars": 1337,
    "preview": "\"\"\"\n批量将icns文件转成png文件\nBatch convert ICNS files to PNG files\n\"\"\"\nimport os\n\ntry:\n    from PIL import Image\nexcept ImportEr"
  },
  {
    "path": "package.json",
    "chars": 2704,
    "preview": "{\n  \"name\": \"pake-cli\",\n  \"version\": \"3.10.1\",\n  \"description\": \"🤱🏻 Turn any webpage into a desktop app with one command"
  },
  {
    "path": "rollup.config.js",
    "chars": 4154,
    "preview": "import path from \"path\";\nimport fs from \"fs\";\nimport appRootPath from \"app-root-path\";\nimport typescript from \"rollup-pl"
  },
  {
    "path": "rust-toolchain.toml",
    "chars": 66,
    "preview": "[toolchain]\nchannel = \"1.93.0\"\ncomponents = [\"rustfmt\", \"clippy\"]\n"
  },
  {
    "path": "src-tauri/.gitignore",
    "chars": 73,
    "preview": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "chars": 1546,
    "preview": "[package]\nname = \"pake\"\nversion = \"3.10.1\"\ndescription = \"🤱🏻 Turn any webpage into a desktop app with Rust.\"\nauthors = ["
  },
  {
    "path": "src-tauri/Info.plist",
    "chars": 442,
    "preview": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1"
  },
  {
    "path": "src-tauri/assets/main.wxs",
    "chars": 16232,
    "preview": "<?if $(sys.BUILDARCH)=\"x86\"?>\n    <?define Win64 = \"no\" ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFilesFolder"
  },
  {
    "path": "src-tauri/build.rs",
    "chars": 39,
    "preview": "fn main() {\n    tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/capabilities/default.json",
    "chars": 899,
    "preview": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"pake-capability\",\n  \"description\": \"Capability for"
  },
  {
    "path": "src-tauri/entitlements.plist",
    "chars": 193,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "src-tauri/pake.json",
    "chars": 1240,
    "preview": "{\n  \"windows\": [\n    {\n      \"url\": \"https://weekly.tw93.fun/en\",\n      \"url_type\": \"web\",\n      \"hide_title_bar\": true,"
  },
  {
    "path": "src-tauri/rust_proxy.toml",
    "chars": 284,
    "preview": "[source.crates-io]\nreplace-with = 'rsproxy-sparse'\n[source.rsproxy]\nregistry = \"https://rsproxy.cn/crates.io-index\"\n[sou"
  },
  {
    "path": "src-tauri/src/app/config.rs",
    "chars": 2137,
    "preview": "use serde::{Deserialize, Serialize};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct WindowConfig {\n    pub "
  },
  {
    "path": "src-tauri/src/app/invoke.rs",
    "chars": 5218,
    "preview": "use crate::util::{check_file_or_append, get_download_message_with_lang, show_toast, MessageType};\nuse std::fs::{self, Fi"
  },
  {
    "path": "src-tauri/src/app/menu.rs",
    "chars": 9100,
    "preview": "// Menu functionality is only used on macOS\n#![cfg(target_os = \"macos\")]\n\nuse crate::app::window::open_additional_window"
  },
  {
    "path": "src-tauri/src/app/mod.rs",
    "chars": 105,
    "preview": "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",
    "chars": 5944,
    "preview": "use crate::app::window::open_additional_window_safe;\nuse std::str::FromStr;\nuse std::sync::{Arc, Mutex};\nuse std::time::"
  },
  {
    "path": "src-tauri/src/app/window.rs",
    "chars": 13084,
    "preview": "use crate::app::config::PakeConfig;\nuse crate::util::get_data_dir;\nuse std::{path::PathBuf, str::FromStr, sync::Mutex};\n"
  },
  {
    "path": "src-tauri/src/inject/auth.js",
    "chars": 1750,
    "preview": "// OAuth and Authentication Logic\n\n// Check if URL matches OAuth/authentication patterns\nfunction matchesAuthUrl(url, ba"
  },
  {
    "path": "src-tauri/src/inject/component.js",
    "chars": 8775,
    "preview": "document.addEventListener(\"DOMContentLoaded\", () => {\n  // Toast\n  function pakeToast(msg) {\n    const m = document.crea"
  },
  {
    "path": "src-tauri/src/inject/custom.js",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src-tauri/src/inject/event.js",
    "chars": 29598,
    "preview": "const shortcuts = {\n  \"[\": () => window.history.back(),\n  \"]\": () => window.history.forward(),\n  \"-\": () => zoomOut(),\n "
  },
  {
    "path": "src-tauri/src/inject/style.js",
    "chars": 15621,
    "preview": "window.addEventListener(\"DOMContentLoaded\", (_event) => {\n  // Customize and transform existing functions\n  const conten"
  },
  {
    "path": "src-tauri/src/inject/theme_refresh.js",
    "chars": 1818,
    "preview": "document.addEventListener(\"DOMContentLoaded\", () => {\n  const debounce = (func, wait) => {\n    let timeout;\n    return ("
  },
  {
    "path": "src-tauri/src/lib.rs",
    "chars": 7809,
    "preview": "#[cfg_attr(mobile, tauri::mobile_entry_point)]\nmod app;\nmod util;\n\nuse tauri::Manager;\nuse tauri_plugin_window_state::Bu"
  },
  {
    "path": "src-tauri/src/main.rs",
    "chars": 139,
    "preview": "#![cfg_attr(\n    all(not(debug_assertions), target_os = \"windows\"),\n    windows_subsystem = \"windows\"\n)]\n\nfn main() {\n  "
  },
  {
    "path": "src-tauri/src/util.rs",
    "chars": 4351,
    "preview": "use crate::app::config::PakeConfig;\nuse std::env;\nuse std::path::PathBuf;\nuse tauri::{AppHandle, Config, Manager, Webvie"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "chars": 364,
    "preview": "{\n  \"productName\": \"Weekly\",\n  \"identifier\": \"com.pake.weekly\",\n  \"version\": \"3.10.1\",\n  \"app\": {\n    \"withGlobalTauri\":"
  },
  {
    "path": "src-tauri/tauri.linux.conf.json",
    "chars": 194,
    "preview": "{\n  \"bundle\": {\n    \"icon\": [\"png/weekly_512.png\"],\n    \"active\": true,\n    \"linux\": {\n      \"deb\": {\n        \"depends\":"
  },
  {
    "path": "src-tauri/tauri.macos.conf.json",
    "chars": 586,
    "preview": "{\n  \"bundle\": {\n    \"icon\": [\"icons/weekly.icns\"],\n    \"active\": true,\n    \"targets\": [\"dmg\"],\n    \"macOS\": {\n      \"sig"
  },
  {
    "path": "src-tauri/tauri.windows.conf.json",
    "chars": 313,
    "preview": "{\n  \"bundle\": {\n    \"icon\": [\"png/weekly_256.ico\", \"png/weekly_32.ico\"],\n    \"active\": true,\n    \"resources\": [\"png/week"
  },
  {
    "path": "tests/config.js",
    "chars": 2303,
    "preview": "/**\n * Test Configuration for Pake CLI\n *\n * This file contains test configuration and utilities\n * shared across differ"
  },
  {
    "path": "tests/index.js",
    "chars": 51629,
    "preview": "#!/usr/bin/env node\n\n/**\n * Unified Test Runner for Pake CLI\n *\n * This is a simplified, unified test runner that replac"
  },
  {
    "path": "tests/integration/workflow-paths.test.js",
    "chars": 7864,
    "preview": "/**\n * Workflow Path Integration Tests\n *\n * These tests verify that the paths used in GitHub Actions workflows\n * match"
  },
  {
    "path": "tests/release.js",
    "chars": 8015,
    "preview": "#!/usr/bin/env node\n\n/**\n * Release Build Test\n *\n * Tests the actual release workflow by building 2 sample apps.\n * Val"
  },
  {
    "path": "tests/unit/builders.test.ts",
    "chars": 4043,
    "preview": "import { describe, it, expect } from 'vitest';\n\n/**\n * Tests for multi-target build parsing logic\n * These tests verify "
  },
  {
    "path": "tests/unit/cli-options.test.ts",
    "chars": 1144,
    "preview": "import { describe, expect, it } from 'vitest';\nimport { getCliProgram } from '../../bin/helpers/cli-program.js';\n\ndescri"
  },
  {
    "path": "tests/unit/file-finding.test.js",
    "chars": 7033,
    "preview": "/**\n * Cross-platform file finding tests\n *\n * These tests verify that file finding logic works correctly\n * across diff"
  },
  {
    "path": "tests/unit/identifier.test.ts",
    "chars": 759,
    "preview": "import { describe, expect, it } from 'vitest';\nimport { getIdentifier, resolveIdentifier } from '@/utils/info';\n\ndescrib"
  },
  {
    "path": "tests/unit/name.test.ts",
    "chars": 3713,
    "preview": "import { describe, it, expect } from 'vitest';\nimport {\n  getSafeAppName,\n  generateLinuxPackageName,\n  generateIdentifi"
  },
  {
    "path": "tsconfig.json",
    "chars": 475,
    "preview": "{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"target\": \"es2020\",\n    \"types\": [\"node\"],\n    \"lib\": [\"es2020\", \"d"
  },
  {
    "path": "vitest.config.ts",
    "chars": 381,
    "preview": "import { defineConfig } from 'vitest/config';\nimport path from 'path';\n\nexport default defineConfig({\n  test: {\n    envi"
  }
]

// ... and 17 more files (download for full content)

About this extraction

This page contains the full source code of the tw93/Pake GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 123 files (466.5 KB), approximately 125.3k tokens, and a symbol index with 262 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!