@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.
Files changed (39) hide show
  1. package/README.md +306 -29
  2. package/bin/dev.js +5 -9
  3. package/bin/dev.mjs +6 -0
  4. package/dist/codegen/zod-introspection.js +13 -4
  5. package/dist/commands/config-schema.d.ts +10 -0
  6. package/dist/commands/config-schema.js +165 -0
  7. package/dist/commands/create.js +22 -1
  8. package/dist/commands/info.js +17 -2
  9. package/dist/commands/login.js +27 -6
  10. package/dist/commands/mcp.js +1 -1
  11. package/dist/commands/pull.d.ts +15 -0
  12. package/dist/commands/pull.js +191 -0
  13. package/dist/commands/serve.js +3 -1
  14. package/dist/commands/set-default.js +78 -15
  15. package/dist/commands/set-rollout.d.ts +19 -0
  16. package/dist/commands/set-rollout.js +260 -0
  17. package/dist/commands/sync.d.ts +18 -0
  18. package/dist/commands/sync.js +142 -0
  19. package/dist/commands/whoami.js +4 -1
  20. package/dist/commands/workspace/bootstrap.d.ts +13 -0
  21. package/dist/commands/workspace/bootstrap.js +155 -0
  22. package/dist/init/init-workspace.js +4 -4
  23. package/dist/init/templates.js +1 -1
  24. package/dist/util/coerce.js +2 -1
  25. package/dist/util/get-client.js +6 -44
  26. package/dist/util/get-valid-token.d.ts +5 -0
  27. package/dist/util/get-valid-token.js +44 -0
  28. package/dist/util/git-ops.d.ts +32 -0
  29. package/dist/util/git-ops.js +164 -0
  30. package/dist/util/gitea-api.d.ts +11 -0
  31. package/dist/util/gitea-api.js +45 -0
  32. package/dist/util/gitea-token-storage.d.ts +8 -0
  33. package/dist/util/gitea-token-storage.js +47 -0
  34. package/dist/util/token-storage.d.ts +1 -0
  35. package/dist/util/token-storage.js +24 -10
  36. package/dist/verify/validate.d.ts +1 -0
  37. package/dist/verify/validate.js +136 -6
  38. package/oclif.manifest.json +404 -9
  39. 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
- await fs.promises.writeFile(getTokenFile(options), JSON.stringify(tokens, null, 2), 'utf8');
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\n';
51
+ configContent += '\n';
52
+ if (profileData.workspaceSlug) {
53
+ configContent += `workspace_slug = ${profileData.workspaceSlug}\n`;
54
+ }
55
+ configContent += '\n';
49
56
  }
50
- await fs.promises.writeFile(getConfigFile(options), configContent, 'utf8');
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 profileRegex = /\[profile\s+(\w+)]\s*\n\s*workspace\s*=\s*([^\s#]+)(?:\s*#\s*(.+))?/g;
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 = profileRegex.exec(data)) !== null) {
76
+ while ((match = profileBlockRegex.exec(data)) !== null) {
67
77
  const profileName = match[1];
68
- const workspace = match[2];
69
- const workspaceName = match[3]?.trim();
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;
@@ -36,6 +36,7 @@ export interface ValidationStats {
36
36
  rules: number;
37
37
  segmentRefsChecked: number;
38
38
  schemaRefsChecked: number;
39
+ envRefsChecked: number;
39
40
  uniqueKeysVerified: number;
40
41
  }
41
42
  export interface ValidationResult {
@@ -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();