@probelabs/visor 0.1.69 → 0.1.71

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.
Files changed (36) hide show
  1. package/README.md +124 -30
  2. package/dist/check-execution-engine.d.ts +13 -0
  3. package/dist/check-execution-engine.d.ts.map +1 -1
  4. package/dist/cli-main.d.ts.map +1 -1
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/failure-condition-evaluator.d.ts +1 -1
  7. package/dist/failure-condition-evaluator.d.ts.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +570 -35
  10. package/dist/providers/command-check-provider.d.ts +1 -1
  11. package/dist/providers/command-check-provider.d.ts.map +1 -1
  12. package/dist/sdk/check-execution-engine-TBF6LPYH.mjs +9 -0
  13. package/dist/sdk/check-execution-engine-TBF6LPYH.mjs.map +1 -0
  14. package/dist/sdk/chunk-745RDQD3.mjs +8299 -0
  15. package/dist/sdk/chunk-745RDQD3.mjs.map +1 -0
  16. package/dist/sdk/chunk-FIL2OGF6.mjs +68 -0
  17. package/dist/sdk/chunk-FIL2OGF6.mjs.map +1 -0
  18. package/dist/sdk/chunk-U5D2LY66.mjs +245 -0
  19. package/dist/sdk/chunk-U5D2LY66.mjs.map +1 -0
  20. package/dist/sdk/chunk-WMJKH4XE.mjs +34 -0
  21. package/dist/sdk/chunk-WMJKH4XE.mjs.map +1 -0
  22. package/dist/sdk/config-merger-TWUBWFC2.mjs +8 -0
  23. package/dist/sdk/config-merger-TWUBWFC2.mjs.map +1 -0
  24. package/dist/sdk/liquid-extensions-KDECAJTV.mjs +12 -0
  25. package/dist/sdk/liquid-extensions-KDECAJTV.mjs.map +1 -0
  26. package/dist/sdk/sdk.d.mts +568 -0
  27. package/dist/sdk/sdk.d.ts +568 -0
  28. package/dist/sdk/sdk.js +9825 -0
  29. package/dist/sdk/sdk.js.map +1 -0
  30. package/dist/sdk/sdk.mjs +1006 -0
  31. package/dist/sdk/sdk.mjs.map +1 -0
  32. package/dist/sdk.d.ts +28 -0
  33. package/dist/sdk.d.ts.map +1 -0
  34. package/dist/types/config.d.ts +63 -0
  35. package/dist/types/config.d.ts.map +1 -1
  36. package/package.json +20 -3
@@ -0,0 +1,1006 @@
1
+ import {
2
+ CheckExecutionEngine
3
+ } from "./chunk-745RDQD3.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