@mp2rss/cli 0.0.0-bootstrap.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @mp2rss/cli
2
+
3
+ [mp2rss](https://mp2rss.bugcode.dev) 的命令行客户端。
4
+
5
+ 本 npm 包是 [mp2rss-cli](https://github.com/areyoubugcoder/mp2rss-cli) Go 二进制的包装,`postinstall` 按平台下载对应可执行文件。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ # 全局
11
+ npm install -g @mp2rss/cli
12
+
13
+ # 或 pnpm
14
+ pnpm add -g @mp2rss/cli
15
+ ```
16
+
17
+ 支持的平台:
18
+
19
+ | OS | amd64 | arm64 |
20
+ |---------|:-----:|:-----:|
21
+ | macOS | ✅ | ✅ |
22
+ | Linux | ✅ | ✅ |
23
+ | Windows | ✅ | - |
24
+
25
+ ## 快速上手
26
+
27
+ ```bash
28
+ mp2rss auth login
29
+ mp2rss mp subscribe https://mp.weixin.qq.com/s/xxxxxxxxxx
30
+ mp2rss mp list
31
+ mp2rss mp list -o json | jq '.items[].mpName'
32
+ ```
33
+
34
+ ## 环境变量
35
+
36
+ | 变量 | 作用 |
37
+ | ---- | ---- |
38
+ | `MP2RSS_VERSION` | 安装时强制使用指定版本(默认读取 package.json `version`) |
39
+ | `MP2RSS_NO_VERIFY` | 安装时跳过 SHA-256 校验(不推荐) |
40
+ | `MP2RSS_FEED_KEY` | 运行时覆盖 Feed Key |
41
+ | `MP2RSS_API_URL` | 运行时覆盖 API 地址 |
42
+
43
+ ## 卸载
44
+
45
+ ```bash
46
+ npm uninstall -g @mp2rss/cli
47
+ ```
48
+
49
+ CLI 自身的本地配置 `~/.mp2rss/config.json` 需要手动删除(避免误清账户绑定)。
50
+
51
+ ## License
52
+
53
+ MIT
package/bin/mp2rss.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // mp2rss launcher.
3
+ //
4
+ // The real binary is downloaded by postinstall.cjs into ./bin/<exe>. This
5
+ // shim spawns it with the user's args, forwarding stdio and the exit code.
6
+ //
7
+ // Why a JS shim instead of a native bin? npm cross-platform installs a JS
8
+ // file as the entry, and Windows .cmd shims rely on a node executable — so
9
+ // `bin/mp2rss.js` is the lowest-friction form on all three OSes.
10
+ 'use strict';
11
+
12
+ const { spawnSync } = require('node:child_process');
13
+ const path = require('node:path');
14
+ const fs = require('node:fs');
15
+
16
+ const binaryName = process.platform === 'win32' ? 'mp2rss.exe' : 'mp2rss';
17
+ const binaryPath = path.join(__dirname, binaryName);
18
+
19
+ if (!fs.existsSync(binaryPath)) {
20
+ console.error('✗ mp2rss 二进制未找到:' + binaryPath);
21
+ console.error(' 请重新安装:npm install -g @mp2rss/cli');
22
+ console.error(' 或参考:https://github.com/areyoubugcoder/mp2rss-cli#install');
23
+ process.exit(1);
24
+ }
25
+
26
+ const res = spawnSync(binaryPath, process.argv.slice(2), {
27
+ stdio: 'inherit',
28
+ windowsHide: false,
29
+ });
30
+
31
+ if (res.error) {
32
+ console.error('✗ 启动 mp2rss 失败:' + res.error.message);
33
+ process.exit(1);
34
+ }
35
+ process.exit(res.status === null ? 1 : res.status);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@mp2rss/cli",
3
+ "version": "0.0.0-bootstrap.0",
4
+ "description": "mp2rss CLI — 微信公众号 RSS 订阅与文章管理命令行工具",
5
+ "keywords": [
6
+ "mp2rss",
7
+ "cli",
8
+ "rss",
9
+ "wechat",
10
+ "mp",
11
+ "subscription"
12
+ ],
13
+ "homepage": "https://github.com/areyoubugcoder/mp2rss-cli#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/areyoubugcoder/mp2rss-cli/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/areyoubugcoder/mp2rss-cli.git"
20
+ },
21
+ "license": "MIT",
22
+ "author": "areyoubugcoder",
23
+ "bin": {
24
+ "mp2rss": "bin/mp2rss.js"
25
+ },
26
+ "scripts": {
27
+ "postinstall": "node postinstall.cjs"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "postinstall.cjs",
32
+ "README.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "os": [
38
+ "darwin",
39
+ "linux",
40
+ "win32"
41
+ ],
42
+ "cpu": [
43
+ "x64",
44
+ "arm64"
45
+ ]
46
+ }
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ // postinstall: download the mp2rss binary matching this host's OS/arch from
3
+ // the GitHub Release that corresponds to package.json:version, verify its
4
+ // SHA-256 against checksums.txt, and place it at ./bin/<exe>.
5
+ 'use strict';
6
+
7
+ const fs = require('node:fs');
8
+ const os = require('node:os');
9
+ const path = require('node:path');
10
+ const crypto = require('node:crypto');
11
+ const https = require('node:https');
12
+ const { spawnSync } = require('node:child_process');
13
+
14
+ const pkg = require('./package.json');
15
+ const REPO = 'areyoubugcoder/mp2rss-cli';
16
+ const VERSION = process.env.MP2RSS_VERSION || pkg.version;
17
+ const NO_VERIFY = !!process.env.MP2RSS_NO_VERIFY;
18
+
19
+ // Skip during local development checkouts (no real release published yet).
20
+ if (VERSION === '0.0.0') {
21
+ console.log('[mp2rss] skipping binary download for placeholder version 0.0.0');
22
+ process.exit(0);
23
+ }
24
+
25
+ const platformMap = { darwin: 'darwin', linux: 'linux', win32: 'windows' };
26
+ const archMap = { x64: 'amd64', arm64: 'arm64' };
27
+ const goos = platformMap[process.platform];
28
+ const goarch = archMap[process.arch];
29
+ if (!goos || !goarch) {
30
+ console.error(`[mp2rss] 暂不支持的平台:${process.platform}/${process.arch}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ const ext = goos === 'windows' ? '.zip' : '.tar.gz';
35
+ const binaryName = goos === 'windows' ? 'mp2rss.exe' : 'mp2rss';
36
+ const asset = `mp2rss-cli_${VERSION}_${goos}_${goarch}${ext}`;
37
+ const base = `https://github.com/${REPO}/releases/download/v${VERSION}`;
38
+ const archiveURL = `${base}/${asset}`;
39
+ const checksumsURL = `${base}/checksums.txt`;
40
+
41
+ const binDir = path.join(__dirname, 'bin');
42
+ const binaryPath = path.join(binDir, binaryName);
43
+
44
+ (async function main() {
45
+ // Already installed at the right version? skip.
46
+ if (fs.existsSync(binaryPath)) {
47
+ const out = spawnSync(binaryPath, ['--version'], { encoding: 'utf8' });
48
+ if (out.status === 0 && String(out.stdout).includes(VERSION)) {
49
+ console.log(`[mp2rss] v${VERSION} 已存在,跳过下载`);
50
+ return;
51
+ }
52
+ }
53
+
54
+ fs.mkdirSync(binDir, { recursive: true });
55
+ const tmpArchive = path.join(os.tmpdir(), `mp2rss-${process.pid}-${asset}`);
56
+
57
+ console.log(`[mp2rss] 下载 ${asset}`);
58
+ try {
59
+ await downloadFile(archiveURL, tmpArchive);
60
+
61
+ if (!NO_VERIFY) {
62
+ console.log('[mp2rss] 校验 SHA-256');
63
+ const sumsPath = path.join(os.tmpdir(), `mp2rss-${process.pid}-checksums.txt`);
64
+ try {
65
+ await downloadFile(checksumsURL, sumsPath);
66
+ const expected = readChecksum(sumsPath, asset);
67
+ if (!expected) throw new Error(`checksums.txt 中找不到 ${asset}`);
68
+ const actual = sha256File(tmpArchive);
69
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
70
+ throw new Error(`checksum 不匹配:expected ${expected}, got ${actual}`);
71
+ }
72
+ } finally {
73
+ safeUnlink(sumsPath);
74
+ }
75
+ } else {
76
+ console.log('[mp2rss] MP2RSS_NO_VERIFY=1 跳过校验');
77
+ }
78
+
79
+ console.log('[mp2rss] 解压');
80
+ extract(tmpArchive, binDir, binaryName);
81
+ fs.chmodSync(binaryPath, 0o755);
82
+ console.log(`[mp2rss] ✓ 已安装:${binaryPath}`);
83
+ } catch (err) {
84
+ console.error('[mp2rss] ✗ 安装失败:' + err.message);
85
+ console.error(' 请重试 npm install -g @mp2rss/cli,或参考');
86
+ console.error(' https://github.com/' + REPO + '#install');
87
+ process.exit(1);
88
+ } finally {
89
+ safeUnlink(tmpArchive);
90
+ }
91
+ })();
92
+
93
+ // ---------- helpers ----------
94
+
95
+ function downloadFile(url, dst, redirectsLeft = 5) {
96
+ return new Promise((resolve, reject) => {
97
+ https
98
+ .get(url, { headers: { 'user-agent': 'mp2rss-cli-npm' } }, (res) => {
99
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
100
+ res.resume();
101
+ if (redirectsLeft <= 0) return reject(new Error('too many redirects'));
102
+ return downloadFile(res.headers.location, dst, redirectsLeft - 1).then(resolve, reject);
103
+ }
104
+ if (res.statusCode !== 200) {
105
+ res.resume();
106
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
107
+ }
108
+ const out = fs.createWriteStream(dst);
109
+ res.pipe(out);
110
+ out.on('finish', () => out.close(resolve));
111
+ out.on('error', reject);
112
+ })
113
+ .on('error', reject);
114
+ });
115
+ }
116
+
117
+ function readChecksum(path, asset) {
118
+ const data = fs.readFileSync(path, 'utf8');
119
+ for (const raw of data.split('\n')) {
120
+ const line = raw.trim();
121
+ if (!line || line.startsWith('#')) continue;
122
+ const m = line.match(/^(\S+)\s+\*?(\S+)$/);
123
+ if (!m) continue;
124
+ const name = m[2];
125
+ if (name === asset || name.endsWith('/' + asset)) return m[1];
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function sha256File(p) {
131
+ const buf = fs.readFileSync(p);
132
+ return crypto.createHash('sha256').update(buf).digest('hex');
133
+ }
134
+
135
+ function extract(archive, dir, binaryName) {
136
+ if (archive.endsWith('.zip')) {
137
+ // Windows: use built-in tar (tar.exe supports zip since Win10) or PowerShell Expand-Archive.
138
+ const r = spawnSync('tar', ['-xf', archive, '-C', dir, binaryName], { stdio: 'inherit' });
139
+ if (r.status !== 0) {
140
+ const ps = spawnSync(
141
+ 'powershell',
142
+ ['-NoProfile', '-Command', `Expand-Archive -Path '${archive}' -DestinationPath '${dir}' -Force`],
143
+ { stdio: 'inherit' },
144
+ );
145
+ if (ps.status !== 0) throw new Error('解压 ZIP 失败');
146
+ }
147
+ } else {
148
+ const r = spawnSync('tar', ['-xzf', archive, '-C', dir, binaryName], { stdio: 'inherit' });
149
+ if (r.status !== 0) throw new Error('解压 tar.gz 失败');
150
+ }
151
+ if (!fs.existsSync(path.join(dir, binaryName))) {
152
+ throw new Error(`解压后未找到 ${binaryName}`);
153
+ }
154
+ }
155
+
156
+ function safeUnlink(p) {
157
+ try {
158
+ fs.unlinkSync(p);
159
+ } catch (_) {
160
+ /* ignore */
161
+ }
162
+ }