codelark 0.1.0 → 0.1.1

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.
@@ -27,6 +27,7 @@ function printUsage(): void {
27
27
  ' --app-secret <secret> App Secret for the isolated smoke app; prefer env file/env vars to avoid npm echo',
28
28
  ' --site <feishu|lark> Site brand; default feishu',
29
29
  ' --keep-temp Keep temporary root for diagnosis; default cleans it in success and failure paths',
30
+ ' --skip-lark-cli-bind Test-only: write the private lark-cli runtime projection without invoking macOS Keychain',
30
31
  ' --simulate-failure-after-sync Test cleanup on a post-lark-cli failure',
31
32
  ' --help Show this help',
32
33
  '',
@@ -67,6 +68,38 @@ function assertInside(parentPath: string, childPath: string): void {
67
68
  throw new Error(`Path escaped temp root: ${child}`);
68
69
  }
69
70
 
71
+ function writeJsonFile(filePath: string, value: unknown): void {
72
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
73
+ const tmpPath = `${filePath}.tmp`;
74
+ fs.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
75
+ fs.renameSync(tmpPath, filePath);
76
+ }
77
+
78
+ function writeTestLarkCliRuntimeProjection(options: {
79
+ sourcePath: string;
80
+ runtimePath: string;
81
+ appId: string;
82
+ appSecret: string;
83
+ site: FeishuSite;
84
+ }): void {
85
+ writeJsonFile(options.sourcePath, {
86
+ accounts: {
87
+ app: {
88
+ id: options.appId,
89
+ secret: options.appSecret,
90
+ tenant: options.site,
91
+ },
92
+ },
93
+ });
94
+ writeJsonFile(options.runtimePath, {
95
+ apps: [{
96
+ appId: options.appId,
97
+ appSecret: options.appSecret,
98
+ brand: options.site,
99
+ }],
100
+ });
101
+ }
102
+
70
103
  async function main(): Promise<void> {
71
104
  const argv = process.argv.slice(2);
72
105
  if (hasFlag(argv, '--help')) {
@@ -85,6 +118,7 @@ async function main(): Promise<void> {
85
118
  path.join(os.tmpdir(), `clk-setup-wizard-real-e2e-${Date.now()}`),
86
119
  ));
87
120
  const keepTemp = hasFlag(argv, '--keep-temp');
121
+ const skipLarkCliBind = hasFlag(argv, '--skip-lark-cli-bind');
88
122
  const appId = valueArg(
89
123
  argv,
90
124
  '--app-id',
@@ -105,22 +139,22 @@ async function main(): Promise<void> {
105
139
  const runtimeHome = path.join(runRoot, 'home');
106
140
  const codelarkHome = path.join(runRoot, 'clk-home');
107
141
  const workspaceRoot = path.join(runRoot, 'workspace');
108
- const larkConfigPath = path.join(runtimeHome, '.lark-cli', 'config.json');
142
+ const larkSourceConfigPath = path.join(codelarkHome, 'runtime', 'lark-cli-source', 'config.json');
143
+ const larkRuntimeConfigPath = path.join(codelarkHome, 'runtime', 'lark-cli', 'lark-channel', 'config.json');
109
144
  const configEnvPath = path.join(codelarkHome, 'config.env');
110
145
  const configJsonPath = path.join(codelarkHome, 'config.json');
146
+ const configTomlPath = path.join(codelarkHome, 'config.toml');
111
147
 
112
148
  try {
113
149
  assertInside(os.tmpdir(), runRoot);
114
150
  fs.mkdirSync(runtimeHome, { recursive: true });
115
151
  fs.mkdirSync(codelarkHome, { recursive: true });
116
152
  fs.mkdirSync(workspaceRoot, { recursive: true });
117
- fs.writeFileSync(configEnvPath, '# custom env survives setup\nCUSTOM_KEEP=1\n', { mode: 0o600 });
118
153
 
119
154
  process.env.HOME = runtimeHome;
120
155
  process.env.USERPROFILE = runtimeHome;
121
156
  process.env.CODELARK_HOME = codelarkHome;
122
157
 
123
- const configuration = await import('../src/configuration/index.js');
124
158
  const setupWizard = await import('../src/entrypoints/setup-wizard.js');
125
159
 
126
160
  const credentials = {
@@ -130,55 +164,56 @@ async function main(): Promise<void> {
130
164
  alias: 'codelark',
131
165
  };
132
166
 
133
- await setupWizard.syncLarkCliCredentials(credentials, runtimeHome);
134
- const entriesAfterSync = setupWizard.readLarkCliAppEntries(larkConfigPath);
135
- const syncedEntry = entriesAfterSync.find((entry) => entry.appId === appId);
136
- if (!syncedEntry) throw new Error(`lark-cli config missing synced app ${appId}`);
137
- if (syncedEntry.brand !== site) throw new Error(`lark-cli brand mismatch: ${syncedEntry.brand}`);
138
- if (syncedEntry.appSecret) {
139
- throw new Error('real lark-cli e2e expected lark-cli to avoid plaintext appSecret in this environment');
140
- }
141
- if (syncedEntry.secretStorage !== 'keychain') {
142
- throw new Error(`expected keychain/local encrypted lark-cli secret storage, got ${syncedEntry.secretStorage}`);
143
- }
144
- const importableApps = setupWizard.readLarkCliApps(larkConfigPath);
145
- const importableApp = importableApps.find((entry) => entry.appId === appId);
146
- if (!importableApp) throw new Error(`CodeLark could not import synced lark-cli app ${appId}`);
147
- if (importableApp.appSecret !== appSecret) {
148
- throw new Error('CodeLark did not recover App Secret from lark-cli local encrypted storage');
167
+ const current = setupWizard.loadSetupConfig(codelarkHome);
168
+ setupWizard.saveSetupConfigToHomeToml(
169
+ setupWizard.buildSetupConfig(current, credentials, 'codex', workspaceRoot),
170
+ codelarkHome,
171
+ );
172
+
173
+ const savedConfig = setupWizard.loadSetupConfig(codelarkHome);
174
+ if (skipLarkCliBind) {
175
+ writeTestLarkCliRuntimeProjection({
176
+ sourcePath: larkSourceConfigPath,
177
+ runtimePath: larkRuntimeConfigPath,
178
+ appId,
179
+ appSecret,
180
+ site,
181
+ });
182
+ } else {
183
+ const localService = await import('../src/local-service/manager.js');
184
+ const larkRuntime = await localService.ensureLarkCliRuntimeConfig(savedConfig, { allowUserAuthorization: true });
185
+ if (larkRuntime.warning) throw new Error(larkRuntime.warning);
186
+ if (!larkRuntime.ready) throw new Error('CodeLark private lark-cli runtime was not initialized');
149
187
  }
150
188
  if (hasFlag(argv, '--simulate-failure-after-sync')) {
151
189
  throw new Error('simulated setup wizard real e2e failure after lark-cli sync');
152
190
  }
153
191
 
154
- const current = configuration.loadConfig();
155
- configuration.saveConfig(setupWizard.buildSetupConfig(current, credentials, 'codex', workspaceRoot));
156
-
157
- const savedConfig = configuration.loadConfig();
158
192
  const savedFeishu = savedConfig.channels?.find((channel) => channel.provider === 'feishu');
159
- if (savedConfig.runtime !== 'codex') throw new Error(`runtime mismatch: ${savedConfig.runtime}`);
160
- if (savedConfig.defaultWorkspaceRoot !== workspaceRoot) {
161
- throw new Error(`workspace mismatch: ${savedConfig.defaultWorkspaceRoot}`);
193
+ if (savedConfig.runtime.agent !== 'codex') throw new Error(`runtime mismatch: ${savedConfig.runtime.agent}`);
194
+ if (savedConfig.bridge.defaultWorkspace !== workspaceRoot) {
195
+ throw new Error(`workspace mismatch: ${savedConfig.bridge.defaultWorkspace}`);
162
196
  }
163
- if (savedFeishu?.config.appId !== appId) throw new Error('config.v1 appId mismatch');
164
- if (savedFeishu?.config.appSecret !== appSecret) throw new Error('config.v1 appSecret mismatch');
165
- if (savedFeishu?.config.site !== site) throw new Error('config.v1 site mismatch');
197
+ if (savedFeishu?.config.appId !== appId) throw new Error('config appId mismatch');
198
+ if (savedFeishu?.config.appSecret !== appSecret) throw new Error('config appSecret mismatch');
199
+ if (savedFeishu?.config.site !== site) throw new Error('config site mismatch');
166
200
 
167
- const envContent = fs.readFileSync(configEnvPath, 'utf-8');
168
- if (!envContent.includes('CUSTOM_KEEP=1')) throw new Error('custom config.env line was not preserved');
169
- if (!envContent.includes(`CODELARK_FEISHU_APP_ID=${appId}`)) throw new Error('config.env appId missing');
170
- if (!envContent.includes(`CODELARK_FEISHU_APP_SECRET=${appSecret}`)) throw new Error('config.env appSecret missing');
171
- if (!fs.existsSync(configJsonPath)) throw new Error('config.json missing');
201
+ if (!fs.existsSync(configTomlPath)) throw new Error('config.toml missing');
202
+ if (fs.existsSync(configEnvPath)) throw new Error('setup should not create config.env');
203
+ if (fs.existsSync(configJsonPath)) throw new Error('setup should not create config.json');
204
+ if (!fs.existsSync(larkSourceConfigPath)) throw new Error('CodeLark lark-cli source config missing');
205
+ if (!fs.existsSync(larkRuntimeConfigPath)) throw new Error('CodeLark private lark-cli runtime config missing');
172
206
 
173
207
  const result = {
174
208
  ok: true,
175
209
  runRoot,
176
210
  runtimeHome,
177
211
  codelarkHome,
178
- larkConfigPath,
212
+ larkSourceConfigPath,
213
+ larkRuntimeConfigPath,
179
214
  configEnvPath,
180
215
  configJsonPath,
181
- larkSecretStorage: syncedEntry.secretStorage,
216
+ configTomlPath,
182
217
  cleanedRunRoot: !keepTemp,
183
218
  };
184
219
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { createRequire } from 'node:module';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ import { parse } from 'smol-toml';
10
+ import { feishuSetupUserAuthScopeArgument } from '../src/channels/feishu/permissions.js';
11
+
12
+ type FeishuSite = 'feishu' | 'lark';
13
+
14
+ interface PtyProcess {
15
+ write(data: string): void;
16
+ kill(signal?: string): void;
17
+ onData(callback: (data: string) => void): void;
18
+ onExit(callback: (event: { exitCode: number; signal?: number }) => void): void;
19
+ }
20
+
21
+ interface PtyModule {
22
+ spawn(
23
+ command: string,
24
+ args: string[],
25
+ options: { name: string; cols: number; rows: number; cwd: string; env: NodeJS.ProcessEnv },
26
+ ): PtyProcess;
27
+ }
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+
32
+ function valueArg(args: string[], name: string, fallback = ''): string {
33
+ const index = args.indexOf(name);
34
+ if (index < 0) return fallback;
35
+ return args[index + 1] || fallback;
36
+ }
37
+
38
+ function hasFlag(args: string[], name: string): boolean {
39
+ return args.includes(name);
40
+ }
41
+
42
+ function printUsage(): void {
43
+ process.stdout.write([
44
+ 'Usage:',
45
+ ' CODELARK_SETUP_WIZARD_REAL_E2E=1 npm run real:setup-wizard:wizard-e2e -- [options]',
46
+ '',
47
+ 'Options:',
48
+ ' --run-root <path> Temporary root; default /tmp/clk-setup-wizard-wizard-e2e-<timestamp>',
49
+ ' --timeout-ms <number> Overall wizard timeout; default 600000',
50
+ ' --keep-temp Keep temporary root for diagnosis; default cleans it after success',
51
+ ' --help Show this help',
52
+ '',
53
+ 'The script starts the real setup wizard in a fresh mock HOME, accepts default',
54
+ 'answers, prints setup/login URLs for manual scanning, then writes the created',
55
+ `app credentials to ${defaultRealFeishuTestEnvFile()}.`,
56
+ '',
57
+ ].join('\n'));
58
+ }
59
+
60
+ function assertInside(parentPath: string, childPath: string): void {
61
+ const parent = path.resolve(parentPath);
62
+ const child = path.resolve(childPath);
63
+ const relative = path.relative(parent, child);
64
+ if (relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))) return;
65
+ throw new Error(`Path escaped temp root: ${child}`);
66
+ }
67
+
68
+ function npxCommand(): string {
69
+ return process.platform === 'win32' ? 'npx.cmd' : 'npx';
70
+ }
71
+
72
+ function defaultRealFeishuTestEnvFile(): string {
73
+ const codelarkHome = process.env.CODELARK_HOME || path.join(os.homedir(), '.codelark');
74
+ return path.join(codelarkHome, 'real-feishu-e2e', 'test.env');
75
+ }
76
+
77
+ async function loadPtyModule(): Promise<PtyModule> {
78
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise<unknown>;
79
+ const loaded = await dynamicImport('@homebridge/node-pty-prebuilt-multiarch') as { default?: unknown };
80
+ return (loaded.default || loaded) as PtyModule;
81
+ }
82
+
83
+ function runCommand(
84
+ command: string,
85
+ args: string[],
86
+ options: { cwd: string; env: NodeJS.ProcessEnv; timeoutMs: number },
87
+ ): Promise<{ code: number; stdout: string; stderr: string }> {
88
+ return new Promise((resolve, reject) => {
89
+ const child = spawn(command, args, {
90
+ cwd: options.cwd,
91
+ env: options.env,
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ });
94
+ let stdout = '';
95
+ let stderr = '';
96
+ const timeout = setTimeout(() => {
97
+ child.kill('SIGTERM');
98
+ reject(new Error(`${command} ${args.join(' ')} timed out after ${options.timeoutMs}ms`));
99
+ }, options.timeoutMs);
100
+ child.stdout?.setEncoding('utf8');
101
+ child.stderr?.setEncoding('utf8');
102
+ child.stdout?.on('data', (chunk) => { stdout += chunk; });
103
+ child.stderr?.on('data', (chunk) => { stderr += chunk; });
104
+ child.on('error', (error) => {
105
+ clearTimeout(timeout);
106
+ reject(error);
107
+ });
108
+ child.on('close', (code) => {
109
+ clearTimeout(timeout);
110
+ resolve({ code: code ?? 1, stdout, stderr });
111
+ });
112
+ });
113
+ }
114
+
115
+ async function runWizardWithDefaults(options: {
116
+ env: NodeJS.ProcessEnv;
117
+ timeoutMs: number;
118
+ }): Promise<string> {
119
+ const pty = await loadPtyModule();
120
+ const child = pty.spawn(npxCommand(), ['tsx', 'src/entrypoints/cli.ts', 'setup'], {
121
+ name: 'xterm-256color',
122
+ cols: 120,
123
+ rows: 40,
124
+ cwd: packageRoot,
125
+ env: options.env,
126
+ });
127
+
128
+ let output = '';
129
+ const printedUrls = new Set<string>();
130
+ let defaultConfirmCount = 0;
131
+ const maxDefaultConfirms = 40;
132
+ let exited = false;
133
+
134
+ const defaultInput = setInterval(() => {
135
+ if (exited || defaultConfirmCount >= maxDefaultConfirms) return;
136
+ child.write('\r');
137
+ defaultConfirmCount += 1;
138
+ }, 900);
139
+
140
+ return await new Promise<string>((resolve, reject) => {
141
+ const timeout = setTimeout(() => {
142
+ clearInterval(defaultInput);
143
+ child.kill('SIGTERM');
144
+ reject(new Error(`setup wizard timed out after ${options.timeoutMs}ms`));
145
+ }, options.timeoutMs);
146
+
147
+ child.onData((data) => {
148
+ output += data;
149
+ process.stdout.write(data);
150
+ for (const match of output.matchAll(/https?:\/\/[^\s<>"'`]+/giu)) {
151
+ const url = match[0].replace(/[),.;\]},。;)】]+$/u, '');
152
+ if (!url || printedUrls.has(url)) continue;
153
+ printedUrls.add(url);
154
+ process.stdout.write(`\n[setup-wizard-real-wizard-e2e] 授权链接:${url}\n`);
155
+ }
156
+ if (output.length > 80_000) output = output.slice(-40_000);
157
+ });
158
+
159
+ child.onExit((event) => {
160
+ exited = true;
161
+ clearInterval(defaultInput);
162
+ clearTimeout(timeout);
163
+ if (event.exitCode === 0) {
164
+ resolve(output);
165
+ return;
166
+ }
167
+ reject(new Error(`setup wizard exited with ${event.signal || event.exitCode}\n${output.slice(-4000)}`));
168
+ });
169
+ });
170
+ }
171
+
172
+ interface CreatedWizardCredentials {
173
+ appId: string;
174
+ appSecret: string;
175
+ site: FeishuSite;
176
+ }
177
+
178
+ function buildLarkCliRuntimeEnv(codelarkHome: string): NodeJS.ProcessEnv {
179
+ return {
180
+ LARK_CHANNEL: '1',
181
+ LARK_CHANNEL_HOME: codelarkHome,
182
+ LARK_CHANNEL_CONFIG: path.join(codelarkHome, 'runtime', 'lark-cli-source', 'config.json'),
183
+ LARKSUITE_CLI_CONFIG_DIR: path.join(codelarkHome, 'runtime', 'lark-cli'),
184
+ };
185
+ }
186
+
187
+ async function assertLarkCliAuthorization(options: {
188
+ env: NodeJS.ProcessEnv;
189
+ timeoutMs: number;
190
+ }): Promise<void> {
191
+ const larkCliScript = require.resolve('@larksuite/cli/scripts/run.js');
192
+ const status = await runCommand(process.execPath, [larkCliScript, 'auth', 'status'], {
193
+ cwd: packageRoot,
194
+ env: options.env,
195
+ timeoutMs: options.timeoutMs,
196
+ });
197
+ if (status.code !== 0) {
198
+ throw new Error(`lark-cli auth status failed\n${status.stdout}\n${status.stderr}`);
199
+ }
200
+
201
+ const check = await runCommand(
202
+ process.execPath,
203
+ [larkCliScript, 'auth', 'check', '--scope', feishuSetupUserAuthScopeArgument()],
204
+ {
205
+ cwd: packageRoot,
206
+ env: options.env,
207
+ timeoutMs: options.timeoutMs,
208
+ },
209
+ );
210
+ if (check.code !== 0) {
211
+ throw new Error(`lark-cli auth check failed\n${check.stdout}\n${check.stderr}`);
212
+ }
213
+ }
214
+
215
+ function assertCodeLarkConfig(options: {
216
+ codelarkHome: string;
217
+ workspaceRoot: string;
218
+ }): CreatedWizardCredentials {
219
+ const configTomlPath = path.join(options.codelarkHome, 'config.toml');
220
+ const configJsonPath = path.join(options.codelarkHome, 'config.json');
221
+ const configEnvPath = path.join(options.codelarkHome, 'config.env');
222
+ const larkCliRuntimeConfigPath = path.join(options.codelarkHome, 'runtime', 'lark-cli', 'lark-channel', 'config.json');
223
+ const parsed = parse(fs.readFileSync(configTomlPath, 'utf-8')) as {
224
+ runtime?: { provider?: string };
225
+ bridge?: { default_workspace?: string };
226
+ channels?: Array<{ provider?: string; enabled?: boolean; config?: { app_id?: string; app_secret?: string; site?: string } }>;
227
+ };
228
+ const feishu = parsed.channels?.find((channel) => channel.provider === 'feishu');
229
+
230
+ if (parsed.runtime?.provider !== 'codex') throw new Error(`runtime provider mismatch: ${parsed.runtime?.provider}`);
231
+ if (parsed.bridge?.default_workspace !== options.workspaceRoot) {
232
+ throw new Error(`workspace mismatch: ${parsed.bridge?.default_workspace}`);
233
+ }
234
+ if (feishu?.enabled !== true) throw new Error('Feishu channel is not enabled');
235
+ const appId = feishu?.config?.app_id?.trim();
236
+ const appSecret = feishu?.config?.app_secret?.trim();
237
+ const site = feishu?.config?.site === 'lark' ? 'lark' : 'feishu';
238
+ if (!appId) throw new Error('CodeLark config appId missing');
239
+ if (!appSecret) throw new Error('CodeLark config appSecret missing');
240
+ if (fs.existsSync(configEnvPath)) throw new Error('setup should not create config.env');
241
+ if (fs.existsSync(configJsonPath)) throw new Error('setup should not create config.json');
242
+ if (!fs.existsSync(larkCliRuntimeConfigPath)) throw new Error('CodeLark private lark-cli runtime config missing');
243
+ return { appId, appSecret, site };
244
+ }
245
+
246
+ function writeDefaultRealFeishuTestEnvFile(filePath: string, credentials: CreatedWizardCredentials): void {
247
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
248
+ const tmpPath = `${filePath}.tmp`;
249
+ fs.writeFileSync(
250
+ tmpPath,
251
+ [
252
+ '# Generated by setup-wizard-real-wizard-e2e.ts',
253
+ `CODELARK_REAL_FEISHU_TEST_APP_ID=${credentials.appId}`,
254
+ `CODELARK_REAL_FEISHU_TEST_APP_SECRET=${credentials.appSecret}`,
255
+ `CODELARK_REAL_FEISHU_TEST_SITE=${credentials.site}`,
256
+ '',
257
+ ].join('\n'),
258
+ { mode: 0o600 },
259
+ );
260
+ fs.renameSync(tmpPath, filePath);
261
+ }
262
+
263
+ async function main(): Promise<void> {
264
+ const argv = process.argv.slice(2);
265
+ if (hasFlag(argv, '--help')) {
266
+ printUsage();
267
+ return;
268
+ }
269
+ if (process.env.CODELARK_SETUP_WIZARD_REAL_E2E !== '1') {
270
+ throw new Error('Refusing to run setup wizard real e2e without CODELARK_SETUP_WIZARD_REAL_E2E=1.');
271
+ }
272
+
273
+ const outputTestEnvFile = defaultRealFeishuTestEnvFile();
274
+ const runRoot = path.resolve(valueArg(
275
+ argv,
276
+ '--run-root',
277
+ path.join(os.tmpdir(), `clk-setup-wizard-wizard-e2e-${Date.now()}`),
278
+ ));
279
+ const keepTemp = hasFlag(argv, '--keep-temp');
280
+ const timeoutMs = Number(valueArg(argv, '--timeout-ms', '600000'));
281
+
282
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
283
+ throw new Error(`Invalid --timeout-ms: ${timeoutMs}`);
284
+ }
285
+
286
+ const runtimeHome = path.join(runRoot, 'home');
287
+ const codelarkHome = path.join(runRoot, 'codelark-home');
288
+ const workspaceRoot = path.join(runRoot, 'workspace');
289
+ const codexHome = path.join(runtimeHome, '.codex');
290
+
291
+ try {
292
+ assertInside(os.tmpdir(), runRoot);
293
+ fs.mkdirSync(runtimeHome, { recursive: true });
294
+ fs.mkdirSync(codexHome, { recursive: true });
295
+ fs.mkdirSync(workspaceRoot, { recursive: true });
296
+
297
+ const env = {
298
+ ...process.env,
299
+ HOME: runtimeHome,
300
+ USERPROFILE: runtimeHome,
301
+ CODEX_HOME: codexHome,
302
+ CODELARK_HOME: codelarkHome,
303
+ FORCE_COLOR: '0',
304
+ NO_COLOR: '1',
305
+ TERM: 'xterm-256color',
306
+ };
307
+ delete env.CI;
308
+
309
+ await runWizardWithDefaults({ env, timeoutMs });
310
+
311
+ const larkCliEnv = {
312
+ ...env,
313
+ ...buildLarkCliRuntimeEnv(codelarkHome),
314
+ };
315
+ await assertLarkCliAuthorization({ env: larkCliEnv, timeoutMs: 60_000 });
316
+ const credentials = assertCodeLarkConfig({ codelarkHome, workspaceRoot });
317
+ writeDefaultRealFeishuTestEnvFile(outputTestEnvFile, credentials);
318
+
319
+ const result = {
320
+ ok: true,
321
+ runRoot,
322
+ runtimeHome,
323
+ codelarkHome,
324
+ workspaceRoot,
325
+ testEnvFile: outputTestEnvFile,
326
+ appId: credentials.appId,
327
+ site: credentials.site,
328
+ larkCliRuntimeDir: path.join(codelarkHome, 'runtime', 'lark-cli'),
329
+ cleanedRunRoot: !keepTemp,
330
+ };
331
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
332
+ } finally {
333
+ if (!keepTemp) {
334
+ fs.rmSync(runRoot, { recursive: true, force: true });
335
+ }
336
+ }
337
+ }
338
+
339
+ main().catch((error) => {
340
+ process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
341
+ process.exit(1);
342
+ });
@@ -36,7 +36,6 @@ $StatusFile = Join-Path $RuntimeDir 'status.json'
36
36
  $LogFile = Join-Path (Join-Path $CodelarkHome 'logs') 'bridge.log'
37
37
  $ErrLogFile = Join-Path (Join-Path $CodelarkHome 'logs') 'bridge.stderr.log'
38
38
  $DaemonMjs = Join-Path (Join-Path $SkillDir 'dist') 'daemon.mjs'
39
- $ConfigFile = Join-Path $CodelarkHome 'config.env'
40
39
 
41
40
  $ServiceName = 'CodeLarkBridge'
42
41
 
@@ -68,32 +67,6 @@ function Ensure-Built {
68
67
  }
69
68
  }
70
69
 
71
- function Get-ConfigEnvironment {
72
- $configEnv = @{}
73
- if (-not (Test-Path $ConfigFile)) {
74
- return $configEnv
75
- }
76
-
77
- foreach ($line in Get-Content $ConfigFile) {
78
- $trimmed = $line.Trim()
79
- if (-not $trimmed -or $trimmed.StartsWith('#')) { continue }
80
- $eqIndex = $trimmed.IndexOf('=')
81
- if ($eqIndex -lt 1) { continue }
82
-
83
- $name = $trimmed.Substring(0, $eqIndex).Trim()
84
- $value = $trimmed.Substring($eqIndex + 1).Trim()
85
- if (
86
- ($value.StartsWith('"') -and $value.EndsWith('"')) -or
87
- ($value.StartsWith("'") -and $value.EndsWith("'"))
88
- ) {
89
- $value = $value.Substring(1, $value.Length - 2)
90
- }
91
- $configEnv[$name] = $value
92
- }
93
-
94
- return $configEnv
95
- }
96
-
97
70
  function Read-Pid {
98
71
  if (Test-Path $PidFile) { return (Get-Content $PidFile -Raw).Trim() }
99
72
  return $null
@@ -144,7 +117,7 @@ function Show-FailureHelp {
144
117
  function Get-NodePath {
145
118
  $nodePath = (Get-Command node -ErrorAction SilentlyContinue).Source
146
119
  if (-not $nodePath) {
147
- Write-Error "Node.js not found in PATH. Install Node.js >= 20."
120
+ Write-Error "Node.js not found in PATH. Install Node.js >= 24."
148
121
  exit 1
149
122
  }
150
123
  return $nodePath
@@ -167,7 +140,6 @@ function Install-WinSWService {
167
140
  param([string]$WinSWPath)
168
141
  $nodePath = Get-NodePath
169
142
  $xmlPath = Join-Path $SkillDir "$ServiceName.xml"
170
- $configEnv = Get-ConfigEnvironment
171
143
 
172
144
  # Run as current user so the service can access ~/.codelark and Codex login state
173
145
  $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
@@ -204,13 +176,6 @@ function Install-WinSWService {
204
176
  </service>
205
177
  "@
206
178
 
207
- $extraEnvXml = ($configEnv.GetEnumerator() | ForEach-Object {
208
- ' <env name="{0}" value="{1}"/>' -f $_.Key, [System.Security.SecurityElement]::Escape($_.Value)
209
- }) -join "`r`n"
210
- if ($extraEnvXml) {
211
- $xml = $xml -replace ' <logpath>', "$extraEnvXml`r`n <logpath>"
212
- }
213
-
214
179
  $xml | Set-Content -Path $xmlPath -Encoding UTF8
215
180
 
216
181
  # Copy WinSW next to the XML with matching name
@@ -227,7 +192,6 @@ function Install-WinSWService {
227
192
  function Install-NSSMService {
228
193
  param([string]$NSSMPath)
229
194
  $nodePath = Get-NodePath
230
- $configEnv = Get-ConfigEnvironment
231
195
 
232
196
  # Run as current user so the service can access ~/.codelark and Codex login state
233
197
  $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
@@ -250,9 +214,6 @@ function Install-NSSMService {
250
214
  "LOCALAPPDATA=$env:LOCALAPPDATA",
251
215
  "CODELARK_HOME=$CodelarkHome"
252
216
  )
253
- foreach ($entry in $configEnv.GetEnumerator()) {
254
- $envArgs += "$($entry.Key)=$($entry.Value)"
255
- }
256
217
  & $NSSMPath set $ServiceName AppEnvironmentExtra @envArgs
257
218
 
258
219
  Write-Host "Service '$ServiceName' installed via NSSM."
@@ -265,7 +226,6 @@ function Install-NSSMService {
265
226
 
266
227
  function Start-Fallback {
267
228
  $nodePath = Get-NodePath
268
- $configEnv = Get-ConfigEnvironment
269
229
 
270
230
  # Clean env
271
231
  $envClone = [System.Collections.Hashtable]::new()
@@ -276,10 +236,6 @@ function Start-Fallback {
276
236
  [System.Environment]::SetEnvironmentVariable('CLAUDECODE', $null)
277
237
  [System.Environment]::SetEnvironmentVariable('CODELARK_HOME', $CodelarkHome, 'Process')
278
238
 
279
- foreach ($entry in $configEnv.GetEnumerator()) {
280
- [System.Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process')
281
- }
282
-
283
239
  try {
284
240
  $proc = Start-Process -FilePath $nodePath `
285
241
  -ArgumentList $DaemonMjs `
@@ -292,11 +248,6 @@ function Start-Fallback {
292
248
  foreach ($key in $envClone.Keys) {
293
249
  [System.Environment]::SetEnvironmentVariable($key, $envClone[$key], 'Process')
294
250
  }
295
- foreach ($entry in $configEnv.GetEnumerator()) {
296
- if (-not $envClone.ContainsKey($entry.Key)) {
297
- [System.Environment]::SetEnvironmentVariable($entry.Key, $null, 'Process')
298
- }
299
- }
300
251
  if (-not $envClone.ContainsKey('CODELARK_HOME')) {
301
252
  [System.Environment]::SetEnvironmentVariable('CODELARK_HOME', $null, 'Process')
302
253
  }
@@ -23,6 +23,10 @@ description: 在 CodeLark 中需要向 IM 用户发起结构化确认、选择
23
23
 
24
24
  CodeLark 会把最终回复里的 `<clk-ask>` 块转换成飞书/Lark 问题卡片。用户提交后的结果会作为下一条用户消息回到同一个 bridge session。
25
25
 
26
+ ## 生效时机
27
+
28
+ `<clk-ask>` 必须放在 assistant 的 completed/final 回复里,CodeLark 才会解析成问题卡片。不要把 `<clk-ask>` 放在工作过程消息、commentary/intermediate update、流式状态更新或工具调用说明里;这些路径只会进入 streaming card,不会生成弹窗。
29
+
26
30
  ## 输出格式
27
31
 
28
32
  在一个 `<clk-ask>` 块里输出合法 JSON。不要把 JSON 放进 markdown 代码块。
@@ -47,6 +51,7 @@ CodeLark 会把最终回复里的 `<clk-ask>` 块转换成飞书/Lark 问题卡
47
51
  ## 规则
48
52
 
49
53
  - 只有在确实需要用户输入才能继续时才使用。
54
+ - 必须把 `<clk-ask>` 放在 completed/final 回复中;不要放在工作过程消息或流式更新中。
50
55
  - 普通说明放在 `<clk-ask>` 块外。
51
56
  - 只是在总结、写代码、汇报已完成工作时,不要发问题卡片。
52
57
  - `options` 要短,并且互斥;最多展示 8 个选项。