返回 CodeWhale
validate-config.mjs
根目录 / integrations / feishu-bridge / scripts / validate-config.mjs
1 #!/usr/bin/env node
2 import { constants as fsConstants } from "node:fs";
3 import fs from "node:fs/promises";
4 import path from "node:path";
5
6 import {
7 cleanEnvValue,
8 formatValidationReport,
9 parseEnvText,
10 validateBridgeConfig
11 } from "../src/lib.mjs";
12
13 const args = parseArgs(process.argv.slice(2));
14
15 try {
16 const bridgeEnv = args.env ? parseEnvText(await fs.readFile(args.env, "utf8")) : process.env;
17 const runtimeEnv = args.runtimeEnv
18 ? parseEnvText(await fs.readFile(args.runtimeEnv, "utf8"))
19 : null;
20 const result = validateBridgeConfig(bridgeEnv, {
21 runtimeEnv,
22 workspaceRoot: args.workspaceRoot || "/opt/whalebro"
23 });
24
25 if (args.checkFilesystem) {
26 await appendFilesystemChecks(result, bridgeEnv, args);
27 }
28
29 if (args.json) {
30 console.log(JSON.stringify(result, null, 2));
31 } else {
32 console.log(formatValidationReport(result));
33 }
34 process.exitCode = result.ok ? 0 : 1;
35 } catch (error) {
36 console.error(`Config validation failed: ${error.message}`);
37 process.exitCode = 1;
38 }
39
40 function parseArgs(argv) {
41 const parsed = {
42 env: "",
43 runtimeEnv: "",
44 workspaceRoot: "/opt/whalebro",
45 checkFilesystem: false,
46 json: false
47 };
48 for (let index = 0; index < argv.length; index += 1) {
49 const arg = argv[index];
50 switch (arg) {
51 case "--env":
52 parsed.env = argv[++index];
53 break;
54 case "--runtime-env":
55 parsed.runtimeEnv = argv[++index];
56 break;
57 case "--workspace-root":
58 parsed.workspaceRoot = argv[++index];
59 break;
60 case "--check-filesystem":
61 parsed.checkFilesystem = true;
62 break;
63 case "--json":
64 parsed.json = true;
65 break;
66 case "-h":
67 case "--help":
68 printHelp();
69 process.exit(0);
70 break;
71 default:
72 throw new Error(`unknown argument: ${arg}`);
73 }
74 }
75 return parsed;
76 }
77
78 async function appendFilesystemChecks(result, env, args) {
79 const workspace = cleanEnvValue(env.CODEWHALE_WORKSPACE ?? env.DEEPSEEK_WORKSPACE);
80 if (workspace) {
81 await checkReadableDirectory(result, workspace, "workspace");
82 }
83
84 const threadMapPath = cleanEnvValue(env.FEISHU_THREAD_MAP_PATH);
85 if (threadMapPath) {
86 const parent = path.dirname(threadMapPath);
87 await checkWritableDirectory(result, parent, "thread map directory");
88 }
89
90 if (args.env) {
91 await checkReadableFile(result, args.env, "bridge env file");
92 }
93 if (args.runtimeEnv) {
94 await checkReadableFile(result, args.runtimeEnv, "runtime env file");
95 }
96 }
97
98 async function checkReadableDirectory(result, dir, label) {
99 try {
100 const stat = await fs.stat(dir);
101 if (!stat.isDirectory()) {
102 result.errors.push({ code: "not_directory", message: `${label} is not a directory: ${dir}` });
103 result.ok = false;
104 return;
105 }
106 await fs.access(dir, fsConstants.R_OK | fsConstants.X_OK);
107 result.info.push({ code: "readable_directory", message: `${label} is readable: ${dir}` });
108 } catch (error) {
109 result.errors.push({ code: "directory_access", message: `${label} is not readable: ${dir}` });
110 result.ok = false;
111 }
112 }
113
114 async function checkWritableDirectory(result, dir, label) {
115 try {
116 const stat = await fs.stat(dir);
117 if (!stat.isDirectory()) {
118 result.errors.push({ code: "not_directory", message: `${label} is not a directory: ${dir}` });
119 result.ok = false;
120 return;
121 }
122 await fs.access(dir, fsConstants.R_OK | fsConstants.W_OK | fsConstants.X_OK);
123 result.info.push({ code: "writable_directory", message: `${label} is writable: ${dir}` });
124 } catch {
125 result.errors.push({ code: "directory_access", message: `${label} is not writable: ${dir}` });
126 result.ok = false;
127 }
128 }
129
130 async function checkReadableFile(result, filePath, label) {
131 try {
132 const stat = await fs.stat(filePath);
133 if (!stat.isFile()) {
134 result.errors.push({ code: "not_file", message: `${label} is not a file: ${filePath}` });
135 result.ok = false;
136 return;
137 }
138 await fs.access(filePath, fsConstants.R_OK);
139 result.info.push({ code: "readable_file", message: `${label} is readable: ${filePath}` });
140 } catch {
141 result.errors.push({ code: "file_access", message: `${label} is not readable: ${filePath}` });
142 result.ok = false;
143 }
144 }
145
146 function printHelp() {
147 console.log(`Usage: node scripts/validate-config.mjs [options]
148
149 Options:
150 --env FILE Read bridge env from FILE instead of process.env.
151 --runtime-env FILE Read runtime env and verify the shared bearer token.
152 --workspace-root DIR Expected remote workspace root (default: /opt/whalebro).
153 --check-filesystem Verify workspace and thread-map paths are usable.
154 --json Print machine-readable JSON.
155 -h, --help Show this help.
156 `);
157 }
158
158 lines Plain Text