Skip to content

Commit 9b916af

Browse files
committed
feat: 一键安装并配置
1 parent 2a30124 commit 9b916af

4 files changed

Lines changed: 278 additions & 18 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@larksuite/cli",
3-
"version": "1.0.10",
3+
"version": "1.0.11",
44
"description": "The official CLI for Lark/Feishu open platform",
55
"bin": {
66
"lark-cli": "scripts/run.js"
@@ -27,6 +27,7 @@
2727
"license": "MIT",
2828
"files": [
2929
"scripts/install.js",
30+
"scripts/install-wizard.js",
3031
"scripts/run.js",
3132
"CHANGELOG.md"
3233
]

scripts/install-wizard.js

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#!/usr/bin/env node
2+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
3+
// SPDX-License-Identifier: MIT
4+
5+
const { execFileSync, execSync } = require("child_process");
6+
const readline = require("readline");
7+
8+
const PKG = "@larksuite/cli";
9+
const SKILLS_REPO = "https://github.com/larksuite/cli";
10+
const isWindows = process.platform === "win32";
11+
12+
// ---------------------------------------------------------------------------
13+
// Helpers
14+
// ---------------------------------------------------------------------------
15+
16+
function ask(question) {
17+
return new Promise((resolve) => {
18+
const rl = readline.createInterface({
19+
input: process.stdin,
20+
output: process.stdout,
21+
});
22+
rl.question(question, (answer) => {
23+
rl.close();
24+
resolve(answer.trim());
25+
});
26+
});
27+
}
28+
29+
async function confirm(question) {
30+
const answer = (await ask(`${question} (Y/n) `)).toLowerCase();
31+
if (answer === "" || answer === "y" || answer === "yes") return true;
32+
return false;
33+
}
34+
35+
function execCmd(cmd, args, opts) {
36+
if (isWindows && /\.(cmd|bat)$/i.test(cmd)) {
37+
return execFileSync("cmd.exe", ["/c", cmd, ...args], opts);
38+
}
39+
return execFileSync(cmd, args, opts);
40+
}
41+
42+
function run(cmd, args, opts = {}) {
43+
execCmd(cmd, args, { stdio: "inherit", ...opts });
44+
}
45+
46+
function runSilent(cmd, args, opts = {}) {
47+
return execCmd(cmd, args, {
48+
stdio: ["ignore", "pipe", "pipe"],
49+
...opts,
50+
});
51+
}
52+
53+
/** Resolve the path of globally installed lark-cli. */
54+
function whichLarkCli() {
55+
try {
56+
const cmd = isWindows ? "where" : "which";
57+
return execSync(`${cmd} lark-cli`, { stdio: ["ignore", "pipe", "pipe"] })
58+
.toString()
59+
.split("\n")[0]
60+
.trim();
61+
} catch (_) {
62+
return null;
63+
}
64+
}
65+
66+
/** Get the version of a lark-cli binary, or null. */
67+
function getLarkCliVersion(binPath) {
68+
try {
69+
const out = runSilent(binPath, ["--version"], { timeout: 10000 });
70+
const match = out.toString().match(/(\d+\.\d+\.\d+)/);
71+
return match ? match[1] : null;
72+
} catch (_) {
73+
return null;
74+
}
75+
}
76+
77+
/** Check whether lark-cli config already exists. Returns app ID or null. */
78+
function getExistingAppId(binPath) {
79+
try {
80+
const out = runSilent(binPath, ["config", "show"], { timeout: 10000 });
81+
const json = JSON.parse(out.toString());
82+
return json.appId || null;
83+
} catch (_) {
84+
return null;
85+
}
86+
}
87+
88+
// ---------------------------------------------------------------------------
89+
// Steps
90+
// ---------------------------------------------------------------------------
91+
92+
async function stepInstallGlobally() {
93+
console.log("\n[1/4] Installing %s globally...", PKG);
94+
95+
const existing = whichLarkCli();
96+
if (existing) {
97+
const ver = getLarkCliVersion(existing);
98+
console.log(" Already installed (v%s). Skipped.", ver || "unknown");
99+
return;
100+
}
101+
102+
try {
103+
run("npm", ["install", "-g", PKG]);
104+
console.log(" Done.");
105+
} catch (_) {
106+
console.error(
107+
"\n Failed to install globally.\n" +
108+
" Try running manually: npm install -g " + PKG + "\n"
109+
);
110+
process.exit(1);
111+
}
112+
}
113+
114+
function skillsAlreadyInstalled() {
115+
try {
116+
const out = runSilent("npx", ["-y", "skills", "ls", "-g", "--json"], {
117+
timeout: 30000,
118+
});
119+
const list = JSON.parse(out.toString());
120+
return Array.isArray(list) && list.some((s) => s.name && s.name.startsWith("lark-"));
121+
} catch (_) {
122+
return false;
123+
}
124+
}
125+
126+
async function stepInstallSkills() {
127+
console.log("\n[2/4] Installing AI Skills...");
128+
129+
if (skillsAlreadyInstalled()) {
130+
console.log(" Already installed. Skipped.");
131+
return;
132+
}
133+
134+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
135+
let i = 0;
136+
const spinner = setInterval(() => {
137+
process.stdout.write(`\r ${frames[i++ % frames.length]} Installing skills...`);
138+
}, 80);
139+
140+
try {
141+
runSilent("npx", ["-y", "skills", "add", SKILLS_REPO, "-y", "-g"], {
142+
timeout: 120000,
143+
});
144+
clearInterval(spinner);
145+
process.stdout.write("\r Done. \n");
146+
} catch (_) {
147+
clearInterval(spinner);
148+
process.stdout.write("\r");
149+
console.error(
150+
"\n Skills installation failed.\n" +
151+
" Try running manually: npx skills add " + SKILLS_REPO + " -y -g\n"
152+
);
153+
process.exit(1);
154+
}
155+
}
156+
157+
async function stepConfigInit() {
158+
console.log("\n[3/4] Configuring your app...");
159+
160+
const larkCli = whichLarkCli();
161+
if (!larkCli) {
162+
console.error(" lark-cli not found after installation. Aborting.");
163+
process.exit(1);
164+
}
165+
166+
const appId = getExistingAppId(larkCli);
167+
if (appId) {
168+
console.log(" Found existing app (App ID: %s).", appId);
169+
const reuse = await confirm(" Use this app?");
170+
if (reuse) {
171+
console.log(" Skipped.");
172+
return;
173+
}
174+
}
175+
176+
try {
177+
run(larkCli, ["config", "init", "--new"]);
178+
console.log(" Done.");
179+
} catch (_) {
180+
console.error(
181+
"\n App configuration failed.\n" +
182+
' Try running manually: lark-cli config init --new\n'
183+
);
184+
process.exit(1);
185+
}
186+
}
187+
188+
async function stepAuthLogin() {
189+
console.log("\n[4/4] User authorization...");
190+
191+
const larkCli = whichLarkCli();
192+
if (!larkCli) {
193+
console.warn(" lark-cli not found. Skipping authorization.");
194+
return;
195+
}
196+
197+
const yes = await confirm(
198+
" Allow AI to access your Feishu/Lark data (messages, docs, calendar, etc.)?"
199+
);
200+
if (!yes) {
201+
console.log(' Skipped. You can run "lark-cli auth login" later.');
202+
return;
203+
}
204+
205+
try {
206+
run(larkCli, ["auth", "login"]);
207+
console.log(" Done.");
208+
} catch (_) {
209+
console.warn(
210+
' Warning: Authorization failed. You can retry later:\n' +
211+
" lark-cli auth login"
212+
);
213+
}
214+
}
215+
216+
// ---------------------------------------------------------------------------
217+
// Main
218+
// ---------------------------------------------------------------------------
219+
220+
async function main() {
221+
console.log("Setting up Feishu/Lark CLI...");
222+
223+
await stepInstallGlobally();
224+
await stepInstallSkills();
225+
await stepConfigInit();
226+
await stepAuthLogin();
227+
228+
console.log("\n✅ You're all set!");
229+
console.log(
230+
'Now try asking your AI tool (Claude Code, Trae, etc.):\n' +
231+
' "What can Feishu/Lark CLI help me with, and where should I start?"'
232+
);
233+
}
234+
235+
main().catch((err) => {
236+
console.error("Unexpected error:", err.message || err);
237+
process.exit(1);
238+
});

scripts/install.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const path = require("path");
66
const { execSync } = require("child_process");
77
const os = require("os");
88

9-
const VERSION = require("../package.json").version;
9+
const VERSION = require("../package.json").version.replace(/-.*$/, "");
1010
const REPO = "larksuite/cli";
1111
const NAME = "lark-cli";
1212

@@ -85,6 +85,16 @@ function install() {
8585
}
8686
}
8787

