@quonfig/cli 0.0.71 → 0.0.73
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 +97 -73
- package/dist/commands/cleanup/status.js +5 -1
- package/dist/commands/config-schema.js +8 -3
- package/dist/commands/get.js +6 -4
- package/dist/commands/info.js +9 -15
- package/dist/commands/log-level.js +7 -10
- package/dist/commands/set-default.d.ts +1 -0
- package/dist/commands/set-default.js +106 -71
- package/dist/commands/set-rollout.d.ts +1 -0
- package/dist/commands/set-rollout.js +65 -18
- package/dist/commands/workspace.d.ts +10 -1
- package/dist/commands/workspace.js +72 -13
- package/dist/index.d.ts +30 -0
- package/dist/index.js +105 -8
- package/dist/init/templates.js +43 -0
- package/dist/push/run-push.d.ts +21 -0
- package/dist/push/run-push.js +26 -3
- package/dist/util/rules.d.ts +98 -0
- package/dist/util/rules.js +131 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +18 -5
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule helpers shared by the CLI's writers (`set-default`, `set-rollout`,
|
|
3
|
+
* `log-level`) and readers (`info`, `cleanup status`) — beads qfg-qjdm and
|
|
4
|
+
* qfg-9kr8.
|
|
5
|
+
*
|
|
6
|
+
* The CLI cannot import app-quonfig code, so this file is a deliberate,
|
|
7
|
+
* small mirror of the server's rulebook. Keep the semantics byte-identical
|
|
8
|
+
* with:
|
|
9
|
+
* - app-quonfig src/lib/domain/rule-display.ts → isCatchAllRule
|
|
10
|
+
* - app-quonfig src/lib/public-api/write-scope.ts → countTargetingRules
|
|
11
|
+
* - app-quonfig src/lib/public-api/log-level-write.ts → upsertFallbackRule
|
|
12
|
+
* Both surfaces write the same documents; a second, subtly different
|
|
13
|
+
* rulebook is a correctness hazard, not a style problem.
|
|
14
|
+
*/
|
|
15
|
+
/** A fresh unconditional rule carrying `value`. */
|
|
16
|
+
export function catchAllRule(value) {
|
|
17
|
+
return { criteria: [{ operator: 'ALWAYS_TRUE' }], value };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* THE ONE DEFINITION of "is this rule unconditional" (qfg-gv54).
|
|
21
|
+
*
|
|
22
|
+
* A catch-all — the base-case rule that serves everyone in its scope — has
|
|
23
|
+
* two live JSON spellings, and every SDK evaluator treats them identically:
|
|
24
|
+
*
|
|
25
|
+
* {criteria: [{operator: 'ALWAYS_TRUE'}]} ← UI, API, MCP, `qfg create`
|
|
26
|
+
* {criteria: []} ← older `qfg set-default` writes
|
|
27
|
+
*
|
|
28
|
+
* Both are in production data and neither is being backfilled (Jeff,
|
|
29
|
+
* 2026-09-02), so every READER accepts both. Criteria are ANDed, so one
|
|
30
|
+
* conditional criterion makes the whole rule conditional.
|
|
31
|
+
*
|
|
32
|
+
* Fails CLOSED: anything unreadable as a rule (no `criteria` key, not an
|
|
33
|
+
* object) is NOT a catch-all — the same choice app-quonfig's
|
|
34
|
+
* `isCatchAllRuleUnknown` makes for raw parsed JSON. On the write path that
|
|
35
|
+
* means a malformed rule is left alone and a fresh fallback is appended,
|
|
36
|
+
* rather than a malformed rule being silently overwritten.
|
|
37
|
+
*/
|
|
38
|
+
export function isCatchAllRule(rule) {
|
|
39
|
+
if (typeof rule !== 'object' || rule === null || Array.isArray(rule))
|
|
40
|
+
return false;
|
|
41
|
+
const { criteria } = rule;
|
|
42
|
+
if (!Array.isArray(criteria))
|
|
43
|
+
return false;
|
|
44
|
+
// An empty list satisfies `every` vacuously — that IS the `criteria: []`
|
|
45
|
+
// spelling — so this one expression covers both shapes.
|
|
46
|
+
return criteria.every((criterion) => typeof criterion === 'object' &&
|
|
47
|
+
criterion !== null &&
|
|
48
|
+
criterion.operator === 'ALWAYS_TRUE');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* How many real TARGETING rules a scope holds.
|
|
52
|
+
*
|
|
53
|
+
* A targeted scope is conventionally `[…targeting rules, catch-all]`: the
|
|
54
|
+
* trailing unconditional rule is the scope's fallback, not a rule that
|
|
55
|
+
* targets anyone, so it is discounted. ONLY a trailing catch-all is — an
|
|
56
|
+
* unconditional rule anywhere earlier shadows every rule after it, which is
|
|
57
|
+
* a targeting decision in its own right and stays counted.
|
|
58
|
+
*/
|
|
59
|
+
export function countTargetingRules(rules) {
|
|
60
|
+
const last = rules.at(-1);
|
|
61
|
+
return last !== undefined && isCatchAllRule(last) ? rules.length - 1 : rules.length;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Set the scope's fallback value WITHOUT disturbing its targeting rules:
|
|
65
|
+
* the first catch-all's value is replaced in place (criteria untouched — an
|
|
66
|
+
* existing bare `criteria: []` fallback keeps its spelling, exactly as the
|
|
67
|
+
* server does), and if the scope has no catch-all at all a fresh
|
|
68
|
+
* ALWAYS_TRUE rule is appended at the end. Never mutates `rules`.
|
|
69
|
+
*/
|
|
70
|
+
export function upsertFallbackRule(rules, value) {
|
|
71
|
+
const index = rules.findIndex((rule) => isCatchAllRule(rule));
|
|
72
|
+
if (index === -1)
|
|
73
|
+
return [...rules, catchAllRule(value)];
|
|
74
|
+
const next = [...rules];
|
|
75
|
+
next[index] = { ...next[index], value };
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The rules a write should start from for one scope.
|
|
80
|
+
*
|
|
81
|
+
* `envName` undefined (or empty) means the document's `default` block, which
|
|
82
|
+
* always exists and is never seeded. A named environment with no block of
|
|
83
|
+
* its own inherits `default.rules` at evaluation time, so writing it starts
|
|
84
|
+
* from a DEEP CLONE of those rules — the same shape the UI produces when an
|
|
85
|
+
* environment stops inheriting. Starting from `[]` instead would silently
|
|
86
|
+
* drop the inherited targeting for that environment.
|
|
87
|
+
*
|
|
88
|
+
* `seeded` is true only when rules were actually copied, so callers do not
|
|
89
|
+
* report a copy that moved nothing.
|
|
90
|
+
*/
|
|
91
|
+
export function seedScopeRules(config, envName) {
|
|
92
|
+
const defaultRules = config.default?.rules ?? [];
|
|
93
|
+
if (!envName)
|
|
94
|
+
return { rules: defaultRules, seeded: false };
|
|
95
|
+
const env = (config.environments ?? []).find((e) => e.id === envName);
|
|
96
|
+
if (env)
|
|
97
|
+
return { rules: env.rules ?? [], seeded: false };
|
|
98
|
+
const cloned = structuredClone(defaultRules);
|
|
99
|
+
return { rules: cloned, seeded: cloned.length > 0 };
|
|
100
|
+
}
|
|
101
|
+
/** The environments array with `envName`'s rules replaced, appending the entry when it has none. */
|
|
102
|
+
export function upsertEnvRules(envs, envName, rules) {
|
|
103
|
+
const index = envs.findIndex((e) => e.id === envName);
|
|
104
|
+
if (index === -1)
|
|
105
|
+
return [...envs, { id: envName, rules }];
|
|
106
|
+
const next = [...envs];
|
|
107
|
+
next[index] = { ...next[index], rules };
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
// ── Reporting ────────────────────────────────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// One wording, used by every verb that writes a fallback, so `set-default`,
|
|
113
|
+
// `set-rollout` and the public API cannot drift apart in what they claim to
|
|
114
|
+
// have done.
|
|
115
|
+
/** '<env> had no rules of its own; copied them from default.' */
|
|
116
|
+
export function seededFromDefaultNote(envName) {
|
|
117
|
+
return `${envName} had no rules of its own; copied them from default.`;
|
|
118
|
+
}
|
|
119
|
+
/** What a surgical write kept. Empty when the scope held no targeting rules. */
|
|
120
|
+
export function keptTargetingNote(count) {
|
|
121
|
+
if (count === 0)
|
|
122
|
+
return '';
|
|
123
|
+
return (`Kept ${count} targeting rule(s); matched users still receive their targeted value. ` +
|
|
124
|
+
`To set for everyone, add --replace-targeting.`);
|
|
125
|
+
}
|
|
126
|
+
/** What a --replace-targeting write destroyed, and where to find it again. */
|
|
127
|
+
export function replacedTargetingNote(count, previousCommitSha) {
|
|
128
|
+
if (count === 0)
|
|
129
|
+
return `Previous version ${previousCommitSha}.`;
|
|
130
|
+
return `Replaced ${count} targeting rule(s); previous version ${previousCommitSha}.`;
|
|
131
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.0.
|
|
1
|
+
declare const _default: "0.0.73";
|
|
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.73';
|
package/oclif.manifest.json
CHANGED
|
@@ -2254,11 +2254,11 @@
|
|
|
2254
2254
|
"name": "name"
|
|
2255
2255
|
}
|
|
2256
2256
|
},
|
|
2257
|
-
"description": "Set the
|
|
2257
|
+
"description": "Set the fallback value for a flag or config in one environment.\n\nThe fallback is the unconditional rule at the end of the environment's rule\nlist: what users receive when no targeting rule matches. Targeting rules and\npercentage rollouts above it are kept, and the command tells you how many.\nIf the environment has no rules of its own yet, they are copied from the\nflag's default rules first, so inherited targeting is kept too.\n\nTo turn a flag on or off for EVERYONE, including users matched by targeting\nrules, add --replace-targeting. This deletes the environment's targeting\nrules (they stay in git history).\n\nTo set a percentage rollout (gradual rollout / A/B test / canary deploy) instead:\n qfg set-rollout my.flag --environment production --true-percent 20\n\nTo see all current values and rules for a flag:\n qfg info my.flag",
|
|
2258
2258
|
"examples": [
|
|
2259
2259
|
"<%= config.bin %> <%= command.id %> my.flag.name # prompts for value and env",
|
|
2260
2260
|
"<%= config.bin %> <%= command.id %> my.flag.name --value=true --environment=staging",
|
|
2261
|
-
"<%= config.bin %> <%= command.id %> my.flag.name --value=false --environment=production # kill-switch:
|
|
2261
|
+
"<%= config.bin %> <%= command.id %> my.flag.name --value=false --environment=production --replace-targeting # kill-switch: off for everyone",
|
|
2262
2262
|
"<%= config.bin %> <%= command.id %> my.flag.name --value=true --secret",
|
|
2263
2263
|
"<%= config.bin %> <%= command.id %> my.config.name --env-var=MY_ENV_VAR_NAME --environment=production",
|
|
2264
2264
|
"# For a percentage rollout, use set-rollout instead:",
|
|
@@ -2337,6 +2337,12 @@
|
|
|
2337
2337
|
"multiple": false,
|
|
2338
2338
|
"type": "option"
|
|
2339
2339
|
},
|
|
2340
|
+
"replace-targeting": {
|
|
2341
|
+
"description": "set the value for EVERYONE: delete this environment's targeting rules instead of keeping them (they stay in git history)",
|
|
2342
|
+
"name": "replace-targeting",
|
|
2343
|
+
"allowNo": false,
|
|
2344
|
+
"type": "boolean"
|
|
2345
|
+
},
|
|
2340
2346
|
"value": {
|
|
2341
2347
|
"description": "new default value",
|
|
2342
2348
|
"name": "value",
|
|
@@ -2388,12 +2394,13 @@
|
|
|
2388
2394
|
"name": "name"
|
|
2389
2395
|
}
|
|
2390
2396
|
},
|
|
2391
|
-
"description": "Configure a percentage rollout (gradual rollout / A/B test / canary deploy) for a flag.\n\nThe flag must already exist. Create it first if needed:\n qfg create my.flag --type boolean-flag\n\nWeights are specified as whole-number percentages (0–100). They must sum to 100.\n\nFor a boolean flag, use --true-percent to set the percentage of users that receive\ntrue (the remainder automatically receive false):\n qfg set-rollout my.flag --environment production --true-percent 20\n → 20% of users get true, 80% get false\n\nFor multi-value flags or custom splits, use --weights with a comma-separated list:\n qfg set-rollout my.flag --environment production --weights \"red:33,green:33,blue:34\"\n\nHashing: users are bucketed by the value of hashByPropertyName (default: \"user.key\").\nUse --hash-by to change which property drives the bucket assignment:\n qfg set-rollout my.flag --environment production --true-percent 10 --hash-by user.id\n\nFor scripted / agent use, add --confirm to skip the interactive confirmation prompt.\n\nTo combine a rollout with segment targeting (e.g. beta ON, holdout OFF, everyone else 50/50),\nyou need to edit the JSON config file directly:\n qfg config-schema # operator reference + worked multi-rule example\n qfg pull --dir ./config # clone workspace to edit\n\nTo revert to a single value for everyone, use:\n qfg set-default my.flag --environment production --value false\n\nTo inspect the current rollout:\n qfg info my.flag",
|
|
2397
|
+
"description": "Configure a percentage rollout (gradual rollout / A/B test / canary deploy) for a flag.\n\nThe flag must already exist. Create it first if needed:\n qfg create my.flag --type boolean-flag\n\nWeights are specified as whole-number percentages (0–100). They must sum to 100.\n\nFor a boolean flag, use --true-percent to set the percentage of users that receive\ntrue (the remainder automatically receive false):\n qfg set-rollout my.flag --environment production --true-percent 20\n → 20% of users get true, 80% get false\n\nFor multi-value flags or custom splits, use --weights with a comma-separated list:\n qfg set-rollout my.flag --environment production --weights \"red:33,green:33,blue:34\"\n\nHashing: users are bucketed by the value of hashByPropertyName (default: \"user.key\").\nUse --hash-by to change which property drives the bucket assignment:\n qfg set-rollout my.flag --environment production --true-percent 10 --hash-by user.id\n\nThe rollout becomes the environment's fallback: the unconditional rule at the end\nof its rule list, what users receive when no targeting rule matches. Targeting\nrules above it are kept, and the command tells you how many. If the environment\nhas no rules of its own yet, they are copied from the flag's default rules first,\nso inherited targeting is kept too.\n\nTo roll out to EVERYONE, including users matched by targeting rules, add\n--replace-targeting. This deletes the environment's targeting rules (they stay in\ngit history).\n\nFor scripted / agent use, add --confirm to skip the interactive confirmation prompt.\n\nTo combine a rollout with segment targeting (e.g. beta ON, holdout OFF, everyone else 50/50),\nyou need to edit the JSON config file directly:\n qfg config-schema # operator reference + worked multi-rule example\n qfg pull --dir ./config # clone workspace to edit\n\nTo revert to a single value for everyone, use:\n qfg set-default my.flag --environment production --value false --replace-targeting\n\nTo inspect the current rollout:\n qfg info my.flag",
|
|
2392
2398
|
"examples": [
|
|
2393
2399
|
"<%= config.bin %> <%= command.id %> my.feature.flag --environment production --true-percent 20",
|
|
2394
2400
|
"<%= config.bin %> <%= command.id %> my.feature.flag --environment staging --true-percent 50",
|
|
2395
2401
|
"<%= config.bin %> <%= command.id %> my.feature.flag --environment production --true-percent 100 # full rollout",
|
|
2396
|
-
"
|
|
2402
|
+
"# For a kill switch — false for EVERYONE, including targeted users — use set-default:",
|
|
2403
|
+
"<%= config.bin %> set-default my.feature.flag --value=false --environment=production --replace-targeting",
|
|
2397
2404
|
"<%= config.bin %> <%= command.id %> my.variant.flag --environment production --weights \"control:50,treatment:50\"",
|
|
2398
2405
|
"<%= config.bin %> <%= command.id %> my.variant.flag --environment production --weights \"a:33,b:33,c:34\" --hash-by user.id"
|
|
2399
2406
|
],
|
|
@@ -2470,6 +2477,12 @@
|
|
|
2470
2477
|
"multiple": false,
|
|
2471
2478
|
"type": "option"
|
|
2472
2479
|
},
|
|
2480
|
+
"replace-targeting": {
|
|
2481
|
+
"description": "roll out to EVERYONE: delete this environment's targeting rules instead of keeping them (they stay in git history)",
|
|
2482
|
+
"name": "replace-targeting",
|
|
2483
|
+
"allowNo": false,
|
|
2484
|
+
"type": "boolean"
|
|
2485
|
+
},
|
|
2473
2486
|
"weights": {
|
|
2474
2487
|
"description": "comma-separated \"value:percent\" pairs that must sum to 100 (e.g. \"true:20,false:80\" or \"red:33,green:33,blue:34\")",
|
|
2475
2488
|
"name": "weights",
|
|
@@ -4136,5 +4149,5 @@
|
|
|
4136
4149
|
]
|
|
4137
4150
|
}
|
|
4138
4151
|
},
|
|
4139
|
-
"version": "0.0.
|
|
4152
|
+
"version": "0.0.73"
|
|
4140
4153
|
}
|
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.73",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/quonfig/cli/issues"
|
|
7
7
|
},
|