@ybbapp/push-cli 0.0.1-alpha.0 → 0.0.2-alpha.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 CHANGED
@@ -1,77 +1,38 @@
1
1
  # push-cli
2
2
 
3
- `push-cli` 是供 Agent 低成本接入 Lark 群聊机器人的单向消息推送参考实现。它只通过自定义机器人 webhook 发送消息,不接收事件、不提供回调、消息读取或其他 Lark API 能力。
3
+ 向 Lark 群聊机器人单向发送 Markdown 消息的 CLI。只使用自定义机器人 webhook,不读取消息或提供其他 Lark API。
4
4
 
5
- 仓库根目录是 Go CLI 参考实现;`skills/lark-push/` 是可单独安装的 Agent Skill,包含操作说明和示例。CLI 仅调用群聊自定义机器人 webhook 推送消息,不提供其他 Lark API。
5
+ ## 安装
6
6
 
7
- 可用 GitHub CLI 安装:
7
+ 需要 Node.js 18 或更新版本。
8
8
 
9
9
  ```sh
10
- gh skill install ybbapp/push-cli lark-push
11
- ```
12
-
13
- 也可以把 `skills/lark-push/` 复制到 Agent 支持的 skills 目录。Skill 提供操作说明和示例;`push-cli` 可执行文件及本地 webhook 配置需要另行准备。构建命令:
14
-
15
- ```sh
16
- go build -o push-cli ./cmd
17
- ```
18
-
19
- ## 分发和安装
20
-
21
- 发布 tag 会构建并分发 npm 包、GitHub Release 二进制和 GHCR Docker 镜像。npm 包是推荐入口;它第一次运行时会按当前 OS/CPU 下载对应 Release 二进制并校验 SHA-256。
22
-
23
- alpha 版本:
24
-
25
- ```sh
26
- npx --yes @ybbapp/push-cli@alpha --version
10
+ npm install --global @ybbapp/push-cli@alpha
27
11
  ```
28
12
 
29
- 直接下载 Release 二进制:
13
+ 也可以一次性运行:
30
14
 
31
15
  ```sh
32
- gh release download alpha-0.0.1 \
33
- --repo ybbapp/push-cli \
34
- --pattern 'push-cli-alpha-0.0.1-*-*.tar.gz'
16
+ npx --yes @ybbapp/push-cli@alpha lark send --title '每日市场报告' --markdown '报告正文'
35
17
  ```
36
18
 
37
- Docker 镜像支持 Linux 多架构:
38
-
39
- ```sh
40
- docker run --rm --env LARK_WEBHOOK_URL \
41
- ghcr.io/ybbapp/push-cli:alpha \
42
- lark send --title '每日市场报告' --markdown '报告正文'
43
- ```
44
-
45
- 首次引导:npm 要求包先存在,才能配置 Trusted Publisher。仓库公开后,先推送 `alpha-0.0.1` 生成公开 Release,再从该版本源码目录手动运行一次 `npm publish --access public --tag alpha`(使用 npm 交互式登录;该版本是 `0.0.1-alpha.0`)。包存在后,在 npm 包的 **Settings → Trusted publishing** 绑定 GitHub Actions:owner `ybbapp`、repository `push-cli`、workflow filename `release.yml`,并允许 `npm publish`。之后每次推送 `alpha-*` 或 `v*` tag,workflow 使用 OIDC 发布,不需要保存 npm token。包尚未创建时,workflow 会跳过 npm job,但仍会发布 GitHub Release 和 GHCR 镜像。
46
-
47
- 首次向 GHCR 发布后,需要在 GitHub Packages 的 `push-cli` 镜像设置中将可见性改为 Public,公开后 Docker 用户才能匿名拉取。
48
-
49
19
  ## 配置和使用
50
20
 
51
- 推荐通过环境变量注入 webhook。也可以在当前工作目录放置 `.env`,或使用 `--env` 指定文件。`.env` 不应提交到仓库。
21
+ 设置群聊机器人的 webhook:
52
22
 
53
23
  ```sh
54
- export LARK_WEBHOOK_URL='https://open.larksuite.com/open-apis/bot/v2/hook/REPLACE_WITH_YOUR_TOKEN'
24
+ export LARK_WEBHOOK_URL='https://open.larksuite.com/open-apis/bot/v2/hook/你的机器人密钥'
55
25
  ```
56
26
 
57
27
  发送 Markdown:
58
28
 
59
29
  ```sh
30
+ push-cli lark send --title '每日市场报告' --markdown '报告正文'
60
31
  push-cli lark send --title '每日市场报告' --file report.md
61
- cat report.md | push-cli lark send --title '每日市场报告'
62
32
  ```
63
33
 
64
- 附带图表发送示例(在仓库根目录运行):
34
+ 也可以通过 `--chart-file` 附加原生图表;仓库内的 [Lark skill](skills/lark-push/SKILL.md) 提供 Agent 调用说明和示例。安装 skill:
65
35
 
66
36
  ```sh
67
- push-cli lark send \
68
- --title 'NVDA 行情' \
69
- --markdown 'NVDA 收盘价走势' \
70
- --chart-file skills/lark-push/examples/nvda-line-chart.json
37
+ gh skill install ybbapp/push-cli lark-push
71
38
  ```
