cli 教程

本教程介绍如何在 MoonBit 之外用 cli 可执行程序计算分数快照:从 shell、像索引构建器那样从 Python,以及从 Node.js。你需要本仓库的检出、MoonBit 工具链以及 Node.js 20.16、22.3 或更高版本。

快速开始

在仓库根目录构建可执行程序:

moon build src/cli --target js

把一个包的信号写入文件:

cat > payload.json <<'JSON'
{"dependents": 20, "recent_dependents": 4, "downloads": 300,
 "days_since_release": 40, "historical_dependents": 10,
 "historical_recent_dependents": 2, "historical_downloads": 0,
 "historical_days_since_release": 10}
JSON
node _build/js/debug/build/cli/cli.js score-snapshot --input payload.json

命令在一行中输出快照:

{"score":301.7852882193072,"score_30d_ago":135.2764584196223,"score_growth_30d":166.5088297996849,"score_growth_ratio_30d":1.2308780976744749,"rank_label":"S","momentum_label":"Rising","activity_multiplier":1.06}

这些数字与 MoonBit 中 @score.compute_score_snapshot(20, 4, 300, 40, 10, 2, 0, 10) 的结果相同。

日常任务

省略未知的信息

每个键都是可选的,默认为 0。一个五天前发布、有三个依赖方、其余信息未知的包:

echo '{"dependents": 3, "days_since_release": 5}' > new.json
node _build/js/debug/build/cli/cli.js score-snapshot --input new.json
{"score":59.00068800926255,"score_30d_ago":0,"score_growth_30d":59.00068800926255,"score_growth_ratio_30d":1,"rank_label":"C","momentum_label":"Stable","activity_multiplier":1.12}

历史信号全为 0,所以 score_30d_ago 为 0,增长率报告为 1。

从 Python 调用

索引构建器为每个包运行一次该命令。其辅助函数的最小版本如下:

import json, os, subprocess, tempfile

CLI = "_build/js/debug/build/cli/cli.js"

def score_snapshot(signals: dict) -> dict:
    with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
        json.dump(signals, f)
        path = f.name
    try:
        done = subprocess.run(
            ["node", CLI, "score-snapshot", "--input", path],
            check=True, capture_output=True, text=True,
        )
    finally:
        os.unlink(path)
    return json.loads(done.stdout)

print(score_snapshot({"dependents": 20, "recent_dependents": 4,
                      "downloads": 300, "days_since_release": 40})["rank_label"])
S

check=True 会把每个非零退出状态变成异常,这同时涵盖了 JSON 错误消息和输入文件缺失的情况。

从 Node.js 调用

import { execFileSync } from "node:child_process";
import { writeFileSync } from "node:fs";

writeFileSync("p.json", JSON.stringify({ dependents: 936, days_since_release: 120 }));
const out = execFileSync("node", [
  "_build/js/debug/build/cli/cli.js", "score-snapshot", "--input", "p.json",
]);
console.log(JSON.parse(out).rank_label);
S

当乘数为 11 时,仅凭 936 个依赖方就足以达到等级 S;score 设计推导了原因。

处理错误

错误会输出一个带 error 键的 JSON 对象,并以状态 1 退出:

echo 'not json' > bad.json
node _build/js/debug/build/cli/cli.js score-snapshot --input bad.json; echo "status $?"
{"error":"Failed to parse JSON input"}
status 1

解析输出之前先检查退出状态。文件缺失时同样以 1 退出,但会向标准错误输出 Node.js 堆栈跟踪,而不是 JSON。

进阶

  • 批量处理。 每次调用都会启动一个 Node.js 进程,其开销远大于计算分数本身。在一个 MoonBit 程序中处理大量包时,请像 score 教程那样直接调用 @score.compute_score_snapshot。
  • 信号从哪里来。 架构指南解释了索引构建器在调用此命令之前如何统计依赖方、近期依赖方和历史值。
  • 严格的输入。 该命令接受不规范的输入:字符串和 null 变为 0,小数被截断。如果一个悄无声息的 0 会掩盖 bug,请在调用方验证负载。

常见陷阱

  • 数字写成字符串。 {"dependents": "20"} 会按零个依赖方评分。请写 20,而不是 "20"。
  • 参数顺序错误。 score-snapshot 必须是脚本之后的第一个参数;--input file.json score-snapshot 会输出用法错误。
  • 构建过期。 索引构建器会复用已有的 cli.js。修改 src/score 后请重新构建,否则数据库会按旧规则评分。
  • 过大的计数。 2147483647 或任何更大的数(转换会饱和到最大的 Int)会使分数变为 NaN。JSON 没有 NaN,因此输出中本应是数字的位置会出现字符串 "NaN":{"score":"NaN",...}。

下一步

  • cli API:精确的输入、输出和错误约定。
  • cli 设计:为何这座桥是基于文件的命令。
  • score API:每个输出字段的含义。