@quonfig/cli 0.0.64 → 0.0.66
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 +53 -51
- package/dist/commands/create.js +9 -0
- package/dist/commands/migrate.d.ts +1 -0
- package/dist/commands/migrate.js +10 -0
- package/dist/migrate/key-rewriter.d.ts +50 -0
- package/dist/migrate/key-rewriter.js +109 -0
- package/dist/migrate/key-sanitize.d.ts +13 -0
- package/dist/migrate/key-sanitize.js +88 -0
- package/dist/migrate/local-write.d.ts +2 -0
- package/dist/migrate/local-write.js +7 -0
- package/dist/migrate/migration-report.d.ts +14 -0
- package/dist/migrate/migration-report.js +32 -0
- package/dist/migrate/push-to-cloud.d.ts +2 -0
- package/dist/migrate/push-to-cloud.js +6 -0
- package/dist/migrate/quonfig-target/ruleset.js +5 -1
- package/dist/migrate/sources/flagsmith/translate.js +10 -2
- package/dist/migrate/sources/launch/translate.d.ts +6 -4
- package/dist/migrate/sources/launch/translate.js +25 -5
- package/dist/migrate/sources/launchdarkly/translate.js +7 -4
- package/dist/util/policy-a-key.d.ts +26 -0
- package/dist/util/policy-a-key.js +42 -0
- package/dist/verify/validate.d.ts +3 -1
- package/dist/verify/validate.js +109 -69
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +158 -152
- package/package.json +8 -2
package/dist/verify/validate.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - Valid JSON
|
|
10
10
|
* - Schema compliance (StoredConfigSchema from Zod)
|
|
11
11
|
* - Key matches filename
|
|
12
|
-
* - Key constraints (1-
|
|
12
|
+
* - Key constraints (1-200 chars, no slashes, not "new", FS-safety floor)
|
|
13
|
+
* - Case-insensitive key uniqueness (no Foo/foo collisions)
|
|
13
14
|
* - Config type matches directory
|
|
14
15
|
* - Segment constraints (valueType=bool, sendToClientSdk=false)
|
|
15
16
|
* - Log level constraints (valueType=log_level)
|
|
@@ -552,40 +553,11 @@ export function validateWorkspace(workspaceDir) {
|
|
|
552
553
|
}
|
|
553
554
|
}
|
|
554
555
|
}
|
|
555
|
-
// Check for duplicate keys across directories
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
keyToFiles.set(c.key, existing);
|
|
561
|
-
}
|
|
562
|
-
for (const [key, files] of keyToFiles) {
|
|
563
|
-
if (files.length > 1) {
|
|
564
|
-
issues.push({
|
|
565
|
-
file: files.join(', '),
|
|
566
|
-
message: `Duplicate key "${key}" found in multiple files`,
|
|
567
|
-
severity: 'error',
|
|
568
|
-
suggestion: `Each key must be unique across the workspace`,
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
const schemaKeyToFiles = new Map();
|
|
573
|
-
for (const schemaFile of allSchemaFiles) {
|
|
574
|
-
const existing = schemaKeyToFiles.get(schemaFile.key) || [];
|
|
575
|
-
existing.push(schemaFile.file);
|
|
576
|
-
schemaKeyToFiles.set(schemaFile.key, existing);
|
|
577
|
-
}
|
|
578
|
-
for (const [key, files] of schemaKeyToFiles) {
|
|
579
|
-
if (files.length > 1) {
|
|
580
|
-
issues.push({
|
|
581
|
-
file: files.join(', '),
|
|
582
|
-
message: `Duplicate schema key "${key}" found in multiple files`,
|
|
583
|
-
severity: 'error',
|
|
584
|
-
suggestion: `Each schema key must be unique across schema directories`,
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
stats.uniqueKeysVerified = keyToFiles.size + schemaKeyToFiles.size;
|
|
556
|
+
// Check for duplicate keys across directories (case-insensitive — see
|
|
557
|
+
// detectDuplicateKeys).
|
|
558
|
+
const uniqueConfigKeys = detectDuplicateKeys(allConfigs, 'key', issues);
|
|
559
|
+
const uniqueSchemaKeys = detectDuplicateKeys(allSchemaFiles, 'schema key', issues);
|
|
560
|
+
stats.uniqueKeysVerified = uniqueConfigKeys + uniqueSchemaKeys;
|
|
589
561
|
const hasErrors = issues.some((i) => i.severity === 'error');
|
|
590
562
|
return { issues, filesChecked, valid: !hasErrors, stats };
|
|
591
563
|
}
|
|
@@ -800,37 +772,9 @@ export function validateFileMap(files) {
|
|
|
800
772
|
}
|
|
801
773
|
}
|
|
802
774
|
}
|
|
803
|
-
// Duplicate keys
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const existing = keyToFiles.get(c.key) || [];
|
|
807
|
-
existing.push(c.file);
|
|
808
|
-
keyToFiles.set(c.key, existing);
|
|
809
|
-
}
|
|
810
|
-
for (const [key, fileList] of keyToFiles) {
|
|
811
|
-
if (fileList.length > 1) {
|
|
812
|
-
issues.push({
|
|
813
|
-
file: fileList.join(', '),
|
|
814
|
-
message: `Duplicate key "${key}" found in multiple files`,
|
|
815
|
-
severity: 'error',
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
const schemaKeyToFiles = new Map();
|
|
820
|
-
for (const schemaFile of allSchemaFiles) {
|
|
821
|
-
const existing = schemaKeyToFiles.get(schemaFile.key) || [];
|
|
822
|
-
existing.push(schemaFile.file);
|
|
823
|
-
schemaKeyToFiles.set(schemaFile.key, existing);
|
|
824
|
-
}
|
|
825
|
-
for (const [key, fileList] of schemaKeyToFiles) {
|
|
826
|
-
if (fileList.length > 1) {
|
|
827
|
-
issues.push({
|
|
828
|
-
file: fileList.join(', '),
|
|
829
|
-
message: `Duplicate schema key "${key}" found in multiple files`,
|
|
830
|
-
severity: 'error',
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
}
|
|
775
|
+
// Duplicate keys (case-insensitive — see detectDuplicateKeys).
|
|
776
|
+
detectDuplicateKeys(allConfigs, 'key', issues);
|
|
777
|
+
detectDuplicateKeys(allSchemaFiles, 'schema key', issues);
|
|
834
778
|
const hasErrors = issues.some((i) => i.severity === 'error');
|
|
835
779
|
return { issues, filesChecked, valid: !hasErrors, stats: emptyStats() };
|
|
836
780
|
}
|
|
@@ -855,12 +799,43 @@ function validateEnvironmentIds(environments, file, issues) {
|
|
|
855
799
|
}
|
|
856
800
|
}
|
|
857
801
|
}
|
|
858
|
-
|
|
802
|
+
// FS-safety floor checks (qfg-6na9.4). These are the names that pass the loose
|
|
803
|
+
// historical rule but produce a file that cannot be cloned/checked-out on a
|
|
804
|
+
// customer machine (macOS/Windows). The audit (project/plans/26-06-tighter-naming.md,
|
|
805
|
+
// "FS-safety floor") found ZERO existing violations, so every one is a hard
|
|
806
|
+
// error from day one.
|
|
807
|
+
//
|
|
808
|
+
// IMPORTANT: this floor must stay conceptually in lockstep with app-quonfig's
|
|
809
|
+
// src/lib/domain/config-schemas.ts (the charset precedent there is
|
|
810
|
+
// `SchemaKeySchema`; Policy A is `PolicyAKeySchema`). The general charset check
|
|
811
|
+
// (`^[A-Za-z0-9._-]+$`) is deliberately NOT here — it ships separately as a
|
|
812
|
+
// warning (bead qfg-6na9.5). This module is the FS-floor + dedup only.
|
|
813
|
+
//
|
|
814
|
+
// Structured as a table so severity is trivially adjustable later.
|
|
815
|
+
const FS_SAFETY_FLOOR_CHECKS = [
|
|
816
|
+
{ test: (k) => k.startsWith('.'), message: `Key has a leading dot (files starting with "." are skipped by verify)` },
|
|
817
|
+
// eslint-disable-next-line no-control-regex -- matching control chars/NUL is the point
|
|
818
|
+
{ test: (k) => /[\u0000-\u001F\u007F]/.test(k), message: `Key contains control chars or NUL` },
|
|
819
|
+
{ test: (k) => /["*:<>?|]/.test(k), message: `Key contains Windows-reserved chars (: * ? " < > |)` },
|
|
820
|
+
{
|
|
821
|
+
test: (k) => /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(k.split('.')[0]),
|
|
822
|
+
message: `Key is a Windows reserved device name (con/prn/aux/nul/com1-9/lpt1-9)`,
|
|
823
|
+
},
|
|
824
|
+
{ test: (k) => /[ .]$/.test(k), message: `Key has a trailing dot or space (silently stripped on Windows)` },
|
|
825
|
+
];
|
|
826
|
+
// Exported so the FS-safety floor (incl. the leading-dot rule, whose files are
|
|
827
|
+
// pre-filtered by the directory walk before they'd reach the file-map path) can
|
|
828
|
+
// be unit-tested directly.
|
|
829
|
+
export function validateKey(key, file, issues) {
|
|
830
|
+
const errorsBefore = issues.length;
|
|
859
831
|
if (key.length === 0) {
|
|
860
832
|
issues.push({ file, message: `Key is empty`, severity: 'error' });
|
|
861
833
|
}
|
|
862
|
-
|
|
863
|
-
|
|
834
|
+
// Length cap lowered 512 -> 200 (project/plans/26-06-tighter-naming.md,
|
|
835
|
+
// "Length cap: 200"): ASCII-only Policy A means 200 chars == 200 bytes, well
|
|
836
|
+
// under the 255-byte filesystem path-component limit even with `.json`.
|
|
837
|
+
if (key.length > 200) {
|
|
838
|
+
issues.push({ file, message: `Key exceeds 200 characters (${key.length})`, severity: 'error' });
|
|
864
839
|
}
|
|
865
840
|
if (key === 'new') {
|
|
866
841
|
issues.push({ file, message: `Key cannot be "new" (reserved)`, severity: 'error' });
|
|
@@ -868,6 +843,71 @@ function validateKey(key, file, issues) {
|
|
|
868
843
|
if (/[/\\]/.test(key)) {
|
|
869
844
|
issues.push({ file, message: `Key contains slash or backslash`, severity: 'error' });
|
|
870
845
|
}
|
|
846
|
+
for (const check of FS_SAFETY_FLOOR_CHECKS) {
|
|
847
|
+
if (check.test(key)) {
|
|
848
|
+
issues.push({ file, message: check.message, severity: 'error' });
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// qfg-6na9.5: the general Policy A charset as a WARNING — the warn stage of the
|
|
852
|
+
// warn->error soak (project/plans/26-06-tighter-naming.md). A config that trips
|
|
853
|
+
// this must STILL push, so severity is 'warning' (does not set result.valid =
|
|
854
|
+
// false); qfg-6na9.6 will flip it to a hard error after a clean soak. The
|
|
855
|
+
// charset `^[A-Za-z0-9._-]+$` is kept in sync with app-quonfig's
|
|
856
|
+
// PolicyAKeySchema (hard at create). Skipped when this key already earned a
|
|
857
|
+
// hard error above (e.g. a Windows-reserved char is both a floor error AND a
|
|
858
|
+
// charset miss) so we don't double-report the same key.
|
|
859
|
+
// eslint-disable-next-line unicorn/better-regex -- keep the explicit charset in sync with app-quonfig
|
|
860
|
+
if (issues.length === errorsBefore && !/^[A-Za-z0-9._-]+$/.test(key)) {
|
|
861
|
+
issues.push({
|
|
862
|
+
file,
|
|
863
|
+
message: `Key "${key}" has characters outside the allowed set (letters, numbers, ".", "-", "_"). This will become a hard error in a future release — rename it to conform.`,
|
|
864
|
+
severity: 'warning',
|
|
865
|
+
suggestion: `Rename to use only letters, numbers, ".", "-", and "_"`,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Case-insensitive duplicate-key detection (qfg-6na9.4,
|
|
870
|
+
// project/plans/26-06-tighter-naming.md "Case-insensitive uniqueness").
|
|
871
|
+
// `Foo` and `foo` are distinct keys server-side (case-sensitive Linux ext4) but
|
|
872
|
+
// collide to one file on a case-insensitive macOS/Windows clone, silently
|
|
873
|
+
// dropping a config. Group by the DOWNCASED key and branch the message:
|
|
874
|
+
// - all original keys byte-identical -> exact-duplicate message
|
|
875
|
+
// - they differ only by case -> case-collision message
|
|
876
|
+
// The audit found 0 such collisions today, so this is a hard error from day one.
|
|
877
|
+
function detectDuplicateKeys(entries, label, issues) {
|
|
878
|
+
const byLowercase = new Map();
|
|
879
|
+
for (const e of entries) {
|
|
880
|
+
const lower = e.key.toLowerCase();
|
|
881
|
+
const existing = byLowercase.get(lower) || [];
|
|
882
|
+
existing.push(e);
|
|
883
|
+
byLowercase.set(lower, existing);
|
|
884
|
+
}
|
|
885
|
+
for (const group of byLowercase.values()) {
|
|
886
|
+
if (group.length <= 1)
|
|
887
|
+
continue;
|
|
888
|
+
const files = group.map((g) => g.file).join(', ');
|
|
889
|
+
const originalKeys = group.map((g) => g.key);
|
|
890
|
+
const allIdentical = originalKeys.every((k) => k === originalKeys[0]);
|
|
891
|
+
if (allIdentical) {
|
|
892
|
+
issues.push({
|
|
893
|
+
file: files,
|
|
894
|
+
message: `Duplicate ${label} "${originalKeys[0]}" found in multiple files`,
|
|
895
|
+
severity: 'error',
|
|
896
|
+
suggestion: label === 'schema key'
|
|
897
|
+
? `Each schema key must be unique across schema directories`
|
|
898
|
+
: `Each key must be unique across the workspace`,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
issues.push({
|
|
903
|
+
file: files,
|
|
904
|
+
message: `Keys differ only by case (${originalKeys.join(', ')}); they collide to one file on case-insensitive filesystems (macOS/Windows)`,
|
|
905
|
+
severity: 'error',
|
|
906
|
+
suggestion: `Rename so the ${label}s are also unique when lowercased`,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return byLowercase.size;
|
|
871
911
|
}
|
|
872
912
|
function validateRules(rules, file, context, expectedValueType, issues, variants = []) {
|
|
873
913
|
if (rules.length === 0) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.0.
|
|
1
|
+
declare const _default: "0.0.66";
|
|
2
2
|
export default _default;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export default '0.0.
|
|
2
|
+
export default '0.0.66';
|