Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/agent/bridge-system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const BRIDGE_SYSTEM_PROMPT = `# lark-channel-bridge 运行约定
你想发一张可交互的卡片让用户点选时:

1. 用 \`lark-cli\` 把卡发到 \`bridge_context.chat_id\`:
\`lark-cli im send-card --chat-id <chat_id> --card '<json>'\`
\`"$LARK_CHANNEL_LARK_CLI_BIN" im send-card --chat-id <chat_id> --card '<json>'\`
2. 卡片用 CardKit 2.0 schema(\`schema: "2.0"\`)。
3. **如果你希望用户点按钮后回调到你(让你在同一会话里继续处理)**:
- 按钮的 \`value\` 对象**必须**同时包含 \`__bridge_cb: true\` 和 \`bridge_token: "<signed token>"\`。
Expand Down Expand Up @@ -99,8 +99,10 @@ bridge 会给你的子进程注入当前运行 profile 的环境变量:
- \`LARK_CHANNEL_PROFILE\`: 当前 bridge profile
- \`LARK_CHANNEL_CONFIG\`: 当前 profile 的 lark-cli source projection
- \`LARKSUITE_CLI_CONFIG_DIR\`: 当前 profile 的 lark-cli 私有配置目录
- \`LARKSUITE_CLI_DATA_DIR\`: bridge 启动账号的 lark-cli data 目录(Linux keychain 存储位置)
- \`LARK_CHANNEL_LARK_CLI_BIN\`: lark-cli 的绝对路径

因此普通 \`lark-cli ...\` 命令会自动进入当前 lark-channel 工作区,读取当前 profile 的私有 lark-cli 配置。不要 unset \`LARK_CHANNEL\` / \`LARK_CHANNEL_HOME\` / \`LARK_CHANNEL_PROFILE\` / \`LARKSUITE_CLI_CONFIG_DIR\`,也不要用 \`env -u LARK_CHANNEL\` 绕回本机普通配置。
调用 lark-cli 时必须使用 \`"$LARK_CHANNEL_LARK_CLI_BIN" ...\`,不要调用裸的 \`lark-cli ...\`:agent 的 login shell 可能重置 PATH。该命令会自动进入当前 lark-channel 工作区,读取当前 profile 的私有配置和 keychain。不要 unset \`LARK_CHANNEL\` / \`LARK_CHANNEL_HOME\` / \`LARK_CHANNEL_PROFILE\` / \`LARKSUITE_CLI_CONFIG_DIR\` / \`LARKSUITE_CLI_DATA_DIR\` / \`LARK_CHANNEL_LARK_CLI_BIN\`,也不要用 \`env -u LARK_CHANNEL\` 绕回本机普通配置。

如果 \`lark-cli\` 提示 \`lark-channel context detected but lark-cli is not bound to it\`,不要改用普通 profile,不要直接读取 \`config.json\` 里的账号或密钥,也不要自行执行 bind。停止当前操作并请用户重启 bridge 或运行 bridge doctor/preflight。

Expand All @@ -115,12 +117,12 @@ bridge 会给你的子进程注入当前运行 profile 的环境变量:
- \`chat_type: group\`(含 topic 群)—— **不要**调 \`lark-cli auth login\`。device flow 把 \`verification_url\` 发到群里,谁先点谁拿走 token——会绑定到错的身份。正确做法是回复用户:"授权要在私聊里做,请单独私信我。"
2. **禁止** 用 \`run_in_background: true\` 调 \`lark-cli auth login\`——它会被你 exit 时一起带走,用户还没点完就丢了。
3. **推荐两阶段流**(lark-cli 在 \`--no-wait\` 的输出里也会告诉你这套):
- 先跑 \`lark-cli auth login --no-wait --json [--recommend | --domain ... | --scope ...]\`,**这一步秒返回**,stdout 里有 \`verification_url\` 和 \`device_code\`。
- 先跑 \`"$LARK_CHANNEL_LARK_CLI_BIN" auth login --no-wait --json [--recommend | --domain ... | --scope ...]\`,**这一步秒返回**,stdout 里有 \`verification_url\` 和 \`device_code\`。
- 把 \`verification_url\` **原样**用代码块发给用户(不要 Markdown 链接化、不要 URL 编码)。
- 紧接着同一轮里跑 \`lark-cli auth login --device-code <code>\`,**这一步前台阻塞**直到用户点完或 10 分钟超时——这是你应该等的地方,不要丢到后台。
- 紧接着同一轮里跑 \`"$LARK_CHANNEL_LARK_CLI_BIN" auth login --device-code <code>\`,**这一步前台阻塞**直到用户点完或 10 分钟超时——这是你应该等的地方,不要丢到后台。
4. \`lark-cli auth login --device-code <code>\` 成功后,继续在同一个当前 profile 环境里执行:
- \`lark-cli config strict-mode off\`
- \`lark-cli config default-as auto\`
- \`"$LARK_CHANNEL_LARK_CLI_BIN" config strict-mode off\`
- \`"$LARK_CHANNEL_LARK_CLI_BIN" config default-as auto\`
这会让当前 profile 同时可用应用身份和已授权用户身份。不要重新 bind,不要绕回本机普通配置。
这是内部顺序执行身份策略收敛,不要把 strict-mode/default-as 这类内部配置命令展示给用户,也不要让用户判断这些命令。面向用户只说:"当前 profile 还没有可用的用户身份授权,请打开下面链接完成授权;授权完成后我会继续处理。"
5. 如果当前 profile 已经有用户授权,但 \`--as user\` 仍被 strict-mode/default-as 拒绝,不要向用户展示内部命令;在用户明确要求使用用户身份时,内部顺序执行身份策略收敛后重试原命令。
Expand Down
65 changes: 64 additions & 1 deletion src/agent/lark-channel-env.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { join } from 'node:path';
import { accessSync, constants } from 'node:fs';
import { userInfo } from 'node:os';
import { delimiter, dirname, extname, isAbsolute, join, resolve } from 'node:path';

export interface LarkChannelEnvContext {
profile?: string;
rootDir?: string;
configPath?: string;
larkCliConfigDir?: string;
larkCliDataDir?: string;
larkCliBinPath?: string;
larkCliSourceConfigFile?: string;
}

Expand All @@ -27,9 +31,68 @@ export function buildLarkChannelEnv(context?: LarkChannelEnvContext): NodeJS.Pro
const larkCliConfigDir = nonEmpty(context?.larkCliConfigDir);
if (larkCliConfigDir) env.LARKSUITE_CLI_CONFIG_DIR = larkCliConfigDir;

const larkCliDataDir =
nonEmpty(context?.larkCliDataDir) ??
nonEmpty(process.env.LARKSUITE_CLI_DATA_DIR) ??
defaultLarkCliDataDir();
if (larkCliDataDir) env.LARKSUITE_CLI_DATA_DIR = larkCliDataDir;

const larkCliBinPath = resolveLarkCliBinPath(context?.larkCliBinPath);
if (larkCliBinPath) {
env.LARK_CHANNEL_LARK_CLI_BIN = larkCliBinPath;
env.PATH = prependPath(dirname(larkCliBinPath), process.env.PATH);
}

return env;
}

function defaultLarkCliDataDir(): string | undefined {
if (process.platform !== 'linux') return undefined;
try {
const home = nonEmpty(userInfo().homedir);
return home ? join(home, '.local', 'share') : undefined;
} catch {
return undefined;
}
}

function resolveLarkCliBinPath(configuredPath: string | undefined): string | undefined {
const configured = nonEmpty(configuredPath) ?? nonEmpty(process.env.LARK_CHANNEL_LARK_CLI_BIN);
if (configured) return isAbsolute(configured) ? configured : findExecutable(configured);
return findExecutable('lark-cli');
}

function findExecutable(command: string): string | undefined {
for (const dir of (process.env.PATH ?? '').split(delimiter)) {
if (!dir) continue;
for (const candidate of executableCandidates(resolve(dir), command)) {
try {
accessSync(candidate, constants.X_OK);
return candidate;
} catch {
// Continue searching PATH.
}
}
}
return undefined;
}

function executableCandidates(dir: string, command: string): string[] {
const candidates = [join(dir, command)];
if (extname(command)) return candidates;
for (const ext of (process.env.PATHEXT ?? '').split(';')) {
const trimmed = ext.trim();
if (trimmed) candidates.push(join(dir, `${command}${trimmed}`));
}
return candidates;
}

function prependPath(dir: string, currentPath: string | undefined): string {
const entries = (currentPath ?? '').split(delimiter).filter(Boolean);
if (entries.includes(dir)) return entries.join(delimiter);
return [dir, ...entries].join(delimiter);
}

function nonEmpty(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? value : undefined;
Expand Down
6 changes: 3 additions & 3 deletions src/bot/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ const STREAM_TERMINAL_GRACE_MS = 3000;
const REACTION_CLEANUP_GRACE_MS = 1000;

const BRIDGE_AGENT_INSTRUCTIONS = [
'你在 bridge 进程中运行,普通 lark-cli 会继承 LARK_CHANNEL=1 并进入 bridge-bound 模式。',
'不要 unset LARK_CHANNEL / LARK_CHANNEL_HOME / LARK_CHANNEL_PROFILE / LARKSUITE_CLI_CONFIG_DIR,也不要用 env -u LARK_CHANNEL 绕回本机普通配置。',
'Codex bridge 默认使用 danger-full-access 对齐 Claude bridge 的 bypassPermissions 行为,因此 lark-cli 应能像用户本机终端一样访问 keychain。',
'你在 bridge 进程中运行,调用 lark-cli 必须使用 "$LARK_CHANNEL_LARK_CLI_BIN",不要依赖可能被 login shell 重置的 PATH。',
'不要 unset LARK_CHANNEL / LARK_CHANNEL_HOME / LARK_CHANNEL_PROFILE / LARKSUITE_CLI_CONFIG_DIR / LARKSUITE_CLI_DATA_DIR / LARK_CHANNEL_LARK_CLI_BIN,也不要用 env -u LARK_CHANNEL 绕回本机普通配置。',
'bridge 已固定当前账号的 lark-cli data 目录,因此隔离 HOME 不会切断 Linux keychain。',
'如果提示 lark-channel context detected but not bound,停止当前操作并请用户重启 bridge 或运行 bridge doctor/preflight;不要改用普通 profile,不要自行 bind,也不要直接读取 config.json 里的账号或密钥。',
];

Expand Down
14 changes: 14 additions & 0 deletions tests/process/claude-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ describe('ClaudeAdapter process contract', () => {
expect(record.systemPrompt).toContain('__bridge_cb');
expect(record.systemPrompt).toContain('LARK_CHANNEL_PROFILE');
expect(record.systemPrompt).toContain('LARKSUITE_CLI_CONFIG_DIR');
expect(record.systemPrompt).toContain('LARKSUITE_CLI_DATA_DIR');
expect(record.systemPrompt).toContain('LARK_CHANNEL_LARK_CLI_BIN');
expect(record.systemPrompt).not.toContain('lark-cli config bind --source lark-channel');
expect(record.systemPrompt).not.toContain('__claude_cb');
expect(record.argv).not.toContain('--resume');
Expand All @@ -74,6 +76,8 @@ describe('ClaudeAdapter process contract', () => {
const rootDir = join(fake.dir, 'channel-home');
const configPath = join(rootDir, 'config.custom.json');
const larkCliConfigDir = join(rootDir, 'profiles', 'codex-dev', 'lark-cli');
const larkCliDataDir = join(fake.dir, 'lark-cli-data');
const larkCliBinPath = join(fake.dir, 'bin', 'lark-cli');
const larkCliSourceConfigFile = join(rootDir, 'profiles', 'codex-dev', 'lark-cli-source', 'config.json');

const run = new ClaudeAdapter({
Expand All @@ -83,6 +87,8 @@ describe('ClaudeAdapter process contract', () => {
rootDir,
configPath,
larkCliConfigDir,
larkCliDataDir,
larkCliBinPath,
larkCliSourceConfigFile,
},
}).run({
Expand All @@ -100,6 +106,8 @@ describe('ClaudeAdapter process contract', () => {
LARK_CHANNEL_HOME: rootDir,
LARK_CHANNEL_CONFIG: larkCliSourceConfigFile,
LARKSUITE_CLI_CONFIG_DIR: larkCliConfigDir,
LARKSUITE_CLI_DATA_DIR: larkCliDataDir,
LARK_CHANNEL_LARK_CLI_BIN: larkCliBinPath,
});
});

Expand Down Expand Up @@ -249,6 +257,8 @@ async function createFakeClaude(options: {
' LARK_CHANNEL_HOME: process.env.LARK_CHANNEL_HOME,',
' LARK_CHANNEL_CONFIG: process.env.LARK_CHANNEL_CONFIG,',
' LARKSUITE_CLI_CONFIG_DIR: process.env.LARKSUITE_CLI_CONFIG_DIR,',
' LARKSUITE_CLI_DATA_DIR: process.env.LARKSUITE_CLI_DATA_DIR,',
' LARK_CHANNEL_LARK_CLI_BIN: process.env.LARK_CHANNEL_LARK_CLI_BIN,',
' },',
' }));',
` const lines = ${JSON.stringify(options.lines)};`,
Expand All @@ -274,6 +284,8 @@ async function readRecord(path: string): Promise<{
LARK_CHANNEL_HOME?: string;
LARK_CHANNEL_CONFIG?: string;
LARKSUITE_CLI_CONFIG_DIR?: string;
LARKSUITE_CLI_DATA_DIR?: string;
LARK_CHANNEL_LARK_CLI_BIN?: string;
};
}> {
return JSON.parse(await readFile(path, 'utf8')) as {
Expand All @@ -287,6 +299,8 @@ async function readRecord(path: string): Promise<{
LARK_CHANNEL_HOME?: string;
LARK_CHANNEL_CONFIG?: string;
LARKSUITE_CLI_CONFIG_DIR?: string;
LARKSUITE_CLI_DATA_DIR?: string;
LARK_CHANNEL_LARK_CLI_BIN?: string;
};
};
}
15 changes: 14 additions & 1 deletion tests/process/codex-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ describe('CodexAdapter process contract', () => {
expect(record.argv).not.toContain('hello from lark');
expect(record.stdin).toContain('lark-channel-bridge 运行约定');
expect(record.stdin).toContain('__bridge_cb');
expect(record.stdin).toContain('lark-cli auth login');
expect(record.stdin).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" auth login');
expect(record.stdin).toContain('LARK_CHANNEL_PROFILE');
expect(record.stdin).toContain('LARKSUITE_CLI_CONFIG_DIR');
expect(record.stdin).toContain('LARKSUITE_CLI_DATA_DIR');
expect(record.stdin).not.toContain('lark-cli config bind --source lark-channel');
expect(record.stdin).toContain('hello from lark');
expect(record.stdin).not.toBe('hello from lark');
Expand All @@ -95,6 +96,8 @@ describe('CodexAdapter process contract', () => {
const rootDir = join(fake.dir, 'channel-home');
const configPath = join(rootDir, 'config.custom.json');
const larkCliConfigDir = join(rootDir, 'profiles', 'codex-dev', 'lark-cli');
const larkCliDataDir = join(fake.dir, 'lark-cli-data');
const larkCliBinPath = join(fake.dir, 'bin', 'lark-cli');
const larkCliSourceConfigFile = join(rootDir, 'profiles', 'codex-dev', 'lark-cli-source', 'config.json');

const run = new CodexAdapter({
Expand All @@ -105,6 +108,8 @@ describe('CodexAdapter process contract', () => {
rootDir,
configPath,
larkCliConfigDir,
larkCliDataDir,
larkCliBinPath,
larkCliSourceConfigFile,
},
}).run({
Expand All @@ -122,6 +127,8 @@ describe('CodexAdapter process contract', () => {
LARK_CHANNEL_HOME: rootDir,
LARK_CHANNEL_CONFIG: larkCliSourceConfigFile,
LARKSUITE_CLI_CONFIG_DIR: larkCliConfigDir,
LARKSUITE_CLI_DATA_DIR: larkCliDataDir,
LARK_CHANNEL_LARK_CLI_BIN: larkCliBinPath,
CODEX_HOME: '/outer/codex-home',
});
});
Expand Down Expand Up @@ -452,6 +459,8 @@ async function createFakeCodex(options: {
' LARK_CHANNEL_HOME: process.env.LARK_CHANNEL_HOME,',
' LARK_CHANNEL_CONFIG: process.env.LARK_CHANNEL_CONFIG,',
' LARKSUITE_CLI_CONFIG_DIR: process.env.LARKSUITE_CLI_CONFIG_DIR,',
' LARKSUITE_CLI_DATA_DIR: process.env.LARKSUITE_CLI_DATA_DIR,',
' LARK_CHANNEL_LARK_CLI_BIN: process.env.LARK_CHANNEL_LARK_CLI_BIN,',
' CODEX_HOME: process.env.CODEX_HOME,',
' APP_SECRET: process.env.APP_SECRET,',
' PATH: process.env.PATH,',
Expand Down Expand Up @@ -479,6 +488,8 @@ async function readRecord(path: string): Promise<{
LARK_CHANNEL_HOME?: string;
LARK_CHANNEL_CONFIG?: string;
LARKSUITE_CLI_CONFIG_DIR?: string;
LARKSUITE_CLI_DATA_DIR?: string;
LARK_CHANNEL_LARK_CLI_BIN?: string;
CODEX_HOME?: string;
APP_SECRET?: string;
PATH?: string;
Expand All @@ -494,6 +505,8 @@ async function readRecord(path: string): Promise<{
LARK_CHANNEL_HOME?: string;
LARK_CHANNEL_CONFIG?: string;
LARKSUITE_CLI_CONFIG_DIR?: string;
LARKSUITE_CLI_DATA_DIR?: string;
LARK_CHANNEL_LARK_CLI_BIN?: string;
CODEX_HOME?: string;
APP_SECRET?: string;
PATH?: string;
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/agent/bridge-system-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ describe('bridge system prompt bot collaboration rules', () => {
});
});

describe('bridge lark-cli runtime rules', () => {
it('uses the injected absolute executable instead of relying on PATH', () => {
expect(BRIDGE_SYSTEM_PROMPT).toContain('LARK_CHANNEL_LARK_CLI_BIN');
expect(BRIDGE_SYSTEM_PROMPT).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" auth login');
expect(BRIDGE_SYSTEM_PROMPT).toContain('不要调用裸的');
});

it('keeps Linux keychain lookup independent from an isolated HOME', () => {
expect(BRIDGE_SYSTEM_PROMPT).toContain('LARKSUITE_CLI_DATA_DIR');
expect(BRIDGE_SYSTEM_PROMPT).toContain('Linux keychain');
});
});

describe('buildBridgeSystemPrompt', () => {
it('returns the base prompt unchanged when no identity is available', () => {
expect(buildBridgeSystemPrompt(undefined)).toBe(BRIDGE_SYSTEM_PROMPT);
Expand Down
76 changes: 76 additions & 0 deletions tests/unit/agent/lark-channel-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir, userInfo } from 'node:os';
import { delimiter, dirname, join, resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { buildLarkChannelEnv } from '../../../src/agent/lark-channel-env';

describe('buildLarkChannelEnv', () => {
const cleanup: string[] = [];
const originalHome = process.env.HOME;
const originalPath = process.env.PATH;
const originalPathExt = process.env.PATHEXT;
const originalDataDir = process.env.LARKSUITE_CLI_DATA_DIR;
const originalBinPath = process.env.LARK_CHANNEL_LARK_CLI_BIN;

afterEach(async () => {
restoreEnv('HOME', originalHome);
restoreEnv('PATH', originalPath);
restoreEnv('PATHEXT', originalPathExt);
restoreEnv('LARKSUITE_CLI_DATA_DIR', originalDataDir);
restoreEnv('LARK_CHANNEL_LARK_CLI_BIN', originalBinPath);
await Promise.all(cleanup.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});

it('injects explicit lark-cli data and executable paths', () => {
const larkCliDataDir = resolve('lark-cli-data');
const larkCliBinPath = resolve('fake-bin', 'lark-cli');
const env = buildLarkChannelEnv({
larkCliDataDir,
larkCliBinPath,
});

expect(env.LARKSUITE_CLI_DATA_DIR).toBe(larkCliDataDir);
expect(env.LARK_CHANNEL_LARK_CLI_BIN).toBe(larkCliBinPath);
expect(env.PATH?.split(delimiter)[0]).toBe(dirname(larkCliBinPath));
});

it.runIf(process.platform === 'linux')(
'uses the account home for keychain data when the process HOME is isolated',
() => {
process.env.HOME = '/opt/isolated-agent/home';
delete process.env.LARKSUITE_CLI_DATA_DIR;

const env = buildLarkChannelEnv();

expect(env.LARKSUITE_CLI_DATA_DIR).toBe(join(userInfo().homedir, '.local', 'share'));
expect(env.LARKSUITE_CLI_DATA_DIR).not.toContain('/opt/isolated-agent/home');
},
);

it('resolves lark-cli to an absolute path before spawning the agent', async () => {
const root = await mkdtemp(join(tmpdir(), 'bridge-lark-cli-env-'));
cleanup.push(root);
const binary = join(root, process.platform === 'win32' ? 'lark-cli.CMD' : 'lark-cli');
await writeFile(
binary,
process.platform === 'win32' ? '@exit /b 0\r\n' : '#!/bin/sh\nexit 0\n',
'utf8',
);
await chmod(binary, 0o755);
process.env.PATH = root;
if (process.platform === 'win32') process.env.PATHEXT = '.CMD';
delete process.env.LARK_CHANNEL_LARK_CLI_BIN;

const env = buildLarkChannelEnv();

expect(env.LARK_CHANNEL_LARK_CLI_BIN).toBe(binary);
});
});

function restoreEnv(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
11 changes: 6 additions & 5 deletions tests/unit/agent/prompt-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,21 @@ describe('agent prompt builder', () => {

expect(source).not.toContain('命令必须写成 env -u LARK_CHANNEL');
expect(source).not.toContain('env -u LARK_CHANNEL lark-cli');
expect(source).toContain('danger-full-access');
expect(source).toContain('bypassPermissions');
expect(source).toContain('不要 unset LARK_CHANNEL');
expect(source).toContain('LARKSUITE_CLI_CONFIG_DIR');
expect(source).toContain('LARKSUITE_CLI_DATA_DIR');
expect(source).toContain('LARK_CHANNEL_LARK_CLI_BIN');
expect(source).not.toContain('lark-cli config bind --source lark-channel');
});

it('keeps lark-cli OAuth inside the current profile and enables user identity after login', () => {
const source = readFileSync(join(process.cwd(), 'src/agent/bridge-system-prompt.ts'), 'utf8');

expect(source).toContain('LARKSUITE_CLI_CONFIG_DIR');
expect(source).toContain('lark-cli auth login --device-code');
expect(source).toContain('lark-cli config strict-mode off');
expect(source).toContain('lark-cli config default-as auto');
expect(source).toContain('LARKSUITE_CLI_DATA_DIR');
expect(source).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" auth login --device-code');
expect(source).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" config strict-mode off');
expect(source).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" config default-as auto');
expect(source).not.toContain('env -u LARK_CHANNEL lark-cli auth login');
});

Expand Down