72
-
73
- ## 版本和平台
74
-
75
- 首个 Git tag `alpha-0.0.1` 对应 npm SemVer `0.0.1-alpha.0`,发布到 npm 的 `alpha` dist-tag;GitHub Release 与 Docker tag 保留 `alpha-0.0.1`,并更新 Docker `alpha` tag。
76
-
77
- 原生二进制支持 macOS amd64/arm64、Linux amd64/arm64/riscv64、Windows amd64/arm64。Docker 镜像支持 Linux amd64/arm64/riscv64。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ybbapp/push-cli",
3
- "version": "0.0.1-alpha.0",
4
- "description": "Small Lark group-bot push CLI for agents",
3
+ "version": "0.0.2-alpha.0",
4
+ "description": "One-way messages to Lark group bots.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
@@ -37,20 +37,28 @@ function assetName(tag, platform, arch) {
37
37
  return `push-cli-${tag}-${platform}-${arch}.${extension}`;
38
38
  }
39
39
 
40
- async function get(url, redirects = 0) {
41
- const parsed = new URL(url);
42
- if (!REDIRECT_HOSTS.has(parsed.hostname)) throw new Error(`unexpected download host: ${parsed.hostname}`);
43
- const response = await fetch(url, { redirect: "manual" });
44
- if ([301, 302, 303, 307, 308].includes(response.status)) {
45
- if (redirects >= 3) throw new Error("too many redirects while downloading the release");
46
- const location = response.headers.get("location");
47
- if (!location) throw new Error("release download redirect has no location");
48
- return get(new URL(location, url).toString(), redirects + 1);
40
+ function download(url, destination) {
41
+ const initialHost = new URL(url).hostname;
42
+ if (!REDIRECT_HOSTS.has(initialHost)) throw new Error(`unexpected download host: ${initialHost}`);
43
+ const result = spawnSync("curl", [
44
+ "--fail", "--location", "--silent", "--show-error",
45
+ "--connect-timeout", "10", "--max-time", "120", "--max-redirs", "3",
46
+ "--proto", "=https", "--proto-redir", "=https",
47
+ "--output", destination, "--write-out", "%{url_effective}", url,
48
+ ], { encoding: "utf8", maxBuffer: 1024 * 1024 });
49
+ if (result.error) {
50
+ if (process.platform !== "win32") throw new Error("curl is required to download the push-cli binary");
51
+ const command = "$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -UseBasicParsing -Uri $env:PUSH_CLI_URL -OutFile $env:PUSH_CLI_DEST";
52
+ const fallback = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], {
53
+ stdio: "ignore",
54
+ env: { ...process.env, PUSH_CLI_URL: url, PUSH_CLI_DEST: destination },
55
+ });
56
+ if (fallback.error || fallback.status !== 0) throw new Error("could not download the push-cli release asset");
57
+ return;
49
58
  }
50
- if (!response.ok) throw new Error(`download failed (${response.status}): ${url}`);
51
- const finalHost = new URL(response.url).hostname;
59
+ if (result.status !== 0) throw new Error(`release download failed (curl exit ${result.status})`);
60
+ const finalHost = new URL(result.stdout.trim()).hostname;
52
61
  if (!REDIRECT_HOSTS.has(finalHost)) throw new Error(`unexpected download host: ${finalHost}`);
53
- return Buffer.from(await response.arrayBuffer());
54
62
  }
55
63
 
56
64
  function extract(archive, directory, platform) {
@@ -69,29 +77,27 @@ function extract(archive, directory, platform) {
69
77
  if (result.status !== 0) throw new Error("could not extract the release archive; install tar and retry");
70
78
  }
71
79
 
72
- async function ensureBinary() {
80
+ function ensureBinary() {
73
81
  if (fs.existsSync(BINARY_PATH)) return BINARY_PATH;
74
82
 
75
83
  const { platform, arch } = target();
76
84
  const tag = releaseTag();
77
85
  const asset = assetName(tag, platform, arch);
78
86
  const base = `https://github.com/${OWNER}/${REPOSITORY}/releases/download/${encodeURIComponent(tag)}`;
79
- const [archiveData, checksums] = await Promise.all([
80
- get(`${base}/${asset}`),
81
- get(`${base}/checksums.txt`),
82
- ]);
83
- const checksumLine = checksums.toString("utf8").split(/\r?\n/).find((line) => line.trim().endsWith(` ${asset}`));
84
- if (!checksumLine) throw new Error(`release checksums do not include ${asset}`);
85
- const expected = checksumLine.trim().split(/\s+/)[0].toLowerCase();
86
- const actual = crypto.createHash("sha256").update(archiveData).digest("hex");
87
- if (!/^[a-f0-9]{64}$/.test(expected) || actual !== expected) {
88
- throw new Error(`SHA-256 verification failed for ${asset}`);
89
- }
90
-
91
87
  const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "push-cli-"));
92
88
  try {
93
89
  const archivePath = path.join(temporary, asset);
94
- fs.writeFileSync(archivePath, archiveData, { mode: 0o600 });
90
+ const checksumsPath = path.join(temporary, "checksums.txt");
91
+ download(`${base}/${asset}`, archivePath);
92
+ download(`${base}/checksums.txt`, checksumsPath);
93
+ const checksums = fs.readFileSync(checksumsPath, "utf8");
94
+ const checksumLine = checksums.split(/\r?\n/).find((line) => line.trim().endsWith(` ${asset}`));
95
+ if (!checksumLine) throw new Error(`release checksums do not include ${asset}`);
96
+ const expected = checksumLine.trim().split(/\s+/)[0].toLowerCase();
97
+ const actual = crypto.createHash("sha256").update(fs.readFileSync(archivePath)).digest("hex");
98
+ if (!/^[a-f0-9]{64}$/.test(expected) || actual !== expected) {
99
+ throw new Error(`SHA-256 verification failed for ${asset}`);
100
+ }
95
101
  extract(archivePath, temporary, platform);
96
102
  const extracted = path.join(temporary, BINARY_NAME);
97
103
  if (!fs.existsSync(extracted)) throw new Error(`release archive does not contain ${BINARY_NAME}`);