@quonfig/cli 0.0.68 → 0.0.70
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 +50 -50
- package/dist/codegen/code-generators/base-generator.d.ts +8 -1
- package/dist/codegen/code-generators/base-generator.js +3 -1
- package/dist/codegen/code-generators/base-typescript-generator.d.ts +34 -1
- package/dist/codegen/code-generators/base-typescript-generator.js +78 -2
- package/dist/codegen/code-generators/node-typescript-generator.js +7 -12
- package/dist/codegen/code-generators/react-typescript-generator.js +9 -12
- package/dist/commands/generate.js +4 -2
- package/dist/init/schema.js +11 -2
- package/dist/migrate/import-state.d.ts +3 -2
- package/dist/migrate/import-state.js +20 -7
- package/dist/migrate/key-rewriter.d.ts +70 -14
- package/dist/migrate/key-rewriter.js +186 -38
- package/dist/migrate/local-write.d.ts +10 -0
- package/dist/migrate/local-write.js +27 -1
- package/dist/migrate/migration-report.js +1 -0
- package/dist/migrate/push-to-cloud.js +24 -12
- package/dist/migrate/quonfig-target/report.d.ts +7 -1
- package/dist/migrate/quonfig-target/ruleset.js +15 -3
- package/dist/migrate/quonfig-target/weights.d.ts +20 -0
- package/dist/migrate/quonfig-target/weights.js +48 -0
- package/dist/migrate/source.d.ts +9 -0
- package/dist/migrate/sources/flagsmith/translate.js +9 -0
- package/dist/migrate/sources/launch/translate.d.ts +1 -1
- package/dist/migrate/sources/launch/translate.js +133 -2
- package/dist/migrate/sources/launch.js +10 -0
- package/dist/migrate/sources/launchdarkly/translate.js +12 -2
- package/dist/migrate/sources/launchdarkly.js +15 -2
- package/dist/util/policy-a-key.d.ts +15 -1
- package/dist/util/policy-a-key.js +25 -1
- package/dist/verify/standalone.d.ts +7 -0
- package/dist/verify/standalone.js +15 -9
- package/dist/verify/validate.js +111 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -99,26 +99,32 @@ async function runGitHook() {
|
|
|
99
99
|
* exists to catch (verified live on staging: a `configs/bad charset key.json`
|
|
100
100
|
* push was accepted unvalidated). A listed-but-unreadable file is now a hard
|
|
101
101
|
* failure (fail closed), not a silent skip.
|
|
102
|
+
*
|
|
103
|
+
* qfg-hbuy.4: enumerates RECURSIVELY (`-r`) and with NO name filtering. The
|
|
104
|
+
* old non-recursive listing plus `endsWith('.json') && !includes('/.')` filter
|
|
105
|
+
* meant dotfiles, nested paths (configs/sub/x.json), and case-variant
|
|
106
|
+
* extensions (FOO.JSON) never reached validation at all — ghost files that
|
|
107
|
+
* push fine but no loader reads. The hook must see EVERYTHING under the
|
|
108
|
+
* validated dirs; validateFileMap is the layer that decides what is an error.
|
|
102
109
|
*/
|
|
103
110
|
export function readFilesFromCommit(commitOid, cwd) {
|
|
104
111
|
const files = new Map();
|
|
105
112
|
const dirs = ['configs', 'feature-flags', 'segments', 'log-levels', 'schemas', 'schemas-protected'];
|
|
106
113
|
for (const dir of dirs) {
|
|
107
|
-
// List
|
|
108
|
-
// doesn't exist yields an empty listing (exit 0); a
|
|
109
|
-
// throws — fail closed, the hook rejects the push.
|
|
110
|
-
const listing = execFileSync('git', ['ls-tree', '-z', '--name-only', commitOid, `${dir}/`], {
|
|
114
|
+
// List every leaf entry under this directory at the given commit. A
|
|
115
|
+
// directory that doesn't exist yields an empty listing (exit 0); a
|
|
116
|
+
// bad/unreadable OID throws — fail closed, the hook rejects the push.
|
|
117
|
+
const listing = execFileSync('git', ['ls-tree', '-z', '-r', '--name-only', commitOid, `${dir}/`], {
|
|
111
118
|
cwd,
|
|
112
119
|
encoding: 'utf8',
|
|
113
120
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
114
121
|
});
|
|
115
122
|
const filenames = listing.split('\0').filter(Boolean);
|
|
116
123
|
for (const filePath of filenames) {
|
|
117
|
-
if
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// hook's per-ref handler, which rejects the push.
|
|
124
|
+
// Fail closed: if a listed entry can't be read (bad object, submodule
|
|
125
|
+
// gitlink, ...), the push must not be accepted with that entry
|
|
126
|
+
// unvalidated — let the error propagate to the hook's per-ref handler,
|
|
127
|
+
// which rejects the push.
|
|
122
128
|
const content = execFileSync('git', ['show', `${commitOid}:${filePath}`], {
|
|
123
129
|
cwd,
|
|
124
130
|
encoding: 'utf8',
|
package/dist/verify/validate.js
CHANGED
|
@@ -160,6 +160,86 @@ const DIR_TO_TYPE = {
|
|
|
160
160
|
const CONFIG_DIRS = new Set(Object.keys(DIR_TO_TYPE));
|
|
161
161
|
const SCHEMA_DIRS = new Set(['schemas', 'schemas-protected']);
|
|
162
162
|
const KNOWN_DIRS = new Set([...CONFIG_DIRS, ...SCHEMA_DIRS]);
|
|
163
|
+
// ── Ghost-file prevention (qfg-hbuy.4) ──────────────────────────────────
|
|
164
|
+
// Within the validated content dirs (KNOWN_DIRS), every entry must be a
|
|
165
|
+
// top-level, non-dot, lowercase-`.json` file. Anything else used to be
|
|
166
|
+
// silently SKIPPED by the enumeration filters — a "ghost" file that pushes
|
|
167
|
+
// fine but is invisible to the hook and to every loader, and a mixed-case
|
|
168
|
+
// FOO.JSON can collide with foo.json on a case-insensitive customer clone
|
|
169
|
+
// (the exact clonability failure Policy A exists to prevent). A skipped
|
|
170
|
+
// entry is a bypass; an error is a rejected push. Paths OUTSIDE the
|
|
171
|
+
// validated dirs (quonfig.json, README.md, .qf/, ...) are not this rule's
|
|
172
|
+
// business and stay untouched.
|
|
173
|
+
function ghostEntryIssue(dir, relPath, name, nested) {
|
|
174
|
+
if (nested) {
|
|
175
|
+
return {
|
|
176
|
+
file: relPath,
|
|
177
|
+
message: `Subdirectories are not allowed inside "${dir}/" — nested files are never loaded (ghost file)`,
|
|
178
|
+
severity: 'error',
|
|
179
|
+
suggestion: `Move config files to the top level: "${dir}/<key>.json"`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (name.startsWith('.')) {
|
|
183
|
+
return {
|
|
184
|
+
file: relPath,
|
|
185
|
+
message: `Dotfile "${name}" is not allowed inside "${dir}/" — hidden files are never loaded (ghost file)`,
|
|
186
|
+
severity: 'error',
|
|
187
|
+
suggestion: `Remove the file or rename it without the leading dot`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (!name.endsWith('.json')) {
|
|
191
|
+
if (name.toLowerCase().endsWith('.json')) {
|
|
192
|
+
return {
|
|
193
|
+
file: relPath,
|
|
194
|
+
message: `File extension must be lowercase ".json" (found "${name}") — a case-variant extension is never loaded and can collide with another file on a case-insensitive filesystem`,
|
|
195
|
+
severity: 'error',
|
|
196
|
+
suggestion: `Rename to "${name.slice(0, -5)}.json"`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
file: relPath,
|
|
201
|
+
message: `Only ".json" files are allowed inside "${dir}/" (found "${name}")`,
|
|
202
|
+
severity: 'error',
|
|
203
|
+
suggestion: `Remove the file or rename it to "<key>.json"`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Disk-walk variant of the ghost rule (validateWorkspace). Returns the list
|
|
210
|
+
* of names that should proceed to content validation, pushing an issue for
|
|
211
|
+
* every ghost entry.
|
|
212
|
+
*
|
|
213
|
+
* Dotfiles are the one place the disk rule is softer than the committed-tree
|
|
214
|
+
* rule: `qfg push` never sends dotfiles (collectFiles skips them), so a local
|
|
215
|
+
* `configs/.DS_Store` is inert OS junk — erroring on it would break every
|
|
216
|
+
* macOS user. A `.json`-looking dotfile is almost certainly a mistake, so it
|
|
217
|
+
* gets a WARNING; other dotfiles are skipped silently.
|
|
218
|
+
*/
|
|
219
|
+
function listValidatedDirEntries(dirPath, dir, issues) {
|
|
220
|
+
const names = [];
|
|
221
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
222
|
+
const relPath = `${dir}/${entry.name}`;
|
|
223
|
+
if (entry.name.startsWith('.')) {
|
|
224
|
+
if (entry.name.toLowerCase().endsWith('.json')) {
|
|
225
|
+
issues.push({
|
|
226
|
+
file: relPath,
|
|
227
|
+
message: `Dotfile "${entry.name}" in "${dir}/" is ignored — it is never validated, pushed, or loaded`,
|
|
228
|
+
severity: 'warning',
|
|
229
|
+
suggestion: `Remove the file or rename it without the leading dot`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const ghost = ghostEntryIssue(dir, relPath, entry.name, entry.isDirectory());
|
|
235
|
+
if (ghost) {
|
|
236
|
+
issues.push(ghost);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
names.push(entry.name);
|
|
240
|
+
}
|
|
241
|
+
return names;
|
|
242
|
+
}
|
|
163
243
|
// Operators that reference segments
|
|
164
244
|
const SEGMENT_OPERATORS = new Set(['IN_SEG', 'NOT_IN_SEG']);
|
|
165
245
|
// Operators that require a propertyName
|
|
@@ -318,12 +398,13 @@ export function validateWorkspace(workspaceDir) {
|
|
|
318
398
|
suggestion: `Create quonfig.json with format: {"environments": ["production", "staging"]}`,
|
|
319
399
|
});
|
|
320
400
|
}
|
|
321
|
-
// First pass: parse all files and collect keys
|
|
401
|
+
// First pass: parse all files and collect keys. Ghost entries (dotfiles,
|
|
402
|
+
// subdirectories, non-lowercase-.json names) are reported here (qfg-hbuy.4).
|
|
322
403
|
for (const dir of KNOWN_DIRS) {
|
|
323
404
|
const dirPath = path.join(workspaceDir, dir);
|
|
324
405
|
if (!fs.existsSync(dirPath))
|
|
325
406
|
continue;
|
|
326
|
-
const files =
|
|
407
|
+
const files = listValidatedDirEntries(dirPath, dir, issues);
|
|
327
408
|
for (const file of files) {
|
|
328
409
|
const filePath = path.join(dirPath, file);
|
|
329
410
|
const relPath = `${dir}/${file}`;
|
|
@@ -482,7 +563,8 @@ export function validateWorkspace(workspaceDir) {
|
|
|
482
563
|
allConfigs.push({ key: config.key, type: config.type, dir, file: relPath });
|
|
483
564
|
}
|
|
484
565
|
}
|
|
485
|
-
// Second pass: referential integrity
|
|
566
|
+
// Second pass: referential integrity. Keep the plain skip-filter here —
|
|
567
|
+
// ghost entries were already reported (once) by the first pass.
|
|
486
568
|
for (const dir of KNOWN_DIRS) {
|
|
487
569
|
const dirPath = path.join(workspaceDir, dir);
|
|
488
570
|
if (!fs.existsSync(dirPath))
|
|
@@ -629,8 +711,15 @@ export function validateFileMap(files) {
|
|
|
629
711
|
const file = parts.at(-1);
|
|
630
712
|
if (!KNOWN_DIRS.has(dir))
|
|
631
713
|
continue;
|
|
632
|
-
if (!file
|
|
714
|
+
if (!file)
|
|
715
|
+
continue;
|
|
716
|
+
// qfg-hbuy.4: a dotfile, nested path, or non-lowercase-.json entry inside
|
|
717
|
+
// a validated dir is a hard error (rejected push), not a silent skip.
|
|
718
|
+
const ghost = ghostEntryIssue(dir, relPath, file, parts.length > 2);
|
|
719
|
+
if (ghost) {
|
|
720
|
+
issues.push(ghost);
|
|
633
721
|
continue;
|
|
722
|
+
}
|
|
634
723
|
filesChecked++;
|
|
635
724
|
let parsed;
|
|
636
725
|
try {
|
|
@@ -980,6 +1069,24 @@ function validateRules(rules, file, context, expectedValueType, issues, variants
|
|
|
980
1069
|
}
|
|
981
1070
|
}
|
|
982
1071
|
}
|
|
1072
|
+
// Weight predicate (qfg-wis6.10): stored weights must be either an
|
|
1073
|
+
// even split (all weights equal and > 0) or percentages summing to
|
|
1074
|
+
// MAX_WEIGHT. Anything else was written by a broken client: evaluators
|
|
1075
|
+
// normalize by total, so it silently serves different percentages
|
|
1076
|
+
// than any display of the raw weights suggests.
|
|
1077
|
+
if (wv.weightedValues.length > 0) {
|
|
1078
|
+
const weights = wv.weightedValues.map((entry) => entry.weight);
|
|
1079
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
1080
|
+
const evenSplit = weights[0] > 0 && weights.every((w) => w === weights[0]);
|
|
1081
|
+
if (!evenSplit && total !== MAX_WEIGHT) {
|
|
1082
|
+
issues.push({
|
|
1083
|
+
file,
|
|
1084
|
+
message: `${ruleCtx}: weighted values must either be an even split (all weights equal and > 0) or sum to ${MAX_WEIGHT}; got [${weights.join(', ')}] summing to ${total}`,
|
|
1085
|
+
severity: 'error',
|
|
1086
|
+
suggestion: `Use equal weights for an even split, or make the weights sum to ${MAX_WEIGHT} (1000 units per percent).`,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
983
1090
|
}
|
|
984
1091
|
// Check criteria
|
|
985
1092
|
for (let j = 0; j < rule.criteria.length; j++) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.0.
|
|
1
|
+
declare const _default: "0.0.70";
|
|
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.70';
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
|
|
3
3
|
"name": "@quonfig/cli",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.70",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/quonfig/cli/issues"
|
|
7
7
|
},
|