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.
- package/README.md +36 -67
- package/SECURITY.md +1 -1
- package/dist/cli.mjs +22031 -5268
- package/dist/daemon.mjs +49838 -26610
- package/dist/defaults.toml +51 -0
- package/dist/ui-server.mjs +31695 -12270
- package/package.json +7 -3
- package/schemas/config.v1.schema.json +4 -1
- package/schemas/data/sessions.v1.schema.json +1 -1
- package/scripts/build.js +4 -1
- package/scripts/check-npm-pack.js +12 -0
- package/scripts/daemon.sh +0 -3
- package/scripts/doctor.sh +102 -82
- package/scripts/real-feishu-e2e.ts +36 -48
- package/scripts/run-tests.js +42 -6
- package/scripts/setup-wizard-real-e2e.ts +71 -36
- package/scripts/setup-wizard-real-wizard-e2e.ts +342 -0
- package/scripts/supervisor-windows.ps1 +1 -50
- package/skills/codelark-question/SKILL.md +5 -0
|
@@ -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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
161
|
-
throw new Error(`workspace mismatch: ${savedConfig.
|
|
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
|
|
164
|
-
if (savedFeishu?.config.appSecret !== appSecret) throw new Error('config
|
|
165
|
-
if (savedFeishu?.config.site !== site) throw new Error('config
|
|
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
|
-
|
|
168
|
-
if (
|
|
169
|
-
if (
|
|
170
|
-
if (!
|
|
171
|
-
if (!fs.existsSync(
|
|
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
|
-
|
|
212
|
+
larkSourceConfigPath,
|
|
213
|
+
larkRuntimeConfigPath,
|
|
179
214
|
configEnvPath,
|
|
180
215
|
configJsonPath,
|
|
181
|
-
|
|
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 >=
|
|
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 个选项。
|