@atrim/instrument-node 0.1.3-fea6398-20251118005809 → 0.4.0-08fae61-20251118193009
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 +328 -7
- package/package.json +6 -5
- package/target/dist/index.cjs +177 -531
- package/target/dist/index.cjs.map +1 -1
- package/target/dist/index.d.cts +57 -3
- package/target/dist/index.d.ts +57 -3
- package/target/dist/index.js +104 -469
- package/target/dist/index.js.map +1 -1
- package/target/dist/integrations/effect/index.cjs +54 -409
- package/target/dist/integrations/effect/index.cjs.map +1 -1
- package/target/dist/integrations/effect/index.d.cts +17 -1
- package/target/dist/integrations/effect/index.d.ts +17 -1
- package/target/dist/integrations/effect/index.js +43 -398
- package/target/dist/integrations/effect/index.js.map +1 -1
|
@@ -1,91 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Layer, Effect, FiberSet as FiberSet$1, Tracer } from 'effect';
|
|
2
2
|
import * as Otlp from '@effect/opentelemetry/Otlp';
|
|
3
3
|
import { FetchHttpClient } from '@effect/platform';
|
|
4
4
|
import { TraceFlags, trace, context } from '@opentelemetry/api';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { parse } from 'yaml';
|
|
8
|
-
import { z } from 'zod';
|
|
5
|
+
import { ConfigLoaderLive, logger, initializePatternMatcher, ConfigLoader } from '@atrim/instrument-core';
|
|
6
|
+
import { NodeContext } from '@effect/platform-node';
|
|
9
7
|
|
|
10
8
|
// src/integrations/effect/effect-tracer.ts
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
var
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
var HttpFilteringConfigSchema = z.object({
|
|
42
|
-
// Patterns to ignore for outgoing HTTP requests (string patterns only in YAML)
|
|
43
|
-
ignore_outgoing_urls: z.array(z.string()).optional(),
|
|
44
|
-
// Patterns to ignore for incoming HTTP requests (string patterns only in YAML)
|
|
45
|
-
ignore_incoming_paths: z.array(z.string()).optional(),
|
|
46
|
-
// Require parent span for outgoing requests (prevents root spans for HTTP calls)
|
|
47
|
-
require_parent_for_outgoing_spans: z.boolean().optional()
|
|
48
|
-
});
|
|
49
|
-
var InstrumentationConfigSchema = z.object({
|
|
50
|
-
version: z.string(),
|
|
51
|
-
instrumentation: z.object({
|
|
52
|
-
enabled: z.boolean(),
|
|
53
|
-
description: z.string().optional(),
|
|
54
|
-
logging: z.enum(["on", "off", "minimal"]).optional().default("on"),
|
|
55
|
-
instrument_patterns: z.array(PatternConfigSchema),
|
|
56
|
-
ignore_patterns: z.array(PatternConfigSchema)
|
|
57
|
-
}),
|
|
58
|
-
effect: z.object({
|
|
59
|
-
auto_extract_metadata: z.boolean(),
|
|
60
|
-
auto_isolation: AutoIsolationConfigSchema.optional()
|
|
61
|
-
}).optional(),
|
|
62
|
-
http: HttpFilteringConfigSchema.optional()
|
|
63
|
-
});
|
|
64
|
-
(class extends Data.TaggedError("ConfigError") {
|
|
65
|
-
});
|
|
66
|
-
var ConfigUrlError = class extends Data.TaggedError("ConfigUrlError") {
|
|
67
|
-
};
|
|
68
|
-
var ConfigValidationError = class extends Data.TaggedError("ConfigValidationError") {
|
|
69
|
-
};
|
|
70
|
-
var ConfigFileError = class extends Data.TaggedError("ConfigFileError") {
|
|
71
|
-
};
|
|
72
|
-
(class extends Data.TaggedError("ServiceDetectionError") {
|
|
73
|
-
});
|
|
74
|
-
(class extends Data.TaggedError("InitializationError") {
|
|
75
|
-
});
|
|
76
|
-
(class extends Data.TaggedError("ExportError") {
|
|
77
|
-
});
|
|
78
|
-
(class extends Data.TaggedError("ShutdownError") {
|
|
79
|
-
});
|
|
80
|
-
var SECURITY_DEFAULTS = {
|
|
81
|
-
maxConfigSize: 1e6,
|
|
82
|
-
// 1MB
|
|
83
|
-
requestTimeout: 5e3,
|
|
84
|
-
allowedProtocols: ["https:"],
|
|
85
|
-
// Only HTTPS for remote configs
|
|
86
|
-
cacheTimeout: 3e5
|
|
87
|
-
// 5 minutes
|
|
88
|
-
};
|
|
9
|
+
var NodeConfigLoaderLive = ConfigLoaderLive.pipe(
|
|
10
|
+
Layer.provide(Layer.mergeAll(NodeContext.layer, FetchHttpClient.layer))
|
|
11
|
+
);
|
|
12
|
+
var cachedLoaderPromise = null;
|
|
13
|
+
function getCachedLoader() {
|
|
14
|
+
if (!cachedLoaderPromise) {
|
|
15
|
+
cachedLoaderPromise = Effect.runPromise(
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
return yield* ConfigLoader;
|
|
18
|
+
}).pipe(Effect.provide(NodeConfigLoaderLive))
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return cachedLoaderPromise;
|
|
22
|
+
}
|
|
23
|
+
async function loadConfig(uri, options) {
|
|
24
|
+
if (options?.cacheTimeout === 0) {
|
|
25
|
+
const program = Effect.gen(function* () {
|
|
26
|
+
const loader2 = yield* ConfigLoader;
|
|
27
|
+
return yield* loader2.loadFromUri(uri);
|
|
28
|
+
});
|
|
29
|
+
return Effect.runPromise(program.pipe(Effect.provide(NodeConfigLoaderLive)));
|
|
30
|
+
}
|
|
31
|
+
const loader = await getCachedLoader();
|
|
32
|
+
return Effect.runPromise(loader.loadFromUri(uri));
|
|
33
|
+
}
|
|
34
|
+
async function loadConfigFromInline(content) {
|
|
35
|
+
const loader = await getCachedLoader();
|
|
36
|
+
return Effect.runPromise(loader.loadFromInline(content));
|
|
37
|
+
}
|
|
89
38
|
function getDefaultConfig() {
|
|
90
39
|
return {
|
|
91
40
|
version: "1.0",
|
|
@@ -109,340 +58,36 @@ function getDefaultConfig() {
|
|
|
109
58
|
}
|
|
110
59
|
};
|
|
111
60
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
catch: (error) => new ConfigValidationError({
|
|
115
|
-
reason: "Invalid configuration schema",
|
|
116
|
-
cause: error
|
|
117
|
-
})
|
|
118
|
-
});
|
|
119
|
-
var loadConfigFromFileEffect = (filePath) => Effect.gen(function* () {
|
|
120
|
-
const fileContents = yield* Effect.try({
|
|
121
|
-
try: () => readFileSync(filePath, "utf8"),
|
|
122
|
-
catch: (error) => new ConfigFileError({
|
|
123
|
-
reason: `Failed to read config file at ${filePath}`,
|
|
124
|
-
cause: error
|
|
125
|
-
})
|
|
126
|
-
});
|
|
127
|
-
if (fileContents.length > SECURITY_DEFAULTS.maxConfigSize) {
|
|
128
|
-
return yield* Effect.fail(
|
|
129
|
-
new ConfigFileError({
|
|
130
|
-
reason: `Config file exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
|
|
131
|
-
})
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
let rawConfig;
|
|
135
|
-
try {
|
|
136
|
-
rawConfig = parse(fileContents);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
return yield* Effect.fail(
|
|
139
|
-
new ConfigValidationError({
|
|
140
|
-
reason: "Invalid YAML syntax",
|
|
141
|
-
cause: error
|
|
142
|
-
})
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
return yield* validateConfigEffect(rawConfig);
|
|
146
|
-
});
|
|
147
|
-
var fetchAndParseConfig = (url) => Effect.gen(function* () {
|
|
148
|
-
let urlObj;
|
|
149
|
-
try {
|
|
150
|
-
urlObj = new URL(url);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
return yield* Effect.fail(
|
|
153
|
-
new ConfigUrlError({
|
|
154
|
-
reason: `Invalid URL: ${url}`,
|
|
155
|
-
cause: error
|
|
156
|
-
})
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
if (!SECURITY_DEFAULTS.allowedProtocols.includes(urlObj.protocol)) {
|
|
160
|
-
return yield* Effect.fail(
|
|
161
|
-
new ConfigUrlError({
|
|
162
|
-
reason: `Insecure protocol ${urlObj.protocol}. Only ${SECURITY_DEFAULTS.allowedProtocols.join(", ")} are allowed`
|
|
163
|
-
})
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
const response = yield* Effect.tryPromise({
|
|
167
|
-
try: () => fetch(url, {
|
|
168
|
-
redirect: "follow",
|
|
169
|
-
headers: {
|
|
170
|
-
Accept: "application/yaml, text/yaml, text/x-yaml"
|
|
171
|
-
}
|
|
172
|
-
}),
|
|
173
|
-
catch: (error) => new ConfigUrlError({
|
|
174
|
-
reason: `Failed to load config from URL ${url}`,
|
|
175
|
-
cause: error
|
|
176
|
-
})
|
|
177
|
-
}).pipe(
|
|
178
|
-
Effect.timeout(Duration.millis(SECURITY_DEFAULTS.requestTimeout)),
|
|
179
|
-
Effect.retry({
|
|
180
|
-
times: 3,
|
|
181
|
-
schedule: Schedule.exponential(Duration.millis(100))
|
|
182
|
-
}),
|
|
183
|
-
Effect.catchAll((error) => {
|
|
184
|
-
if (error._tag === "TimeoutException") {
|
|
185
|
-
return Effect.fail(
|
|
186
|
-
new ConfigUrlError({
|
|
187
|
-
reason: `Config fetch timeout after ${SECURITY_DEFAULTS.requestTimeout}ms`
|
|
188
|
-
})
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
return Effect.fail(error);
|
|
192
|
-
})
|
|
193
|
-
);
|
|
194
|
-
if (!response.ok) {
|
|
195
|
-
return yield* Effect.fail(
|
|
196
|
-
new ConfigUrlError({
|
|
197
|
-
reason: `HTTP ${response.status}: ${response.statusText}`
|
|
198
|
-
})
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
const contentLength = response.headers.get("content-length");
|
|
202
|
-
if (contentLength && parseInt(contentLength) > SECURITY_DEFAULTS.maxConfigSize) {
|
|
203
|
-
return yield* Effect.fail(
|
|
204
|
-
new ConfigUrlError({
|
|
205
|
-
reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
|
|
206
|
-
})
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
const text = yield* Effect.tryPromise({
|
|
210
|
-
try: () => response.text(),
|
|
211
|
-
catch: (error) => new ConfigUrlError({
|
|
212
|
-
reason: "Failed to read response body",
|
|
213
|
-
cause: error
|
|
214
|
-
})
|
|
215
|
-
});
|
|
216
|
-
if (text.length > SECURITY_DEFAULTS.maxConfigSize) {
|
|
217
|
-
return yield* Effect.fail(
|
|
218
|
-
new ConfigUrlError({
|
|
219
|
-
reason: `Config exceeds maximum size of ${SECURITY_DEFAULTS.maxConfigSize} bytes`
|
|
220
|
-
})
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
let rawConfig;
|
|
224
|
-
try {
|
|
225
|
-
rawConfig = parse(text);
|
|
226
|
-
} catch (error) {
|
|
227
|
-
return yield* Effect.fail(
|
|
228
|
-
new ConfigValidationError({
|
|
229
|
-
reason: "Invalid YAML syntax",
|
|
230
|
-
cause: error
|
|
231
|
-
})
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
return yield* validateConfigEffect(rawConfig);
|
|
235
|
-
});
|
|
236
|
-
var makeConfigCache = () => Cache.make({
|
|
237
|
-
capacity: 100,
|
|
238
|
-
timeToLive: Duration.minutes(5),
|
|
239
|
-
lookup: (url) => fetchAndParseConfig(url)
|
|
240
|
-
});
|
|
241
|
-
var cacheInstance = null;
|
|
242
|
-
var getCache = Effect.gen(function* () {
|
|
243
|
-
if (!cacheInstance) {
|
|
244
|
-
cacheInstance = yield* makeConfigCache();
|
|
245
|
-
}
|
|
246
|
-
return cacheInstance;
|
|
247
|
-
});
|
|
248
|
-
var loadConfigFromUrlEffect = (url, cacheTimeout = SECURITY_DEFAULTS.cacheTimeout) => Effect.gen(function* () {
|
|
249
|
-
if (cacheTimeout === 0) {
|
|
250
|
-
return yield* fetchAndParseConfig(url);
|
|
251
|
-
}
|
|
252
|
-
const cache = yield* getCache;
|
|
253
|
-
return yield* cache.get(url);
|
|
254
|
-
});
|
|
255
|
-
var loadConfigEffect = (options = {}) => Effect.gen(function* () {
|
|
61
|
+
async function loadConfigWithOptions(options = {}) {
|
|
62
|
+
const loadOptions = options.cacheTimeout !== void 0 ? { cacheTimeout: options.cacheTimeout } : void 0;
|
|
256
63
|
if (options.config) {
|
|
257
|
-
return
|
|
64
|
+
return loadConfigFromInline(options.config);
|
|
258
65
|
}
|
|
259
66
|
const envConfigPath = process.env.ATRIM_INSTRUMENTATION_CONFIG;
|
|
260
67
|
if (envConfigPath) {
|
|
261
|
-
|
|
262
|
-
return yield* loadConfigFromUrlEffect(envConfigPath, options.cacheTimeout);
|
|
263
|
-
}
|
|
264
|
-
return yield* loadConfigFromFileEffect(envConfigPath);
|
|
68
|
+
return loadConfig(envConfigPath, loadOptions);
|
|
265
69
|
}
|
|
266
70
|
if (options.configUrl) {
|
|
267
|
-
return
|
|
71
|
+
return loadConfig(options.configUrl, loadOptions);
|
|
268
72
|
}
|
|
269
73
|
if (options.configPath) {
|
|
270
|
-
return
|
|
74
|
+
return loadConfig(options.configPath, loadOptions);
|
|
271
75
|
}
|
|
76
|
+
const { existsSync } = await import('fs');
|
|
77
|
+
const { join } = await import('path');
|
|
272
78
|
const defaultPath = join(process.cwd(), "instrumentation.yaml");
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return yield* loadConfigFromFileEffect(defaultPath);
|
|
79
|
+
if (existsSync(defaultPath)) {
|
|
80
|
+
return loadConfig(defaultPath, loadOptions);
|
|
276
81
|
}
|
|
277
82
|
return getDefaultConfig();
|
|
278
|
-
});
|
|
279
|
-
async function loadConfig(options = {}) {
|
|
280
|
-
return Effect.runPromise(
|
|
281
|
-
loadConfigEffect(options).pipe(
|
|
282
|
-
// Convert typed errors to regular Error with reason message for backward compatibility
|
|
283
|
-
Effect.mapError((error) => {
|
|
284
|
-
const message = error.reason;
|
|
285
|
-
const newError = new Error(message);
|
|
286
|
-
newError.cause = error.cause;
|
|
287
|
-
return newError;
|
|
288
|
-
})
|
|
289
|
-
)
|
|
290
|
-
);
|
|
291
83
|
}
|
|
292
|
-
var PatternMatcher = class {
|
|
293
|
-
constructor(config) {
|
|
294
|
-
__publicField(this, "ignorePatterns", []);
|
|
295
|
-
__publicField(this, "instrumentPatterns", []);
|
|
296
|
-
__publicField(this, "enabled", true);
|
|
297
|
-
this.enabled = config.instrumentation.enabled;
|
|
298
|
-
this.ignorePatterns = config.instrumentation.ignore_patterns.map((p) => this.compilePattern(p));
|
|
299
|
-
this.instrumentPatterns = config.instrumentation.instrument_patterns.filter((p) => p.enabled !== false).map((p) => this.compilePattern(p));
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Compile a pattern configuration into a RegExp
|
|
303
|
-
*/
|
|
304
|
-
compilePattern(pattern) {
|
|
305
|
-
try {
|
|
306
|
-
const compiled = {
|
|
307
|
-
regex: new RegExp(pattern.pattern),
|
|
308
|
-
enabled: pattern.enabled !== false
|
|
309
|
-
};
|
|
310
|
-
if (pattern.description !== void 0) {
|
|
311
|
-
compiled.description = pattern.description;
|
|
312
|
-
}
|
|
313
|
-
return compiled;
|
|
314
|
-
} catch (error) {
|
|
315
|
-
throw new Error(
|
|
316
|
-
`Failed to compile pattern "${pattern.pattern}": ${error instanceof Error ? error.message : String(error)}`
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Check if a span should be instrumented
|
|
322
|
-
*
|
|
323
|
-
* Returns true if the span should be created, false otherwise.
|
|
324
|
-
*
|
|
325
|
-
* Logic:
|
|
326
|
-
* 1. If instrumentation disabled globally, return false
|
|
327
|
-
* 2. Check ignore patterns - if any match, return false
|
|
328
|
-
* 3. Check instrument patterns - if any match, return true
|
|
329
|
-
* 4. Default: return true (fail-open - create span if no patterns match)
|
|
330
|
-
*/
|
|
331
|
-
shouldInstrument(spanName) {
|
|
332
|
-
if (!this.enabled) {
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
335
|
-
for (const pattern of this.ignorePatterns) {
|
|
336
|
-
if (pattern.regex.test(spanName)) {
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
for (const pattern of this.instrumentPatterns) {
|
|
341
|
-
if (pattern.enabled && pattern.regex.test(spanName)) {
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return true;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Get statistics about pattern matching (for debugging/monitoring)
|
|
349
|
-
*/
|
|
350
|
-
getStats() {
|
|
351
|
-
return {
|
|
352
|
-
enabled: this.enabled,
|
|
353
|
-
ignorePatternCount: this.ignorePatterns.length,
|
|
354
|
-
instrumentPatternCount: this.instrumentPatterns.filter((p) => p.enabled).length
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
function initializePatternMatcher(config) {
|
|
359
|
-
new PatternMatcher(config);
|
|
360
|
-
}
|
|
361
|
-
var Logger = class {
|
|
362
|
-
constructor() {
|
|
363
|
-
__publicField(this, "level", "on");
|
|
364
|
-
__publicField(this, "hasLoggedMinimal", false);
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Set the logging level
|
|
368
|
-
*/
|
|
369
|
-
setLevel(level) {
|
|
370
|
-
this.level = level;
|
|
371
|
-
this.hasLoggedMinimal = false;
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Get the current logging level
|
|
375
|
-
*/
|
|
376
|
-
getLevel() {
|
|
377
|
-
return this.level;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Log a minimal initialization message (only shown once in minimal mode)
|
|
381
|
-
*/
|
|
382
|
-
minimal(message) {
|
|
383
|
-
if (this.level === "off") {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (this.level === "minimal" && !this.hasLoggedMinimal) {
|
|
387
|
-
console.log(message);
|
|
388
|
-
this.hasLoggedMinimal = true;
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (this.level === "on") {
|
|
392
|
-
console.log(message);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Log an informational message
|
|
397
|
-
*/
|
|
398
|
-
log(...args) {
|
|
399
|
-
if (this.level === "on") {
|
|
400
|
-
console.log(...args);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Log a warning message (shown in minimal mode)
|
|
405
|
-
*/
|
|
406
|
-
warn(...args) {
|
|
407
|
-
if (this.level !== "off") {
|
|
408
|
-
console.warn(...args);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Log an error message (shown in minimal mode)
|
|
413
|
-
*/
|
|
414
|
-
error(...args) {
|
|
415
|
-
if (this.level !== "off") {
|
|
416
|
-
console.error(...args);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Check if full logging is enabled
|
|
421
|
-
*/
|
|
422
|
-
isEnabled() {
|
|
423
|
-
return this.level === "on";
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Check if minimal logging is enabled
|
|
427
|
-
*/
|
|
428
|
-
isMinimal() {
|
|
429
|
-
return this.level === "minimal";
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Check if logging is completely disabled
|
|
433
|
-
*/
|
|
434
|
-
isDisabled() {
|
|
435
|
-
return this.level === "off";
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
var logger = new Logger();
|
|
439
84
|
|
|
440
85
|
// src/integrations/effect/effect-tracer.ts
|
|
441
86
|
function createEffectInstrumentation(options = {}) {
|
|
442
87
|
return Layer.unwrapEffect(
|
|
443
88
|
Effect.gen(function* () {
|
|
444
89
|
const config = yield* Effect.tryPromise({
|
|
445
|
-
try: () =>
|
|
90
|
+
try: () => loadConfigWithOptions(options),
|
|
446
91
|
catch: (error) => ({
|
|
447
92
|
_tag: "ConfigError",
|
|
448
93
|
message: error instanceof Error ? error.message : String(error)
|