import { spawn } from 'node:child_process'; import fs from 'node:fs/promises'; import path from 'node:path'; export const LARK_CLI = 'lark-cli'; const LARK_DOMAIN = 'https://open.feishu.cn'; let larkCliCommandPath = ''; async function pathExists(candidate) { try { await fs.access(candidate); return true; } catch { return false; } } export async function resolveLarkCliCommand() { if (larkCliCommandPath) { return larkCliCommandPath; } const candidates = []; if (process.env.LARK_CLI_PATH) { candidates.push(process.env.LARK_CLI_PATH); } if (process.platform === 'win32') { if (process.env.APPDATA) { candidates.push(path.join(process.env.APPDATA, 'npm', 'node_modules', '@larksuite', 'cli', 'bin', 'lark-cli.exe')); candidates.push(path.join(process.env.APPDATA, 'npm', 'lark-cli.cmd')); } const pathValue = process.env.Path || process.env.PATH || ''; for (const dir of pathValue.split(path.delimiter).filter(Boolean)) { candidates.push(path.join(dir, 'lark-cli.exe')); candidates.push(path.join(dir, 'lark-cli.cmd')); } } for (const candidate of candidates) { if (await pathExists(candidate)) { larkCliCommandPath = candidate; return candidate; } } larkCliCommandPath = LARK_CLI; return LARK_CLI; } export function larkCliEnvironment(baseEnv = process.env) { const env = { ...baseEnv }; const appId = String(env.LARK_APP_ID || env.CODEXMOBILE_FEISHU_APP_ID || '').trim(); const appSecret = String(env.LARK_APP_SECRET || env.CODEXMOBILE_FEISHU_APP_SECRET || '').trim(); if (appId) { env.LARK_APP_ID = appId; } if (appSecret) { env.LARK_APP_SECRET = appSecret; } env.LARK_DOMAIN = String(env.LARK_DOMAIN || LARK_DOMAIN).trim() || LARK_DOMAIN; env.LARK_CLI_NO_PROXY = '1'; env.NO_PROXY = '*'; env.no_proxy = '*'; for (const key of ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy']) { delete env[key]; } return env; } export function prependPathEntry(env, dir) { const current = env.Path || env.PATH || ''; const next = [dir, current].filter(Boolean).join(path.delimiter); env.Path = next; env.PATH = next; } function windowsCmdQuote(value) { return `"${String(value || '').replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`; } export function larkCliSpawnOptions(command, args) { if (process.platform === 'win32' && /\.cmd$|\.bat$/i.test(command)) { return { command: process.env.ComSpec || 'cmd.exe', args: ['/d', '/c', ['call', windowsCmdQuote(command), ...args.map(windowsCmdQuote)].join(' ')], windowsVerbatimArguments: true }; } return { command, args, windowsVerbatimArguments: false }; } export function redacted(value) { return String(value || '') .replace(/"appSecret"\s*:\s*"[^"]+"/gi, '"appSecret":"****"') .replace(/"access[_-]?token"\s*:\s*"[^"]+"/gi, '"accessToken":"****"') .replace(/"refresh[_-]?token"\s*:\s*"[^"]+"/gi, '"refreshToken":"****"') .replace(/\b(u|ur|t)-[A-Za-z0-9._-]{20,}\b/g, '$1-[hidden]') .replace(/sk-[A-Za-z0-9._-]+/g, 'sk-[hidden]'); } function parseJsonObject(text) { const value = String(text || '').trim(); if (!value) { return null; } try { return JSON.parse(value); } catch { const start = value.indexOf('{'); const end = value.lastIndexOf('}'); if (start >= 0 && end > start) { try { return JSON.parse(value.slice(start, end + 1)); } catch { return null; } } } return null; } export function larkError(message, details = {}) { const error = new Error(message); Object.assign(error, details); return error; } export async function runLarkCli(args, options = {}) { const { input = '', timeoutMs = 15000, cwd = process.cwd() } = options; const command = await resolveLarkCliCommand(); const spawnOptions = larkCliSpawnOptions(command, args); return await new Promise((resolve) => { let stdout = ''; let stderr = ''; let settled = false; let child = null; try { child = spawn(spawnOptions.command, spawnOptions.args, { cwd, env: larkCliEnvironment(), windowsHide: true, windowsVerbatimArguments: spawnOptions.windowsVerbatimArguments }); } catch (error) { resolve({ ok: false, code: null, signal: '', stdout: '', stderr: '', json: null, error: error.message }); return; } const timeout = setTimeout(() => { if (settled) { return; } settled = true; child.kill(); resolve({ ok: false, code: null, signal: 'timeout', stdout: redacted(stdout), stderr: redacted(stderr), json: parseJsonObject(stdout), error: `lark-cli timed out after ${timeoutMs}ms` }); }, timeoutMs); child.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf8'); }); child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); }); child.on('error', (error) => { if (settled) { return; } settled = true; clearTimeout(timeout); resolve({ ok: false, code: null, signal: '', stdout: redacted(stdout), stderr: redacted(stderr), json: null, error: error.message }); }); child.on('close', (code, signal) => { if (settled) { return; } settled = true; clearTimeout(timeout); resolve({ ok: code === 0, code, signal, stdout: redacted(stdout), stderr: redacted(stderr), json: parseJsonObject(stdout), error: code === 0 ? '' : redacted(stderr || stdout || `lark-cli exited with code ${code}`) }); }); if (input && child.stdin) { child.stdin.write(input); } child.stdin?.end(); }); }