88+
// When triggered as a postinstall hook under npx, skip the binary download.
89+
// The "install" wizard doesn't need it, and run.js calls install.js directly
90+
// (with LARK_CLI_RUN=1) for other commands that do need the binary.
91+
const isNpxPostinstall =
92+
process.env.npm_command === "exec" && !process.env.LARK_CLI_RUN;
93+
94+
if (isNpxPostinstall) {
95+
process.exit(0);
96+
}
97+
8898
try {
8999
install();
90100
} catch (err) {

scripts/run.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,32 @@ if (process.platform === "win32" && fs.existsSync(oldBin)) {
4141
}
4242
}
4343

44-
if (!fs.existsSync(bin)) {
45-
console.error(
46-
`Error: lark-cli binary not found at ${bin}\n\n` +
47-
`This usually means the postinstall script was skipped.\n` +
48-
`Common causes:\n` +
49-
` - npm is configured with ignore-scripts=true\n` +
50-
` - The postinstall download failed\n\n` +
51-
`To fix, run the install script manually:\n` +
52-
` node "${path.join(__dirname, "install.js")}"\n`
53-
);
54-
process.exit(1);
55-
}
44+
// Intercept "install" subcommand — run the setup wizard directly,
45+
// bypassing the native binary (which may not exist yet under npx).
46+
const args = process.argv.slice(2);
47+
if (args[0] === "install") {
48+
require("./install-wizard.js");
49+
} else {
50+
// Auto-download binary if missing (e.g. npx skipped postinstall).
51+
if (!fs.existsSync(bin)) {
52+
try {
53+
execFileSync(process.execPath, [path.join(__dirname, "install.js")], {
54+
stdio: "inherit",
55+
env: { ...process.env, LARK_CLI_RUN: "true" },
56+
});
57+
} catch (_) {
58+
console.error(
59+
`\nFailed to auto-install lark-cli binary.\n` +
60+
`To fix, run the install script manually:\n` +
61+
` node "${path.join(__dirname, "install.js")}"\n`
62+
);
63+
process.exit(1);
64+
}
65+
}
5666

57-
try {
58-
execFileSync(bin, process.argv.slice(2), { stdio: "inherit" });
59-
} catch (e) {
60-
process.exit(e.status || 1);
67+
try {
68+
execFileSync(bin, args, { stdio: "inherit" });
69+
} catch (e) {
70+
process.exit(e.status || 1);
71+
}
6172
}

0 commit comments

Comments
 (0)