@starlens-app/cli 0.1.2 → 0.1.4
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/package.json +1 -1
- package/src/index.mjs +340 -211
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -31,7 +31,7 @@ const helpText = [
|
|
|
31
31
|
" stars note <repo-id|owner/repo> (--set <text>|--clear) [--api-base-url <url>] [--token-path <path>] [--format table|json]",
|
|
32
32
|
" stars tag add <repo-id|owner/repo> <tag> [--api-base-url <url>] [--token-path <path>] [--format table|json]",
|
|
33
33
|
" stars tag remove <repo-id|owner/repo> <tag> [--api-base-url <url>] [--token-path <path>] [--format table|json]",
|
|
34
|
-
" stars install-skill [--api-base-url <url>] [--token <token>] [--client claude
|
|
34
|
+
" stars install-skill [--api-base-url <url>] [--token <token>] [--client claude,cursor,...] (多客户端逗号分隔)",
|
|
35
35
|
" stars version",
|
|
36
36
|
"",
|
|
37
37
|
"Configuration:",
|
|
@@ -861,6 +861,109 @@ async function wizardPromptSecret(question) {
|
|
|
861
861
|
});
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
+
function maskToken(token) {
|
|
865
|
+
if (!token || token.length < 8) return "***";
|
|
866
|
+
return token.slice(0, 4) + "..." + token.slice(-3);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function readExistingToken() {
|
|
870
|
+
const agentEnvPath = join(homedir(), ".starlens", "agent.env");
|
|
871
|
+
try {
|
|
872
|
+
const content = await readFile(agentEnvPath, "utf8");
|
|
873
|
+
const match = content.match(/^export STARLENS_TOKEN="([^"]+)"/m);
|
|
874
|
+
return match ? match[1] : null;
|
|
875
|
+
} catch {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function wizardCheckbox(items) {
|
|
881
|
+
const isTTY = typeof process.stdin.setRawMode === "function";
|
|
882
|
+
|
|
883
|
+
if (!isTTY) {
|
|
884
|
+
// Non-TTY fallback: comma-separated input
|
|
885
|
+
const labels = items.map((it, i) => ` ${i + 1}) ${it.label}${it.skillOnly ? " [仅 Skill]" : ""}`).join("\n");
|
|
886
|
+
console.log(labels);
|
|
887
|
+
const rl = createReadlineInterface();
|
|
888
|
+
return new Promise((resolve) => {
|
|
889
|
+
rl.question("输入序号(逗号分隔,如 1,2): ", (answer) => {
|
|
890
|
+
rl.close();
|
|
891
|
+
const selected = answer.trim().split(",").map(s => {
|
|
892
|
+
const n = parseInt(s.trim(), 10);
|
|
893
|
+
return items[n - 1]?.value ?? null;
|
|
894
|
+
}).filter(Boolean);
|
|
895
|
+
resolve(selected.length > 0 ? selected : [items[0].value]);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return new Promise((resolve) => {
|
|
901
|
+
let cursor = 0;
|
|
902
|
+
const selected = new Set([items[0].value]); // 默认选中第一项
|
|
903
|
+
|
|
904
|
+
const RESET = "\x1b[0m";
|
|
905
|
+
const BOLD = "\x1b[1m";
|
|
906
|
+
const CYAN = "\x1b[36m";
|
|
907
|
+
const DIM = "\x1b[2m";
|
|
908
|
+
|
|
909
|
+
function render() {
|
|
910
|
+
// 清除之前的输出行
|
|
911
|
+
process.stdout.write("\x1b[" + items.length + "A\x1b[0J");
|
|
912
|
+
for (let i = 0; i < items.length; i++) {
|
|
913
|
+
const item = items[i];
|
|
914
|
+
const isActive = i === cursor;
|
|
915
|
+
const isSelected = selected.has(item.value);
|
|
916
|
+
const icon = isSelected ? "◉" : "◯";
|
|
917
|
+
const label = item.label + (item.skillOnly ? ` ${DIM}[仅 Skill]${RESET}` : "");
|
|
918
|
+
const line = isActive
|
|
919
|
+
? `${BOLD}${CYAN}> ${icon} ${label}${RESET}`
|
|
920
|
+
: ` ${icon} ${label}`;
|
|
921
|
+
process.stdout.write(line + "\n");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// 初次渲染
|
|
926
|
+
console.log(`请选择 AI 客户端(${BOLD}↑↓${RESET} 移动,${BOLD}空格${RESET} 选中/取消,${BOLD}回车${RESET} 确认):\n`);
|
|
927
|
+
for (let i = 0; i < items.length; i++) {
|
|
928
|
+
process.stdout.write("\n");
|
|
929
|
+
}
|
|
930
|
+
render();
|
|
931
|
+
|
|
932
|
+
const stdin = process.stdin;
|
|
933
|
+
stdin.setRawMode(true);
|
|
934
|
+
stdin.resume();
|
|
935
|
+
stdin.setEncoding("utf8");
|
|
936
|
+
|
|
937
|
+
const onData = (chunk) => {
|
|
938
|
+
if (chunk === "\x1b[A") { // 上箭头
|
|
939
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
940
|
+
render();
|
|
941
|
+
} else if (chunk === "\x1b[B") { // 下箭头
|
|
942
|
+
cursor = (cursor + 1) % items.length;
|
|
943
|
+
render();
|
|
944
|
+
} else if (chunk === " ") { // 空格
|
|
945
|
+
const val = items[cursor].value;
|
|
946
|
+
if (selected.has(val)) selected.delete(val);
|
|
947
|
+
else selected.add(val);
|
|
948
|
+
render();
|
|
949
|
+
} else if (chunk === "\r" || chunk === "\n") { // 回车
|
|
950
|
+
stdin.setRawMode(false);
|
|
951
|
+
stdin.removeListener("data", onData);
|
|
952
|
+
stdin.pause();
|
|
953
|
+
process.stdout.write("\n");
|
|
954
|
+
const result = items.map(it => it.value).filter(v => selected.has(v));
|
|
955
|
+
resolve(result.length > 0 ? result : [items[0].value]);
|
|
956
|
+
} else if (chunk === "\x03") { // Ctrl+C
|
|
957
|
+
stdin.setRawMode(false);
|
|
958
|
+
process.stdout.write("\n");
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
stdin.on("data", onData);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
864
967
|
function buildMcpArgs(projectRoot) {
|
|
865
968
|
return ["-lc", `source "$HOME/.starlens/agent.env" && cd "${projectRoot}" && corepack pnpm mcp:start`];
|
|
866
969
|
}
|
|
@@ -924,6 +1027,70 @@ function renderOpencodeSnippet(projectRoot) {
|
|
|
924
1027
|
);
|
|
925
1028
|
}
|
|
926
1029
|
|
|
1030
|
+
async function mergeJson(filePath, mergeFn) {
|
|
1031
|
+
let existing = {};
|
|
1032
|
+
try {
|
|
1033
|
+
const raw = await readFile(filePath, "utf8");
|
|
1034
|
+
existing = JSON.parse(raw);
|
|
1035
|
+
} catch { /* 文件不存在或格式错误则从空对象开始 */ }
|
|
1036
|
+
const merged = mergeFn(existing);
|
|
1037
|
+
await mkdir(filePath.replace(/\/[^/]+$/, ""), { recursive: true });
|
|
1038
|
+
await writeFile(filePath, JSON.stringify(merged, null, 2) + "\n");
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
async function appendTomlSection(filePath, sectionKey, content) {
|
|
1042
|
+
let existing = "";
|
|
1043
|
+
try { existing = await readFile(filePath, "utf8"); } catch { /* 新建 */ }
|
|
1044
|
+
if (existing.includes(`[${sectionKey}]`)) {
|
|
1045
|
+
return { ok: false, reason: `[${sectionKey}] 节点已存在,跳过写入` };
|
|
1046
|
+
}
|
|
1047
|
+
await mkdir(filePath.replace(/\/[^/]+$/, ""), { recursive: true });
|
|
1048
|
+
await writeFile(filePath, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + "\n" + content + "\n");
|
|
1049
|
+
return { ok: true };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function writeMcpConfig(client, { apiBaseUrl, token, projectRoot, hosted }) {
|
|
1053
|
+
const home = homedir();
|
|
1054
|
+
try {
|
|
1055
|
+
if (client === "cursor") {
|
|
1056
|
+
const cursorMcpPath = join(home, ".cursor", "mcp.json");
|
|
1057
|
+
const starlensEntry = hosted
|
|
1058
|
+
? { url: `${apiBaseUrl}/mcp`, headers: { Authorization: `Bearer ${token || "stl_xxx"}` } }
|
|
1059
|
+
: { command: "corepack", args: ["pnpm", "mcp:start"], cwd: projectRoot, env: { STARLENS_TOKEN: "(从 ~/.starlens/agent.env 读取)", STARLENS_API_BASE_URL: "(从 ~/.starlens/agent.env 读取)" } };
|
|
1060
|
+
await mergeJson(cursorMcpPath, (obj) => ({
|
|
1061
|
+
...obj,
|
|
1062
|
+
mcpServers: { ...(obj.mcpServers ?? {}), starlens: starlensEntry },
|
|
1063
|
+
}));
|
|
1064
|
+
return { ok: true, path: cursorMcpPath };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (client === "opencode") {
|
|
1068
|
+
const opencodePath = join(home, ".config", "opencode", "opencode.json");
|
|
1069
|
+
const starlensEntry = hosted
|
|
1070
|
+
? { type: "http", url: `${apiBaseUrl}/mcp`, headers: { Authorization: `Bearer ${token || "stl_xxx"}` }, enabled: true }
|
|
1071
|
+
: { type: "local", command: ["zsh", "-lc", `source "$HOME/.starlens/agent.env" && cd "${projectRoot}" && corepack pnpm mcp:start`], enabled: true, timeout: 10000 };
|
|
1072
|
+
await mergeJson(opencodePath, (obj) => ({
|
|
1073
|
+
...obj,
|
|
1074
|
+
mcp: { ...(obj.mcp ?? {}), starlens: starlensEntry },
|
|
1075
|
+
}));
|
|
1076
|
+
return { ok: true, path: opencodePath };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (client === "codex") {
|
|
1080
|
+
const codexPath = join(home, ".codex", "config.toml");
|
|
1081
|
+
const content = hosted
|
|
1082
|
+
? `[mcp_servers.starlens]\ntype = "http"\nurl = "${apiBaseUrl}/mcp"\n\n[mcp_servers.starlens.headers]\nAuthorization = "Bearer ${token || "stl_xxx"}"\nstartup_timeout_sec = 30\ndefault_tools_approval_mode = "approve"`
|
|
1083
|
+
: `[mcp_servers.starlens]\ntype = "stdio"\ncommand = "zsh"\nargs = ["-lc", "source \\"$HOME/.starlens/agent.env\\" && cd \\"${projectRoot}\\" && corepack pnpm mcp:start"]\nstartup_timeout_sec = 30\ndefault_tools_approval_mode = "approve"`;
|
|
1084
|
+
const result = await appendTomlSection(codexPath, "mcp_servers.starlens", content);
|
|
1085
|
+
return result.ok ? { ok: true, path: codexPath } : { ok: false, reason: result.reason };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return { ok: false, reason: `${client} 不支持自动写入,请手动配置` };
|
|
1089
|
+
} catch (e) {
|
|
1090
|
+
return { ok: false, reason: e.message };
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
927
1094
|
async function spawnCommand(command, args) {
|
|
928
1095
|
return new Promise((resolve) => {
|
|
929
1096
|
const child = spawn(command, args, { stdio: "inherit" });
|
|
@@ -1004,267 +1171,229 @@ async function runInstallSkillWizard(args, env) {
|
|
|
1004
1171
|
const clientArg = readOption(rest, "--client");
|
|
1005
1172
|
rest = clientArg.rest;
|
|
1006
1173
|
|
|
1174
|
+
const clientLabels = {
|
|
1175
|
+
claude: "Claude Code", cursor: "Cursor", vscode: "VS Code (Copilot)",
|
|
1176
|
+
codex: "Codex CLI", opencode: "OpenCode", openclaw: "OpenClaw", hermes: "Hermes", other: "其他",
|
|
1177
|
+
};
|
|
1178
|
+
const MCP_SUPPORTED = new Set(["claude", "cursor", "codex", "opencode"]);
|
|
1179
|
+
|
|
1180
|
+
const CLIENT_ITEMS = [
|
|
1181
|
+
{ value: "claude", label: "Claude Code" },
|
|
1182
|
+
{ value: "cursor", label: "Cursor" },
|
|
1183
|
+
{ value: "vscode", label: "VS Code (Copilot)", skillOnly: true },
|
|
1184
|
+
{ value: "codex", label: "Codex CLI" },
|
|
1185
|
+
{ value: "opencode", label: "OpenCode" },
|
|
1186
|
+
{ value: "openclaw", label: "OpenClaw", skillOnly: true },
|
|
1187
|
+
{ value: "hermes", label: "Hermes", skillOnly: true },
|
|
1188
|
+
{ value: "other", label: "其他(仅输出配置片段)", skillOnly: true },
|
|
1189
|
+
];
|
|
1190
|
+
|
|
1007
1191
|
console.log("");
|
|
1008
|
-
console.log("Starlens
|
|
1192
|
+
console.log("Starlens 安装向导");
|
|
1009
1193
|
console.log("═".repeat(40));
|
|
1010
|
-
console.log("本向导将引导你完成 MCP Server
|
|
1194
|
+
console.log("本向导将引导你完成 Skill 安装及可选的 MCP Server 配置。");
|
|
1011
1195
|
console.log("");
|
|
1012
1196
|
|
|
1013
1197
|
// Step 0: check global install
|
|
1014
1198
|
const isGlobalInstall = !process.argv[1]?.includes("apps/cli");
|
|
1015
1199
|
if (!isGlobalInstall) {
|
|
1016
1200
|
console.log("提示:你正在从源码运行。如需让其他工具通过 `stars` 命令使用,");
|
|
1017
|
-
console.log(" 请先全局安装:npm install -g starlens");
|
|
1201
|
+
console.log(" 请先全局安装:npm install -g @starlens-app/cli");
|
|
1018
1202
|
console.log("");
|
|
1019
1203
|
}
|
|
1020
1204
|
|
|
1021
|
-
|
|
1205
|
+
// rl 在所有 raw-mode 交互结束后再创建,避免 wizardCheckbox 的 \r 泄漏进 readline 缓冲区
|
|
1206
|
+
let rl;
|
|
1022
1207
|
|
|
1023
1208
|
try {
|
|
1024
|
-
// Step 1:
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
let projectRoot;
|
|
1034
|
-
|
|
1035
|
-
if (isSelfHosted) {
|
|
1036
|
-
console.log("");
|
|
1037
|
-
apiBaseUrl = (await wizardPrompt(rl, "Starlens API base URL", defaultUrl === HOSTED_MCP_BASE_URL ? DEFAULT_API_BASE_URL : defaultUrl)).replace(/\/+$/, "");
|
|
1038
|
-
// only ask for project root in self-hosted stdio mode
|
|
1039
|
-
if (!isHostedUrl(apiBaseUrl)) {
|
|
1040
|
-
const detectedRoot = detectProjectRoot();
|
|
1041
|
-
console.log(`检测到项目根目录:${detectedRoot}`);
|
|
1042
|
-
projectRoot = (await wizardPrompt(rl, "项目路径(回车确认)", detectedRoot)).replace(/\/$/, "");
|
|
1043
|
-
}
|
|
1209
|
+
// Step 1: 多选客户端
|
|
1210
|
+
let clients;
|
|
1211
|
+
const clientArgRaw = clientArg.value?.toLowerCase();
|
|
1212
|
+
if (clientArgRaw) {
|
|
1213
|
+
// --client 参数:逗号分隔
|
|
1214
|
+
const nameMap = Object.fromEntries(CLIENT_ITEMS.map(it => [it.value, it.value]));
|
|
1215
|
+
clients = clientArgRaw.split(",").map(s => nameMap[s.trim()]).filter(Boolean);
|
|
1216
|
+
if (clients.length === 0) clients = ["claude"];
|
|
1217
|
+
console.log(`已选择客户端:${clients.map(c => clientLabels[c]).join("、")}`);
|
|
1044
1218
|
} else {
|
|
1045
|
-
|
|
1046
|
-
|
|
1219
|
+
console.log("");
|
|
1220
|
+
clients = await wizardCheckbox(CLIENT_ITEMS);
|
|
1221
|
+
console.log(`已选择:${clients.map(c => clientLabels[c]).join("、")}`);
|
|
1047
1222
|
}
|
|
1048
1223
|
|
|
1049
|
-
|
|
1224
|
+
// wizardCheckbox(raw mode)完成后再创建 readline,避免 Enter 残留字符被提前消费
|
|
1225
|
+
rl = createReadlineInterface();
|
|
1050
1226
|
|
|
1051
|
-
|
|
1052
|
-
const clientMap = {
|
|
1053
|
-
"1": "claude", "2": "cursor", "3": "vscode", "4": "codex", "5": "opencode", "6": "openclaw", "7": "hermes", "8": "other",
|
|
1054
|
-
"claude": "claude", "cursor": "cursor", "vscode": "vscode", "codex": "codex", "opencode": "opencode", "openclaw": "openclaw", "hermes": "hermes", "other": "other",
|
|
1055
|
-
};
|
|
1227
|
+
const cwd = process.cwd();
|
|
1056
1228
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1229
|
+
// Step 2: 安装 Skill(默认是)
|
|
1230
|
+
console.log("");
|
|
1231
|
+
console.log("─".repeat(40));
|
|
1232
|
+
const installSkill = await wizardPrompt(rl, "是否安装 Starlens Skill 文件?(Y/n)", "Y");
|
|
1233
|
+
if (!/^n$/i.test(installSkill)) {
|
|
1059
1234
|
console.log("");
|
|
1060
|
-
console.log("
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1235
|
+
console.log("安装 Starlens Agent Skill...");
|
|
1236
|
+
for (const client of clients) {
|
|
1237
|
+
const skillResult = await installSkillFiles(client, cwd);
|
|
1238
|
+
if (skillResult.results) {
|
|
1239
|
+
for (const r of skillResult.results) {
|
|
1240
|
+
if (r.ok) {
|
|
1241
|
+
console.log(`✓ Skill 已安装:${r.path}`);
|
|
1242
|
+
} else {
|
|
1243
|
+
console.log(`⚠ Skill 安装失败:${r.path}(${r.reason})`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} else if (!skillResult.ok) {
|
|
1247
|
+
console.log(`⚠ ${clientLabels[client]}:${skillResult.reason}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1071
1250
|
} else {
|
|
1072
|
-
|
|
1251
|
+
console.log("跳过 Skill 安装。");
|
|
1073
1252
|
}
|
|
1074
1253
|
|
|
1075
|
-
|
|
1076
|
-
console.log(`已选择客户端:${clientLabels[client]}`);
|
|
1077
|
-
|
|
1078
|
-
// Step 3: token
|
|
1254
|
+
// Step 3: 配置 MCP(可选,默认否)
|
|
1079
1255
|
console.log("");
|
|
1080
|
-
console.log("
|
|
1256
|
+
console.log("─".repeat(40));
|
|
1257
|
+
const mcpClients = clients.filter(c => MCP_SUPPORTED.has(c));
|
|
1081
1258
|
let token = tokenArg.value ?? "";
|
|
1082
|
-
|
|
1083
|
-
token = await wizardPromptSecret("API Token(输入不可见)");
|
|
1084
|
-
}
|
|
1085
|
-
if (!token) {
|
|
1086
|
-
console.log("⚠ 未输入 Token,配置片段中将显示占位符 stl_xxx,请事后手动替换。");
|
|
1087
|
-
}
|
|
1259
|
+
let apiBaseUrl = "";
|
|
1088
1260
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1261
|
+
if (mcpClients.length === 0) {
|
|
1262
|
+
console.log("所选客户端均不支持 MCP,跳过 MCP 配置。");
|
|
1263
|
+
} else {
|
|
1264
|
+
const doMcp = await wizardPrompt(rl, `是否配置 MCP Server?(支持:${mcpClients.map(c => clientLabels[c]).join("、")})(y/N)`, "N");
|
|
1265
|
+
if (/^y$/i.test(doMcp)) {
|
|
1266
|
+
// 部署模式
|
|
1267
|
+
console.log("");
|
|
1268
|
+
const defaultUrl = apiBaseUrlArg.value ?? env.STARLENS_API_BASE_URL ?? HOSTED_MCP_BASE_URL;
|
|
1269
|
+
console.log("部署模式:");
|
|
1270
|
+
console.log(" 1) 托管服务(推荐)— 使用 starlens.520ai.xin,无需本地启动服务");
|
|
1271
|
+
console.log(" 2) 自部署 — 使用你自己的服务器或本地开发环境");
|
|
1272
|
+
const modeChoice = await wizardPrompt(rl, "选择模式", "1");
|
|
1273
|
+
const isSelfHosted = modeChoice.trim() === "2";
|
|
1274
|
+
|
|
1275
|
+
let projectRoot;
|
|
1276
|
+
if (isSelfHosted) {
|
|
1277
|
+
console.log("");
|
|
1278
|
+
apiBaseUrl = (await wizardPrompt(rl, "Starlens API base URL", defaultUrl === HOSTED_MCP_BASE_URL ? DEFAULT_API_BASE_URL : defaultUrl)).replace(/\/+$/, "");
|
|
1279
|
+
if (!isHostedUrl(apiBaseUrl)) {
|
|
1280
|
+
const detectedRoot = detectProjectRoot();
|
|
1281
|
+
console.log(`检测到项目根目录:${detectedRoot}`);
|
|
1282
|
+
projectRoot = (await wizardPrompt(rl, "项目路径(回车确认)", detectedRoot)).replace(/\/$/, "");
|
|
1283
|
+
}
|
|
1284
|
+
} else {
|
|
1285
|
+
apiBaseUrl = HOSTED_MCP_BASE_URL;
|
|
1286
|
+
console.log(`✓ 使用托管服务:${HOSTED_MCP_BASE_URL}`);
|
|
1287
|
+
}
|
|
1094
1288
|
|
|
1095
|
-
|
|
1096
|
-
try {
|
|
1097
|
-
await access(agentEnvPath);
|
|
1098
|
-
envExists = true;
|
|
1099
|
-
} catch {
|
|
1100
|
-
// doesn't exist
|
|
1101
|
-
}
|
|
1289
|
+
const hosted = isHostedUrl(apiBaseUrl);
|
|
1102
1290
|
|
|
1103
|
-
|
|
1291
|
+
// Token(支持历史复用,脱敏展示)
|
|
1104
1292
|
console.log("");
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
"",
|
|
1116
|
-
].join("\n");
|
|
1117
|
-
await writeFile(agentEnvPath, envContent, { mode: 0o600 });
|
|
1118
|
-
console.log(`✓ 已写入 ${agentEnvPath}`);
|
|
1119
|
-
} else {
|
|
1120
|
-
console.log("跳过写入 agent.env。");
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1293
|
+
console.log("在 Starlens 设置页创建 API Token(stl_xxx),然后粘贴到这里。");
|
|
1294
|
+
if (!token) {
|
|
1295
|
+
const existingToken = await readExistingToken();
|
|
1296
|
+
const tokenHint = existingToken ? `回车复用已有 token: ${maskToken(existingToken)},或输入新值` : "输入不可见";
|
|
1297
|
+
const inputToken = await wizardPromptSecret(`API Token(${tokenHint})`);
|
|
1298
|
+
token = inputToken || existingToken || "";
|
|
1299
|
+
}
|
|
1300
|
+
if (!token) {
|
|
1301
|
+
console.log("⚠ 未输入 Token,配置片段中将显示占位符 stl_xxx,请事后手动替换。");
|
|
1302
|
+
}
|
|
1123
1303
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1304
|
+
// 写入 agent.env(自部署非托管模式)
|
|
1305
|
+
if (!hosted && token) {
|
|
1306
|
+
const agentEnvDir = join(homedir(), ".starlens");
|
|
1307
|
+
const agentEnvPath = join(agentEnvDir, "agent.env");
|
|
1308
|
+
let envExists = false;
|
|
1309
|
+
try { await access(agentEnvPath); envExists = true; } catch { /* 不存在 */ }
|
|
1310
|
+
|
|
1311
|
+
let skipEnvWrite = false;
|
|
1312
|
+
if (envExists) {
|
|
1313
|
+
console.log("");
|
|
1314
|
+
const overwrite = await wizardPrompt(rl, "~/.starlens/agent.env 已存在,是否覆盖?(y/N)", "N");
|
|
1315
|
+
skipEnvWrite = !/^y$/i.test(overwrite);
|
|
1316
|
+
}
|
|
1127
1317
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
console.log(snippet);
|
|
1135
|
-
console.log("");
|
|
1136
|
-
const autoRun = await wizardPrompt(rl, "是否立即执行上述命令?(y/N)", "N");
|
|
1137
|
-
if (/^y$/i.test(autoRun)) {
|
|
1138
|
-
const mcpJson = JSON.stringify({
|
|
1139
|
-
type: "http",
|
|
1140
|
-
url: `${apiBaseUrl}/mcp`,
|
|
1141
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
1142
|
-
});
|
|
1143
|
-
console.log("正在注册 MCP server...");
|
|
1144
|
-
const ok = await spawnCommand("claude", ["mcp", "add-json", "starlens", mcpJson]);
|
|
1145
|
-
if (ok) {
|
|
1146
|
-
console.log("✓ MCP server 已注册到 Claude Code。");
|
|
1318
|
+
if (!skipEnvWrite) {
|
|
1319
|
+
await mkdir(agentEnvDir, { recursive: true });
|
|
1320
|
+
await chmod(agentEnvDir, 0o700);
|
|
1321
|
+
const envContent = [`export STARLENS_TOKEN="${token}"`, `export STARLENS_API_BASE_URL="${apiBaseUrl}"`, ""].join("\n");
|
|
1322
|
+
await writeFile(agentEnvPath, envContent, { mode: 0o600 });
|
|
1323
|
+
console.log(`✓ 已写入 ${agentEnvPath}`);
|
|
1147
1324
|
} else {
|
|
1148
|
-
console.log("
|
|
1325
|
+
console.log("跳过写入 agent.env。");
|
|
1149
1326
|
}
|
|
1150
1327
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
console.log("");
|
|
1154
|
-
console.log(renderHostedCursorSnippet(apiBaseUrl, token));
|
|
1155
|
-
} else if (client === "codex") {
|
|
1156
|
-
console.log("将以下内容追加到 ~/.codex/config.toml:");
|
|
1157
|
-
console.log("");
|
|
1158
|
-
console.log(renderHostedCodexSnippet(apiBaseUrl, token));
|
|
1159
|
-
} else if (client === "opencode") {
|
|
1160
|
-
console.log("将以下内容合并到 ~/.config/opencode/opencode.json:");
|
|
1161
|
-
console.log("");
|
|
1162
|
-
console.log(renderHostedOpencodeSnippet(apiBaseUrl, token));
|
|
1163
|
-
} else if (client === "vscode" || client === "openclaw" || client === "hermes") {
|
|
1164
|
-
console.log(`${clientLabels[client]} 不支持 HTTP MCP,Skill 文件已自动安装。`);
|
|
1165
|
-
console.log("如需 HTTP API 直连,请参考文档:");
|
|
1166
|
-
console.log(` ${apiBaseUrl}/docs/integrations`);
|
|
1167
|
-
} else {
|
|
1168
|
-
console.log("HTTP MCP 端点信息:");
|
|
1169
|
-
console.log("");
|
|
1170
|
-
console.log(` URL: ${apiBaseUrl}/mcp`);
|
|
1171
|
-
console.log(` Authorization: Bearer ${token || "stl_xxx"}`);
|
|
1172
|
-
}
|
|
1173
|
-
} else {
|
|
1174
|
-
// ── Self-hosted mode: stdio MCP ──
|
|
1175
|
-
if (client === "claude") {
|
|
1176
|
-
const snippet = renderClaudeCodeSnippet(projectRoot);
|
|
1177
|
-
console.log("Claude Code 配置命令:");
|
|
1178
|
-
console.log("");
|
|
1179
|
-
console.log(snippet);
|
|
1328
|
+
|
|
1329
|
+
// 对每个支持 MCP 的客户端自动写入配置
|
|
1180
1330
|
console.log("");
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1331
|
+
console.log("─".repeat(40));
|
|
1332
|
+
console.log("配置 MCP Server...");
|
|
1333
|
+
for (const client of mcpClients) {
|
|
1334
|
+
if (client === "claude") {
|
|
1335
|
+
const mcpJson = hosted
|
|
1336
|
+
? JSON.stringify({ type: "http", url: `${apiBaseUrl}/mcp`, headers: { Authorization: `Bearer ${token || "stl_xxx"}` } })
|
|
1337
|
+
: JSON.stringify({ type: "stdio", command: "zsh", args: buildMcpArgs(projectRoot) });
|
|
1338
|
+
console.log(`\n ${clientLabels.claude} 配置命令:`);
|
|
1339
|
+
console.log(` claude mcp add-json starlens '${mcpJson}'`);
|
|
1340
|
+
console.log("");
|
|
1341
|
+
const autoRun = await wizardPrompt(rl, " 是否立即执行?(y/N)", "N");
|
|
1342
|
+
if (/^y$/i.test(autoRun)) {
|
|
1343
|
+
const ok = await spawnCommand("claude", ["mcp", "add-json", "starlens", mcpJson]);
|
|
1344
|
+
console.log(ok ? " ✓ MCP server 已注册到 Claude Code。" : " ✗ 注册失败,请手动执行上方命令。");
|
|
1345
|
+
}
|
|
1188
1346
|
} else {
|
|
1189
|
-
|
|
1347
|
+
const result = await writeMcpConfig(client, { apiBaseUrl, token, projectRoot, hosted });
|
|
1348
|
+
if (result.ok) {
|
|
1349
|
+
console.log(`✓ MCP 配置已写入:${result.path}`);
|
|
1350
|
+
} else {
|
|
1351
|
+
console.log(`⚠ ${clientLabels[client]}:${result.reason}`);
|
|
1352
|
+
}
|
|
1190
1353
|
}
|
|
1191
1354
|
}
|
|
1192
|
-
} else if (client === "cursor") {
|
|
1193
|
-
console.log("将以下内容写入 .cursor/mcp.json(合并到 mcpServers 节点):");
|
|
1194
|
-
console.log("");
|
|
1195
|
-
console.log(renderCursorSnippet(projectRoot));
|
|
1196
|
-
} else if (client === "codex") {
|
|
1197
|
-
console.log("将以下内容追加到 ~/.codex/config.toml:");
|
|
1198
|
-
console.log("");
|
|
1199
|
-
console.log(renderCodexSnippet(projectRoot));
|
|
1200
|
-
} else if (client === "opencode") {
|
|
1201
|
-
console.log("将以下内容合并到 ~/.config/opencode/opencode.json:");
|
|
1202
|
-
console.log("");
|
|
1203
|
-
console.log(renderOpencodeSnippet(projectRoot));
|
|
1204
|
-
} else {
|
|
1205
|
-
console.log("通用 Agent Skill 环境变量配置(vscode/openclaw/hermes 等):");
|
|
1206
|
-
console.log("");
|
|
1207
|
-
console.log(` STARLENS_TOKEN="${token || "stl_xxx"}"`);
|
|
1208
|
-
console.log(` STARLENS_API_BASE_URL="${apiBaseUrl}"`);
|
|
1209
|
-
console.log("");
|
|
1210
|
-
console.log("Skill 文件已自动安装到对应客户端目录,无需额外 MCP 配置。");
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Step 6: install skill files
|
|
1215
|
-
console.log("");
|
|
1216
|
-
console.log("─".repeat(40));
|
|
1217
|
-
console.log("安装 Starlens Agent Skill...");
|
|
1218
|
-
const skillResult = await installSkillFiles(client, projectRoot ?? process.cwd());
|
|
1219
|
-
if (skillResult.results) {
|
|
1220
|
-
for (const r of skillResult.results) {
|
|
1221
|
-
if (r.ok) {
|
|
1222
|
-
console.log(`✓ Skill 已安装:${r.path}`);
|
|
1223
|
-
} else {
|
|
1224
|
-
console.log(`⚠ Skill 安装失败:${r.path}(${r.reason})`);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
} else if (!skillResult.ok) {
|
|
1228
|
-
console.log(`⚠ ${skillResult.reason}`);
|
|
1229
|
-
}
|
|
1230
1355
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1356
|
+
// 验证 Token(可选)
|
|
1357
|
+
if (token) {
|
|
1358
|
+
console.log("");
|
|
1359
|
+
const doVerify = await wizardPrompt(rl, "是否验证 Token 可用性?(y/N)", "N");
|
|
1360
|
+
if (/^y$/i.test(doVerify)) {
|
|
1361
|
+
console.log("验证中...");
|
|
1362
|
+
try {
|
|
1363
|
+
const res = await fetchWithTimeout(
|
|
1364
|
+
`${apiBaseUrl}/api/search?q=test&pageSize=1`,
|
|
1365
|
+
{ headers: { Accept: "application/json", Authorization: `Bearer ${token}` } },
|
|
1366
|
+
8_000,
|
|
1367
|
+
);
|
|
1368
|
+
if (res.ok) {
|
|
1369
|
+
console.log("✓ Token 验证成功,API 连接正常。");
|
|
1370
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
1371
|
+
console.log(`✗ Token 无效(HTTP ${res.status})。请检查 Token 是否正确。`);
|
|
1372
|
+
} else {
|
|
1373
|
+
console.log(`⚠ 服务器返回 HTTP ${res.status},请检查 API base URL 是否正确。`);
|
|
1374
|
+
}
|
|
1375
|
+
} catch {
|
|
1376
|
+
console.log(`✗ 无法连接到 ${apiBaseUrl},请检查服务是否启动。`);
|
|
1377
|
+
}
|
|
1249
1378
|
}
|
|
1250
|
-
} catch {
|
|
1251
|
-
console.log(`✗ 无法连接到 ${apiBaseUrl},请检查服务是否启动。`);
|
|
1252
1379
|
}
|
|
1380
|
+
} else {
|
|
1381
|
+
console.log("跳过 MCP 配置。");
|
|
1253
1382
|
}
|
|
1254
1383
|
}
|
|
1255
1384
|
|
|
1256
|
-
//
|
|
1385
|
+
// 完成
|
|
1257
1386
|
console.log("");
|
|
1258
1387
|
console.log("─".repeat(40));
|
|
1259
1388
|
console.log("✓ 配置完成!");
|
|
1260
1389
|
console.log("");
|
|
1261
1390
|
console.log("下一步:");
|
|
1262
|
-
console.log(" 1. 重启你的 AI
|
|
1263
|
-
console.log(" 2. 在客户端中输入「搜索我收藏的关于 React
|
|
1391
|
+
console.log(" 1. 重启你的 AI 客户端,使配置生效。");
|
|
1392
|
+
console.log(" 2. 在客户端中输入「搜索我收藏的关于 React 的仓库」测试是否可用。");
|
|
1264
1393
|
console.log(` 3. 完整文档:${HOSTED_MCP_BASE_URL}/docs/integrations`);
|
|
1265
1394
|
console.log("");
|
|
1266
1395
|
} finally {
|
|
1267
|
-
rl
|
|
1396
|
+
rl?.close();
|
|
1268
1397
|
}
|
|
1269
1398
|
}
|
|
1270
1399
|
|