Add npm distribution support with PyInstaller binary
- Add npm package structure (@canghe_ai/wechat-cli) with platform-specific optionalDependencies - Add JS wrapper (bin/wechat-cli.js) and postinstall script - Add PyInstaller entry point and build script - Update scanner_macos.py for PyInstaller compatibility (sys._MEIPASS) - Update README with npm install instructions (macOS arm64) - Fix repo URLs to freestylefly/wechat-cli
This commit is contained in:
parent
e64006bafe
commit
f51e89ce12
7
.gitignore
vendored
7
.gitignore
vendored
@ -7,6 +7,12 @@ __pycache__/
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# npm platform binaries (published to npm, not git)
|
||||||
|
npm/platforms/*/bin/
|
||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
@ -31,6 +37,7 @@ Thumbs.db
|
|||||||
# Sensitive data — NEVER commit
|
# Sensitive data — NEVER commit
|
||||||
*.json
|
*.json
|
||||||
!pyproject.toml
|
!pyproject.toml
|
||||||
|
!npm/**/package.json
|
||||||
|
|
||||||
# Temp files
|
# Temp files
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|||||||
12
README.md
12
README.md
@ -19,14 +19,24 @@ A command-line tool to query your local WeChat data — chat history, contacts,
|
|||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
|
**Via pip (requires Python >= 3.10):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install wechat-cli
|
pip install wechat-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Via npm (standalone binary, no Python needed):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @canghe_ai/wechat-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
> Currently only **macOS arm64** binary is available. Other platforms (macOS Intel, Linux, Windows) can use the pip install method. PRs with additional platform binaries are welcome.
|
||||||
|
|
||||||
Or install from source:
|
Or install from source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/canghe/wechat-cli.git
|
git clone https://github.com/freestylefly/wechat-cli.git
|
||||||
cd wechat-cli
|
cd wechat-cli
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|||||||
12
README_CN.md
12
README_CN.md
@ -19,14 +19,24 @@
|
|||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
|
**通过 pip(需要 Python >= 3.10):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install wechat-cli
|
pip install wechat-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**通过 npm(独立二进制,无需 Python):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @canghe_ai/wechat-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
> 目前仅提供 **macOS arm64** 二进制。其他平台(macOS Intel、Linux、Windows)可使用 pip 安装。欢迎提交其他平台的二进制 PR。
|
||||||
|
|
||||||
或从源码安装:
|
或从源码安装:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/canghe/wechat-cli.git
|
git clone https://github.com/freestylefly/wechat-cli.git
|
||||||
cd wechat-cli
|
cd wechat-cli
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|||||||
5
entry.py
Normal file
5
entry.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""PyInstaller entry point — avoids relative import issues."""
|
||||||
|
from wechat_cli.main import cli
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
12
npm/platforms/darwin-arm64/package.json
Normal file
12
npm/platforms/darwin-arm64/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@canghe_ai/wechat-cli-darwin-arm64",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "wechat-cli binary for macOS arm64",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["arm64"],
|
||||||
|
"files": ["bin/"],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
npm/platforms/darwin-x64/package.json
Normal file
12
npm/platforms/darwin-x64/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@canghe_ai/wechat-cli-darwin-x64",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "wechat-cli binary for macOS x64",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["x64"],
|
||||||
|
"files": ["bin/"],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
npm/platforms/linux-arm64/package.json
Normal file
12
npm/platforms/linux-arm64/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@canghe_ai/wechat-cli-linux-arm64",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "wechat-cli binary for Linux arm64",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"],
|
||||||
|
"files": ["bin/"],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
npm/platforms/linux-x64/package.json
Normal file
12
npm/platforms/linux-x64/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@canghe_ai/wechat-cli-linux-x64",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "wechat-cli binary for Linux x64",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"],
|
||||||
|
"files": ["bin/"],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
npm/platforms/win32-x64/package.json
Normal file
12
npm/platforms/win32-x64/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@canghe_ai/wechat-cli-win32-x64",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "wechat-cli binary for Windows x64",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["x64"],
|
||||||
|
"files": ["bin/"],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
139
npm/scripts/build.py
Normal file
139
npm/scripts/build.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build wechat-cli standalone binaries with PyInstaller."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
NPM_DIR = ROOT / "npm"
|
||||||
|
PLATFORMS_DIR = NPM_DIR / "platforms"
|
||||||
|
|
||||||
|
PLATFORM_MAP = {
|
||||||
|
"darwin-arm64": {"target": "macos"},
|
||||||
|
"darwin-x64": {"target": "macos"},
|
||||||
|
"linux-x64": {"target": "linux"},
|
||||||
|
"linux-arm64": {"target": "linux"},
|
||||||
|
"win32-x64": {"target": "win"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_pyinstaller():
|
||||||
|
try:
|
||||||
|
import PyInstaller # noqa: F401
|
||||||
|
return
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
print("[+] Installing PyInstaller...")
|
||||||
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "pyinstaller"])
|
||||||
|
|
||||||
|
|
||||||
|
def build_platform(platform: str):
|
||||||
|
info = PLATFORM_MAP[platform]
|
||||||
|
os_name, arch = platform.split("-")
|
||||||
|
ext = ".exe" if os_name == "win32" else ""
|
||||||
|
binary_name = f"wechat-cli{ext}"
|
||||||
|
|
||||||
|
output_dir = PLATFORMS_DIR / platform / "bin"
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Building for {platform}...")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-m", "PyInstaller",
|
||||||
|
"--onefile",
|
||||||
|
"--name", "wechat-cli",
|
||||||
|
"--distpath", str(output_dir),
|
||||||
|
"--workpath", str(ROOT / "build" / f"wechat-cli_{platform}"),
|
||||||
|
"--specpath", str(ROOT / "build"),
|
||||||
|
"--noconfirm",
|
||||||
|
"--clean",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Bundle C binaries for key extraction
|
||||||
|
bin_dir = ROOT / "wechat_cli" / "bin"
|
||||||
|
if bin_dir.exists():
|
||||||
|
for f in bin_dir.iterdir():
|
||||||
|
if not f.name.startswith(".") and f.is_file():
|
||||||
|
cmd.extend(["--add-binary", f"{f}:wechat_cli/bin"])
|
||||||
|
|
||||||
|
# Hidden imports
|
||||||
|
hidden = ["pysqlcipher3", "sqlcipher3", "Cryptodome", "zstandard"]
|
||||||
|
for h in hidden:
|
||||||
|
cmd.extend(["--hidden-import", h])
|
||||||
|
|
||||||
|
cmd.append(str(ROOT / "entry.py"))
|
||||||
|
|
||||||
|
print(f"[+] Running: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd, cwd=str(ROOT))
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"[-] Build failed for {platform}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
binary_path = output_dir / binary_name
|
||||||
|
if not binary_path.exists():
|
||||||
|
print(f"[-] Binary not found: {binary_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"[+] Built: {binary_path}")
|
||||||
|
print(f" Size: {binary_path.stat().st_size / 1024 / 1024:.1f} MB")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
platforms = sys.argv[1:]
|
||||||
|
else:
|
||||||
|
# Default: build for current platform only
|
||||||
|
import platform as _pf
|
||||||
|
current = f"{_pf.system().lower()}-{_pf.machine()}"
|
||||||
|
# Normalize
|
||||||
|
if current == "darwin-arm64":
|
||||||
|
platforms = ["darwin-arm64"]
|
||||||
|
elif current == "darwin-x86_64" or current == "darwin-amd64":
|
||||||
|
platforms = ["darwin-x64"]
|
||||||
|
else:
|
||||||
|
# Try to match
|
||||||
|
platforms = []
|
||||||
|
for p in PLATFORM_MAP:
|
||||||
|
os_name, arch = p.split("-")
|
||||||
|
if os_name in current and (arch in current or
|
||||||
|
(arch == "x64" and ("x86_64" in current or "amd64" in current))):
|
||||||
|
platforms = [p]
|
||||||
|
break
|
||||||
|
if not platforms:
|
||||||
|
print(f"Cannot determine platform from '{current}'")
|
||||||
|
print(f"Usage: {sys.argv[0]} [platform...]")
|
||||||
|
print(f" Platforms: {', '.join(PLATFORM_MAP.keys())}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"[+] Building for: {', '.join(platforms)}")
|
||||||
|
ensure_pyinstaller()
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for p in platforms:
|
||||||
|
if p not in PLATFORM_MAP:
|
||||||
|
print(f"[-] Unknown platform: {p}")
|
||||||
|
results[p] = False
|
||||||
|
continue
|
||||||
|
results[p] = build_platform(p)
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print("Build Summary:")
|
||||||
|
for p, ok in results.items():
|
||||||
|
status = "OK" if ok else "FAILED"
|
||||||
|
print(f" {p}: {status}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
if not all(results.values()):
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
55
npm/wechat-cli/bin/wechat-cli.js
Normal file
55
npm/wechat-cli/bin/wechat-cli.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const PLATFORM_PACKAGES = {
|
||||||
|
'darwin-arm64': '@canghe_ai/wechat-cli-darwin-arm64',
|
||||||
|
'darwin-x64': '@canghe_ai/wechat-cli-darwin-x64',
|
||||||
|
'linux-x64': '@canghe_ai/wechat-cli-linux-x64',
|
||||||
|
'linux-arm64': '@canghe_ai/wechat-cli-linux-arm64',
|
||||||
|
'win32-x64': '@canghe_ai/wechat-cli-win32-x64',
|
||||||
|
};
|
||||||
|
|
||||||
|
const platformKey = `${process.platform}-${process.arch}`;
|
||||||
|
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||||
|
|
||||||
|
function getBinaryPath() {
|
||||||
|
// 1. 环境变量覆盖
|
||||||
|
if (process.env.WECHAT_CLI_BINARY) {
|
||||||
|
return process.env.WECHAT_CLI_BINARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从平台包解析
|
||||||
|
const pkg = PLATFORM_PACKAGES[platformKey];
|
||||||
|
if (!pkg) {
|
||||||
|
console.error(`wechat-cli: unsupported platform ${platformKey}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return require.resolve(`${pkg}/bin/wechat-cli${ext}`);
|
||||||
|
} catch {
|
||||||
|
// 3. fallback: 直接找 node_modules 下的路径
|
||||||
|
const modPath = path.join(
|
||||||
|
path.dirname(require.resolve(`${pkg}/package.json`)),
|
||||||
|
`bin/wechat-cli${ext}`
|
||||||
|
);
|
||||||
|
if (fs.existsSync(modPath)) return modPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`wechat-cli: binary not found for ${platformKey}`);
|
||||||
|
console.error('Try: npm install --force @canghe/wechat-cli');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
execFileSync(getBinaryPath(), process.argv.slice(2), {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e && e.status != null) process.exit(e.status);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
36
npm/wechat-cli/install.js
Normal file
36
npm/wechat-cli/install.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const PLATFORM_PACKAGES = {
|
||||||
|
'darwin-arm64': '@canghe_ai/wechat-cli-darwin-arm64',
|
||||||
|
'darwin-x64': '@canghe_ai/wechat-cli-darwin-x64',
|
||||||
|
'linux-x64': '@canghe_ai/wechat-cli-linux-x64',
|
||||||
|
'linux-arm64': '@canghe_ai/wechat-cli-linux-arm64',
|
||||||
|
'win32-x64': '@canghe_ai/wechat-cli-win32-x64',
|
||||||
|
};
|
||||||
|
|
||||||
|
const platformKey = `${process.platform}-${process.arch}`;
|
||||||
|
const pkg = PLATFORM_PACKAGES[platformKey];
|
||||||
|
|
||||||
|
if (!pkg) {
|
||||||
|
console.log(`wechat-cli: no binary for ${platformKey}, skipping`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find and chmod the binary
|
||||||
|
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const binaryPath = require.resolve(`${pkg}/bin/wechat-cli${ext}`);
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
fs.chmodSync(binaryPath, 0o755);
|
||||||
|
console.log(`wechat-cli: set executable permission for ${platformKey}`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Platform package was not installed (npm --no-optional or unsupported)
|
||||||
|
console.log(`wechat-cli: platform package ${pkg} not installed`);
|
||||||
|
console.log('To fix: npm install --force @canghe_ai/wechat-cli');
|
||||||
|
}
|
||||||
30
npm/wechat-cli/package.json
Normal file
30
npm/wechat-cli/package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@canghe_ai/wechat-cli",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "WeChat data query CLI — chat history, contacts, sessions, favorites, and more. Designed for LLM integration.",
|
||||||
|
"bin": {
|
||||||
|
"wechat-cli": "bin/wechat-cli.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "node install.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/",
|
||||||
|
"install.js"
|
||||||
|
],
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@canghe_ai/wechat-cli-darwin-arm64": "0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"keywords": ["wechat", "cli", "wechat-cli", "llm", "ai"],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/freestylefly/wechat-cli"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,9 +18,18 @@ def _find_binary():
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError(f"不支持的 macOS 架构: {machine}")
|
raise RuntimeError(f"不支持的 macOS 架构: {machine}")
|
||||||
|
|
||||||
# 优先查找 bin/ 目录(pip 安装后位于包内)
|
# PyInstaller 运行时:从临时解压目录查找
|
||||||
pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
if getattr(sys, 'frozen', False):
|
||||||
bin_path = os.path.join(pkg_dir, "bin", name)
|
base = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
bin_path = os.path.join(base, "wechat_cli", "bin", name)
|
||||||
|
if os.path.isfile(bin_path):
|
||||||
|
return bin_path
|
||||||
|
|
||||||
|
# fallback: 直接在 bin/ 下
|
||||||
|
bin_path = os.path.join(base, "bin", name)
|
||||||
if os.path.isfile(bin_path):
|
if os.path.isfile(bin_path):
|
||||||
return bin_path
|
return bin_path
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user