@auxiora/config 1.0.0

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/src/index.ts ADDED
@@ -0,0 +1,403 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { getConfigPath, isWindows } from '@auxiora/core';
5
+
6
+ const GatewayConfigSchema = z.object({
7
+ host: z.string().default('0.0.0.0'),
8
+ port: z.number().int().min(1).max(65535).default(18800),
9
+ corsOrigins: z.array(z.string()).default(['http://localhost:18800']),
10
+ });
11
+
12
+ const AuthConfigSchema = z.object({
13
+ mode: z.enum(['none', 'password', 'jwt']).default('none'),
14
+ /** Argon2id hash of the gateway password */
15
+ passwordHash: z.string().optional(),
16
+ /** Secret for signing JWT tokens (min 32 chars recommended) */
17
+ jwtSecret: z.string().optional(),
18
+ jwtExpiresIn: z.string().default('7d'),
19
+ refreshExpiresIn: z.string().default('30d'),
20
+ });
21
+
22
+ const RateLimitConfigSchema = z.object({
23
+ enabled: z.boolean().default(true),
24
+ windowMs: z.number().int().positive().default(60000), // 1 minute
25
+ maxRequests: z.number().int().positive().default(300),
26
+ });
27
+
28
+ const PairingConfigSchema = z.object({
29
+ enabled: z.boolean().default(true),
30
+ codeLength: z.number().int().min(4).max(12).default(6),
31
+ expiryMinutes: z.number().int().positive().default(15),
32
+ autoApproveChannels: z.array(z.string()).default(['webchat']),
33
+ persistPath: z.string().optional(),
34
+ });
35
+
36
+ const ModelRoutingSchema = z.object({
37
+ enabled: z.boolean().default(true),
38
+ defaultModel: z.string().optional(),
39
+ rules: z.array(z.object({
40
+ task: z.enum(['reasoning', 'code', 'creative', 'vision', 'long-context', 'fast', 'private', 'image-gen']),
41
+ provider: z.string(),
42
+ model: z.string(),
43
+ priority: z.number().default(0),
44
+ })).default([]),
45
+ costLimits: z.object({
46
+ dailyBudget: z.number().positive().optional(),
47
+ monthlyBudget: z.number().positive().optional(),
48
+ perMessageMax: z.number().positive().optional(),
49
+ warnAt: z.number().min(0).max(1).default(0.8),
50
+ }).default({}),
51
+ preferences: z.object({
52
+ preferLocal: z.boolean().default(false),
53
+ preferCheap: z.boolean().default(false),
54
+ sensitiveToLocal: z.boolean().default(false),
55
+ }).default({}),
56
+ });
57
+
58
+ const ProviderConfigSchema = z.object({
59
+ primary: z.string().default('anthropic'),
60
+ fallback: z.string().optional(),
61
+ anthropic: z.object({
62
+ model: z.string().default('claude-sonnet-4-20250514'),
63
+ maxTokens: z.number().int().positive().default(4096),
64
+ }).default({}),
65
+ openai: z.object({
66
+ model: z.string().default('gpt-5.2'),
67
+ maxTokens: z.number().int().positive().default(4096),
68
+ }).default({}),
69
+ google: z.object({
70
+ model: z.string().default('gemini-2.5-flash'),
71
+ maxTokens: z.number().int().positive().default(4096),
72
+ }).default({}),
73
+ ollama: z.object({
74
+ model: z.string().default('llama3'),
75
+ maxTokens: z.number().int().positive().default(4096),
76
+ baseUrl: z.string().default('http://localhost:11434'),
77
+ }).default({}),
78
+ openaiCompatible: z.object({
79
+ model: z.string().default(''),
80
+ maxTokens: z.number().int().positive().default(4096),
81
+ baseUrl: z.string().default(''),
82
+ name: z.string().default('custom'),
83
+ }).default({}),
84
+ });
85
+
86
+ const SessionConfigSchema = z.object({
87
+ maxContextTokens: z.number().int().positive().default(100000),
88
+ ttlMinutes: z.number().int().positive().default(1440), // 24 hours
89
+ autoSave: z.boolean().default(true),
90
+ compactionEnabled: z.boolean().default(true),
91
+ });
92
+
93
+ const LoggingConfigSchema = z.object({
94
+ level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
95
+ auditEnabled: z.boolean().default(true),
96
+ maxFileSizeMb: z.number().positive().default(10),
97
+ maxFiles: z.number().int().positive().default(5),
98
+ });
99
+
100
+ const ChannelConfigSchema = z.object({
101
+ discord: z.object({
102
+ enabled: z.boolean().default(false),
103
+ mentionOnly: z.boolean().default(true),
104
+ }).default({}),
105
+ telegram: z.object({
106
+ enabled: z.boolean().default(false),
107
+ webhookMode: z.boolean().default(false),
108
+ }).default({}),
109
+ slack: z.object({
110
+ enabled: z.boolean().default(false),
111
+ socketMode: z.boolean().default(true),
112
+ }).default({}),
113
+ twilio: z.object({
114
+ enabled: z.boolean().default(false),
115
+ smsEnabled: z.boolean().default(true),
116
+ whatsappEnabled: z.boolean().default(false),
117
+ }).default({}),
118
+ webchat: z.object({
119
+ enabled: z.boolean().default(true),
120
+ }).default({}),
121
+ matrix: z.object({
122
+ enabled: z.boolean().default(false),
123
+ autoJoinRooms: z.boolean().default(true),
124
+ }).default({}),
125
+ signal: z.object({
126
+ enabled: z.boolean().default(false),
127
+ }).default({}),
128
+ email: z.object({
129
+ enabled: z.boolean().default(false),
130
+ pollInterval: z.number().int().positive().default(30000),
131
+ }).default({}),
132
+ teams: z.object({
133
+ enabled: z.boolean().default(false),
134
+ }).default({}),
135
+ whatsapp: z.object({
136
+ enabled: z.boolean().default(false),
137
+ }).default({}),
138
+ });
139
+
140
+ const VoiceConfigSchema = z.object({
141
+ enabled: z.boolean().default(false),
142
+ sttProvider: z.enum(['openai-whisper']).default('openai-whisper'),
143
+ ttsProvider: z.enum(['openai-tts']).default('openai-tts'),
144
+ defaultVoice: z.string().default('alloy'),
145
+ language: z.string().default('en'),
146
+ maxAudioDuration: z.number().int().positive().default(30),
147
+ sampleRate: z.number().int().positive().default(16000),
148
+ });
149
+
150
+ const WebhookConfigSchema = z.object({
151
+ enabled: z.boolean().default(false),
152
+ basePath: z.string().default('/api/v1/webhooks'),
153
+ signatureHeader: z.string().default('x-webhook-signature'),
154
+ maxPayloadSize: z.number().int().positive().default(65536),
155
+ });
156
+
157
+ const DashboardConfigSchema = z.object({
158
+ enabled: z.boolean().default(true),
159
+ sessionTtlMs: z.number().int().positive().default(86_400_000),
160
+ });
161
+
162
+ // [P6] Desktop config
163
+ export const DesktopConfigSchema = z.object({
164
+ autoStart: z.boolean().default(false),
165
+ minimizeToTray: z.boolean().default(true),
166
+ hotkey: z.string().default('CommandOrControl+Shift+A'),
167
+ notificationsEnabled: z.boolean().default(true),
168
+ updateChannel: z.enum(['stable', 'beta', 'nightly']).default('stable'),
169
+ ollamaEnabled: z.boolean().default(false),
170
+ ollamaPort: z.number().int().min(1).max(65535).default(11434),
171
+ windowWidth: z.number().int().positive().default(1024),
172
+ windowHeight: z.number().int().positive().default(768),
173
+ });
174
+ export type DesktopConfig = z.infer<typeof DesktopConfigSchema>;
175
+
176
+ // [P7] Cloud config
177
+ export const CloudConfigSchema = z.object({
178
+ enabled: z.boolean().default(false),
179
+ baseDataDir: z.string().default('/data/tenants'),
180
+ jwtSecret: z.string().default(''),
181
+ stripeSecretKey: z.string().optional(),
182
+ stripeWebhookSecret: z.string().optional(),
183
+ domain: z.string().default('localhost'),
184
+ });
185
+ export type CloudConfig = z.infer<typeof CloudConfigSchema>;
186
+
187
+ // [P12] Trust / Autonomy
188
+ const TrustConfigSchema = z.object({
189
+ enabled: z.boolean().default(true),
190
+ defaultLevel: z.number().int().min(0).max(4).default(0),
191
+ autoPromote: z.boolean().default(true),
192
+ promotionThreshold: z.number().int().positive().default(10),
193
+ demotionThreshold: z.number().int().positive().default(3),
194
+ autoPromoteCeiling: z.number().int().min(0).max(4).default(3),
195
+ });
196
+
197
+ const ResearchConfigSchema = z.object({
198
+ enabled: z.boolean().default(true),
199
+ braveApiKey: z.string().optional(),
200
+ defaultDepth: z.enum(['quick', 'standard', 'deep']).default('standard'),
201
+ maxConcurrentSources: z.number().int().positive().default(5),
202
+ searchTimeout: z.number().int().positive().default(10000),
203
+ fetchTimeout: z.number().int().positive().default(15000),
204
+ });
205
+
206
+ const IntentConfigSchema = z.object({
207
+ enabled: z.boolean().default(true),
208
+ confidenceThreshold: z.number().min(0).max(1).default(0.3),
209
+ });
210
+
211
+ const PluginsConfigSchema = z.object({
212
+ enabled: z.boolean().default(true),
213
+ dir: z.string().optional(),
214
+ marketplace: z.object({
215
+ registryUrl: z.string().default('https://registry.auxiora.dev'),
216
+ autoUpdate: z.boolean().default(false),
217
+ }).default({}),
218
+ pluginConfigs: z.record(z.string(), z.unknown()).default({}),
219
+ approvedPermissions: z.record(z.string(), z.array(z.enum([
220
+ 'NETWORK', 'FILESYSTEM', 'SHELL', 'PROVIDER_ACCESS', 'CHANNEL_ACCESS', 'MEMORY_ACCESS',
221
+ ]))).default({}),
222
+ });
223
+
224
+ const MemoryConfigSchema = z.object({
225
+ enabled: z.boolean().default(true),
226
+ autoExtract: z.boolean().default(true),
227
+ maxEntries: z.number().int().positive().default(1000),
228
+ encryptAtRest: z.boolean().default(false),
229
+ cleanupIntervalMinutes: z.number().int().positive().default(60),
230
+ adaptivePersonality: z.boolean().default(true),
231
+ patternDetection: z.boolean().default(true),
232
+ relationshipTracking: z.boolean().default(true),
233
+ importanceDecay: z.number().min(0).max(1).default(0.01),
234
+ });
235
+
236
+ const OrchestrationConfigSchema = z.object({
237
+ enabled: z.boolean().default(true),
238
+ maxConcurrentAgents: z.number().int().min(1).max(10).default(5),
239
+ defaultTimeout: z.number().int().positive().default(60000),
240
+ totalTimeout: z.number().int().positive().default(300000),
241
+ allowedPatterns: z.array(z.enum([
242
+ 'parallel', 'sequential', 'debate', 'map-reduce', 'supervisor',
243
+ ])).default(['parallel', 'sequential', 'debate', 'map-reduce', 'supervisor']),
244
+ costMultiplierWarning: z.number().positive().default(3),
245
+ });
246
+
247
+ const UserPreferencesSchema = z.object({
248
+ verbosity: z.number().min(0).max(1).default(0.5),
249
+ formality: z.number().min(0).max(1).default(0.5),
250
+ proactiveness: z.number().min(0).max(1).default(0.5),
251
+ riskTolerance: z.number().min(0).max(1).default(0.5),
252
+ humor: z.number().min(0).max(1).default(0.3),
253
+ feedbackStyle: z.enum(['direct', 'sandwich', 'gentle']).default('direct'),
254
+ expertiseAssumption: z.enum(['beginner', 'intermediate', 'expert']).default('intermediate'),
255
+ });
256
+
257
+ const ModeIdSchema = z.enum([
258
+ 'operator', 'analyst', 'advisor', 'writer',
259
+ 'socratic', 'legal', 'roast', 'companion',
260
+ ]);
261
+
262
+ const ModesConfigSchema = z.object({
263
+ enabled: z.boolean().default(true),
264
+ defaultMode: z.union([ModeIdSchema, z.literal('auto'), z.literal('off')]).default('auto'),
265
+ autoDetection: z.boolean().default(true),
266
+ confirmationThreshold: z.number().min(0).max(1).default(0.4),
267
+ preferences: UserPreferencesSchema.default({}),
268
+ });
269
+
270
+ const AgentIdentitySchema = z.object({
271
+ name: z.string().default('Auxiora'),
272
+ pronouns: z.string().default('they/them'),
273
+ avatar: z.string().optional(),
274
+ vibe: z.string().max(200).optional(),
275
+ customInstructions: z.string().max(4000).optional(),
276
+ personality: z.string().default('professional'),
277
+ tone: z.object({
278
+ warmth: z.number().min(0).max(1).default(0.6),
279
+ directness: z.number().min(0).max(1).default(0.5),
280
+ humor: z.number().min(0).max(1).default(0.3),
281
+ formality: z.number().min(0).max(1).default(0.5),
282
+ }).default({}),
283
+ expertise: z.array(z.string()).default([]),
284
+ errorStyle: z.enum(['apologetic', 'matter_of_fact', 'self_deprecating', 'professional', 'gentle', 'detailed', 'encouraging', 'terse', 'educational']).default('professional'),
285
+ catchphrases: z.object({
286
+ greeting: z.string().optional(),
287
+ farewell: z.string().optional(),
288
+ thinking: z.string().optional(),
289
+ success: z.string().optional(),
290
+ error: z.string().optional(),
291
+ }).default({}),
292
+ boundaries: z.object({
293
+ neverJokeAbout: z.array(z.string()).default([]),
294
+ neverAdviseOn: z.array(z.string()).default([]),
295
+ }).default({}),
296
+ });
297
+
298
+ export type AgentIdentity = z.infer<typeof AgentIdentitySchema>;
299
+
300
+ export const ConfigSchema = z.object({
301
+ gateway: GatewayConfigSchema.default({}),
302
+ auth: AuthConfigSchema.default({}),
303
+ rateLimit: RateLimitConfigSchema.default({}),
304
+ pairing: PairingConfigSchema.default({}),
305
+ provider: ProviderConfigSchema.default({}),
306
+ routing: ModelRoutingSchema.default({}),
307
+ session: SessionConfigSchema.default({}),
308
+ logging: LoggingConfigSchema.default({}),
309
+ channels: ChannelConfigSchema.default({}),
310
+ voice: VoiceConfigSchema.default({}),
311
+ webhooks: WebhookConfigSchema.default({}),
312
+ dashboard: DashboardConfigSchema.default({}),
313
+ plugins: PluginsConfigSchema.default({}),
314
+ memory: MemoryConfigSchema.default({}),
315
+ orchestration: OrchestrationConfigSchema.default({}),
316
+ agent: AgentIdentitySchema.default({}),
317
+ modes: ModesConfigSchema.default({}),
318
+ research: ResearchConfigSchema.default({}),
319
+ // [P12] Trust / Autonomy
320
+ trust: TrustConfigSchema.default({}),
321
+ intent: IntentConfigSchema.default({}),
322
+ // [P6] Desktop
323
+ desktop: DesktopConfigSchema.default({}),
324
+ // [P7] Cloud
325
+ cloud: CloudConfigSchema.default({}),
326
+ });
327
+
328
+ export type Config = z.infer<typeof ConfigSchema>;
329
+ export type ModelRouting = z.infer<typeof ModelRoutingSchema>;
330
+ export type OrchestrationConfig = z.infer<typeof OrchestrationConfigSchema>;
331
+ export type ResearchConfig = z.infer<typeof ResearchConfigSchema>;
332
+ export type TrustConfig = z.infer<typeof TrustConfigSchema>;
333
+ export type IntentConfig = z.infer<typeof IntentConfigSchema>;
334
+ export type ModesConfig = z.infer<typeof ModesConfigSchema>;
335
+
336
+ const ENV_PREFIX = 'AUXIORA_';
337
+
338
+ function camelToSnake(str: string): string {
339
+ return str.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase();
340
+ }
341
+
342
+ function getEnvValue(path: string[]): string | undefined {
343
+ const envKey = ENV_PREFIX + path.map(camelToSnake).join('_');
344
+ return process.env[envKey];
345
+ }
346
+
347
+ function applyEnvOverrides(config: Record<string, unknown>, path: string[] = []): void {
348
+ for (const [key, value] of Object.entries(config)) {
349
+ const currentPath = [...path, key];
350
+
351
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
352
+ applyEnvOverrides(value as Record<string, unknown>, currentPath);
353
+ } else {
354
+ const envValue = getEnvValue(currentPath);
355
+ if (envValue !== undefined) {
356
+ if (typeof value === 'boolean') {
357
+ config[key] = envValue.toLowerCase() === 'true';
358
+ } else if (typeof value === 'number') {
359
+ config[key] = Number(envValue);
360
+ } else {
361
+ config[key] = envValue;
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ export async function loadConfig(): Promise<Config> {
369
+ const configPath = getConfigPath();
370
+ let rawConfig: Record<string, unknown> = {};
371
+
372
+ try {
373
+ const content = await fs.readFile(configPath, 'utf-8');
374
+ rawConfig = JSON.parse(content);
375
+ } catch (error) {
376
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
377
+ throw error;
378
+ }
379
+ // File doesn't exist, use defaults
380
+ }
381
+
382
+ // Apply environment variable overrides
383
+ applyEnvOverrides(rawConfig);
384
+
385
+ // Validate and return
386
+ return ConfigSchema.parse(rawConfig);
387
+ }
388
+
389
+ export async function saveConfig(config: Config): Promise<void> {
390
+ const configPath = getConfigPath();
391
+ const configDir = path.dirname(configPath);
392
+
393
+ await fs.mkdir(configDir, { recursive: true });
394
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
395
+
396
+ if (!isWindows()) {
397
+ await fs.chmod(configPath, 0o600);
398
+ }
399
+ }
400
+
401
+ export async function getDefaultConfig(): Promise<Config> {
402
+ return ConfigSchema.parse({});
403
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Configuration validation with detailed error messages
3
+ */
4
+
5
+ import { type Config } from './index.js';
6
+ import type { ZodError } from 'zod';
7
+
8
+ export interface ValidationResult {
9
+ valid: boolean;
10
+ errors: ValidationError[];
11
+ warnings: ValidationWarning[];
12
+ }
13
+
14
+ export interface ValidationError {
15
+ path: string;
16
+ message: string;
17
+ value?: any;
18
+ suggestion?: string;
19
+ }
20
+
21
+ export interface ValidationWarning {
22
+ path: string;
23
+ message: string;
24
+ suggestion: string;
25
+ }
26
+
27
+ /**
28
+ * Validate configuration and provide helpful error messages
29
+ */
30
+ export function validateConfig(config: Config): ValidationResult {
31
+ const errors: ValidationError[] = [];
32
+ const warnings: ValidationWarning[] = [];
33
+
34
+ // Validate gateway configuration
35
+ if (config.gateway.host === '0.0.0.0' && config.auth.mode === 'none') {
36
+ warnings.push({
37
+ path: 'gateway.host',
38
+ message: 'Gateway bound to 0.0.0.0 with no authentication',
39
+ suggestion: 'Consider binding to 127.0.0.1 or enabling JWT authentication for security',
40
+ });
41
+ }
42
+
43
+ if (config.gateway.port < 1024) {
44
+ warnings.push({
45
+ path: 'gateway.port',
46
+ message: `Port ${config.gateway.port} requires elevated privileges`,
47
+ suggestion: 'Use a port >= 1024 to avoid needing root/admin access',
48
+ });
49
+ }
50
+
51
+ // Validate authentication configuration
52
+ if (config.auth.mode === 'jwt') {
53
+ if (!config.auth.jwtSecret) {
54
+ errors.push({
55
+ path: 'auth.jwtSecret',
56
+ message: 'JWT secret required when auth mode is "jwt"',
57
+ suggestion: 'Generate a secret: openssl rand -hex 32',
58
+ });
59
+ } else if (config.auth.jwtSecret.length < 32) {
60
+ warnings.push({
61
+ path: 'auth.jwtSecret',
62
+ message: 'JWT secret should be at least 32 characters',
63
+ suggestion: 'Use a longer secret for better security: openssl rand -hex 32',
64
+ });
65
+ }
66
+ }
67
+
68
+ if (config.auth.mode === 'password' && !config.auth.passwordHash) {
69
+ errors.push({
70
+ path: 'auth.passwordHash',
71
+ message: 'Password hash required when auth mode is "password"',
72
+ suggestion: 'Set a password using the CLI: auxiora auth set-password',
73
+ });
74
+ }
75
+
76
+ // Validate rate limiting
77
+ if (!config.rateLimit.enabled) {
78
+ warnings.push({
79
+ path: 'rateLimit.enabled',
80
+ message: 'Rate limiting is disabled',
81
+ suggestion: 'Enable rate limiting to prevent abuse',
82
+ });
83
+ }
84
+
85
+ if (config.rateLimit.maxRequests > 1000) {
86
+ warnings.push({
87
+ path: 'rateLimit.maxRequests',
88
+ message: `High rate limit (${config.rateLimit.maxRequests} requests)`,
89
+ suggestion: 'Consider a lower limit to prevent API quota exhaustion',
90
+ });
91
+ }
92
+
93
+ // Validate provider configuration
94
+ if (config.provider.primary === config.provider.fallback) {
95
+ warnings.push({
96
+ path: 'provider.fallback',
97
+ message: 'Fallback provider same as primary',
98
+ suggestion: 'Use a different provider for fallback or remove fallback',
99
+ });
100
+ }
101
+
102
+ // Validate session configuration
103
+ if (config.session.maxContextTokens > 200000) {
104
+ warnings.push({
105
+ path: 'session.maxContextTokens',
106
+ message: 'Very large context window may cause performance issues',
107
+ suggestion: 'Consider reducing to 100000 tokens for better performance',
108
+ });
109
+ }
110
+
111
+ if (config.session.ttlMinutes < 60) {
112
+ warnings.push({
113
+ path: 'session.ttlMinutes',
114
+ message: 'Short session TTL may cause frequent session loss',
115
+ suggestion: 'Consider at least 60 minutes for better user experience',
116
+ });
117
+ }
118
+
119
+ // Validate channel configuration
120
+ const enabledChannels = Object.entries(config.channels)
121
+ .filter(([_, channelConfig]) => channelConfig.enabled)
122
+ .map(([name]) => name);
123
+
124
+ if (enabledChannels.length === 0) {
125
+ warnings.push({
126
+ path: 'channels',
127
+ message: 'No channels enabled',
128
+ suggestion: 'Enable at least webchat for testing',
129
+ });
130
+ }
131
+
132
+ // Validate logging configuration
133
+ if (config.logging.maxFileSizeMb > 100) {
134
+ warnings.push({
135
+ path: 'logging.maxFileSizeMb',
136
+ message: 'Large log file size may consume significant disk space',
137
+ suggestion: 'Consider a smaller size with log rotation',
138
+ });
139
+ }
140
+
141
+ return {
142
+ valid: errors.length === 0,
143
+ errors,
144
+ warnings,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Format validation errors for display
150
+ */
151
+ export function formatValidationErrors(result: ValidationResult): string {
152
+ const lines: string[] = [];
153
+
154
+ if (result.errors.length > 0) {
155
+ lines.push('āŒ Configuration Errors:\n');
156
+ for (const error of result.errors) {
157
+ lines.push(` ${error.path}: ${error.message}`);
158
+ if (error.suggestion) {
159
+ lines.push(` šŸ’” ${error.suggestion}`);
160
+ }
161
+ lines.push('');
162
+ }
163
+ }
164
+
165
+ if (result.warnings.length > 0) {
166
+ lines.push('āš ļø Configuration Warnings:\n');
167
+ for (const warning of result.warnings) {
168
+ lines.push(` ${warning.path}: ${warning.message}`);
169
+ lines.push(` šŸ’” ${warning.suggestion}`);
170
+ lines.push('');
171
+ }
172
+ }
173
+
174
+ return lines.join('\n');
175
+ }
176
+
177
+ /**
178
+ * Format Zod validation errors
179
+ */
180
+ export function formatZodError(error: ZodError): string {
181
+ const lines: string[] = ['āŒ Configuration Validation Failed:\n'];
182
+
183
+ for (const issue of error.issues) {
184
+ const path = issue.path.join('.');
185
+ lines.push(` ${path || 'config'}: ${issue.message}`);
186
+
187
+ // Add helpful suggestions based on error type
188
+ if (issue.code === 'invalid_type') {
189
+ lines.push(` šŸ’” Expected ${issue.expected}, got ${issue.received}`);
190
+ } else if (issue.code === 'invalid_enum_value') {
191
+ lines.push(` šŸ’” Valid options: ${issue.options.join(', ')}`);
192
+ } else if (issue.code === 'too_small') {
193
+ lines.push(` šŸ’” Minimum value: ${issue.minimum}`);
194
+ } else if (issue.code === 'too_big') {
195
+ lines.push(` šŸ’” Maximum value: ${issue.maximum}`);
196
+ }
197
+
198
+ lines.push('');
199
+ }
200
+
201
+ lines.push('See .env.example for configuration reference');
202
+
203
+ return lines.join('\n');
204
+ }
205
+
206
+ /**
207
+ * Validate and report configuration issues
208
+ */
209
+ export function validateAndReport(config: Config): boolean {
210
+ const result = validateConfig(config);
211
+
212
+ if (!result.valid || result.warnings.length > 0) {
213
+ console.error(formatValidationErrors(result));
214
+ }
215
+
216
+ if (!result.valid) {
217
+ console.error('\nā›” Cannot start with invalid configuration\n');
218
+ return false;
219
+ }
220
+
221
+ if (result.warnings.length > 0) {
222
+ console.warn('āš ļø Starting with warnings (see above)\n');
223
+ }
224
+
225
+ return true;
226
+ }