@probelabs/visor 0.1.70 → 0.1.72
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 +124 -30
- package/dist/ai-review-service.d.ts +1 -0
- package/dist/ai-review-service.d.ts.map +1 -1
- package/dist/check-execution-engine.d.ts +13 -0
- package/dist/check-execution-engine.d.ts.map +1 -1
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/failure-condition-evaluator.d.ts +1 -1
- package/dist/failure-condition-evaluator.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4003 -44
- package/dist/providers/command-check-provider.d.ts +1 -1
- package/dist/providers/command-check-provider.d.ts.map +1 -1
- package/dist/sdk/check-execution-engine-RXV4MUD2.mjs +9 -0
- package/dist/sdk/check-execution-engine-RXV4MUD2.mjs.map +1 -0
- package/dist/sdk/chunk-FIL2OGF6.mjs +68 -0
- package/dist/sdk/chunk-FIL2OGF6.mjs.map +1 -0
- package/dist/sdk/chunk-J355UUEI.mjs +8301 -0
- package/dist/sdk/chunk-J355UUEI.mjs.map +1 -0
- package/dist/sdk/chunk-U5D2LY66.mjs +245 -0
- package/dist/sdk/chunk-U5D2LY66.mjs.map +1 -0
- package/dist/sdk/chunk-WMJKH4XE.mjs +34 -0
- package/dist/sdk/chunk-WMJKH4XE.mjs.map +1 -0
- package/dist/sdk/config-merger-TWUBWFC2.mjs +8 -0
- package/dist/sdk/config-merger-TWUBWFC2.mjs.map +1 -0
- package/dist/sdk/liquid-extensions-KDECAJTV.mjs +12 -0
- package/dist/sdk/liquid-extensions-KDECAJTV.mjs.map +1 -0
- package/dist/sdk/sdk.d.mts +568 -0
- package/dist/sdk/sdk.d.ts +568 -0
- package/dist/sdk/sdk.js +9827 -0
- package/dist/sdk/sdk.js.map +1 -0
- package/dist/sdk/sdk.mjs +1006 -0
- package/dist/sdk/sdk.mjs.map +1 -0
- package/dist/sdk.d.ts +28 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/types/config.d.ts +63 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +20 -3
package/dist/sdk/sdk.mjs
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CheckExecutionEngine
|
|
3
|
+
} from "./chunk-J355UUEI.mjs";
|
|
4
|
+
import "./chunk-FIL2OGF6.mjs";
|
|
5
|
+
import {
|
|
6
|
+
ConfigMerger
|
|
7
|
+
} from "./chunk-U5D2LY66.mjs";
|
|
8
|
+
import {
|
|
9
|
+
__require
|
|
10
|
+
} from "./chunk-WMJKH4XE.mjs";
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
import * as yaml2 from "js-yaml";
|
|
14
|
+
import * as fs2 from "fs";
|
|
15
|
+
import * as path2 from "path";
|
|
16
|
+
import simpleGit from "simple-git";
|
|
17
|
+
|
|
18
|
+
// src/utils/config-loader.ts
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
import * as yaml from "js-yaml";
|
|
22
|
+
var ConfigLoader = class {
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
this.options = {
|
|
26
|
+
allowRemote: true,
|
|
27
|
+
cacheTTL: 5 * 60 * 1e3,
|
|
28
|
+
// 5 minutes
|
|
29
|
+
timeout: 30 * 1e3,
|
|
30
|
+
// 30 seconds
|
|
31
|
+
maxDepth: 10,
|
|
32
|
+
allowedRemotePatterns: [],
|
|
33
|
+
// Empty by default for security
|
|
34
|
+
projectRoot: this.findProjectRoot(),
|
|
35
|
+
...options
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
cache = /* @__PURE__ */ new Map();
|
|
39
|
+
loadedConfigs = /* @__PURE__ */ new Set();
|
|
40
|
+
/**
|
|
41
|
+
* Determine the source type from a string
|
|
42
|
+
*/
|
|
43
|
+
getSourceType(source) {
|
|
44
|
+
if (source === "default") {
|
|
45
|
+
return "default" /* DEFAULT */;
|
|
46
|
+
}
|
|
47
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
48
|
+
return "remote" /* REMOTE */;
|
|
49
|
+
}
|
|
50
|
+
return "local" /* LOCAL */;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fetch configuration from any source
|
|
54
|
+
*/
|
|
55
|
+
async fetchConfig(source, currentDepth = 0) {
|
|
56
|
+
if (currentDepth >= (this.options.maxDepth || 10)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Maximum extends depth (${this.options.maxDepth}) exceeded. Check for circular dependencies.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const normalizedSource = this.normalizeSource(source);
|
|
62
|
+
if (this.loadedConfigs.has(normalizedSource)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Circular dependency detected: ${normalizedSource} is already in the extends chain`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const sourceType = this.getSourceType(source);
|
|
68
|
+
try {
|
|
69
|
+
this.loadedConfigs.add(normalizedSource);
|
|
70
|
+
switch (sourceType) {
|
|
71
|
+
case "default" /* DEFAULT */:
|
|
72
|
+
return await this.fetchDefaultConfig();
|
|
73
|
+
case "remote" /* REMOTE */:
|
|
74
|
+
if (!this.options.allowRemote) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Remote extends are disabled. Enable with --allow-remote-extends or remove VISOR_NO_REMOTE_EXTENDS environment variable."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return await this.fetchRemoteConfig(source);
|
|
80
|
+
case "local" /* LOCAL */:
|
|
81
|
+
return await this.fetchLocalConfig(source);
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`Unknown configuration source: ${source}`);
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
this.loadedConfigs.delete(normalizedSource);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Normalize source path/URL for comparison
|
|
91
|
+
*/
|
|
92
|
+
normalizeSource(source) {
|
|
93
|
+
const sourceType = this.getSourceType(source);
|
|
94
|
+
switch (sourceType) {
|
|
95
|
+
case "default" /* DEFAULT */:
|
|
96
|
+
return "default";
|
|
97
|
+
case "remote" /* REMOTE */:
|
|
98
|
+
return source.toLowerCase();
|
|
99
|
+
case "local" /* LOCAL */:
|
|
100
|
+
const basePath = this.options.baseDir || process.cwd();
|
|
101
|
+
return path.resolve(basePath, source);
|
|
102
|
+
default:
|
|
103
|
+
return source;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Load configuration from local file system
|
|
108
|
+
*/
|
|
109
|
+
async fetchLocalConfig(filePath) {
|
|
110
|
+
const basePath = this.options.baseDir || process.cwd();
|
|
111
|
+
const resolvedPath = path.resolve(basePath, filePath);
|
|
112
|
+
this.validateLocalPath(resolvedPath);
|
|
113
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
114
|
+
throw new Error(`Configuration file not found: ${resolvedPath}`);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const content = fs.readFileSync(resolvedPath, "utf8");
|
|
118
|
+
const config = yaml.load(content);
|
|
119
|
+
if (!config || typeof config !== "object") {
|
|
120
|
+
throw new Error(`Invalid YAML in configuration file: ${resolvedPath}`);
|
|
121
|
+
}
|
|
122
|
+
const previousBaseDir = this.options.baseDir;
|
|
123
|
+
this.options.baseDir = path.dirname(resolvedPath);
|
|
124
|
+
try {
|
|
125
|
+
if (config.extends) {
|
|
126
|
+
const processedConfig = await this.processExtends(config);
|
|
127
|
+
return processedConfig;
|
|
128
|
+
}
|
|
129
|
+
return config;
|
|
130
|
+
} finally {
|
|
131
|
+
this.options.baseDir = previousBaseDir;
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof Error) {
|
|
135
|
+
throw new Error(`Failed to load configuration from ${resolvedPath}: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Fetch configuration from remote URL
|
|
142
|
+
*/
|
|
143
|
+
async fetchRemoteConfig(url) {
|
|
144
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
145
|
+
throw new Error(`Invalid URL: ${url}. Only HTTP and HTTPS protocols are supported.`);
|
|
146
|
+
}
|
|
147
|
+
this.validateRemoteURL(url);
|
|
148
|
+
const cacheEntry = this.cache.get(url);
|
|
149
|
+
if (cacheEntry && Date.now() - cacheEntry.timestamp < cacheEntry.ttl) {
|
|
150
|
+
const outputFormat2 = process.env.VISOR_OUTPUT_FORMAT;
|
|
151
|
+
const logFn2 = outputFormat2 === "json" || outputFormat2 === "sarif" ? console.error : console.log;
|
|
152
|
+
logFn2(`\u{1F4E6} Using cached configuration from: ${url}`);
|
|
153
|
+
return cacheEntry.config;
|
|
154
|
+
}
|
|
155
|
+
const outputFormat = process.env.VISOR_OUTPUT_FORMAT;
|
|
156
|
+
const logFn = outputFormat === "json" || outputFormat === "sarif" ? console.error : console.log;
|
|
157
|
+
logFn(`\u2B07\uFE0F Fetching remote configuration from: ${url}`);
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const timeoutMs = this.options.timeout ?? 3e4;
|
|
160
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch(url, {
|
|
163
|
+
signal: controller.signal,
|
|
164
|
+
headers: {
|
|
165
|
+
"User-Agent": "Visor/1.0"
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);
|
|
170
|
+
}
|
|
171
|
+
const content = await response.text();
|
|
172
|
+
const config = yaml.load(content);
|
|
173
|
+
if (!config || typeof config !== "object") {
|
|
174
|
+
throw new Error(`Invalid YAML in remote configuration: ${url}`);
|
|
175
|
+
}
|
|
176
|
+
this.cache.set(url, {
|
|
177
|
+
config,
|
|
178
|
+
timestamp: Date.now(),
|
|
179
|
+
ttl: this.options.cacheTTL || 5 * 60 * 1e3
|
|
180
|
+
});
|
|
181
|
+
if (config.extends) {
|
|
182
|
+
return await this.processExtends(config);
|
|
183
|
+
}
|
|
184
|
+
return config;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error instanceof Error) {
|
|
187
|
+
if (error.name === "AbortError") {
|
|
188
|
+
throw new Error(`Timeout fetching configuration from ${url} (${timeoutMs}ms)`);
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Failed to fetch remote configuration from ${url}: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
} finally {
|
|
194
|
+
clearTimeout(timeoutId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Load bundled default configuration
|
|
199
|
+
*/
|
|
200
|
+
async fetchDefaultConfig() {
|
|
201
|
+
const possiblePaths = [
|
|
202
|
+
// When running as GitHub Action (bundled in dist/)
|
|
203
|
+
path.join(__dirname, "defaults", ".visor.yaml"),
|
|
204
|
+
// When running from source
|
|
205
|
+
path.join(__dirname, "..", "..", "defaults", ".visor.yaml"),
|
|
206
|
+
// Try via package root
|
|
207
|
+
this.findPackageRoot() ? path.join(this.findPackageRoot(), "defaults", ".visor.yaml") : "",
|
|
208
|
+
// GitHub Action environment variable
|
|
209
|
+
process.env.GITHUB_ACTION_PATH ? path.join(process.env.GITHUB_ACTION_PATH, "defaults", ".visor.yaml") : "",
|
|
210
|
+
process.env.GITHUB_ACTION_PATH ? path.join(process.env.GITHUB_ACTION_PATH, "dist", "defaults", ".visor.yaml") : ""
|
|
211
|
+
].filter((p) => p);
|
|
212
|
+
let defaultConfigPath;
|
|
213
|
+
for (const possiblePath of possiblePaths) {
|
|
214
|
+
if (fs.existsSync(possiblePath)) {
|
|
215
|
+
defaultConfigPath = possiblePath;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (defaultConfigPath && fs.existsSync(defaultConfigPath)) {
|
|
220
|
+
console.error(`\u{1F4E6} Loading bundled default configuration from ${defaultConfigPath}`);
|
|
221
|
+
const content = fs.readFileSync(defaultConfigPath, "utf8");
|
|
222
|
+
const config = yaml.load(content);
|
|
223
|
+
if (!config || typeof config !== "object") {
|
|
224
|
+
throw new Error("Invalid default configuration");
|
|
225
|
+
}
|
|
226
|
+
if (config.extends) {
|
|
227
|
+
return await this.processExtends(config);
|
|
228
|
+
}
|
|
229
|
+
return config;
|
|
230
|
+
}
|
|
231
|
+
console.warn("\u26A0\uFE0F Bundled default configuration not found, using minimal defaults");
|
|
232
|
+
return {
|
|
233
|
+
version: "1.0",
|
|
234
|
+
checks: {},
|
|
235
|
+
output: {
|
|
236
|
+
pr_comment: {
|
|
237
|
+
format: "markdown",
|
|
238
|
+
group_by: "check",
|
|
239
|
+
collapse: true
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Process extends directive in a configuration
|
|
246
|
+
*/
|
|
247
|
+
async processExtends(config) {
|
|
248
|
+
if (!config.extends) {
|
|
249
|
+
return config;
|
|
250
|
+
}
|
|
251
|
+
const extends_ = Array.isArray(config.extends) ? config.extends : [config.extends];
|
|
252
|
+
const { extends: _extendsField, ...configWithoutExtends } = config;
|
|
253
|
+
const parentConfigs = [];
|
|
254
|
+
for (const source of extends_) {
|
|
255
|
+
const parentConfig = await this.fetchConfig(source, this.loadedConfigs.size);
|
|
256
|
+
parentConfigs.push(parentConfig);
|
|
257
|
+
}
|
|
258
|
+
const { ConfigMerger: ConfigMerger2 } = await import("./config-merger-TWUBWFC2.mjs");
|
|
259
|
+
const merger = new ConfigMerger2();
|
|
260
|
+
let mergedParents = {};
|
|
261
|
+
for (const parentConfig of parentConfigs) {
|
|
262
|
+
mergedParents = merger.merge(mergedParents, parentConfig);
|
|
263
|
+
}
|
|
264
|
+
return merger.merge(mergedParents, configWithoutExtends);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Find project root directory (for security validation)
|
|
268
|
+
*/
|
|
269
|
+
findProjectRoot() {
|
|
270
|
+
try {
|
|
271
|
+
const { execSync } = __require("child_process");
|
|
272
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
273
|
+
if (gitRoot) return gitRoot;
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
const packageRoot = this.findPackageRoot();
|
|
277
|
+
if (packageRoot) return packageRoot;
|
|
278
|
+
return process.cwd();
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Validate remote URL against allowlist
|
|
282
|
+
*/
|
|
283
|
+
validateRemoteURL(url) {
|
|
284
|
+
const allowedPatterns = this.options.allowedRemotePatterns || [];
|
|
285
|
+
if (allowedPatterns.length === 0) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const isAllowed = allowedPatterns.some((pattern) => url.startsWith(pattern));
|
|
289
|
+
if (!isAllowed) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Security error: URL ${url} is not in the allowed list. Allowed patterns: ${allowedPatterns.join(", ")}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Validate local path against traversal attacks
|
|
297
|
+
*/
|
|
298
|
+
validateLocalPath(resolvedPath) {
|
|
299
|
+
const projectRoot = this.options.projectRoot || process.cwd();
|
|
300
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
301
|
+
const normalizedRoot = path.normalize(projectRoot);
|
|
302
|
+
if (!normalizedPath.startsWith(normalizedRoot)) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Security error: Path traversal detected. Cannot access files outside project root: ${projectRoot}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const sensitivePatterns = [
|
|
308
|
+
"/etc/passwd",
|
|
309
|
+
"/etc/shadow",
|
|
310
|
+
"/.ssh/",
|
|
311
|
+
"/.aws/",
|
|
312
|
+
"/.env",
|
|
313
|
+
"/private/"
|
|
314
|
+
];
|
|
315
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
316
|
+
for (const pattern of sensitivePatterns) {
|
|
317
|
+
if (lowerPath.includes(pattern)) {
|
|
318
|
+
throw new Error(`Security error: Cannot access potentially sensitive file: ${pattern}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Find package root directory
|
|
324
|
+
*/
|
|
325
|
+
findPackageRoot() {
|
|
326
|
+
let currentDir = __dirname;
|
|
327
|
+
const root = path.parse(currentDir).root;
|
|
328
|
+
while (currentDir !== root) {
|
|
329
|
+
const packageJsonPath = path.join(currentDir, "package.json");
|
|
330
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
331
|
+
try {
|
|
332
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
333
|
+
if (packageJson.name === "@probelabs/visor") {
|
|
334
|
+
return currentDir;
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
currentDir = path.dirname(currentDir);
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Clear the configuration cache
|
|
345
|
+
*/
|
|
346
|
+
clearCache() {
|
|
347
|
+
this.cache.clear();
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Reset the loaded configs tracking (for testing)
|
|
351
|
+
*/
|
|
352
|
+
reset() {
|
|
353
|
+
this.loadedConfigs.clear();
|
|
354
|
+
this.clearCache();
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// src/config.ts
|
|
359
|
+
var ConfigManager = class {
|
|
360
|
+
validCheckTypes = [
|
|
361
|
+
"ai",
|
|
362
|
+
"claude-code",
|
|
363
|
+
"command",
|
|
364
|
+
"http",
|
|
365
|
+
"http_input",
|
|
366
|
+
"http_client",
|
|
367
|
+
"noop",
|
|
368
|
+
"log"
|
|
369
|
+
];
|
|
370
|
+
validEventTriggers = [
|
|
371
|
+
"pr_opened",
|
|
372
|
+
"pr_updated",
|
|
373
|
+
"pr_closed",
|
|
374
|
+
"issue_opened",
|
|
375
|
+
"issue_comment",
|
|
376
|
+
"manual",
|
|
377
|
+
"schedule",
|
|
378
|
+
"webhook_received"
|
|
379
|
+
];
|
|
380
|
+
validOutputFormats = ["table", "json", "markdown", "sarif"];
|
|
381
|
+
validGroupByOptions = ["check", "file", "severity"];
|
|
382
|
+
/**
|
|
383
|
+
* Load configuration from a file
|
|
384
|
+
*/
|
|
385
|
+
async loadConfig(configPath, options = {}) {
|
|
386
|
+
const { validate = true, mergeDefaults = true, allowedRemotePatterns } = options;
|
|
387
|
+
try {
|
|
388
|
+
if (!fs2.existsSync(configPath)) {
|
|
389
|
+
throw new Error(`Configuration file not found: ${configPath}`);
|
|
390
|
+
}
|
|
391
|
+
const configContent = fs2.readFileSync(configPath, "utf8");
|
|
392
|
+
let parsedConfig;
|
|
393
|
+
try {
|
|
394
|
+
parsedConfig = yaml2.load(configContent);
|
|
395
|
+
} catch (yamlError) {
|
|
396
|
+
const errorMessage = yamlError instanceof Error ? yamlError.message : String(yamlError);
|
|
397
|
+
throw new Error(`Invalid YAML syntax in ${configPath}: ${errorMessage}`);
|
|
398
|
+
}
|
|
399
|
+
if (!parsedConfig || typeof parsedConfig !== "object") {
|
|
400
|
+
throw new Error("Configuration file must contain a valid YAML object");
|
|
401
|
+
}
|
|
402
|
+
if (parsedConfig.extends) {
|
|
403
|
+
const loaderOptions = {
|
|
404
|
+
baseDir: path2.dirname(configPath),
|
|
405
|
+
allowRemote: this.isRemoteExtendsAllowed(),
|
|
406
|
+
maxDepth: 10,
|
|
407
|
+
allowedRemotePatterns
|
|
408
|
+
};
|
|
409
|
+
const loader = new ConfigLoader(loaderOptions);
|
|
410
|
+
const merger = new ConfigMerger();
|
|
411
|
+
const extends_ = Array.isArray(parsedConfig.extends) ? parsedConfig.extends : [parsedConfig.extends];
|
|
412
|
+
const { extends: _extendsField, ...configWithoutExtends } = parsedConfig;
|
|
413
|
+
let mergedConfig = {};
|
|
414
|
+
for (const source of extends_) {
|
|
415
|
+
console.log(`\u{1F4E6} Extending from: ${source}`);
|
|
416
|
+
const parentConfig = await loader.fetchConfig(source);
|
|
417
|
+
mergedConfig = merger.merge(mergedConfig, parentConfig);
|
|
418
|
+
}
|
|
419
|
+
parsedConfig = merger.merge(mergedConfig, configWithoutExtends);
|
|
420
|
+
parsedConfig = merger.removeDisabledChecks(parsedConfig);
|
|
421
|
+
}
|
|
422
|
+
if (validate) {
|
|
423
|
+
this.validateConfig(parsedConfig);
|
|
424
|
+
}
|
|
425
|
+
let finalConfig = parsedConfig;
|
|
426
|
+
if (mergeDefaults) {
|
|
427
|
+
finalConfig = this.mergeWithDefaults(parsedConfig);
|
|
428
|
+
}
|
|
429
|
+
return finalConfig;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (error instanceof Error) {
|
|
432
|
+
if (error.message.includes("not found") || error.message.includes("Invalid YAML") || error.message.includes("extends") || error.message.includes("EACCES") || error.message.includes("EISDIR")) {
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
if (error.message.includes("ENOENT")) {
|
|
436
|
+
throw new Error(`Configuration file not found: ${configPath}`);
|
|
437
|
+
}
|
|
438
|
+
if (error.message.includes("EPERM")) {
|
|
439
|
+
throw new Error(`Permission denied reading configuration file: ${configPath}`);
|
|
440
|
+
}
|
|
441
|
+
throw new Error(`Failed to read configuration file ${configPath}: ${error.message}`);
|
|
442
|
+
}
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Find and load configuration from default locations
|
|
448
|
+
*/
|
|
449
|
+
async findAndLoadConfig(options = {}) {
|
|
450
|
+
const gitRoot = await this.findGitRepositoryRoot();
|
|
451
|
+
const searchDirs = [gitRoot, process.cwd()].filter(Boolean);
|
|
452
|
+
for (const baseDir of searchDirs) {
|
|
453
|
+
const possiblePaths = [path2.join(baseDir, ".visor.yaml"), path2.join(baseDir, ".visor.yml")];
|
|
454
|
+
for (const configPath of possiblePaths) {
|
|
455
|
+
if (fs2.existsSync(configPath)) {
|
|
456
|
+
return this.loadConfig(configPath, options);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const bundledConfig = this.loadBundledDefaultConfig();
|
|
461
|
+
if (bundledConfig) {
|
|
462
|
+
return bundledConfig;
|
|
463
|
+
}
|
|
464
|
+
return this.getDefaultConfig();
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Find the git repository root directory
|
|
468
|
+
*/
|
|
469
|
+
async findGitRepositoryRoot() {
|
|
470
|
+
try {
|
|
471
|
+
const git = simpleGit();
|
|
472
|
+
const isRepo = await git.checkIsRepo();
|
|
473
|
+
if (!isRepo) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const rootDir = await git.revparse(["--show-toplevel"]);
|
|
477
|
+
return rootDir.trim();
|
|
478
|
+
} catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Get default configuration
|
|
484
|
+
*/
|
|
485
|
+
async getDefaultConfig() {
|
|
486
|
+
return {
|
|
487
|
+
version: "1.0",
|
|
488
|
+
checks: {},
|
|
489
|
+
max_parallelism: 3,
|
|
490
|
+
output: {
|
|
491
|
+
pr_comment: {
|
|
492
|
+
format: "markdown",
|
|
493
|
+
group_by: "check",
|
|
494
|
+
collapse: true
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Load bundled default configuration from the package
|
|
501
|
+
*/
|
|
502
|
+
loadBundledDefaultConfig() {
|
|
503
|
+
try {
|
|
504
|
+
const possiblePaths = [];
|
|
505
|
+
if (typeof __dirname !== "undefined") {
|
|
506
|
+
possiblePaths.push(
|
|
507
|
+
path2.join(__dirname, "defaults", ".visor.yaml"),
|
|
508
|
+
path2.join(__dirname, "..", "defaults", ".visor.yaml")
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const pkgRoot = this.findPackageRoot();
|
|
512
|
+
if (pkgRoot) {
|
|
513
|
+
possiblePaths.push(path2.join(pkgRoot, "defaults", ".visor.yaml"));
|
|
514
|
+
}
|
|
515
|
+
if (process.env.GITHUB_ACTION_PATH) {
|
|
516
|
+
possiblePaths.push(
|
|
517
|
+
path2.join(process.env.GITHUB_ACTION_PATH, "defaults", ".visor.yaml"),
|
|
518
|
+
path2.join(process.env.GITHUB_ACTION_PATH, "dist", "defaults", ".visor.yaml")
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
let bundledConfigPath;
|
|
522
|
+
for (const possiblePath of possiblePaths) {
|
|
523
|
+
if (fs2.existsSync(possiblePath)) {
|
|
524
|
+
bundledConfigPath = possiblePath;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (bundledConfigPath && fs2.existsSync(bundledConfigPath)) {
|
|
529
|
+
console.error(`\u{1F4E6} Loading bundled default configuration from ${bundledConfigPath}`);
|
|
530
|
+
const configContent = fs2.readFileSync(bundledConfigPath, "utf8");
|
|
531
|
+
const parsedConfig = yaml2.load(configContent);
|
|
532
|
+
if (!parsedConfig || typeof parsedConfig !== "object") {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
this.validateConfig(parsedConfig);
|
|
536
|
+
return this.mergeWithDefaults(parsedConfig);
|
|
537
|
+
}
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.warn(
|
|
540
|
+
"Failed to load bundled default config:",
|
|
541
|
+
error instanceof Error ? error.message : String(error)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Find the root directory of the Visor package
|
|
548
|
+
*/
|
|
549
|
+
findPackageRoot() {
|
|
550
|
+
let currentDir = __dirname;
|
|
551
|
+
while (currentDir !== path2.dirname(currentDir)) {
|
|
552
|
+
const packageJsonPath = path2.join(currentDir, "package.json");
|
|
553
|
+
if (fs2.existsSync(packageJsonPath)) {
|
|
554
|
+
try {
|
|
555
|
+
const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
|
|
556
|
+
if (packageJson.name === "@probelabs/visor") {
|
|
557
|
+
return currentDir;
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
currentDir = path2.dirname(currentDir);
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Merge configuration with CLI options
|
|
568
|
+
*/
|
|
569
|
+
mergeWithCliOptions(config, cliOptions) {
|
|
570
|
+
const mergedConfig = { ...config };
|
|
571
|
+
if (cliOptions.maxParallelism !== void 0) {
|
|
572
|
+
mergedConfig.max_parallelism = cliOptions.maxParallelism;
|
|
573
|
+
}
|
|
574
|
+
if (cliOptions.failFast !== void 0) {
|
|
575
|
+
mergedConfig.fail_fast = cliOptions.failFast;
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
config: mergedConfig,
|
|
579
|
+
cliChecks: cliOptions.checks || [],
|
|
580
|
+
cliOutput: cliOptions.output || "table"
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Load configuration with environment variable overrides
|
|
585
|
+
*/
|
|
586
|
+
async loadConfigWithEnvOverrides() {
|
|
587
|
+
const environmentOverrides = {};
|
|
588
|
+
if (process.env.VISOR_CONFIG_PATH) {
|
|
589
|
+
environmentOverrides.configPath = process.env.VISOR_CONFIG_PATH;
|
|
590
|
+
}
|
|
591
|
+
if (process.env.VISOR_OUTPUT_FORMAT) {
|
|
592
|
+
environmentOverrides.outputFormat = process.env.VISOR_OUTPUT_FORMAT;
|
|
593
|
+
}
|
|
594
|
+
let config;
|
|
595
|
+
if (environmentOverrides.configPath) {
|
|
596
|
+
try {
|
|
597
|
+
config = await this.loadConfig(environmentOverrides.configPath);
|
|
598
|
+
} catch {
|
|
599
|
+
config = await this.findAndLoadConfig();
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
config = await this.findAndLoadConfig();
|
|
603
|
+
}
|
|
604
|
+
return { config, environmentOverrides };
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Validate configuration against schema
|
|
608
|
+
*/
|
|
609
|
+
validateConfig(config) {
|
|
610
|
+
const errors = [];
|
|
611
|
+
if (!config.version) {
|
|
612
|
+
errors.push({
|
|
613
|
+
field: "version",
|
|
614
|
+
message: "Missing required field: version"
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (!config.checks) {
|
|
618
|
+
errors.push({
|
|
619
|
+
field: "checks",
|
|
620
|
+
message: "Missing required field: checks"
|
|
621
|
+
});
|
|
622
|
+
} else {
|
|
623
|
+
for (const [checkName, checkConfig] of Object.entries(config.checks)) {
|
|
624
|
+
if (!checkConfig.type) {
|
|
625
|
+
checkConfig.type = "ai";
|
|
626
|
+
}
|
|
627
|
+
this.validateCheckConfig(checkName, checkConfig, errors);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (config.output) {
|
|
631
|
+
this.validateOutputConfig(config.output, errors);
|
|
632
|
+
}
|
|
633
|
+
if (config.http_server) {
|
|
634
|
+
this.validateHttpServerConfig(
|
|
635
|
+
config.http_server,
|
|
636
|
+
errors
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (config.max_parallelism !== void 0) {
|
|
640
|
+
if (typeof config.max_parallelism !== "number" || config.max_parallelism < 1 || !Number.isInteger(config.max_parallelism)) {
|
|
641
|
+
errors.push({
|
|
642
|
+
field: "max_parallelism",
|
|
643
|
+
message: "max_parallelism must be a positive integer (minimum 1)",
|
|
644
|
+
value: config.max_parallelism
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (config.tag_filter) {
|
|
649
|
+
this.validateTagFilter(config.tag_filter, errors);
|
|
650
|
+
}
|
|
651
|
+
if (errors.length > 0) {
|
|
652
|
+
throw new Error(errors[0].message);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Validate individual check configuration
|
|
657
|
+
*/
|
|
658
|
+
validateCheckConfig(checkName, checkConfig, errors) {
|
|
659
|
+
if (!checkConfig.type) {
|
|
660
|
+
checkConfig.type = "ai";
|
|
661
|
+
}
|
|
662
|
+
if (!this.validCheckTypes.includes(checkConfig.type)) {
|
|
663
|
+
errors.push({
|
|
664
|
+
field: `checks.${checkName}.type`,
|
|
665
|
+
message: `Invalid check type "${checkConfig.type}". Must be: ${this.validCheckTypes.join(", ")}`,
|
|
666
|
+
value: checkConfig.type
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if (checkConfig.type === "ai" && !checkConfig.prompt) {
|
|
670
|
+
errors.push({
|
|
671
|
+
field: `checks.${checkName}.prompt`,
|
|
672
|
+
message: `Invalid check configuration for "${checkName}": missing prompt (required for AI checks)`
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (checkConfig.type === "command" && !checkConfig.exec) {
|
|
676
|
+
errors.push({
|
|
677
|
+
field: `checks.${checkName}.exec`,
|
|
678
|
+
message: `Invalid check configuration for "${checkName}": missing exec field (required for command checks)`
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
if (checkConfig.type === "http") {
|
|
682
|
+
if (!checkConfig.url) {
|
|
683
|
+
errors.push({
|
|
684
|
+
field: `checks.${checkName}.url`,
|
|
685
|
+
message: `Invalid check configuration for "${checkName}": missing url field (required for http checks)`
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
if (!checkConfig.body) {
|
|
689
|
+
errors.push({
|
|
690
|
+
field: `checks.${checkName}.body`,
|
|
691
|
+
message: `Invalid check configuration for "${checkName}": missing body field (required for http checks)`
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (checkConfig.type === "http_input" && !checkConfig.endpoint) {
|
|
696
|
+
errors.push({
|
|
697
|
+
field: `checks.${checkName}.endpoint`,
|
|
698
|
+
message: `Invalid check configuration for "${checkName}": missing endpoint field (required for http_input checks)`
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
if (checkConfig.type === "http_client" && !checkConfig.url) {
|
|
702
|
+
errors.push({
|
|
703
|
+
field: `checks.${checkName}.url`,
|
|
704
|
+
message: `Invalid check configuration for "${checkName}": missing url field (required for http_client checks)`
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
if (checkConfig.schedule) {
|
|
708
|
+
const cronParts = checkConfig.schedule.split(" ");
|
|
709
|
+
if (cronParts.length < 5 || cronParts.length > 6) {
|
|
710
|
+
errors.push({
|
|
711
|
+
field: `checks.${checkName}.schedule`,
|
|
712
|
+
message: `Invalid cron expression for "${checkName}": ${checkConfig.schedule}`,
|
|
713
|
+
value: checkConfig.schedule
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (checkConfig.on) {
|
|
718
|
+
if (!Array.isArray(checkConfig.on)) {
|
|
719
|
+
errors.push({
|
|
720
|
+
field: `checks.${checkName}.on`,
|
|
721
|
+
message: `Invalid check configuration for "${checkName}": 'on' field must be an array`
|
|
722
|
+
});
|
|
723
|
+
} else {
|
|
724
|
+
for (const event of checkConfig.on) {
|
|
725
|
+
if (!this.validEventTriggers.includes(event)) {
|
|
726
|
+
errors.push({
|
|
727
|
+
field: `checks.${checkName}.on`,
|
|
728
|
+
message: `Invalid event "${event}". Must be one of: ${this.validEventTriggers.join(", ")}`,
|
|
729
|
+
value: event
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (checkConfig.reuse_ai_session !== void 0) {
|
|
736
|
+
if (typeof checkConfig.reuse_ai_session !== "boolean") {
|
|
737
|
+
errors.push({
|
|
738
|
+
field: `checks.${checkName}.reuse_ai_session`,
|
|
739
|
+
message: `Invalid reuse_ai_session value for "${checkName}": must be boolean`,
|
|
740
|
+
value: checkConfig.reuse_ai_session
|
|
741
|
+
});
|
|
742
|
+
} else if (checkConfig.reuse_ai_session === true) {
|
|
743
|
+
if (!checkConfig.depends_on || !Array.isArray(checkConfig.depends_on) || checkConfig.depends_on.length === 0) {
|
|
744
|
+
errors.push({
|
|
745
|
+
field: `checks.${checkName}.reuse_ai_session`,
|
|
746
|
+
message: `Check "${checkName}" has reuse_ai_session=true but missing or empty depends_on. Session reuse requires dependency on another check.`,
|
|
747
|
+
value: checkConfig.reuse_ai_session
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (checkConfig.tags !== void 0) {
|
|
753
|
+
if (!Array.isArray(checkConfig.tags)) {
|
|
754
|
+
errors.push({
|
|
755
|
+
field: `checks.${checkName}.tags`,
|
|
756
|
+
message: `Invalid tags value for "${checkName}": must be an array of strings`,
|
|
757
|
+
value: checkConfig.tags
|
|
758
|
+
});
|
|
759
|
+
} else {
|
|
760
|
+
const validTagPattern = /^[a-zA-Z0-9][a-zA-Z0-9-_]*$/;
|
|
761
|
+
checkConfig.tags.forEach((tag, index) => {
|
|
762
|
+
if (typeof tag !== "string") {
|
|
763
|
+
errors.push({
|
|
764
|
+
field: `checks.${checkName}.tags[${index}]`,
|
|
765
|
+
message: `Invalid tag at index ${index} for "${checkName}": must be a string`,
|
|
766
|
+
value: tag
|
|
767
|
+
});
|
|
768
|
+
} else if (!validTagPattern.test(tag)) {
|
|
769
|
+
errors.push({
|
|
770
|
+
field: `checks.${checkName}.tags[${index}]`,
|
|
771
|
+
message: `Invalid tag "${tag}" for "${checkName}": tags must be alphanumeric with hyphens or underscores (start with alphanumeric)`,
|
|
772
|
+
value: tag
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Validate tag filter configuration
|
|
781
|
+
*/
|
|
782
|
+
validateTagFilter(tagFilter, errors) {
|
|
783
|
+
const validTagPattern = /^[a-zA-Z0-9][a-zA-Z0-9-_]*$/;
|
|
784
|
+
if (tagFilter.include !== void 0) {
|
|
785
|
+
if (!Array.isArray(tagFilter.include)) {
|
|
786
|
+
errors.push({
|
|
787
|
+
field: "tag_filter.include",
|
|
788
|
+
message: "tag_filter.include must be an array of strings",
|
|
789
|
+
value: tagFilter.include
|
|
790
|
+
});
|
|
791
|
+
} else {
|
|
792
|
+
tagFilter.include.forEach((tag, index) => {
|
|
793
|
+
if (typeof tag !== "string") {
|
|
794
|
+
errors.push({
|
|
795
|
+
field: `tag_filter.include[${index}]`,
|
|
796
|
+
message: `Invalid tag at index ${index}: must be a string`,
|
|
797
|
+
value: tag
|
|
798
|
+
});
|
|
799
|
+
} else if (!validTagPattern.test(tag)) {
|
|
800
|
+
errors.push({
|
|
801
|
+
field: `tag_filter.include[${index}]`,
|
|
802
|
+
message: `Invalid tag "${tag}": tags must be alphanumeric with hyphens or underscores`,
|
|
803
|
+
value: tag
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (tagFilter.exclude !== void 0) {
|
|
810
|
+
if (!Array.isArray(tagFilter.exclude)) {
|
|
811
|
+
errors.push({
|
|
812
|
+
field: "tag_filter.exclude",
|
|
813
|
+
message: "tag_filter.exclude must be an array of strings",
|
|
814
|
+
value: tagFilter.exclude
|
|
815
|
+
});
|
|
816
|
+
} else {
|
|
817
|
+
tagFilter.exclude.forEach((tag, index) => {
|
|
818
|
+
if (typeof tag !== "string") {
|
|
819
|
+
errors.push({
|
|
820
|
+
field: `tag_filter.exclude[${index}]`,
|
|
821
|
+
message: `Invalid tag at index ${index}: must be a string`,
|
|
822
|
+
value: tag
|
|
823
|
+
});
|
|
824
|
+
} else if (!validTagPattern.test(tag)) {
|
|
825
|
+
errors.push({
|
|
826
|
+
field: `tag_filter.exclude[${index}]`,
|
|
827
|
+
message: `Invalid tag "${tag}": tags must be alphanumeric with hyphens or underscores`,
|
|
828
|
+
value: tag
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Validate HTTP server configuration
|
|
837
|
+
*/
|
|
838
|
+
validateHttpServerConfig(httpServerConfig, errors) {
|
|
839
|
+
if (typeof httpServerConfig.enabled !== "boolean") {
|
|
840
|
+
errors.push({
|
|
841
|
+
field: "http_server.enabled",
|
|
842
|
+
message: "http_server.enabled must be a boolean",
|
|
843
|
+
value: httpServerConfig.enabled
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (httpServerConfig.enabled === true) {
|
|
847
|
+
if (typeof httpServerConfig.port !== "number" || httpServerConfig.port < 1 || httpServerConfig.port > 65535) {
|
|
848
|
+
errors.push({
|
|
849
|
+
field: "http_server.port",
|
|
850
|
+
message: "http_server.port must be a number between 1 and 65535",
|
|
851
|
+
value: httpServerConfig.port
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (httpServerConfig.auth) {
|
|
855
|
+
const auth = httpServerConfig.auth;
|
|
856
|
+
const validAuthTypes = ["bearer_token", "hmac", "basic", "none"];
|
|
857
|
+
if (!auth.type || !validAuthTypes.includes(auth.type)) {
|
|
858
|
+
errors.push({
|
|
859
|
+
field: "http_server.auth.type",
|
|
860
|
+
message: `Invalid auth type. Must be one of: ${validAuthTypes.join(", ")}`,
|
|
861
|
+
value: auth.type
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (httpServerConfig.tls && typeof httpServerConfig.tls === "object") {
|
|
866
|
+
const tls = httpServerConfig.tls;
|
|
867
|
+
if (tls.enabled === true) {
|
|
868
|
+
if (!tls.cert) {
|
|
869
|
+
errors.push({
|
|
870
|
+
field: "http_server.tls.cert",
|
|
871
|
+
message: "TLS certificate is required when TLS is enabled"
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
if (!tls.key) {
|
|
875
|
+
errors.push({
|
|
876
|
+
field: "http_server.tls.key",
|
|
877
|
+
message: "TLS key is required when TLS is enabled"
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (httpServerConfig.endpoints && Array.isArray(httpServerConfig.endpoints)) {
|
|
883
|
+
for (let i = 0; i < httpServerConfig.endpoints.length; i++) {
|
|
884
|
+
const endpoint = httpServerConfig.endpoints[i];
|
|
885
|
+
if (!endpoint.path || typeof endpoint.path !== "string") {
|
|
886
|
+
errors.push({
|
|
887
|
+
field: `http_server.endpoints[${i}].path`,
|
|
888
|
+
message: "Endpoint path must be a string",
|
|
889
|
+
value: endpoint.path
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Validate output configuration
|
|
898
|
+
*/
|
|
899
|
+
validateOutputConfig(outputConfig, errors) {
|
|
900
|
+
if (outputConfig.pr_comment) {
|
|
901
|
+
const prComment = outputConfig.pr_comment;
|
|
902
|
+
if (typeof prComment.format === "string" && !this.validOutputFormats.includes(prComment.format)) {
|
|
903
|
+
errors.push({
|
|
904
|
+
field: "output.pr_comment.format",
|
|
905
|
+
message: `Invalid output format "${prComment.format}". Must be one of: ${this.validOutputFormats.join(", ")}`,
|
|
906
|
+
value: prComment.format
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (typeof prComment.group_by === "string" && !this.validGroupByOptions.includes(prComment.group_by)) {
|
|
910
|
+
errors.push({
|
|
911
|
+
field: "output.pr_comment.group_by",
|
|
912
|
+
message: `Invalid group_by option "${prComment.group_by}". Must be one of: ${this.validGroupByOptions.join(", ")}`,
|
|
913
|
+
value: prComment.group_by
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Check if remote extends are allowed
|
|
920
|
+
*/
|
|
921
|
+
isRemoteExtendsAllowed() {
|
|
922
|
+
if (process.env.VISOR_NO_REMOTE_EXTENDS === "true" || process.env.VISOR_NO_REMOTE_EXTENDS === "1") {
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Merge configuration with default values
|
|
929
|
+
*/
|
|
930
|
+
mergeWithDefaults(config) {
|
|
931
|
+
const defaultConfig = {
|
|
932
|
+
version: "1.0",
|
|
933
|
+
checks: {},
|
|
934
|
+
max_parallelism: 3,
|
|
935
|
+
output: {
|
|
936
|
+
pr_comment: {
|
|
937
|
+
format: "markdown",
|
|
938
|
+
group_by: "check",
|
|
939
|
+
collapse: true
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
const merged = { ...defaultConfig, ...config };
|
|
944
|
+
if (merged.output) {
|
|
945
|
+
merged.output.pr_comment = {
|
|
946
|
+
...defaultConfig.output.pr_comment,
|
|
947
|
+
...merged.output.pr_comment
|
|
948
|
+
};
|
|
949
|
+
} else {
|
|
950
|
+
merged.output = defaultConfig.output;
|
|
951
|
+
}
|
|
952
|
+
return merged;
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
// src/sdk.ts
|
|
957
|
+
async function loadConfig(configPath) {
|
|
958
|
+
const cm = new ConfigManager();
|
|
959
|
+
if (configPath) return cm.loadConfig(configPath);
|
|
960
|
+
return cm.findAndLoadConfig();
|
|
961
|
+
}
|
|
962
|
+
function resolveChecks(checkIds, config) {
|
|
963
|
+
if (!config?.checks) return Array.from(new Set(checkIds));
|
|
964
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
965
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
966
|
+
const result = [];
|
|
967
|
+
const dfs = (id, stack = []) => {
|
|
968
|
+
if (resolved.has(id)) return;
|
|
969
|
+
if (visiting.has(id)) {
|
|
970
|
+
const cycle = [...stack, id].join(" -> ");
|
|
971
|
+
throw new Error(`Circular dependency detected involving check: ${id} (path: ${cycle})`);
|
|
972
|
+
}
|
|
973
|
+
visiting.add(id);
|
|
974
|
+
const deps = config.checks[id]?.depends_on || [];
|
|
975
|
+
for (const d of deps) dfs(d, [...stack, id]);
|
|
976
|
+
if (!result.includes(id)) result.push(id);
|
|
977
|
+
visiting.delete(id);
|
|
978
|
+
resolved.add(id);
|
|
979
|
+
};
|
|
980
|
+
for (const id of checkIds) dfs(id);
|
|
981
|
+
return result;
|
|
982
|
+
}
|
|
983
|
+
async function runChecks(opts = {}) {
|
|
984
|
+
const cm = new ConfigManager();
|
|
985
|
+
const config = opts.config ? opts.config : opts.configPath ? await cm.loadConfig(opts.configPath) : await cm.findAndLoadConfig();
|
|
986
|
+
const checks = opts.checks && opts.checks.length > 0 ? resolveChecks(opts.checks, config) : Object.keys(config.checks || {});
|
|
987
|
+
const engine = new CheckExecutionEngine(opts.cwd);
|
|
988
|
+
const result = await engine.executeChecks({
|
|
989
|
+
checks,
|
|
990
|
+
workingDirectory: opts.cwd,
|
|
991
|
+
timeout: opts.timeoutMs,
|
|
992
|
+
maxParallelism: opts.maxParallelism,
|
|
993
|
+
failFast: opts.failFast,
|
|
994
|
+
outputFormat: opts.output?.format,
|
|
995
|
+
config,
|
|
996
|
+
debug: opts.debug,
|
|
997
|
+
tagFilter: opts.tagFilter
|
|
998
|
+
});
|
|
999
|
+
return result;
|
|
1000
|
+
}
|
|
1001
|
+
export {
|
|
1002
|
+
loadConfig,
|
|
1003
|
+
resolveChecks,
|
|
1004
|
+
runChecks
|
|
1005
|
+
};
|
|
1006
|
+
//# sourceMappingURL=sdk.mjs.map
|