@quonfig/cli 0.0.2 → 0.0.5
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 +306 -29
- package/bin/dev.js +5 -9
- package/bin/dev.mjs +6 -0
- package/dist/codegen/zod-introspection.js +13 -4
- package/dist/commands/config-schema.d.ts +10 -0
- package/dist/commands/config-schema.js +165 -0
- package/dist/commands/create.js +22 -1
- package/dist/commands/info.js +17 -2
- 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 +191 -0
- package/dist/commands/serve.js +3 -1
- package/dist/commands/set-default.js +78 -15
- package/dist/commands/set-rollout.d.ts +19 -0
- package/dist/commands/set-rollout.js +260 -0
- 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.js +4 -4
- package/dist/init/templates.js +1 -1
- 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 +24 -10
- package/dist/verify/validate.d.ts +1 -0
- package/dist/verify/validate.js +136 -6
- package/oclif.manifest.json +404 -9
- package/package.json +2 -2
|
@@ -20,7 +20,10 @@ const ensureQuonfigDir = async (options) => {
|
|
|
20
20
|
};
|
|
21
21
|
export const saveTokens = async (tokens, options) => {
|
|
22
22
|
await ensureQuonfigDir(options);
|
|
23
|
-
|
|
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);
|
|
24
27
|
};
|
|
25
28
|
export const loadTokens = async (options) => {
|
|
26
29
|
try {
|
|
@@ -45,9 +48,16 @@ export const saveAuthConfig = async (config, options) => {
|
|
|
45
48
|
if (profileData.workspaceName) {
|
|
46
49
|
configContent += ` # ${profileData.workspaceName}`;
|
|
47
50
|
}
|
|
48
|
-
configContent += '\n
|
|
51
|
+
configContent += '\n';
|
|
52
|
+
if (profileData.workspaceSlug) {
|
|
53
|
+
configContent += `workspace_slug = ${profileData.workspaceSlug}\n`;
|
|
54
|
+
}
|
|
55
|
+
configContent += '\n';
|
|
49
56
|
}
|
|
50
|
-
|
|
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);
|
|
51
61
|
};
|
|
52
62
|
export const loadAuthConfig = async (options) => {
|
|
53
63
|
try {
|
|
@@ -60,16 +70,20 @@ export const loadAuthConfig = async (options) => {
|
|
|
60
70
|
if (defaultMatch && defaultMatch[1]) {
|
|
61
71
|
config.defaultProfile = defaultMatch[1].trim();
|
|
62
72
|
}
|
|
63
|
-
// Parse profiles
|
|
64
|
-
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;
|
|
65
75
|
let match;
|
|
66
|
-
while ((match =
|
|
76
|
+
while ((match = profileBlockRegex.exec(data)) !== null) {
|
|
67
77
|
const profileName = match[1];
|
|
68
|
-
const
|
|
69
|
-
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;
|
|
70
83
|
config.profiles[profileName] = {
|
|
71
|
-
workspace,
|
|
72
|
-
workspaceName,
|
|
84
|
+
workspace: workspaceMatch[1],
|
|
85
|
+
workspaceName: workspaceMatch[2]?.trim(),
|
|
86
|
+
workspaceSlug: slugMatch?.[1]?.trim(),
|
|
73
87
|
};
|
|
74
88
|
}
|
|
75
89
|
return Object.keys(config.profiles).length > 0 ? config : null;
|
package/dist/verify/validate.js
CHANGED
|
@@ -202,6 +202,7 @@ export function validateWorkspace(workspaceDir) {
|
|
|
202
202
|
rules: 0,
|
|
203
203
|
segmentRefsChecked: 0,
|
|
204
204
|
schemaRefsChecked: 0,
|
|
205
|
+
envRefsChecked: 0,
|
|
205
206
|
uniqueKeysVerified: 0,
|
|
206
207
|
};
|
|
207
208
|
// Collect all configs for cross-reference checks
|
|
@@ -209,6 +210,7 @@ export function validateWorkspace(workspaceDir) {
|
|
|
209
210
|
const allSchemaFiles = [];
|
|
210
211
|
const segmentKeys = new Set();
|
|
211
212
|
const schemaKeys = new Set();
|
|
213
|
+
const declaredEnvIds = new Set();
|
|
212
214
|
// Check for unexpected top-level entries
|
|
213
215
|
let entries;
|
|
214
216
|
try {
|
|
@@ -238,6 +240,50 @@ export function validateWorkspace(workspaceDir) {
|
|
|
238
240
|
});
|
|
239
241
|
}
|
|
240
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
|
+
}
|
|
241
287
|
// First pass: parse all files and collect keys
|
|
242
288
|
for (const dir of KNOWN_DIRS) {
|
|
243
289
|
const dirPath = path.join(workspaceDir, dir);
|
|
@@ -351,9 +397,9 @@ export function validateWorkspace(workspaceDir) {
|
|
|
351
397
|
// Validate environment IDs are slugs, not UUIDs
|
|
352
398
|
validateEnvironmentIds(config.environments, relPath, issues);
|
|
353
399
|
// Validate rules
|
|
354
|
-
validateRules(config.default.rules, relPath, "default", config.valueType, issues);
|
|
400
|
+
validateRules(config.default.rules, relPath, "default", config.valueType, issues, config.variants);
|
|
355
401
|
for (const env of config.environments) {
|
|
356
|
-
validateRules(env.rules, relPath, `environments[${env.id}]`, config.valueType, issues);
|
|
402
|
+
validateRules(env.rules, relPath, `environments[${env.id}]`, config.valueType, issues, config.variants);
|
|
357
403
|
}
|
|
358
404
|
// Count by type
|
|
359
405
|
switch (config.type) {
|
|
@@ -435,6 +481,20 @@ export function validateWorkspace(workspaceDir) {
|
|
|
435
481
|
});
|
|
436
482
|
}
|
|
437
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
|
+
}
|
|
497
|
+
}
|
|
438
498
|
}
|
|
439
499
|
}
|
|
440
500
|
// Check for duplicate keys across directories
|
|
@@ -479,7 +539,7 @@ function emptyStats() {
|
|
|
479
539
|
return {
|
|
480
540
|
configs: 0, featureFlags: 0, segments: 0, logLevels: 0, schemas: 0,
|
|
481
541
|
environmentOverrides: 0, rules: 0, segmentRefsChecked: 0,
|
|
482
|
-
schemaRefsChecked: 0, uniqueKeysVerified: 0,
|
|
542
|
+
schemaRefsChecked: 0, envRefsChecked: 0, uniqueKeysVerified: 0,
|
|
483
543
|
};
|
|
484
544
|
}
|
|
485
545
|
// ── Validate from in-memory file map (for git hook) ─────────────────────
|
|
@@ -495,6 +555,35 @@ export function validateFileMap(files) {
|
|
|
495
555
|
const allSchemaFiles = [];
|
|
496
556
|
const allConfigs = [];
|
|
497
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
|
+
}
|
|
498
587
|
for (const [relPath, content] of files) {
|
|
499
588
|
const parts = relPath.split("/");
|
|
500
589
|
if (parts.length < 2)
|
|
@@ -576,9 +665,9 @@ export function validateFileMap(files) {
|
|
|
576
665
|
issues.push({ file: relPath, message: `Log level must have valueType "log_level", got "${config.valueType}"`, severity: "error" });
|
|
577
666
|
}
|
|
578
667
|
validateEnvironmentIds(config.environments, relPath, issues);
|
|
579
|
-
validateRules(config.default.rules, relPath, "default", config.valueType, issues);
|
|
668
|
+
validateRules(config.default.rules, relPath, "default", config.valueType, issues, config.variants);
|
|
580
669
|
for (const env of config.environments) {
|
|
581
|
-
validateRules(env.rules, relPath, `environments[${env.id}]`, config.valueType, issues);
|
|
670
|
+
validateRules(env.rules, relPath, `environments[${env.id}]`, config.valueType, issues, config.variants);
|
|
582
671
|
}
|
|
583
672
|
allConfigs.push({ key: config.key, file: relPath });
|
|
584
673
|
parsedConfigs.push({ relPath, config, dir });
|
|
@@ -604,6 +693,19 @@ export function validateFileMap(files) {
|
|
|
604
693
|
suggestion: `Create schemas/${config.schemaKey}.json or schemas-protected/${config.schemaKey}.json, or remove schemaKey`,
|
|
605
694
|
});
|
|
606
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
|
+
}
|
|
607
709
|
}
|
|
608
710
|
// Duplicate keys
|
|
609
711
|
const keyToFiles = new Map();
|
|
@@ -674,10 +776,12 @@ function validateKey(key, file, issues) {
|
|
|
674
776
|
issues.push({ file, message: `Key contains slash or backslash`, severity: "error" });
|
|
675
777
|
}
|
|
676
778
|
}
|
|
677
|
-
function validateRules(rules, file, context, expectedValueType, issues) {
|
|
779
|
+
function validateRules(rules, file, context, expectedValueType, issues, variants = []) {
|
|
678
780
|
if (rules.length === 0) {
|
|
679
781
|
issues.push({ file, message: `${context}: no rules defined`, severity: "warning" });
|
|
680
782
|
}
|
|
783
|
+
// Pre-compute serialized variant values for matching
|
|
784
|
+
const variantValues = variants.map(v => JSON.stringify(v.value));
|
|
681
785
|
for (let i = 0; i < rules.length; i++) {
|
|
682
786
|
const rule = rules[i];
|
|
683
787
|
const ruleCtx = `${context}.rules[${i}]`;
|
|
@@ -692,6 +796,18 @@ function validateRules(rules, file, context, expectedValueType, issues) {
|
|
|
692
796
|
severity: "error",
|
|
693
797
|
});
|
|
694
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
|
+
}
|
|
695
811
|
}
|
|
696
812
|
else if (rule.value.type === "weighted_values") {
|
|
697
813
|
// Validate weighted values
|
|
@@ -707,6 +823,18 @@ function validateRules(rules, file, context, expectedValueType, issues) {
|
|
|
707
823
|
severity: "error",
|
|
708
824
|
});
|
|
709
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
|
+
}
|
|
710
838
|
}
|
|
711
839
|
}
|
|
712
840
|
// Check criteria
|
|
@@ -799,6 +927,8 @@ export function formatResult(result) {
|
|
|
799
927
|
checks.push(`${s.segmentRefsChecked} segment refs`);
|
|
800
928
|
if (s.schemaRefsChecked > 0)
|
|
801
929
|
checks.push(`${s.schemaRefsChecked} schema refs`);
|
|
930
|
+
if (s.envRefsChecked > 0)
|
|
931
|
+
checks.push(`${s.envRefsChecked} env refs`);
|
|
802
932
|
lines.push(` Checks: ${checks.join(", ")}`);
|
|
803
933
|
// Group issues by file
|
|
804
934
|
const byFile = new Map();
|