@chappibunny/repolens 0.4.3 → 0.6.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/CHANGELOG.md +131 -0
- package/README.md +414 -64
- package/package.json +16 -4
- package/src/ai/provider.js +48 -45
- package/src/cli.js +117 -9
- package/src/core/config-schema.js +43 -1
- package/src/core/config.js +20 -3
- package/src/core/scan.js +184 -3
- package/src/init.js +46 -4
- package/src/integrations/discord.js +261 -0
- package/src/migrate.js +7 -0
- package/src/publishers/confluence.js +428 -0
- package/src/publishers/index.js +112 -4
- package/src/publishers/notion.js +20 -16
- package/src/publishers/publish.js +1 -1
- package/src/renderers/render.js +32 -2
- package/src/utils/branch.js +32 -0
- package/src/utils/logger.js +21 -4
- package/src/utils/metrics.js +361 -0
- package/src/utils/rate-limit.js +289 -0
- package/src/utils/secrets.js +240 -0
- package/src/utils/telemetry.js +375 -0
- package/src/utils/validate.js +382 -0
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chappibunny/repolens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/cli.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"repolens": "
|
|
9
|
+
"repolens": "bin/repolens.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
|
@@ -18,12 +18,19 @@
|
|
|
18
18
|
],
|
|
19
19
|
"keywords": [
|
|
20
20
|
"cli",
|
|
21
|
-
"
|
|
21
|
+
"documentation",
|
|
22
22
|
"architecture",
|
|
23
23
|
"github",
|
|
24
24
|
"notion",
|
|
25
|
+
"confluence",
|
|
25
26
|
"markdown",
|
|
26
|
-
"developer-tools"
|
|
27
|
+
"developer-tools",
|
|
28
|
+
"repolens",
|
|
29
|
+
"observability",
|
|
30
|
+
"metrics",
|
|
31
|
+
"team-collaboration",
|
|
32
|
+
"discord",
|
|
33
|
+
"ai-documentation"
|
|
27
34
|
],
|
|
28
35
|
"repository": {
|
|
29
36
|
"type": "git",
|
|
@@ -34,6 +41,10 @@
|
|
|
34
41
|
"url": "https://github.com/CHAPIBUNNY/repolens/issues"
|
|
35
42
|
},
|
|
36
43
|
"author": "Charl van Zyl",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"registry": "https://registry.npmjs.org/"
|
|
47
|
+
},
|
|
37
48
|
"scripts": {
|
|
38
49
|
"test:notion": "node notion-test.js",
|
|
39
50
|
"docs:publish": "node src/cli.js publish --config ../../.repolens.yml",
|
|
@@ -47,6 +58,7 @@
|
|
|
47
58
|
"release:major": "npm version major"
|
|
48
59
|
},
|
|
49
60
|
"dependencies": {
|
|
61
|
+
"@sentry/node": "^10.42.0",
|
|
50
62
|
"dotenv": "^17.3.1",
|
|
51
63
|
"fast-glob": "^3.3.3",
|
|
52
64
|
"js-yaml": "^4.1.0",
|
package/src/ai/provider.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Provider-agnostic AI text generation
|
|
2
2
|
|
|
3
3
|
import { warn, info } from "../utils/logger.js";
|
|
4
|
+
import { executeAIRequest } from "../utils/rate-limit.js";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_TIMEOUT_MS = 60000;
|
|
6
7
|
const DEFAULT_TEMPERATURE = 0.2;
|
|
@@ -68,54 +69,56 @@ export async function generateText({ system, user, temperature, maxTokens }) {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
async function callOpenAICompatibleAPI({ baseUrl, apiKey, model, system, user, temperature, maxTokens, timeoutMs }) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const controller = new AbortController();
|
|
74
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const response = await fetch(url, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: {
|
|
80
|
-
"Content-Type": "application/json",
|
|
81
|
-
"Authorization": `Bearer ${apiKey}`
|
|
82
|
-
},
|
|
83
|
-
body: JSON.stringify({
|
|
84
|
-
model,
|
|
85
|
-
messages: [
|
|
86
|
-
{ role: "system", content: system },
|
|
87
|
-
{ role: "user", content: user }
|
|
88
|
-
],
|
|
89
|
-
temperature,
|
|
90
|
-
max_tokens: maxTokens
|
|
91
|
-
}),
|
|
92
|
-
signal: controller.signal
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
clearTimeout(timeoutId);
|
|
96
|
-
|
|
97
|
-
if (!response.ok) {
|
|
98
|
-
const errorText = await response.text();
|
|
99
|
-
throw new Error(`API error (${response.status}): ${errorText}`);
|
|
100
|
-
}
|
|
72
|
+
return await executeAIRequest(async () => {
|
|
73
|
+
const url = `${baseUrl}/chat/completions`;
|
|
101
74
|
|
|
102
|
-
const
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
103
77
|
|
|
104
|
-
|
|
105
|
-
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(url, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
"Authorization": `Bearer ${apiKey}`
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
model,
|
|
87
|
+
messages: [
|
|
88
|
+
{ role: "system", content: system },
|
|
89
|
+
{ role: "user", content: user }
|
|
90
|
+
],
|
|
91
|
+
temperature,
|
|
92
|
+
max_tokens: maxTokens
|
|
93
|
+
}),
|
|
94
|
+
signal: controller.signal
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
clearTimeout(timeoutId);
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const errorText = await response.text();
|
|
101
|
+
throw new Error(`API error (${response.status}): ${errorText}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
|
|
106
|
+
if (!data.choices || data.choices.length === 0) {
|
|
107
|
+
throw new Error("No completion returned from API");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return data.choices[0].message.content;
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
|
|
115
|
+
if (error.name === "AbortError") {
|
|
116
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw error;
|
|
106
120
|
}
|
|
107
|
-
|
|
108
|
-
return data.choices[0].message.content;
|
|
109
|
-
|
|
110
|
-
} catch (error) {
|
|
111
|
-
clearTimeout(timeoutId);
|
|
112
|
-
|
|
113
|
-
if (error.name === "AbortError") {
|
|
114
|
-
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
121
|
+
});
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
export function isAIEnabled() {
|
package/src/cli.js
CHANGED
|
@@ -24,6 +24,15 @@ import { info, error } from "./utils/logger.js";
|
|
|
24
24
|
import { checkForUpdates } from "./utils/update-check.js";
|
|
25
25
|
import { generateDocumentSet } from "./docs/generate-doc-set.js";
|
|
26
26
|
import { writeDocumentSet } from "./docs/write-doc-set.js";
|
|
27
|
+
import {
|
|
28
|
+
initTelemetry,
|
|
29
|
+
captureError,
|
|
30
|
+
setContext,
|
|
31
|
+
closeTelemetry,
|
|
32
|
+
trackUsage,
|
|
33
|
+
startTimer,
|
|
34
|
+
stopTimer
|
|
35
|
+
} from "./utils/telemetry.js";
|
|
27
36
|
|
|
28
37
|
async function getPackageVersion() {
|
|
29
38
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -38,8 +47,17 @@ async function getPackageVersion() {
|
|
|
38
47
|
|
|
39
48
|
async function printBanner() {
|
|
40
49
|
const version = await getPackageVersion();
|
|
41
|
-
console.log(
|
|
42
|
-
|
|
50
|
+
console.log(`
|
|
51
|
+
██████╗ ███████╗██████╗ ██████╗ ██╗ ███████╗███╗ ██╗███████╗
|
|
52
|
+
██╔══██╗██╔════╝██╔══██╗██╔═══██╗██║ ██╔════╝████╗ ██║██╔════╝
|
|
53
|
+
██████╔╝█████╗ ██████╔╝██║ ██║██║ █████╗ ██╔██╗ ██║███████╗
|
|
54
|
+
██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██║ ██╔══╝ ██║╚██╗██║╚════██║
|
|
55
|
+
██║ ██║███████╗██║ ╚██████╔╝███████╗███████╗██║ ╚████║███████║
|
|
56
|
+
╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═══╝╚══════╝
|
|
57
|
+
🔍 Repository Intelligence by RABITAI 🐰
|
|
58
|
+
v${version}
|
|
59
|
+
`);
|
|
60
|
+
console.log("─".repeat(70));
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
function getArg(name) {
|
|
@@ -81,16 +99,16 @@ async function findConfig(startDir = process.cwd()) {
|
|
|
81
99
|
|
|
82
100
|
function printHelp() {
|
|
83
101
|
console.log(`
|
|
84
|
-
RepoLens —
|
|
102
|
+
RepoLens — Repository Intelligence CLI by RABITAI 🐰
|
|
85
103
|
|
|
86
104
|
Usage:
|
|
87
105
|
repolens <command> [options]
|
|
88
106
|
|
|
89
107
|
Commands:
|
|
90
|
-
init Scaffold RepoLens files in
|
|
91
|
-
doctor Validate
|
|
108
|
+
init Scaffold RepoLens files in your repository
|
|
109
|
+
doctor Validate your RepoLens setup
|
|
92
110
|
migrate Upgrade workflow files to v0.4.0 format
|
|
93
|
-
publish Scan, render, and publish
|
|
111
|
+
publish Scan, render, and publish documentation
|
|
94
112
|
version Print the current RepoLens version
|
|
95
113
|
|
|
96
114
|
Options:
|
|
@@ -116,6 +134,15 @@ Examples:
|
|
|
116
134
|
async function main() {
|
|
117
135
|
const command = process.argv[2];
|
|
118
136
|
|
|
137
|
+
// Initialize telemetry (opt-in via REPOLENS_TELEMETRY_ENABLED=true)
|
|
138
|
+
initTelemetry();
|
|
139
|
+
setContext("cli", {
|
|
140
|
+
command: command || "publish",
|
|
141
|
+
args: process.argv.slice(2),
|
|
142
|
+
nodeVersion: process.version,
|
|
143
|
+
platform: process.platform,
|
|
144
|
+
});
|
|
145
|
+
|
|
119
146
|
// Check for updates (non-blocking, runs in background)
|
|
120
147
|
checkForUpdates().catch(() => {/* Silently fail */});
|
|
121
148
|
|
|
@@ -134,12 +161,22 @@ async function main() {
|
|
|
134
161
|
await printBanner();
|
|
135
162
|
const targetDir = getArg("--target") || process.cwd();
|
|
136
163
|
info(`Initializing RepoLens in: ${targetDir}`);
|
|
164
|
+
|
|
165
|
+
const timer = startTimer("init");
|
|
137
166
|
try {
|
|
138
167
|
await runInit(targetDir);
|
|
168
|
+
const duration = stopTimer(timer);
|
|
139
169
|
info("✓ RepoLens initialized successfully");
|
|
170
|
+
|
|
171
|
+
trackUsage("init", "success", { duration });
|
|
172
|
+
await closeTelemetry();
|
|
140
173
|
} catch (err) {
|
|
174
|
+
stopTimer(timer);
|
|
175
|
+
captureError(err, { command: "init", targetDir });
|
|
176
|
+
trackUsage("init", "failure");
|
|
141
177
|
error("Failed to initialize RepoLens:");
|
|
142
178
|
error(err.message);
|
|
179
|
+
await closeTelemetry();
|
|
143
180
|
process.exit(1);
|
|
144
181
|
}
|
|
145
182
|
return;
|
|
@@ -149,12 +186,22 @@ async function main() {
|
|
|
149
186
|
await printBanner();
|
|
150
187
|
const targetDir = getArg("--target") || process.cwd();
|
|
151
188
|
info(`Validating RepoLens setup in: ${targetDir}`);
|
|
189
|
+
|
|
190
|
+
const timer = startTimer("doctor");
|
|
152
191
|
try {
|
|
153
192
|
await runDoctor(targetDir);
|
|
193
|
+
const duration = stopTimer(timer);
|
|
154
194
|
info("✓ RepoLens validation passed");
|
|
195
|
+
|
|
196
|
+
trackUsage("doctor", "success", { duration });
|
|
197
|
+
await closeTelemetry();
|
|
155
198
|
} catch (err) {
|
|
199
|
+
stopTimer(timer);
|
|
200
|
+
captureError(err, { command: "doctor", targetDir });
|
|
201
|
+
trackUsage("doctor", "failure");
|
|
156
202
|
error("Validation failed:");
|
|
157
203
|
error(err.message);
|
|
204
|
+
await closeTelemetry();
|
|
158
205
|
process.exit(2);
|
|
159
206
|
}
|
|
160
207
|
return;
|
|
@@ -165,11 +212,25 @@ async function main() {
|
|
|
165
212
|
const dryRun = process.argv.includes("--dry-run");
|
|
166
213
|
const force = process.argv.includes("--force");
|
|
167
214
|
|
|
215
|
+
const timer = startTimer("migrate");
|
|
168
216
|
try {
|
|
169
|
-
await runMigrate(targetDir, { dryRun, force });
|
|
217
|
+
const result = await runMigrate(targetDir, { dryRun, force });
|
|
218
|
+
const duration = stopTimer(timer);
|
|
219
|
+
|
|
220
|
+
trackUsage("migrate", "success", {
|
|
221
|
+
duration,
|
|
222
|
+
migratedCount: result?.migratedCount || 0,
|
|
223
|
+
skippedCount: result?.skippedCount || 0,
|
|
224
|
+
dryRun,
|
|
225
|
+
});
|
|
226
|
+
await closeTelemetry();
|
|
170
227
|
} catch (err) {
|
|
228
|
+
stopTimer(timer);
|
|
229
|
+
captureError(err, { command: "migrate", targetDir, dryRun, force });
|
|
230
|
+
trackUsage("migrate", "failure", { dryRun });
|
|
171
231
|
error("Migration failed:");
|
|
172
232
|
error(err.message);
|
|
233
|
+
await closeTelemetry();
|
|
173
234
|
process.exit(1);
|
|
174
235
|
}
|
|
175
236
|
return;
|
|
@@ -178,12 +239,15 @@ async function main() {
|
|
|
178
239
|
if (command === "publish" || !command || command.startsWith("--")) {
|
|
179
240
|
await printBanner();
|
|
180
241
|
|
|
242
|
+
const commandTimer = startTimer("publish");
|
|
243
|
+
|
|
181
244
|
// Auto-discover config if not provided
|
|
182
245
|
let configPath;
|
|
183
246
|
try {
|
|
184
247
|
configPath = getArg("--config") || await findConfig();
|
|
185
248
|
info(`Using config: ${configPath}`);
|
|
186
249
|
} catch (err) {
|
|
250
|
+
stopTimer(commandTimer);
|
|
187
251
|
error(err.message);
|
|
188
252
|
process.exit(2);
|
|
189
253
|
}
|
|
@@ -193,18 +257,28 @@ async function main() {
|
|
|
193
257
|
info("Loading configuration...");
|
|
194
258
|
cfg = await loadConfig(configPath);
|
|
195
259
|
} catch (err) {
|
|
260
|
+
stopTimer(commandTimer);
|
|
261
|
+
captureError(err, { command: "publish", step: "load-config", configPath });
|
|
262
|
+
trackUsage("publish", "failure", { step: "config-load" });
|
|
196
263
|
error("Failed to load configuration:");
|
|
197
264
|
error(err.message);
|
|
265
|
+
await closeTelemetry();
|
|
198
266
|
process.exit(2);
|
|
199
267
|
}
|
|
200
268
|
|
|
201
269
|
try {
|
|
202
270
|
info("Scanning repository...");
|
|
271
|
+
const scanTimer = startTimer("scan");
|
|
203
272
|
scan = await scanRepo(cfg);
|
|
273
|
+
stopTimer(scanTimer);
|
|
204
274
|
info(`Detected ${scan.modules?.length || 0} modules`);
|
|
205
275
|
} catch (err) {
|
|
276
|
+
stopTimer(commandTimer);
|
|
277
|
+
captureError(err, { command: "publish", step: "scan", patterns: cfg.scan?.patterns });
|
|
278
|
+
trackUsage("publish", "failure", { step: "scan" });
|
|
206
279
|
error("Failed to scan repository:");
|
|
207
280
|
error(err.message);
|
|
281
|
+
await closeTelemetry();
|
|
208
282
|
process.exit(1);
|
|
209
283
|
}
|
|
210
284
|
|
|
@@ -213,7 +287,9 @@ async function main() {
|
|
|
213
287
|
|
|
214
288
|
try {
|
|
215
289
|
info("Generating documentation set...");
|
|
290
|
+
const renderTimer = startTimer("render");
|
|
216
291
|
const docSet = await generateDocumentSet(scan, cfg, rawDiff);
|
|
292
|
+
stopTimer(renderTimer);
|
|
217
293
|
|
|
218
294
|
info("Writing documentation to disk...");
|
|
219
295
|
const writeResult = await writeDocumentSet(docSet, process.cwd());
|
|
@@ -228,15 +304,40 @@ async function main() {
|
|
|
228
304
|
}
|
|
229
305
|
|
|
230
306
|
info("Publishing documentation...");
|
|
231
|
-
|
|
307
|
+
const publishTimer = startTimer("publish_docs");
|
|
308
|
+
await publishDocs(cfg, renderedPages, scan);
|
|
309
|
+
stopTimer(publishTimer);
|
|
310
|
+
|
|
232
311
|
await upsertPrComment(diffData);
|
|
233
312
|
info("✓ Documentation published successfully");
|
|
313
|
+
|
|
314
|
+
const totalDuration = stopTimer(commandTimer);
|
|
315
|
+
|
|
316
|
+
// Track successful publish with comprehensive metrics
|
|
317
|
+
const publishers = [];
|
|
318
|
+
if (cfg.publishers?.notion?.enabled !== false) publishers.push("notion");
|
|
319
|
+
if (cfg.publishers?.markdown?.enabled !== false) publishers.push("markdown");
|
|
320
|
+
|
|
321
|
+
trackUsage("publish", "success", {
|
|
322
|
+
duration: totalDuration,
|
|
323
|
+
fileCount: scan.filesCount || 0,
|
|
324
|
+
moduleCount: scan.modules?.length || 0,
|
|
325
|
+
aiEnabled: Boolean(process.env.REPOLENS_AI_API_KEY || process.env.OPENAI_API_KEY),
|
|
326
|
+
aiProvider: process.env.AI_PROVIDER || "openai",
|
|
327
|
+
publishers,
|
|
328
|
+
documentCount: writeResult.documentCount,
|
|
329
|
+
});
|
|
234
330
|
} catch (err) {
|
|
331
|
+
stopTimer(commandTimer);
|
|
332
|
+
captureError(err, { command: "publish", step: "generate-or-publish", publishers: cfg.publishers });
|
|
333
|
+
trackUsage("publish", "failure", { step: "generate-or-publish" });
|
|
235
334
|
error("Failed to publish documentation:");
|
|
236
335
|
error(err.message);
|
|
336
|
+
await closeTelemetry();
|
|
237
337
|
process.exit(1);
|
|
238
338
|
}
|
|
239
339
|
|
|
340
|
+
await closeTelemetry();
|
|
240
341
|
return;
|
|
241
342
|
}
|
|
242
343
|
|
|
@@ -245,7 +346,13 @@ async function main() {
|
|
|
245
346
|
process.exit(1);
|
|
246
347
|
}
|
|
247
348
|
|
|
248
|
-
main().catch((err) => {
|
|
349
|
+
main().catch(async (err) => {
|
|
350
|
+
// Capture unexpected errors
|
|
351
|
+
captureError(err, {
|
|
352
|
+
type: "uncaught",
|
|
353
|
+
command: process.argv[2] || "unknown"
|
|
354
|
+
});
|
|
355
|
+
|
|
249
356
|
console.error("\n❌ RepoLens encountered an unexpected error:\n");
|
|
250
357
|
|
|
251
358
|
if (err.code === "ENOENT") {
|
|
@@ -267,5 +374,6 @@ main().catch((err) => {
|
|
|
267
374
|
console.error("\nRun with --verbose for full error details.");
|
|
268
375
|
}
|
|
269
376
|
|
|
377
|
+
await closeTelemetry();
|
|
270
378
|
process.exit(1);
|
|
271
379
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const CURRENT_SCHEMA_VERSION = 1;
|
|
11
|
-
const SUPPORTED_PUBLISHERS = ["notion", "markdown"];
|
|
11
|
+
const SUPPORTED_PUBLISHERS = ["notion", "markdown", "confluence"];
|
|
12
12
|
const SUPPORTED_PAGE_KEYS = [
|
|
13
13
|
"system_overview",
|
|
14
14
|
"module_catalog",
|
|
@@ -161,6 +161,48 @@ export function validateConfig(config) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// Validate Discord configuration (optional)
|
|
165
|
+
if (config.discord !== undefined) {
|
|
166
|
+
if (typeof config.discord !== "object" || Array.isArray(config.discord)) {
|
|
167
|
+
errors.push("discord must be an object");
|
|
168
|
+
} else {
|
|
169
|
+
// Validate notifyOn
|
|
170
|
+
if (config.discord.notifyOn !== undefined) {
|
|
171
|
+
const validOptions = ["always", "significant", "never"];
|
|
172
|
+
if (!validOptions.includes(config.discord.notifyOn)) {
|
|
173
|
+
errors.push(`discord.notifyOn must be one of: ${validOptions.join(", ")}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Validate significantThreshold
|
|
178
|
+
if (config.discord.significantThreshold !== undefined) {
|
|
179
|
+
if (typeof config.discord.significantThreshold !== "number") {
|
|
180
|
+
errors.push("discord.significantThreshold must be a number");
|
|
181
|
+
} else if (config.discord.significantThreshold < 0 || config.discord.significantThreshold > 100) {
|
|
182
|
+
errors.push("discord.significantThreshold must be between 0 and 100");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Validate branches filter
|
|
187
|
+
if (config.discord.branches !== undefined) {
|
|
188
|
+
if (!Array.isArray(config.discord.branches)) {
|
|
189
|
+
errors.push("discord.branches must be an array");
|
|
190
|
+
} else {
|
|
191
|
+
config.discord.branches.forEach((branch, idx) => {
|
|
192
|
+
if (typeof branch !== "string") {
|
|
193
|
+
errors.push(`discord.branches[${idx}] must be a string`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Validate enabled flag
|
|
200
|
+
if (config.discord.enabled !== undefined && typeof config.discord.enabled !== "boolean") {
|
|
201
|
+
errors.push("discord.enabled must be a boolean");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
164
206
|
// Validate feature flags (optional)
|
|
165
207
|
if (config.features !== undefined) {
|
|
166
208
|
if (typeof config.features !== "object" || Array.isArray(config.features)) {
|
package/src/core/config.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
|
-
import { validateConfig } from "./config-schema.js";
|
|
4
|
+
import { validateConfig as validateSchema } from "./config-schema.js";
|
|
5
|
+
import { validateConfig } from "../utils/validate.js";
|
|
6
|
+
import { warn, error } from "../utils/logger.js";
|
|
5
7
|
|
|
6
8
|
export async function loadConfig(configPath) {
|
|
7
9
|
const absoluteConfigPath = path.resolve(configPath);
|
|
8
10
|
const raw = await fs.readFile(absoluteConfigPath, "utf8");
|
|
9
11
|
const cfg = yaml.load(raw);
|
|
10
12
|
|
|
11
|
-
// Validate config against schema
|
|
12
|
-
|
|
13
|
+
// Validate config against schema (structure)
|
|
14
|
+
validateSchema(cfg);
|
|
15
|
+
|
|
16
|
+
// Validate config for security (injection, secrets, etc.)
|
|
17
|
+
const securityResult = validateConfig(cfg);
|
|
18
|
+
|
|
19
|
+
// Log warnings
|
|
20
|
+
if (securityResult.warnings.length > 0) {
|
|
21
|
+
securityResult.warnings.forEach(w => warn(`Config warning: ${w}`));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Throw error if invalid
|
|
25
|
+
if (!securityResult.valid) {
|
|
26
|
+
error("Configuration validation failed:");
|
|
27
|
+
securityResult.errors.forEach(e => error(` - ${e}`));
|
|
28
|
+
throw new Error(`Invalid configuration: ${securityResult.errors.join(", ")}`);
|
|
29
|
+
}
|
|
13
30
|
|
|
14
31
|
cfg.__configPath = absoluteConfigPath;
|
|
15
32
|
cfg.__repoRoot = path.dirname(absoluteConfigPath);
|