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:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -7,6 +7,12 @@ __pycache__/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# PyInstaller
|
||||
*.spec
|
||||
|
||||
# npm platform binaries (published to npm, not git)
|
||||
npm/platforms/*/bin/
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
@@ -31,6 +37,7 @@ Thumbs.db
|
||||
# Sensitive data — NEVER commit
|
||||
*.json
|
||||
!pyproject.toml
|
||||
!npm/**/package.json
|
||||
|
||||
# Temp files
|
||||
*.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
|
||||
|
||||
**Via pip (requires Python >= 3.10):**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/canghe/wechat-cli.git
|
||||
git clone https://github.com/freestylefly/wechat-cli.git
|
||||
cd wechat-cli
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
12
README_CN.md
12
README_CN.md
@@ -19,14 +19,24 @@
|
||||
|
||||
### 安装
|
||||
|
||||
**通过 pip(需要 Python >= 3.10):**
|
||||
|
||||
```bash
|
||||
pip install wechat-cli
|
||||
```
|
||||
|
||||
**通过 npm(独立二进制,无需 Python):**
|
||||
|
||||
```bash
|
||||
npm install -g @canghe_ai/wechat-cli
|
||||
```
|
||||
|
||||
> 目前仅提供 **macOS arm64** 二进制。其他平台(macOS Intel、Linux、Windows)可使用 pip 安装。欢迎提交其他平台的二进制 PR。
|
||||
|
||||
或从源码安装:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/canghe/wechat-cli.git
|
||||
git clone https://github.com/freestylefly/wechat-cli.git
|
||||
cd wechat-cli
|
||||
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:
|
||||
raise RuntimeError(f"不支持的 macOS 架构: {machine}")
|
||||
|
||||
# 优先查找 bin/ 目录(pip 安装后位于包内)
|
||||
pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
bin_path = os.path.join(pkg_dir, "bin", name)
|
||||
# PyInstaller 运行时:从临时解压目录查找
|
||||
if getattr(sys, 'frozen', False):
|
||||
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):
|
||||
return bin_path
|
||||
|
||||
|
||||
Reference in New Issue
Block a user