@quonfig/cli 0.0.1 → 0.0.4
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 +157 -21
- package/bin/dev.js +5 -9
- package/bin/dev.mjs +6 -0
- package/dist/codegen/zod-introspection.js +13 -4
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +83 -0
- package/dist/commands/login.js +27 -6
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/pull.d.ts +15 -0
- package/dist/commands/pull.js +176 -0
- package/dist/commands/serve.js +3 -1
- package/dist/commands/sync.d.ts +18 -0
- package/dist/commands/sync.js +142 -0
- package/dist/commands/whoami.js +4 -1
- package/dist/commands/workspace/bootstrap.d.ts +13 -0
- package/dist/commands/workspace/bootstrap.js +155 -0
- package/dist/init/init-workspace.d.ts +25 -0
- package/dist/init/init-workspace.js +162 -0
- package/dist/init/samples.d.ts +11 -0
- package/dist/init/samples.js +118 -0
- package/dist/init/schema.d.ts +9 -0
- package/dist/init/schema.js +334 -0
- package/dist/init/templates.d.ts +12 -0
- package/dist/init/templates.js +161 -0
- package/dist/util/coerce.js +2 -1
- package/dist/util/get-client.js +6 -44
- package/dist/util/get-valid-token.d.ts +5 -0
- package/dist/util/get-valid-token.js +44 -0
- package/dist/util/git-ops.d.ts +32 -0
- package/dist/util/git-ops.js +164 -0
- package/dist/util/gitea-api.d.ts +11 -0
- package/dist/util/gitea-api.js +45 -0
- package/dist/util/gitea-token-storage.d.ts +8 -0
- package/dist/util/gitea-token-storage.js +47 -0
- package/dist/util/token-storage.d.ts +1 -0
- package/dist/util/token-storage.js +30 -11
- package/dist/verify/validate.d.ts +14 -0
- package/dist/verify/validate.js +224 -19
- package/oclif.manifest.json +298 -2
- package/package.json +2 -2
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { getDomain } from './domain-urls.js';
|
|
4
5
|
const getQuonfigDir = (options) => options?.quonfigDir || path.join(os.homedir(), '.quonfig');
|
|
5
|
-
const getTokenFile = (options) =>
|
|
6
|
+
const getTokenFile = (options) => {
|
|
7
|
+
const domain = getDomain();
|
|
8
|
+
const filename = domain !== 'quonfig.com' ? `tokens-${domain.replaceAll('.', '-')}.json` : 'tokens.json';
|
|
9
|
+
return path.join(getQuonfigDir(options), filename);
|
|
10
|
+
};
|
|
6
11
|
const getConfigFile = (options) => path.join(getQuonfigDir(options), 'config');
|
|
7
12
|
const ensureQuonfigDir = async (options) => {
|
|
8
13
|
const quonfigDir = getQuonfigDir(options);
|
|
@@ -15,7 +20,10 @@ const ensureQuonfigDir = async (options) => {
|
|
|
15
20
|
};
|
|
16
21
|
export const saveTokens = async (tokens, options) => {
|
|
17
22
|
await ensureQuonfigDir(options);
|
|
18
|
-
|
|
23
|
+
const targetFile = getTokenFile(options);
|
|
24
|
+
const tmpFile = `${targetFile}.tmp`;
|
|
25
|
+
await fs.promises.writeFile(tmpFile, JSON.stringify(tokens, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
26
|
+
await fs.promises.rename(tmpFile, targetFile);
|
|
19
27
|
};
|
|
20
28
|
export const loadTokens = async (options) => {
|
|
21
29
|
try {
|
|
@@ -40,9 +48,16 @@ export const saveAuthConfig = async (config, options) => {
|
|
|
40
48
|
if (profileData.workspaceName) {
|
|
41
49
|
configContent += ` # ${profileData.workspaceName}`;
|
|
42
50
|
}
|
|
43
|
-
configContent += '\n
|
|
51
|
+
configContent += '\n';
|
|
52
|
+
if (profileData.workspaceSlug) {
|
|
53
|
+
configContent += `workspace_slug = ${profileData.workspaceSlug}\n`;
|
|
54
|
+
}
|
|
55
|
+
configContent += '\n';
|
|
44
56
|
}
|
|
45
|
-
|
|
57
|
+
const configTarget = getConfigFile(options);
|
|
58
|
+
const configTmp = `${configTarget}.tmp`;
|
|
59
|
+
await fs.promises.writeFile(configTmp, configContent, { encoding: 'utf8', mode: 0o600 });
|
|
60
|
+
await fs.promises.rename(configTmp, configTarget);
|
|
46
61
|
};
|
|
47
62
|
export const loadAuthConfig = async (options) => {
|
|
48
63
|
try {
|
|
@@ -55,16 +70,20 @@ export const loadAuthConfig = async (options) => {
|
|
|
55
70
|
if (defaultMatch && defaultMatch[1]) {
|
|
56
71
|
config.defaultProfile = defaultMatch[1].trim();
|
|
57
72
|
}
|
|
58
|
-
// Parse profiles
|
|
59
|
-
const
|
|
73
|
+
// Parse profiles — each block starts with [profile name] and contains key=value lines
|
|
74
|
+
const profileBlockRegex = /\[profile\s+(\w+)]\s*\n((?:[^\[]*\n?)*)/g;
|
|
60
75
|
let match;
|
|
61
|
-
while ((match =
|
|
76
|
+
while ((match = profileBlockRegex.exec(data)) !== null) {
|
|
62
77
|
const profileName = match[1];
|
|
63
|
-
const
|
|
64
|
-
const
|
|
78
|
+
const block = match[2];
|
|
79
|
+
const workspaceMatch = block.match(/workspace\s*=\s*([^\s#]+)(?:\s*#\s*(.+))?/);
|
|
80
|
+
const slugMatch = block.match(/workspace_slug\s*=\s*(\S+)/);
|
|
81
|
+
if (!workspaceMatch)
|
|
82
|
+
continue;
|
|
65
83
|
config.profiles[profileName] = {
|
|
66
|
-
workspace,
|
|
67
|
-
workspaceName,
|
|
84
|
+
workspace: workspaceMatch[1],
|
|
85
|
+
workspaceName: workspaceMatch[2]?.trim(),
|
|
86
|
+
workspaceSlug: slugMatch?.[1]?.trim(),
|
|
68
87
|
};
|
|
69
88
|
}
|
|
70
89
|
return Object.keys(config.profiles).length > 0 ? config : null;
|
|
@@ -26,10 +26,24 @@ export interface ValidationIssue {
|
|
|
26
26
|
severity: Severity;
|
|
27
27
|
suggestion?: string;
|
|
28
28
|
}
|
|
29
|
+
export interface ValidationStats {
|
|
30
|
+
configs: number;
|
|
31
|
+
featureFlags: number;
|
|
32
|
+
segments: number;
|
|
33
|
+
logLevels: number;
|
|
34
|
+
schemas: number;
|
|
35
|
+
environmentOverrides: number;
|
|
36
|
+
rules: number;
|
|
37
|
+
segmentRefsChecked: number;
|
|
38
|
+
schemaRefsChecked: number;
|
|
39
|
+
envRefsChecked: number;
|
|
40
|
+
uniqueKeysVerified: number;
|
|
41
|
+
}
|
|
29
42
|
export interface ValidationResult {
|
|
30
43
|
issues: ValidationIssue[];
|
|
31
44
|
filesChecked: number;
|
|
32
45
|
valid: boolean;
|
|
46
|
+
stats: ValidationStats;
|
|
33
47
|
}
|
|
34
48
|
/**
|
|
35
49
|
* Validate an entire workspace directory on disk.
|
package/dist/verify/validate.js
CHANGED
|
@@ -192,11 +192,25 @@ import * as path from "node:path";
|
|
|
192
192
|
export function validateWorkspace(workspaceDir) {
|
|
193
193
|
const issues = [];
|
|
194
194
|
let filesChecked = 0;
|
|
195
|
+
const stats = {
|
|
196
|
+
configs: 0,
|
|
197
|
+
featureFlags: 0,
|
|
198
|
+
segments: 0,
|
|
199
|
+
logLevels: 0,
|
|
200
|
+
schemas: 0,
|
|
201
|
+
environmentOverrides: 0,
|
|
202
|
+
rules: 0,
|
|
203
|
+
segmentRefsChecked: 0,
|
|
204
|
+
schemaRefsChecked: 0,
|
|
205
|
+
envRefsChecked: 0,
|
|
206
|
+
uniqueKeysVerified: 0,
|
|
207
|
+
};
|
|
195
208
|
// Collect all configs for cross-reference checks
|
|
196
209
|
const allConfigs = [];
|
|
197
210
|
const allSchemaFiles = [];
|
|
198
211
|
const segmentKeys = new Set();
|
|
199
212
|
const schemaKeys = new Set();
|
|
213
|
+
const declaredEnvIds = new Set();
|
|
200
214
|
// Check for unexpected top-level entries
|
|
201
215
|
let entries;
|
|
202
216
|
try {
|
|
@@ -211,6 +225,7 @@ export function validateWorkspace(workspaceDir) {
|
|
|
211
225
|
}],
|
|
212
226
|
filesChecked: 0,
|
|
213
227
|
valid: false,
|
|
228
|
+
stats,
|
|
214
229
|
};
|
|
215
230
|
}
|
|
216
231
|
for (const entry of entries) {
|
|
@@ -225,6 +240,50 @@ export function validateWorkspace(workspaceDir) {
|
|
|
225
240
|
});
|
|
226
241
|
}
|
|
227
242
|
}
|
|
243
|
+
// Validate quonfig.json at workspace root
|
|
244
|
+
const quonfigPath = path.join(workspaceDir, "quonfig.json");
|
|
245
|
+
if (!fs.existsSync(quonfigPath)) {
|
|
246
|
+
issues.push({
|
|
247
|
+
file: "quonfig.json",
|
|
248
|
+
message: `quonfig.json is missing from workspace root`,
|
|
249
|
+
severity: "error",
|
|
250
|
+
suggestion: `Create quonfig.json with format: {"environments": ["production", "staging"]}`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
let quonfigRaw;
|
|
255
|
+
try {
|
|
256
|
+
quonfigRaw = fs.readFileSync(quonfigPath, "utf-8");
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
quonfigRaw = "";
|
|
260
|
+
issues.push({ file: "quonfig.json", message: `Cannot read quonfig.json: ${err.message}`, severity: "error" });
|
|
261
|
+
}
|
|
262
|
+
if (quonfigRaw) {
|
|
263
|
+
let quonfigParsed;
|
|
264
|
+
try {
|
|
265
|
+
quonfigParsed = JSON.parse(quonfigRaw);
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
quonfigParsed = null;
|
|
269
|
+
issues.push({ file: "quonfig.json", message: `Invalid JSON: ${err.message}`, severity: "error" });
|
|
270
|
+
}
|
|
271
|
+
const quonfigResult = z.object({ environments: z.array(z.string()) }).safeParse(quonfigParsed);
|
|
272
|
+
if (!quonfigResult.success) {
|
|
273
|
+
issues.push({
|
|
274
|
+
file: "quonfig.json",
|
|
275
|
+
message: `quonfig.json must have an "environments" array of strings`,
|
|
276
|
+
severity: "error",
|
|
277
|
+
suggestion: `Format: {"environments": ["production", "staging"]}`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
for (const envId of quonfigResult.data.environments) {
|
|
282
|
+
declaredEnvIds.add(envId);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
228
287
|
// First pass: parse all files and collect keys
|
|
229
288
|
for (const dir of KNOWN_DIRS) {
|
|
230
289
|
const dirPath = path.join(workspaceDir, dir);
|
|
@@ -272,6 +331,7 @@ export function validateWorkspace(workspaceDir) {
|
|
|
272
331
|
schemaKeys.add(schemaKey);
|
|
273
332
|
validateKey(schemaKey, relPath, issues);
|
|
274
333
|
allSchemaFiles.push({ key: schemaKey, file: relPath });
|
|
334
|
+
stats.schemas++;
|
|
275
335
|
continue;
|
|
276
336
|
}
|
|
277
337
|
// Validate against StoredConfigSchema
|
|
@@ -337,16 +397,33 @@ export function validateWorkspace(workspaceDir) {
|
|
|
337
397
|
// Validate environment IDs are slugs, not UUIDs
|
|
338
398
|
validateEnvironmentIds(config.environments, relPath, issues);
|
|
339
399
|
// Validate rules
|
|
340
|
-
validateRules(config.default.rules, relPath, "default", config.valueType, issues);
|
|
400
|
+
validateRules(config.default.rules, relPath, "default", config.valueType, issues, config.variants);
|
|
401
|
+
for (const env of config.environments) {
|
|
402
|
+
validateRules(env.rules, relPath, `environments[${env.id}]`, config.valueType, issues, config.variants);
|
|
403
|
+
}
|
|
404
|
+
// Count by type
|
|
405
|
+
switch (config.type) {
|
|
406
|
+
case "config":
|
|
407
|
+
stats.configs++;
|
|
408
|
+
break;
|
|
409
|
+
case "feature_flag":
|
|
410
|
+
stats.featureFlags++;
|
|
411
|
+
break;
|
|
412
|
+
case "segment":
|
|
413
|
+
stats.segments++;
|
|
414
|
+
break;
|
|
415
|
+
case "log_level":
|
|
416
|
+
stats.logLevels++;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
// Count rules and environment overrides
|
|
420
|
+
stats.rules += config.default.rules.length;
|
|
421
|
+
stats.environmentOverrides += config.environments.length;
|
|
341
422
|
for (const env of config.environments) {
|
|
342
|
-
|
|
423
|
+
stats.rules += env.rules.length;
|
|
343
424
|
}
|
|
344
425
|
// Collect for cross-reference
|
|
345
426
|
allConfigs.push({ key: config.key, type: config.type, dir, file: relPath });
|
|
346
|
-
// Check schemaKey reference (collected, validated in second pass)
|
|
347
|
-
if (config.schemaKey) {
|
|
348
|
-
// Will be checked after all files are parsed
|
|
349
|
-
}
|
|
350
427
|
}
|
|
351
428
|
}
|
|
352
429
|
// Second pass: referential integrity
|
|
@@ -382,6 +459,7 @@ export function validateWorkspace(workspaceDir) {
|
|
|
382
459
|
// Check segment references in criteria
|
|
383
460
|
const segRefs = collectSegmentReferences(config);
|
|
384
461
|
for (const ref of segRefs) {
|
|
462
|
+
stats.segmentRefsChecked++;
|
|
385
463
|
if (!segmentKeys.has(ref)) {
|
|
386
464
|
issues.push({
|
|
387
465
|
file: relPath,
|
|
@@ -392,13 +470,30 @@ export function validateWorkspace(workspaceDir) {
|
|
|
392
470
|
}
|
|
393
471
|
}
|
|
394
472
|
// Check schemaKey references
|
|
395
|
-
if (config.schemaKey
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
473
|
+
if (config.schemaKey) {
|
|
474
|
+
stats.schemaRefsChecked++;
|
|
475
|
+
if (!schemaKeys.has(config.schemaKey)) {
|
|
476
|
+
issues.push({
|
|
477
|
+
file: relPath,
|
|
478
|
+
message: `References schema "${config.schemaKey}" which does not exist`,
|
|
479
|
+
severity: "error",
|
|
480
|
+
suggestion: `Create schemas/${config.schemaKey}.json or schemas-protected/${config.schemaKey}.json, or remove schemaKey`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Check environment IDs referenced in config exist in quonfig.json
|
|
485
|
+
if (declaredEnvIds.size > 0) {
|
|
486
|
+
for (const env of config.environments) {
|
|
487
|
+
stats.envRefsChecked++;
|
|
488
|
+
if (!declaredEnvIds.has(env.id)) {
|
|
489
|
+
issues.push({
|
|
490
|
+
file: relPath,
|
|
491
|
+
message: `References environment "${env.id}" which is not declared in quonfig.json`,
|
|
492
|
+
severity: "error",
|
|
493
|
+
suggestion: `Add "${env.id}" to the environments array in quonfig.json, or remove this environment override`,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
402
497
|
}
|
|
403
498
|
}
|
|
404
499
|
}
|
|
@@ -435,8 +530,17 @@ export function validateWorkspace(workspaceDir) {
|
|
|
435
530
|
});
|
|
436
531
|
}
|
|
437
532
|
}
|
|
533
|
+
stats.uniqueKeysVerified = keyToFiles.size + schemaKeyToFiles.size;
|
|
438
534
|
const hasErrors = issues.some(i => i.severity === "error");
|
|
439
|
-
return { issues, filesChecked, valid: !hasErrors };
|
|
535
|
+
return { issues, filesChecked, valid: !hasErrors, stats };
|
|
536
|
+
}
|
|
537
|
+
// Default empty stats for validateFileMap (which doesn't track detailed stats)
|
|
538
|
+
function emptyStats() {
|
|
539
|
+
return {
|
|
540
|
+
configs: 0, featureFlags: 0, segments: 0, logLevels: 0, schemas: 0,
|
|
541
|
+
environmentOverrides: 0, rules: 0, segmentRefsChecked: 0,
|
|
542
|
+
schemaRefsChecked: 0, envRefsChecked: 0, uniqueKeysVerified: 0,
|
|
543
|
+
};
|
|
440
544
|
}
|
|
441
545
|
// ── Validate from in-memory file map (for git hook) ─────────────────────
|
|
442
546
|
/**
|
|
@@ -451,6 +555,35 @@ export function validateFileMap(files) {
|
|
|
451
555
|
const allSchemaFiles = [];
|
|
452
556
|
const allConfigs = [];
|
|
453
557
|
const parsedConfigs = [];
|
|
558
|
+
const declaredEnvIds = new Set();
|
|
559
|
+
// Validate quonfig.json if it's included in the file map (it may not be in every commit)
|
|
560
|
+
const quonfigContent = files.get("quonfig.json");
|
|
561
|
+
if (quonfigContent !== undefined) {
|
|
562
|
+
let quonfigParsed;
|
|
563
|
+
try {
|
|
564
|
+
quonfigParsed = JSON.parse(quonfigContent);
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
quonfigParsed = null;
|
|
568
|
+
issues.push({ file: "quonfig.json", message: `Invalid JSON: ${err.message}`, severity: "error" });
|
|
569
|
+
}
|
|
570
|
+
if (quonfigParsed !== null) {
|
|
571
|
+
const quonfigResult = z.object({ environments: z.array(z.string()) }).safeParse(quonfigParsed);
|
|
572
|
+
if (!quonfigResult.success) {
|
|
573
|
+
issues.push({
|
|
574
|
+
file: "quonfig.json",
|
|
575
|
+
message: `quonfig.json must have an "environments" array of strings`,
|
|
576
|
+
severity: "error",
|
|
577
|
+
suggestion: `Format: {"environments": ["production", "staging"]}`,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
for (const envId of quonfigResult.data.environments) {
|
|
582
|
+
declaredEnvIds.add(envId);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
454
587
|
for (const [relPath, content] of files) {
|
|
455
588
|
const parts = relPath.split("/");
|
|
456
589
|
if (parts.length < 2)
|
|
@@ -532,9 +665,9 @@ export function validateFileMap(files) {
|
|
|
532
665
|
issues.push({ file: relPath, message: `Log level must have valueType "log_level", got "${config.valueType}"`, severity: "error" });
|
|
533
666
|
}
|
|
534
667
|
validateEnvironmentIds(config.environments, relPath, issues);
|
|
535
|
-
validateRules(config.default.rules, relPath, "default", config.valueType, issues);
|
|
668
|
+
validateRules(config.default.rules, relPath, "default", config.valueType, issues, config.variants);
|
|
536
669
|
for (const env of config.environments) {
|
|
537
|
-
validateRules(env.rules, relPath, `environments[${env.id}]`, config.valueType, issues);
|
|
670
|
+
validateRules(env.rules, relPath, `environments[${env.id}]`, config.valueType, issues, config.variants);
|
|
538
671
|
}
|
|
539
672
|
allConfigs.push({ key: config.key, file: relPath });
|
|
540
673
|
parsedConfigs.push({ relPath, config, dir });
|
|
@@ -560,6 +693,19 @@ export function validateFileMap(files) {
|
|
|
560
693
|
suggestion: `Create schemas/${config.schemaKey}.json or schemas-protected/${config.schemaKey}.json, or remove schemaKey`,
|
|
561
694
|
});
|
|
562
695
|
}
|
|
696
|
+
// Check environment IDs referenced in config exist in quonfig.json
|
|
697
|
+
if (declaredEnvIds.size > 0) {
|
|
698
|
+
for (const env of config.environments) {
|
|
699
|
+
if (!declaredEnvIds.has(env.id)) {
|
|
700
|
+
issues.push({
|
|
701
|
+
file: relPath,
|
|
702
|
+
message: `References environment "${env.id}" which is not declared in quonfig.json`,
|
|
703
|
+
severity: "error",
|
|
704
|
+
suggestion: `Add "${env.id}" to the environments array in quonfig.json, or remove this environment override`,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
563
709
|
}
|
|
564
710
|
// Duplicate keys
|
|
565
711
|
const keyToFiles = new Map();
|
|
@@ -593,7 +739,7 @@ export function validateFileMap(files) {
|
|
|
593
739
|
}
|
|
594
740
|
}
|
|
595
741
|
const hasErrors = issues.some(i => i.severity === "error");
|
|
596
|
-
return { issues, filesChecked, valid: !hasErrors };
|
|
742
|
+
return { issues, filesChecked, valid: !hasErrors, stats: emptyStats() };
|
|
597
743
|
}
|
|
598
744
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
599
745
|
function validateEnvironmentIds(environments, file, issues) {
|
|
@@ -630,10 +776,12 @@ function validateKey(key, file, issues) {
|
|
|
630
776
|
issues.push({ file, message: `Key contains slash or backslash`, severity: "error" });
|
|
631
777
|
}
|
|
632
778
|
}
|
|
633
|
-
function validateRules(rules, file, context, expectedValueType, issues) {
|
|
779
|
+
function validateRules(rules, file, context, expectedValueType, issues, variants = []) {
|
|
634
780
|
if (rules.length === 0) {
|
|
635
781
|
issues.push({ file, message: `${context}: no rules defined`, severity: "warning" });
|
|
636
782
|
}
|
|
783
|
+
// Pre-compute serialized variant values for matching
|
|
784
|
+
const variantValues = variants.map(v => JSON.stringify(v.value));
|
|
637
785
|
for (let i = 0; i < rules.length; i++) {
|
|
638
786
|
const rule = rules[i];
|
|
639
787
|
const ruleCtx = `${context}.rules[${i}]`;
|
|
@@ -648,6 +796,18 @@ function validateRules(rules, file, context, expectedValueType, issues) {
|
|
|
648
796
|
severity: "error",
|
|
649
797
|
});
|
|
650
798
|
}
|
|
799
|
+
// Check that value matches one of the defined variants
|
|
800
|
+
if (variants.length > 0) {
|
|
801
|
+
const serialized = JSON.stringify(rule.value);
|
|
802
|
+
if (!variantValues.includes(serialized)) {
|
|
803
|
+
issues.push({
|
|
804
|
+
file,
|
|
805
|
+
message: `${ruleCtx}: value does not match any defined variant`,
|
|
806
|
+
severity: "error",
|
|
807
|
+
suggestion: `Rule value must be one of the defined variants`,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
651
811
|
}
|
|
652
812
|
else if (rule.value.type === "weighted_values") {
|
|
653
813
|
// Validate weighted values
|
|
@@ -663,6 +823,18 @@ function validateRules(rules, file, context, expectedValueType, issues) {
|
|
|
663
823
|
severity: "error",
|
|
664
824
|
});
|
|
665
825
|
}
|
|
826
|
+
// Check that each weighted value matches one of the defined variants
|
|
827
|
+
if (variants.length > 0) {
|
|
828
|
+
const serialized = JSON.stringify(wv.weightedValues[j].value);
|
|
829
|
+
if (!variantValues.includes(serialized)) {
|
|
830
|
+
issues.push({
|
|
831
|
+
file,
|
|
832
|
+
message: `${ruleCtx}.weightedValues[${j}]: value does not match any defined variant`,
|
|
833
|
+
severity: "error",
|
|
834
|
+
suggestion: `Weighted value must be one of the defined variants`,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
666
838
|
}
|
|
667
839
|
}
|
|
668
840
|
// Check criteria
|
|
@@ -720,14 +892,44 @@ function typeToDir(type) {
|
|
|
720
892
|
// ── Output formatting ───────────────────────────────────────────────────
|
|
721
893
|
export function formatResult(result) {
|
|
722
894
|
const lines = [];
|
|
895
|
+
const s = result.stats;
|
|
723
896
|
if (result.valid) {
|
|
724
|
-
lines.push(`OK: ${result.filesChecked} files checked, no errors found
|
|
897
|
+
lines.push(`OK: ${result.filesChecked} files checked, no errors found.\n`);
|
|
725
898
|
}
|
|
726
899
|
else {
|
|
727
900
|
const errors = result.issues.filter(i => i.severity === "error");
|
|
728
901
|
const warnings = result.issues.filter(i => i.severity === "warning");
|
|
729
902
|
lines.push(`FAILED: ${errors.length} error(s), ${warnings.length} warning(s) in ${result.filesChecked} files.\n`);
|
|
730
903
|
}
|
|
904
|
+
// Show what was checked
|
|
905
|
+
const fileCounts = [];
|
|
906
|
+
if (s.configs > 0)
|
|
907
|
+
fileCounts.push(`${s.configs} configs`);
|
|
908
|
+
if (s.featureFlags > 0)
|
|
909
|
+
fileCounts.push(`${s.featureFlags} feature flags`);
|
|
910
|
+
if (s.segments > 0)
|
|
911
|
+
fileCounts.push(`${s.segments} segments`);
|
|
912
|
+
if (s.logLevels > 0)
|
|
913
|
+
fileCounts.push(`${s.logLevels} log levels`);
|
|
914
|
+
if (s.schemas > 0)
|
|
915
|
+
fileCounts.push(`${s.schemas} schemas`);
|
|
916
|
+
if (fileCounts.length > 0) {
|
|
917
|
+
lines.push(` Files: ${fileCounts.join(", ")}`);
|
|
918
|
+
}
|
|
919
|
+
const checks = [];
|
|
920
|
+
checks.push(`${result.filesChecked} JSON parsed`);
|
|
921
|
+
checks.push(`${result.filesChecked} schema-validated`);
|
|
922
|
+
checks.push(`${s.uniqueKeysVerified} unique keys`);
|
|
923
|
+
checks.push(`${s.rules} rules checked`);
|
|
924
|
+
if (s.environmentOverrides > 0)
|
|
925
|
+
checks.push(`${s.environmentOverrides} env overrides`);
|
|
926
|
+
if (s.segmentRefsChecked > 0)
|
|
927
|
+
checks.push(`${s.segmentRefsChecked} segment refs`);
|
|
928
|
+
if (s.schemaRefsChecked > 0)
|
|
929
|
+
checks.push(`${s.schemaRefsChecked} schema refs`);
|
|
930
|
+
if (s.envRefsChecked > 0)
|
|
931
|
+
checks.push(`${s.envRefsChecked} env refs`);
|
|
932
|
+
lines.push(` Checks: ${checks.join(", ")}`);
|
|
731
933
|
// Group issues by file
|
|
732
934
|
const byFile = new Map();
|
|
733
935
|
for (const issue of result.issues) {
|
|
@@ -735,6 +937,9 @@ export function formatResult(result) {
|
|
|
735
937
|
existing.push(issue);
|
|
736
938
|
byFile.set(issue.file, existing);
|
|
737
939
|
}
|
|
940
|
+
if (byFile.size > 0) {
|
|
941
|
+
lines.push("");
|
|
942
|
+
}
|
|
738
943
|
for (const [file, fileIssues] of byFile) {
|
|
739
944
|
lines.push(` ${file}:`);
|
|
740
945
|
for (const issue of fileIssues) {
|