feat: open-source lark-cli — the official CLI for Lark/Feishu

Change-Id: I113d9cdb5403cec347efe4595415e34a18b7decf
This commit is contained in:
梁硕
2026-03-28 10:36:25 +08:00
commit 83dfb068ad
643 changed files with 101763 additions and 0 deletions

82
scripts/fetch_meta.py Normal file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
# SPDX-License-Identifier: MIT
"""Fetch meta_data.json from remote API for build-time embedding.
Usage:
python3 scripts/fetch_meta.py # fetch from feishu (default)
python3 scripts/fetch_meta.py --brand lark # fetch from larksuite
"""
import argparse
import json
import os
import subprocess
import sys
import urllib.request
import urllib.error
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.join(SCRIPT_DIR, "..")
OUT_PATH = os.path.join(ROOT, "internal", "registry", "meta_data.json")
API_HOSTS = {
"feishu": "https://open.feishu.cn/api/tools/open/api_definition",
"lark": "https://open.larksuite.com/api/tools/open/api_definition",
}
TIMEOUT = 10 # seconds
def get_version():
"""Get version from git tags, matching Makefile logic."""
try:
return subprocess.check_output(
["git", "describe", "--tags", "--always", "--dirty"],
stderr=subprocess.DEVNULL,
text=True,
cwd=ROOT,
).strip()
except Exception:
return "dev"
def fetch_remote(brand):
"""Fetch MergedRegistry from remote API."""
base = API_HOSTS.get(brand, API_HOSTS["feishu"])
version = get_version()
url = f"{base}?protocol=meta&client_version={urllib.request.quote(version)}"
print(f"fetch-meta: GET {url}", file=sys.stderr)
req = urllib.request.Request(url)
resp = urllib.request.urlopen(req, timeout=TIMEOUT)
body = resp.read()
envelope = json.loads(body)
if envelope.get("msg") != "succeeded":
raise RuntimeError(f"unexpected response msg: {envelope.get('msg')!r}")
data = envelope.get("data", {})
if not data.get("services"):
raise RuntimeError("remote returned empty services")
return data
def main():
parser = argparse.ArgumentParser(description="Fetch meta_data.json for build-time embedding")
parser.add_argument("--brand", default="feishu", choices=["feishu", "lark"],
help="API brand (default: feishu)")
args = parser.parse_args()
data = fetch_remote(args.brand)
count = len(data.get("services", []))
print(f"fetch-meta: OK, {count} services from remote API", file=sys.stderr)
with open(OUT_PATH, "w") as fp:
json.dump(data, fp, ensure_ascii=False, indent=2)
fp.write("\n")
if __name__ == "__main__":
main()

100
scripts/install.js Normal file
View File

@@ -0,0 +1,100 @@
const fs = require("fs");
const path = require("path");
const https = require("https");
const { execSync } = require("child_process");
const os = require("os");
const VERSION = require("../package.json").version;
const REPO = "larksuite/cli";
const NAME = "lark-cli";
const PLATFORM_MAP = {
darwin: "darwin",
linux: "linux",
win32: "windows",
};
const ARCH_MAP = {
x64: "amd64",
arm64: "arm64",
};
const platform = PLATFORM_MAP[process.platform];
const arch = ARCH_MAP[process.arch];
if (!platform || !arch) {
console.error(
`Unsupported platform: ${process.platform}-${process.arch}`
);
process.exit(1);
}
const isWindows = process.platform === "win32";
const ext = isWindows ? ".zip" : ".tar.gz";
const archiveName = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
const binDir = path.join(__dirname, "..", "bin");
const dest = path.join(binDir, NAME + (isWindows ? ".exe" : ""));
fs.mkdirSync(binDir, { recursive: true });
function download(url, destPath) {
return new Promise((resolve, reject) => {
const client = url.startsWith("https") ? https : require("http");
client
.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
return download(res.headers.location, destPath).then(
resolve,
reject
);
}
if (res.statusCode !== 200) {
return reject(
new Error(`Download failed with status ${res.statusCode}: ${url}`)
);
}
const file = fs.createWriteStream(destPath);
res.pipe(file);
file.on("finish", () => {
file.close();
resolve();
});
})
.on("error", reject);
});
}
async function install() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lark-cli-"));
const archivePath = path.join(tmpDir, archiveName);
try {
await download(url, archivePath);
if (isWindows) {
execSync(
`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}'"`,
{ stdio: "ignore" }
);
} else {
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, {
stdio: "ignore",
});
}
const binaryName = NAME + (isWindows ? ".exe" : "");
const extractedBinary = path.join(tmpDir, binaryName);
fs.copyFileSync(extractedBinary, dest);
fs.chmodSync(dest, 0o755);
console.log(`${NAME} v${VERSION} installed successfully`);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
install().catch((err) => {
console.error(`Failed to install ${NAME}:`, err.message);
process.exit(1);
});

12
scripts/run.js Normal file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env node
const { execFileSync } = require("child_process");
const path = require("path");
const ext = process.platform === "win32" ? ".exe" : "";
const bin = path.join(__dirname, "..", "bin", "lark-cli" + ext);
try {
execFileSync(bin, process.argv.slice(2), { stdio: "inherit" });
} catch (e) {
process.exit(e.status || 1);
}

51
scripts/tag-release.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Read version from package.json
VERSION=$(node -p "require('${REPO_ROOT}/package.json').version")
if [ -z "$VERSION" ]; then
echo "Error: could not read version from package.json" >&2
exit 1
fi
TAG="v${VERSION}"
echo "Version: ${VERSION}"
echo "Tag: ${TAG}"
# Check if tag already exists locally
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag ${TAG} already exists locally, skipping."
exit 0
fi
# Check if tag already exists on remote
if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
echo "Tag ${TAG} already exists on remote, skipping."
exit 0
fi
# Ensure package.json changes are committed before tagging
if git diff --name-only | grep -q 'package.json' || git diff --cached --name-only | grep -q 'package.json'; then
echo "Error: package.json has uncommitted changes. Please commit before tagging." >&2
exit 1
fi
# Ensure current branch is pushed to remote before tagging
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
LOCAL_SHA=$(git rev-parse HEAD)
REMOTE_SHA=$(git rev-parse "origin/${CURRENT_BRANCH}" 2>/dev/null || echo "")
if [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then
echo "Error: local branch '${CURRENT_BRANCH}' is not in sync with remote. Please push your commits first." >&2
exit 1
fi
# Create and push tag
git tag "$TAG"
git push origin "$TAG"
echo "Successfully created and pushed tag ${TAG}"