@cpretzinger/boss-claude 1.0.0 → 1.0.2
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 +304 -1
- package/bin/boss-claude.js +1138 -0
- package/bin/commands/mode.js +250 -0
- package/bin/onyx-guard.js +259 -0
- package/bin/onyx-guard.sh +251 -0
- package/bin/prompts.js +284 -0
- package/bin/rollback.js +85 -0
- package/bin/setup-wizard.js +492 -0
- package/config/.env.example +17 -0
- package/lib/README.md +83 -0
- package/lib/agent-logger.js +61 -0
- package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
- package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
- package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
- package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
- package/lib/agents/memory-supervisor.js +526 -0
- package/lib/agents/registry.js +135 -0
- package/lib/auto-monitor.js +131 -0
- package/lib/checkpoint-hook.js +112 -0
- package/lib/checkpoint.js +319 -0
- package/lib/commentator.js +213 -0
- package/lib/context-scribe.js +120 -0
- package/lib/delegation-strategies.js +326 -0
- package/lib/hierarchy-validator.js +643 -0
- package/lib/index.js +15 -0
- package/lib/init-with-mode.js +261 -0
- package/lib/init.js +44 -6
- package/lib/memory-result-aggregator.js +252 -0
- package/lib/memory.js +35 -7
- package/lib/mode-enforcer.js +473 -0
- package/lib/onyx-banner.js +169 -0
- package/lib/onyx-identity.js +214 -0
- package/lib/onyx-monitor.js +381 -0
- package/lib/onyx-reminder.js +188 -0
- package/lib/onyx-tool-interceptor.js +341 -0
- package/lib/onyx-wrapper.js +315 -0
- package/lib/orchestrator-gate.js +334 -0
- package/lib/output-formatter.js +296 -0
- package/lib/postgres.js +1 -1
- package/lib/prompt-injector.js +220 -0
- package/lib/prompts.js +532 -0
- package/lib/session.js +153 -6
- package/lib/setup/README.md +187 -0
- package/lib/setup/env-manager.js +785 -0
- package/lib/setup/error-recovery.js +630 -0
- package/lib/setup/explain-scopes.js +385 -0
- package/lib/setup/github-instructions.js +333 -0
- package/lib/setup/github-repo.js +254 -0
- package/lib/setup/import-credentials.js +498 -0
- package/lib/setup/index.js +62 -0
- package/lib/setup/init-postgres.js +785 -0
- package/lib/setup/init-redis.js +456 -0
- package/lib/setup/integration-test.js +652 -0
- package/lib/setup/progress.js +357 -0
- package/lib/setup/rollback.js +670 -0
- package/lib/setup/rollback.test.js +452 -0
- package/lib/setup/setup-with-rollback.example.js +351 -0
- package/lib/setup/summary.js +400 -0
- package/lib/setup/test-github-setup.js +10 -0
- package/lib/setup/test-postgres-init.js +98 -0
- package/lib/setup/verify-setup.js +102 -0
- package/lib/task-agent-worker.js +235 -0
- package/lib/token-monitor.js +466 -0
- package/lib/tool-wrapper-integration.js +369 -0
- package/lib/tool-wrapper.js +387 -0
- package/lib/validators/README.md +497 -0
- package/lib/validators/config.js +583 -0
- package/lib/validators/config.test.js +175 -0
- package/lib/validators/github.js +310 -0
- package/lib/validators/github.test.js +61 -0
- package/lib/validators/index.js +15 -0
- package/lib/validators/postgres.js +525 -0
- package/package.json +98 -13
- package/scripts/benchmark-memory.js +433 -0
- package/scripts/check-secrets.sh +12 -0
- package/scripts/fetch-todos.mjs +148 -0
- package/scripts/graceful-shutdown.sh +156 -0
- package/scripts/install-onyx-hooks.js +373 -0
- package/scripts/install.js +119 -18
- package/scripts/redis-monitor.js +284 -0
- package/scripts/redis-setup.js +412 -0
- package/scripts/test-memory-retrieval.js +201 -0
- package/scripts/validate-exports.js +68 -0
- package/scripts/validate-package.js +120 -0
- package/scripts/verify-onyx-deployment.js +309 -0
- package/scripts/verify-redis-deployment.js +354 -0
- package/scripts/verify-redis-init.js +219 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boss Claude Configuration Validator
|
|
3
|
+
* Validates entire ~/.boss-claude/.env file for completeness and correctness
|
|
4
|
+
*
|
|
5
|
+
* Checks:
|
|
6
|
+
* - All required environment variables are present
|
|
7
|
+
* - URLs are properly formatted
|
|
8
|
+
* - Redis URLs are valid
|
|
9
|
+
* - PostgreSQL URLs are valid
|
|
10
|
+
* - GitHub tokens are present (format check only, no API validation)
|
|
11
|
+
* - OpenAI API keys are present and formatted correctly
|
|
12
|
+
* - No obviously broken values (empty strings, placeholder text)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import dotenv from 'dotenv';
|
|
16
|
+
import { existsSync, readFileSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
import { validateUrl as validatePostgresUrl } from './postgres.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Required environment variables for Boss Claude
|
|
23
|
+
*/
|
|
24
|
+
const REQUIRED_VARS = [
|
|
25
|
+
'REDIS_URL',
|
|
26
|
+
'POSTGRES_URL',
|
|
27
|
+
'GITHUB_TOKEN',
|
|
28
|
+
'OPENAI_API_KEY'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Optional environment variables with defaults
|
|
33
|
+
*/
|
|
34
|
+
const OPTIONAL_VARS = {
|
|
35
|
+
'BOSS_LOG_LEVEL': 'info',
|
|
36
|
+
'BOSS_MAX_SESSIONS': '100',
|
|
37
|
+
'BOSS_TOKEN_BANK_LIMIT': '1000000',
|
|
38
|
+
'BOSS_XP_MULTIPLIER': '1.0'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates Redis URL format
|
|
43
|
+
* @param {string} url - Redis URL
|
|
44
|
+
* @returns {Object} Validation result
|
|
45
|
+
*/
|
|
46
|
+
export function validateRedisUrl(url) {
|
|
47
|
+
if (!url || typeof url !== 'string') {
|
|
48
|
+
return {
|
|
49
|
+
valid: false,
|
|
50
|
+
error: 'Redis URL is required and must be a string',
|
|
51
|
+
field: 'REDIS_URL'
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Redis URL formats:
|
|
56
|
+
// redis://[[username:]password@]host[:port][/database]
|
|
57
|
+
// rediss://[[username:]password@]host[:port][/database] (SSL)
|
|
58
|
+
const redisUrlPattern = /^rediss?:\/\/(?:([^:@]+)(?::([^@]+))?@)?([^:\/]+)(?::(\d+))?(?:\/(\d+))?$/;
|
|
59
|
+
const match = url.match(redisUrlPattern);
|
|
60
|
+
|
|
61
|
+
if (!match) {
|
|
62
|
+
return {
|
|
63
|
+
valid: false,
|
|
64
|
+
error: 'Invalid Redis URL format',
|
|
65
|
+
field: 'REDIS_URL',
|
|
66
|
+
expected: 'redis://[username:password@]host:port[/database]',
|
|
67
|
+
received: url.substring(0, 50) + (url.length > 50 ? '...' : '')
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [, username, password, host, port, database] = match;
|
|
72
|
+
|
|
73
|
+
// Validate host is not empty
|
|
74
|
+
if (!host || host.trim() === '') {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: 'Redis host cannot be empty',
|
|
78
|
+
field: 'REDIS_URL'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check for placeholder values
|
|
83
|
+
const placeholders = ['localhost', 'example.com', 'your-redis-url', 'REPLACE_ME'];
|
|
84
|
+
if (placeholders.some(p => url.toLowerCase().includes(p))) {
|
|
85
|
+
return {
|
|
86
|
+
valid: false,
|
|
87
|
+
error: 'Redis URL appears to be a placeholder',
|
|
88
|
+
field: 'REDIS_URL',
|
|
89
|
+
suggestion: 'Replace with actual Redis connection URL'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
valid: true,
|
|
95
|
+
field: 'REDIS_URL',
|
|
96
|
+
components: {
|
|
97
|
+
username: username || null,
|
|
98
|
+
hasPassword: !!password,
|
|
99
|
+
host,
|
|
100
|
+
port: port ? parseInt(port, 10) : 6379,
|
|
101
|
+
database: database ? parseInt(database, 10) : 0,
|
|
102
|
+
ssl: url.startsWith('rediss://'),
|
|
103
|
+
isRailway: host.includes('railway.app') || host.includes('rlwy.net')
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validates GitHub token format (basic check, no API validation)
|
|
110
|
+
* @param {string} token - GitHub token
|
|
111
|
+
* @returns {Object} Validation result
|
|
112
|
+
*/
|
|
113
|
+
export function validateGitHubToken(token) {
|
|
114
|
+
if (!token || typeof token !== 'string') {
|
|
115
|
+
return {
|
|
116
|
+
valid: false,
|
|
117
|
+
error: 'GitHub token is required',
|
|
118
|
+
field: 'GITHUB_TOKEN',
|
|
119
|
+
suggestion: 'Create a token at https://github.com/settings/tokens'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Trim whitespace
|
|
124
|
+
token = token.trim();
|
|
125
|
+
|
|
126
|
+
// Check for empty or placeholder values
|
|
127
|
+
const placeholders = ['YOUR_TOKEN', 'REPLACE_ME', 'your-github-token', 'ghp_placeholder'];
|
|
128
|
+
if (placeholders.some(p => token.toLowerCase().includes(p.toLowerCase()))) {
|
|
129
|
+
return {
|
|
130
|
+
valid: false,
|
|
131
|
+
error: 'GitHub token appears to be a placeholder',
|
|
132
|
+
field: 'GITHUB_TOKEN',
|
|
133
|
+
suggestion: 'Replace with actual GitHub personal access token'
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check token length (GitHub tokens are typically 40+ characters)
|
|
138
|
+
if (token.length < 20) {
|
|
139
|
+
return {
|
|
140
|
+
valid: false,
|
|
141
|
+
error: 'GitHub token appears too short',
|
|
142
|
+
field: 'GITHUB_TOKEN',
|
|
143
|
+
details: `Token length: ${token.length} (expected: 40+)`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check for common token prefixes
|
|
148
|
+
// Classic tokens: ghp_ (personal), gho_ (OAuth), ghu_ (user), ghs_ (server)
|
|
149
|
+
// Fine-grained: github_pat_
|
|
150
|
+
const validPrefixes = ['ghp_', 'gho_', 'ghu_', 'ghs_', 'github_pat_'];
|
|
151
|
+
const hasValidPrefix = validPrefixes.some(prefix => token.startsWith(prefix));
|
|
152
|
+
|
|
153
|
+
if (!hasValidPrefix) {
|
|
154
|
+
return {
|
|
155
|
+
valid: false,
|
|
156
|
+
error: 'GitHub token has unexpected format',
|
|
157
|
+
field: 'GITHUB_TOKEN',
|
|
158
|
+
details: `Expected prefix: ${validPrefixes.join(', ')}`,
|
|
159
|
+
warning: 'Token may still be valid, but format is non-standard'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
valid: true,
|
|
165
|
+
field: 'GITHUB_TOKEN',
|
|
166
|
+
tokenType: token.startsWith('github_pat_') ? 'fine-grained' : 'classic',
|
|
167
|
+
length: token.length
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validates OpenAI API key format
|
|
173
|
+
* @param {string} apiKey - OpenAI API key
|
|
174
|
+
* @returns {Object} Validation result
|
|
175
|
+
*/
|
|
176
|
+
export function validateOpenAiKey(apiKey) {
|
|
177
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
178
|
+
return {
|
|
179
|
+
valid: false,
|
|
180
|
+
error: 'OpenAI API key is required',
|
|
181
|
+
field: 'OPENAI_API_KEY',
|
|
182
|
+
suggestion: 'Get your API key at https://platform.openai.com/api-keys'
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Trim whitespace
|
|
187
|
+
apiKey = apiKey.trim();
|
|
188
|
+
|
|
189
|
+
// Check for placeholder values
|
|
190
|
+
const placeholders = ['YOUR_KEY', 'REPLACE_ME', 'your-openai-key', 'sk-placeholder'];
|
|
191
|
+
if (placeholders.some(p => apiKey.toLowerCase().includes(p.toLowerCase()))) {
|
|
192
|
+
return {
|
|
193
|
+
valid: false,
|
|
194
|
+
error: 'OpenAI API key appears to be a placeholder',
|
|
195
|
+
field: 'OPENAI_API_KEY',
|
|
196
|
+
suggestion: 'Replace with actual OpenAI API key'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// OpenAI API keys start with 'sk-' and are typically 51 characters
|
|
201
|
+
// New format: sk-proj-... (longer)
|
|
202
|
+
if (!apiKey.startsWith('sk-')) {
|
|
203
|
+
return {
|
|
204
|
+
valid: false,
|
|
205
|
+
error: 'OpenAI API key has invalid format',
|
|
206
|
+
field: 'OPENAI_API_KEY',
|
|
207
|
+
details: 'API key must start with "sk-"'
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check minimum length
|
|
212
|
+
if (apiKey.length < 40) {
|
|
213
|
+
return {
|
|
214
|
+
valid: false,
|
|
215
|
+
error: 'OpenAI API key appears too short',
|
|
216
|
+
field: 'OPENAI_API_KEY',
|
|
217
|
+
details: `Key length: ${apiKey.length} (expected: 50+)`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Detect key type
|
|
222
|
+
let keyType = 'legacy';
|
|
223
|
+
if (apiKey.startsWith('sk-proj-')) {
|
|
224
|
+
keyType = 'project';
|
|
225
|
+
} else if (apiKey.startsWith('sk-org-')) {
|
|
226
|
+
keyType = 'organization';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
valid: true,
|
|
231
|
+
field: 'OPENAI_API_KEY',
|
|
232
|
+
keyType,
|
|
233
|
+
length: apiKey.length
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validates a single environment variable
|
|
239
|
+
* @param {string} key - Variable name
|
|
240
|
+
* @param {string} value - Variable value
|
|
241
|
+
* @returns {Object} Validation result
|
|
242
|
+
*/
|
|
243
|
+
function validateVariable(key, value) {
|
|
244
|
+
// Check if value is empty or just whitespace
|
|
245
|
+
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
|
246
|
+
return {
|
|
247
|
+
valid: false,
|
|
248
|
+
error: `${key} is empty`,
|
|
249
|
+
field: key,
|
|
250
|
+
suggestion: 'Provide a valid value'
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Route to specific validator based on key
|
|
255
|
+
switch (key) {
|
|
256
|
+
case 'REDIS_URL':
|
|
257
|
+
return validateRedisUrl(value);
|
|
258
|
+
|
|
259
|
+
case 'POSTGRES_URL':
|
|
260
|
+
return validatePostgresUrl(value);
|
|
261
|
+
|
|
262
|
+
case 'GITHUB_TOKEN':
|
|
263
|
+
return validateGitHubToken(value);
|
|
264
|
+
|
|
265
|
+
case 'OPENAI_API_KEY':
|
|
266
|
+
return validateOpenAiKey(value);
|
|
267
|
+
|
|
268
|
+
default:
|
|
269
|
+
// For other variables, just check they're not empty
|
|
270
|
+
return {
|
|
271
|
+
valid: true,
|
|
272
|
+
field: key,
|
|
273
|
+
value: value.substring(0, 50) + (value.length > 50 ? '...' : '')
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Load and parse .env file from ~/.boss-claude/
|
|
280
|
+
* @returns {Object} Parsed environment variables
|
|
281
|
+
*/
|
|
282
|
+
export function loadEnvFile() {
|
|
283
|
+
const envPath = join(os.homedir(), '.boss-claude', '.env');
|
|
284
|
+
|
|
285
|
+
if (!existsSync(envPath)) {
|
|
286
|
+
return {
|
|
287
|
+
exists: false,
|
|
288
|
+
path: envPath,
|
|
289
|
+
error: 'Configuration file not found',
|
|
290
|
+
suggestion: 'Run: boss-claude init'
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// Read file content
|
|
296
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
297
|
+
|
|
298
|
+
// Parse with dotenv
|
|
299
|
+
const parsed = dotenv.parse(content);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
exists: true,
|
|
303
|
+
path: envPath,
|
|
304
|
+
variables: parsed,
|
|
305
|
+
lineCount: content.split('\n').length,
|
|
306
|
+
fileSize: content.length
|
|
307
|
+
};
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return {
|
|
310
|
+
exists: true,
|
|
311
|
+
path: envPath,
|
|
312
|
+
error: `Failed to parse .env file: ${error.message}`,
|
|
313
|
+
suggestion: 'Check file format and permissions'
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Comprehensive configuration validation
|
|
320
|
+
* @param {Object} options - Validation options
|
|
321
|
+
* @param {boolean} options.includeOptional - Validate optional variables
|
|
322
|
+
* @param {boolean} options.testConnections - Test actual connections (slow)
|
|
323
|
+
* @returns {Promise<Object>} Validation report
|
|
324
|
+
*/
|
|
325
|
+
export async function validateConfig(options = {}) {
|
|
326
|
+
const {
|
|
327
|
+
includeOptional = true,
|
|
328
|
+
testConnections = false
|
|
329
|
+
} = options;
|
|
330
|
+
|
|
331
|
+
const startTime = Date.now();
|
|
332
|
+
|
|
333
|
+
// Load .env file
|
|
334
|
+
const envFile = loadEnvFile();
|
|
335
|
+
if (!envFile.exists) {
|
|
336
|
+
return {
|
|
337
|
+
valid: false,
|
|
338
|
+
envFile,
|
|
339
|
+
variables: {},
|
|
340
|
+
errors: [{
|
|
341
|
+
severity: 'critical',
|
|
342
|
+
message: envFile.error,
|
|
343
|
+
suggestion: envFile.suggestion
|
|
344
|
+
}],
|
|
345
|
+
warnings: [],
|
|
346
|
+
summary: {
|
|
347
|
+
total: 0,
|
|
348
|
+
valid: 0,
|
|
349
|
+
invalid: 0,
|
|
350
|
+
missing: REQUIRED_VARS.length
|
|
351
|
+
},
|
|
352
|
+
duration: Date.now() - startTime
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (envFile.error) {
|
|
357
|
+
return {
|
|
358
|
+
valid: false,
|
|
359
|
+
envFile,
|
|
360
|
+
variables: {},
|
|
361
|
+
errors: [{
|
|
362
|
+
severity: 'critical',
|
|
363
|
+
message: envFile.error,
|
|
364
|
+
suggestion: envFile.suggestion
|
|
365
|
+
}],
|
|
366
|
+
warnings: [],
|
|
367
|
+
summary: {
|
|
368
|
+
total: 0,
|
|
369
|
+
valid: 0,
|
|
370
|
+
invalid: 0,
|
|
371
|
+
missing: REQUIRED_VARS.length
|
|
372
|
+
},
|
|
373
|
+
duration: Date.now() - startTime
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const variables = envFile.variables || {};
|
|
378
|
+
const validationResults = {};
|
|
379
|
+
const errors = [];
|
|
380
|
+
const warnings = [];
|
|
381
|
+
|
|
382
|
+
// Check for missing required variables
|
|
383
|
+
const missing = REQUIRED_VARS.filter(key => !(key in variables));
|
|
384
|
+
if (missing.length > 0) {
|
|
385
|
+
errors.push({
|
|
386
|
+
severity: 'critical',
|
|
387
|
+
field: 'configuration',
|
|
388
|
+
message: `Missing required variables: ${missing.join(', ')}`,
|
|
389
|
+
missingVars: missing,
|
|
390
|
+
suggestion: 'Add missing variables to ~/.boss-claude/.env'
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Validate each required variable
|
|
395
|
+
for (const key of REQUIRED_VARS) {
|
|
396
|
+
if (key in variables) {
|
|
397
|
+
const result = validateVariable(key, variables[key]);
|
|
398
|
+
validationResults[key] = result;
|
|
399
|
+
|
|
400
|
+
if (!result.valid) {
|
|
401
|
+
errors.push({
|
|
402
|
+
severity: 'error',
|
|
403
|
+
field: result.field,
|
|
404
|
+
message: result.error,
|
|
405
|
+
details: result.details,
|
|
406
|
+
suggestion: result.suggestion
|
|
407
|
+
});
|
|
408
|
+
} else if (result.warning) {
|
|
409
|
+
warnings.push({
|
|
410
|
+
severity: 'warning',
|
|
411
|
+
field: result.field,
|
|
412
|
+
message: result.warning,
|
|
413
|
+
details: result.details
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Validate optional variables if requested
|
|
420
|
+
if (includeOptional) {
|
|
421
|
+
for (const [key, defaultValue] of Object.entries(OPTIONAL_VARS)) {
|
|
422
|
+
if (key in variables) {
|
|
423
|
+
const result = validateVariable(key, variables[key]);
|
|
424
|
+
validationResults[key] = result;
|
|
425
|
+
|
|
426
|
+
if (!result.valid) {
|
|
427
|
+
warnings.push({
|
|
428
|
+
severity: 'warning',
|
|
429
|
+
field: result.field,
|
|
430
|
+
message: result.error,
|
|
431
|
+
details: `Will use default: ${defaultValue}`,
|
|
432
|
+
suggestion: result.suggestion
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
warnings.push({
|
|
437
|
+
severity: 'info',
|
|
438
|
+
field: key,
|
|
439
|
+
message: `Optional variable not set`,
|
|
440
|
+
details: `Using default: ${defaultValue}`
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Calculate summary
|
|
447
|
+
const totalVars = Object.keys(validationResults).length;
|
|
448
|
+
const validVars = Object.values(validationResults).filter(r => r.valid).length;
|
|
449
|
+
const invalidVars = totalVars - validVars;
|
|
450
|
+
|
|
451
|
+
const isValid = errors.filter(e => e.severity === 'critical' || e.severity === 'error').length === 0;
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
valid: isValid,
|
|
455
|
+
envFile,
|
|
456
|
+
variables: validationResults,
|
|
457
|
+
errors,
|
|
458
|
+
warnings,
|
|
459
|
+
summary: {
|
|
460
|
+
total: totalVars,
|
|
461
|
+
valid: validVars,
|
|
462
|
+
invalid: invalidVars,
|
|
463
|
+
missing: missing.length,
|
|
464
|
+
configurationHealth: isValid ? 'healthy' : 'unhealthy'
|
|
465
|
+
},
|
|
466
|
+
duration: Date.now() - startTime
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Pretty print configuration validation report
|
|
472
|
+
* @param {Object} report - Report from validateConfig()
|
|
473
|
+
*/
|
|
474
|
+
export function printReport(report) {
|
|
475
|
+
console.log('\n╔════════════════════════════════════════════════════════╗');
|
|
476
|
+
console.log('║ Boss Claude Configuration Validation Report ║');
|
|
477
|
+
console.log('╚════════════════════════════════════════════════════════╝\n');
|
|
478
|
+
|
|
479
|
+
// Environment file status
|
|
480
|
+
console.log('📄 Environment File:');
|
|
481
|
+
if (report.envFile.exists) {
|
|
482
|
+
console.log(` ✓ Found: ${report.envFile.path}`);
|
|
483
|
+
console.log(` Size: ${report.envFile.fileSize} bytes (${report.envFile.lineCount} lines)`);
|
|
484
|
+
} else {
|
|
485
|
+
console.log(` ✗ Not found: ${report.envFile.path}`);
|
|
486
|
+
console.log(` → ${report.envFile.suggestion}\n`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Variables validation
|
|
491
|
+
console.log('\n🔍 Variables Validation:\n');
|
|
492
|
+
|
|
493
|
+
// Required variables
|
|
494
|
+
console.log(' Required Variables:');
|
|
495
|
+
for (const key of REQUIRED_VARS) {
|
|
496
|
+
const result = report.variables[key];
|
|
497
|
+
if (!result) {
|
|
498
|
+
console.log(` ✗ ${key}: MISSING`);
|
|
499
|
+
} else if (result.valid) {
|
|
500
|
+
console.log(` ✓ ${key}: OK`);
|
|
501
|
+
// Show additional info for some variables
|
|
502
|
+
if (key === 'REDIS_URL' && result.components) {
|
|
503
|
+
console.log(` - Host: ${result.components.host}:${result.components.port}`);
|
|
504
|
+
console.log(` - SSL: ${result.components.ssl ? 'Yes' : 'No'}`);
|
|
505
|
+
} else if (key === 'POSTGRES_URL' && result.components) {
|
|
506
|
+
console.log(` - Host: ${result.components.host}:${result.components.port}`);
|
|
507
|
+
console.log(` - Database: ${result.components.database}`);
|
|
508
|
+
} else if (key === 'GITHUB_TOKEN' && result.tokenType) {
|
|
509
|
+
console.log(` - Type: ${result.tokenType}`);
|
|
510
|
+
} else if (key === 'OPENAI_API_KEY' && result.keyType) {
|
|
511
|
+
console.log(` - Type: ${result.keyType}`);
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
console.log(` ✗ ${key}: ${result.error}`);
|
|
515
|
+
if (result.suggestion) {
|
|
516
|
+
console.log(` → ${result.suggestion}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Errors
|
|
522
|
+
if (report.errors.length > 0) {
|
|
523
|
+
console.log('\n❌ Errors:');
|
|
524
|
+
report.errors.forEach((error, i) => {
|
|
525
|
+
console.log(` ${i + 1}. [${error.severity.toUpperCase()}] ${error.message}`);
|
|
526
|
+
if (error.details) {
|
|
527
|
+
console.log(` Details: ${error.details}`);
|
|
528
|
+
}
|
|
529
|
+
if (error.suggestion) {
|
|
530
|
+
console.log(` → ${error.suggestion}`);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Warnings
|
|
536
|
+
if (report.warnings.length > 0) {
|
|
537
|
+
console.log('\n⚠️ Warnings:');
|
|
538
|
+
report.warnings.forEach((warning, i) => {
|
|
539
|
+
console.log(` ${i + 1}. [${warning.severity.toUpperCase()}] ${warning.field}: ${warning.message}`);
|
|
540
|
+
if (warning.details) {
|
|
541
|
+
console.log(` Details: ${warning.details}`);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Summary
|
|
547
|
+
console.log('\n📊 Summary:');
|
|
548
|
+
console.log(` Total Variables: ${report.summary.total}`);
|
|
549
|
+
console.log(` Valid: ${report.summary.valid} ✓`);
|
|
550
|
+
console.log(` Invalid: ${report.summary.invalid} ✗`);
|
|
551
|
+
console.log(` Missing: ${report.summary.missing}`);
|
|
552
|
+
console.log(` Configuration Health: ${report.summary.configurationHealth.toUpperCase()}`);
|
|
553
|
+
console.log(` Validation Duration: ${report.duration}ms`);
|
|
554
|
+
|
|
555
|
+
// Overall status
|
|
556
|
+
console.log('\n' + '─'.repeat(60));
|
|
557
|
+
if (report.valid) {
|
|
558
|
+
console.log(' ✅ Configuration is VALID - Ready to use Boss Claude!');
|
|
559
|
+
} else {
|
|
560
|
+
console.log(' ❌ Configuration is INVALID - Fix errors above');
|
|
561
|
+
console.log(' 💡 Run: boss-claude init (to reset configuration)');
|
|
562
|
+
}
|
|
563
|
+
console.log('─'.repeat(60) + '\n');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Quick validation check (returns boolean)
|
|
568
|
+
* @returns {Promise<boolean>} True if config is valid
|
|
569
|
+
*/
|
|
570
|
+
export async function isConfigValid() {
|
|
571
|
+
const report = await validateConfig({ includeOptional: false });
|
|
572
|
+
return report.valid;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export default {
|
|
576
|
+
loadEnvFile,
|
|
577
|
+
validateConfig,
|
|
578
|
+
printReport,
|
|
579
|
+
isConfigValid,
|
|
580
|
+
validateRedisUrl,
|
|
581
|
+
validateGitHubToken,
|
|
582
|
+
validateOpenAiKey
|
|
583
|
+
};
|