@weact-pipenet/weact-cli 1.0.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.
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Lark Technologies Pte. Ltd.
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const { execFileSync, execFile } = require("child_process");
8
+ const p = require("@clack/prompts");
9
+
10
+ const PKG = "@larksuite/cli";
11
+ const SKILLS_REPO = "https://open.feishu.cn";
12
+ const SKILLS_REPO_FALLBACK = "larksuite/cli";
13
+ const isWindows = process.platform === "win32";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // i18n
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const messages = {
20
+ zh: {
21
+ setup: "正在设置 Feishu/Lark CLI...",
22
+ step1: "正在安装 %s...",
23
+ step1Upgrade: "正在升级 %s (v%s → v%s)...",
24
+ step1Skip: "已安装 (v%s),跳过",
25
+ step1Done: "已全局安装",
26
+ step1Upgraded: "已升级到 v%s",
27
+ step1Fail: "全局安装失败。运行以下命令重试: npm install -g %s",
28
+ step2: "安装 AI Skills",
29
+ step2Skip: "已安装,跳过",
30
+ step2Spinner: "正在安装 Skills...",
31
+ step2Done: "Skills 已安装",
32
+ step2Fail: "Skills 安装失败。运行以下命令重试: npx skills add %s -y -g",
33
+ step3: "正在配置应用...",
34
+ step3NotFound: "未找到 lark-cli,终止",
35
+ step3Found: "发现已配置应用 (App ID: %s),继续使用?",
36
+ step3Skip: "跳过应用配置",
37
+ step3Done: "应用已配置",
38
+ step3Fail: "应用配置失败。运行以下命令重试: lark-cli config init --new",
39
+ step4: "授权",
40
+ step4NotFound: "未找到 lark-cli,跳过授权",
41
+ step4Confirm: "是否允许 AI 访问你个人的消息、文档、日历等飞书 / Lark 数据,并以你的名义执行操作?",
42
+ step4Skip: "跳过授权。后续运行 lark-cli auth login 完成授权",
43
+ step4Done: "授权完成",
44
+ step4Fail: "授权失败。运行以下命令重试: lark-cli auth login",
45
+ done: "安装完成!\n可以和你的 AI 工具(如 Claude Code、Trae等)说:\"飞书/Lark CLI 能帮我做什么?结合我的情况推荐一下从哪里开始\"",
46
+ cancelled: "安装已取消",
47
+ nonTtyHint: "要完成配置,请在终端中运行:\n lark-cli config init --new\n lark-cli auth login",
48
+ },
49
+ en: {
50
+ setup: "Setting up Feishu/Lark CLI...",
51
+ step1: "Installing %s globally...",
52
+ step1Upgrade: "Upgrading %s (v%s → v%s)...",
53
+ step1Skip: "Already installed (v%s). Skipped",
54
+ step1Done: "Installed globally",
55
+ step1Upgraded: "Upgraded to v%s",
56
+ step1Fail: "Failed to install globally. Run manually: npm install -g %s",
57
+ step2: "Install AI skills",
58
+ step2Skip: "Already installed. Skipped",
59
+ step2Spinner: "Installing skills...",
60
+ step2Done: "Skills installed",
61
+ step2Fail: "Failed to install skills. Run manually: npx skills add %s -y -g",
62
+ step3: "Configuring app...",
63
+ step3NotFound: "lark-cli not found. Aborting",
64
+ step3Found: "Found existing app (App ID: %s). Use this app?",
65
+ step3Skip: "Skipped app configuration",
66
+ step3Done: "App configured",
67
+ step3Fail: "Failed to configure app. Run manually: lark-cli config init --new",
68
+ step4: "Authorization",
69
+ step4NotFound: "lark-cli not found. Skipping authorization",
70
+ step4Confirm: "Allow the AI to access your messages, documents, calendar, and more in Feishu/Lark, and perform actions on your behalf?",
71
+ step4Skip: "Skipped. Run lark-cli auth login to authorize later",
72
+ step4Done: "Authorization complete",
73
+ step4Fail: "Failed to authorize. Run lark-cli auth login to retry",
74
+ done: "You are all set!\nNow try asking your AI tool (Claude Code, Trae, etc.): \"What can Feishu/Lark CLI help me with, and where should I start?\"",
75
+ cancelled: "Installation cancelled",
76
+ nonTtyHint: "To complete setup, run interactively:\n lark-cli config init --new\n lark-cli auth login",
77
+ },
78
+ };
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Helpers
82
+ // ---------------------------------------------------------------------------
83
+
84
+ function handleCancel(value, msg) {
85
+ if (p.isCancel(value)) {
86
+ p.cancel(msg.cancelled);
87
+ process.exit(0);
88
+ }
89
+ return value;
90
+ }
91
+
92
+ function execCmd(cmd, args, opts) {
93
+ if (isWindows) {
94
+ return execFileSync("cmd.exe", ["/c", cmd, ...args], opts);
95
+ }
96
+ return execFileSync(cmd, args, opts);
97
+ }
98
+
99
+ function run(cmd, args, opts = {}) {
100
+ execCmd(cmd, args, { stdio: "inherit", ...opts });
101
+ }
102
+
103
+ function runSilent(cmd, args, opts = {}) {
104
+ return execCmd(cmd, args, {
105
+ stdio: ["ignore", "pipe", "pipe"],
106
+ ...opts,
107
+ });
108
+ }
109
+
110
+ function runSilentAsync(cmd, args, opts = {}) {
111
+ const actualCmd = isWindows ? "cmd.exe" : cmd;
112
+ const actualArgs = isWindows ? ["/c", cmd, ...args] : args;
113
+ return new Promise((resolve, reject) => {
114
+ execFile(actualCmd, actualArgs, {
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ ...opts,
117
+ }, (err, stdout) => {
118
+ if (err) reject(err);
119
+ else resolve(stdout);
120
+ });
121
+ });
122
+ }
123
+
124
+ function fmt(template, ...values) {
125
+ let i = 0;
126
+ return template.replace(/%s/g, () => values[i++] ?? "");
127
+ }
128
+
129
+ /** Resolve the path of globally installed lark-cli (skip npx temp copies). */
130
+ function whichLarkCli() {
131
+ try {
132
+ const prefix = execFileSync("npm", ["prefix", "-g"], {
133
+ stdio: ["ignore", "pipe", "pipe"],
134
+ }).toString().trim();
135
+ const bin = isWindows
136
+ ? path.join(prefix, "lark-cli.cmd")
137
+ : path.join(prefix, "bin", "lark-cli");
138
+ if (fs.existsSync(bin)) return bin;
139
+ } catch (_) {
140
+ // fall through
141
+ }
142
+ // Fallback to which/where if npm prefix lookup fails.
143
+ try {
144
+ const cmd = isWindows ? "where" : "which";
145
+ return execFileSync(cmd, ["lark-cli"], { stdio: ["ignore", "pipe", "pipe"] })
146
+ .toString()
147
+ .split("\n")[0]
148
+ .trim();
149
+ } catch (_) {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ /** Get the latest version of @larksuite/cli from the registry. Returns version or null. */
155
+ function getLatestVersion() {
156
+ try {
157
+ const out = runSilent("npm", ["view", PKG, "version"], { timeout: 15000 });
158
+ const ver = out.toString().trim();
159
+ return /^\d+\.\d+\.\d+/.test(ver) ? ver : null;
160
+ } catch (_) {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /** Compare two semver strings. Returns true if a < b. */
166
+ function semverLessThan(a, b) {
167
+ const pa = a.replace(/-.*$/, "").split(".").map(Number);
168
+ const pb = b.replace(/-.*$/, "").split(".").map(Number);
169
+ for (let i = 0; i < 3; i++) {
170
+ if ((pa[i] || 0) < (pb[i] || 0)) return true;
171
+ if ((pa[i] || 0) > (pb[i] || 0)) return false;
172
+ }
173
+ return false;
174
+ }
175
+
176
+ /** Check whether @larksuite/cli is truly installed in npm global prefix. Returns version or null. */
177
+ function getGloballyInstalledVersion() {
178
+ try {
179
+ const out = runSilent("npm", ["list", "-g", PKG], { timeout: 15000 });
180
+ const match = out.toString().match(/@(\d+\.\d+\.\d+[^\s]*)/);
181
+ return match ? match[1] : "unknown";
182
+ } catch (_) {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ /** Check whether lark-cli config already exists. Returns app ID or null. */
188
+ function getExistingAppId(binPath) {
189
+ try {
190
+ const out = runSilent(binPath, ["config", "show"], { timeout: 10000 });
191
+ const json = JSON.parse(out.toString());
192
+ return json.appId || null;
193
+ } catch (_) {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ /** Parse --lang from process.argv, returns "zh", "en", or null. */
199
+ function parseLangArg() {
200
+ const args = process.argv.slice(2);
201
+ for (let i = 0; i < args.length; i++) {
202
+ if (args[i] === "--lang" && args[i + 1]) {
203
+ const val = args[i + 1].toLowerCase();
204
+ if (val === "zh" || val === "en") return val;
205
+ }
206
+ if (args[i].startsWith("--lang=")) {
207
+ const val = args[i].split("=")[1].toLowerCase();
208
+ if (val === "zh" || val === "en") return val;
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Steps
216
+ // ---------------------------------------------------------------------------
217
+
218
+ async function stepSelectLang() {
219
+ const fromArg = parseLangArg();
220
+ if (fromArg) return fromArg;
221
+
222
+ const lang = await p.select({
223
+ message: "请选择语言 / Select language",
224
+ options: [
225
+ { value: "zh", label: "中文" },
226
+ { value: "en", label: "English" },
227
+ ],
228
+ });
229
+ return handleCancel(lang, messages.zh);
230
+ }
231
+
232
+ async function stepInstallGlobally(msg) {
233
+ const installedVer = getGloballyInstalledVersion();
234
+ const latestVer = getLatestVersion();
235
+ const needsUpgrade = installedVer && latestVer && semverLessThan(installedVer, latestVer);
236
+
237
+ if (installedVer && !needsUpgrade) {
238
+ p.log.info(fmt(msg.step1Skip, installedVer));
239
+ return false;
240
+ }
241
+
242
+ const s = p.spinner();
243
+ if (needsUpgrade) {
244
+ s.start(fmt(msg.step1Upgrade, PKG, installedVer, latestVer));
245
+ } else {
246
+ s.start(fmt(msg.step1, PKG));
247
+ }
248
+ try {
249
+ await runSilentAsync("npm", ["install", "-g", PKG], { timeout: 120000 });
250
+ s.stop(needsUpgrade ? fmt(msg.step1Upgraded, latestVer) : msg.step1Done);
251
+ return needsUpgrade;
252
+ } catch (_) {
253
+ s.stop(fmt(msg.step1Fail, PKG));
254
+ process.exit(1);
255
+ }
256
+ }
257
+
258
+ async function skillsAlreadyInstalled() {
259
+ try {
260
+ const out = await runSilentAsync("npx", ["-y", "skills", "ls", "-g"], {
261
+ timeout: 120000,
262
+ });
263
+ return /^lark-/m.test(out.toString());
264
+ } catch (_) {
265
+ return false;
266
+ }
267
+ }
268
+
269
+ async function stepInstallSkills(msg) {
270
+ const s = p.spinner();
271
+ s.start(msg.step2Spinner);
272
+ try {
273
+ if (await skillsAlreadyInstalled()) {
274
+ s.stop(msg.step2Skip);
275
+ return;
276
+ }
277
+ try {
278
+ await runSilentAsync("npx", ["-y", "skills", "add", SKILLS_REPO, "-y", "-g"], {
279
+ timeout: 120000,
280
+ });
281
+ } catch (_) {
282
+ await runSilentAsync("npx", ["-y", "skills", "add", SKILLS_REPO_FALLBACK, "-y", "-g"], {
283
+ timeout: 120000,
284
+ });
285
+ }
286
+ s.stop(msg.step2Done);
287
+ } catch (_) {
288
+ s.stop(fmt(msg.step2Fail, SKILLS_REPO_FALLBACK));
289
+ process.exit(1);
290
+ }
291
+ }
292
+
293
+ async function stepConfigInit(msg, lang) {
294
+ const s = p.spinner();
295
+ s.start(msg.step3);
296
+
297
+ const larkCli = whichLarkCli();
298
+ if (!larkCli) {
299
+ s.stop(msg.step3NotFound);
300
+ process.exit(1);
301
+ }
302
+
303
+ const appId = getExistingAppId(larkCli);
304
+ s.stop(msg.step3);
305
+
306
+ if (appId) {
307
+ const reuse = await p.confirm({
308
+ message: fmt(msg.step3Found, appId),
309
+ });
310
+ if (handleCancel(reuse, msg) && reuse) {
311
+ p.log.info(msg.step3Skip);
312
+ return;
313
+ }
314
+ }
315
+
316
+ try {
317
+ run(larkCli, ["config", "init", "--new", "--lang", lang]);
318
+ p.log.success(msg.step3Done);
319
+ } catch (_) {
320
+ p.log.error(msg.step3Fail);
321
+ process.exit(1);
322
+ }
323
+ }
324
+
325
+ async function stepAuthLogin(msg) {
326
+ const larkCli = whichLarkCli();
327
+ if (!larkCli) {
328
+ p.log.warn(msg.step4NotFound);
329
+ return;
330
+ }
331
+
332
+ const yes = await p.confirm({
333
+ message: msg.step4Confirm,
334
+ });
335
+ if (p.isCancel(yes)) {
336
+ p.cancel(msg.cancelled);
337
+ process.exit(0);
338
+ }
339
+ if (!yes) {
340
+ p.log.info(msg.step4Skip);
341
+ return;
342
+ }
343
+
344
+ p.log.step(msg.step4);
345
+ try {
346
+ run(larkCli, ["auth", "login"]);
347
+ p.log.success(msg.step4Done);
348
+ } catch (_) {
349
+ p.log.warn(msg.step4Fail);
350
+ }
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // Main
355
+ // ---------------------------------------------------------------------------
356
+
357
+ async function main() {
358
+ const isInteractive = !!process.stdin.isTTY;
359
+ const lang = isInteractive ? await stepSelectLang() : (parseLangArg() || "en");
360
+ const msg = messages[lang];
361
+
362
+ if (isInteractive) {
363
+ p.intro(msg.setup);
364
+ await stepInstallGlobally(msg);
365
+ await stepInstallSkills(msg);
366
+ await stepConfigInit(msg, lang);
367
+ await stepAuthLogin(msg);
368
+ p.outro(msg.done);
369
+ } else {
370
+ console.log(msg.setup);
371
+ await stepInstallGlobally(msg);
372
+ await stepInstallSkills(msg);
373
+ console.log(msg.nonTtyHint);
374
+ }
375
+ }
376
+
377
+ main().catch((err) => {
378
+ p.cancel("Unexpected error: " + (err.message || err));
379
+ process.exit(1);
380
+ });