@quonfig/cli 0.5.0 → 0.6.0
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 +125 -55
- package/dist/commands/environments.d.ts +8 -0
- package/dist/commands/environments.js +32 -0
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.js +59 -31
- package/dist/commands/set-default.js +28 -17
- package/dist/commands/set-rollout.js +2 -2
- package/dist/ui/get-confirmation.d.ts +10 -1
- package/dist/ui/get-confirmation.js +13 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +261 -184
- package/package.json +1 -1
package/dist/commands/info.js
CHANGED
|
@@ -279,14 +279,17 @@ Related commands:
|
|
|
279
279
|
formatValue(value, includeQuotes = true) {
|
|
280
280
|
if (!value)
|
|
281
281
|
return 'null';
|
|
282
|
-
// Handle
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
// Handle provided values (ENV_VAR). The stored value is the env var NAME,
|
|
283
|
+
// not a secret, so it is shown even when the value is confidential.
|
|
284
|
+
// Shape: {type: 'provided', value: {source, lookup}} (qfg-r3a47).
|
|
285
|
+
const provided = (value.type === 'provided' ? value.value : value.provided);
|
|
286
|
+
if (provided && typeof provided === 'object') {
|
|
287
|
+
return `\`${provided.lookup}\` via ENV`;
|
|
288
|
+
}
|
|
289
|
+
// Secrets and confidential values show the redacted fingerprint (qfg-r3a47).
|
|
290
|
+
const redacted = this.redactedValue(value);
|
|
291
|
+
if (redacted)
|
|
292
|
+
return redacted;
|
|
290
293
|
// Handle weighted values (A/B testing)
|
|
291
294
|
if (Array.isArray(value.weightedValues)) {
|
|
292
295
|
const weights = value.weightedValues
|
|
@@ -298,16 +301,6 @@ Related commands:
|
|
|
298
301
|
});
|
|
299
302
|
return weights.join(', ');
|
|
300
303
|
}
|
|
301
|
-
// Handle provided values (ENV_VAR)
|
|
302
|
-
if (value.provided) {
|
|
303
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
304
|
-
let str = `${value.provided.lookup}`;
|
|
305
|
-
if (value.confidential) {
|
|
306
|
-
str = `\`${str}\``;
|
|
307
|
-
}
|
|
308
|
-
str += ' via ENV';
|
|
309
|
-
return str;
|
|
310
|
-
}
|
|
311
304
|
// Handle simple values
|
|
312
305
|
// For config display, extract raw value without quotes
|
|
313
306
|
if (!includeQuotes) {
|
|
@@ -337,23 +330,23 @@ Related commands:
|
|
|
337
330
|
}
|
|
338
331
|
return this.extractSimpleValue(value);
|
|
339
332
|
}
|
|
340
|
-
//
|
|
341
|
-
//
|
|
333
|
+
// The redacted display for an encrypted or confidential value, or undefined
|
|
334
|
+
// for a value that may be shown. An encrypted value hashes its ciphertext,
|
|
335
|
+
// a confidential one its plaintext, matching the SDK (src/util/redact.ts).
|
|
342
336
|
// shared predicate, qfg-9kr8.
|
|
343
337
|
hasNonTrivialCriteria(rule) {
|
|
344
338
|
return !isCatchAllRule(rule);
|
|
345
339
|
}
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// ...}, or anything else where `value.value` is itself an object) cannot —
|
|
349
|
-
// String(obj) yields "[object Object]" (qfg-5j9i), and a percent split is
|
|
350
|
-
// never a one-token override anyway.
|
|
340
|
+
// Anything that is not a catch-all targets someone. Both live catch-all
|
|
341
|
+
// spellings (`criteria: []` and all-ALWAYS_TRUE) are unconditional — one
|
|
351
342
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
352
343
|
isSimpleScalarRuleValue(value) {
|
|
353
344
|
if (!value || typeof value !== 'object')
|
|
354
345
|
return false;
|
|
355
346
|
if (value.type === 'weighted_values' || Array.isArray(value.weightedValues))
|
|
356
347
|
return false;
|
|
348
|
+
if (value.type === 'provided' || value.provided)
|
|
349
|
+
return true;
|
|
357
350
|
// Old format: {type: 'string'|'bool'|'int'|'double'|'stringList'|'json', value: X}
|
|
358
351
|
if (typeof value.type === 'string' && value.value !== undefined) {
|
|
359
352
|
// A `json` payload may legitimately be an object — render it as JSON, not [object Object].
|
|
@@ -369,6 +362,11 @@ Related commands:
|
|
|
369
362
|
value.stringList !== undefined ||
|
|
370
363
|
value.json !== undefined);
|
|
371
364
|
}
|
|
365
|
+
// A rule value is a "simple scalar" if it carries a literal value that can be
|
|
366
|
+
// rendered inline next to "[override]". Rollouts ({type: 'weighted_values',
|
|
367
|
+
// ...}, or anything else where `value.value` is itself an object) cannot —
|
|
368
|
+
// String(obj) yields "[object Object]" (qfg-5j9i), and a percent split is
|
|
369
|
+
// never a one-token override anyway.
|
|
372
370
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
373
371
|
parseConfig(config, environments, url) {
|
|
374
372
|
const contents = [];
|
|
@@ -419,7 +417,11 @@ Related commands:
|
|
|
419
417
|
const overrideRule = envConfig.rules.find((r) => this.hasNonTrivialCriteria(r));
|
|
420
418
|
if (overrideRule && this.isSimpleScalarRuleValue(overrideRule.value)) {
|
|
421
419
|
const overrideValue = this.formatValue(overrideRule.value, false);
|
|
422
|
-
|
|
420
|
+
// A redacted display or `VAR` via ENV is already delimited.
|
|
421
|
+
displayValue =
|
|
422
|
+
this.redactedValue(overrideRule.value) || overrideValue.startsWith('`')
|
|
423
|
+
? `[override] ${overrideValue}`
|
|
424
|
+
: `[override] \`${overrideValue}\``;
|
|
423
425
|
}
|
|
424
426
|
else {
|
|
425
427
|
// Rollouts / weighted_values / anything we can't render inline → defer to the URL (qfg-5j9i).
|
|
@@ -427,12 +429,21 @@ Related commands:
|
|
|
427
429
|
}
|
|
428
430
|
}
|
|
429
431
|
else {
|
|
430
|
-
// Only has unconditional rules - show [see rules] to indicate env has
|
|
431
|
-
|
|
432
|
+
// Only has unconditional rules - show [see rules] to indicate env has
|
|
433
|
+
// custom config. A secret / provided fallback shows its redacted
|
|
434
|
+
// fingerprint or env var instead, so a rotation can be confirmed per
|
|
435
|
+
// environment without revealing it (qfg-r3a47).
|
|
436
|
+
const fallbackRule = envConfig.rules.find((r) => isCatchAllRule(r));
|
|
437
|
+
const fallbackValue = fallbackRule?.value;
|
|
438
|
+
displayValue =
|
|
439
|
+
fallbackValue && (this.redactedValue(fallbackValue) || fallbackValue.type === 'provided')
|
|
440
|
+
? this.formatValue(fallbackValue, false)
|
|
441
|
+
: '[see rules]';
|
|
432
442
|
}
|
|
433
443
|
}
|
|
434
444
|
contents.push(`- ${envConfig.name}: ${displayValue}`);
|
|
435
|
-
|
|
445
|
+
// [Default] has no environment id; its URL is the flag page itself (qfg-r3a47).
|
|
446
|
+
const envUrl = envConfig.id ? `${url}?environment=${envConfig.id}` : url;
|
|
436
447
|
json[envConfig.name] = {
|
|
437
448
|
url: envUrl,
|
|
438
449
|
};
|
|
@@ -462,8 +473,11 @@ Related commands:
|
|
|
462
473
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
463
474
|
const overrideRule = envConfig.rules.find((r) => this.hasNonTrivialCriteria(r));
|
|
464
475
|
if (overrideRule) {
|
|
465
|
-
// Extract just the value without formatting
|
|
466
|
-
|
|
476
|
+
// Extract just the value without formatting; a confidential
|
|
477
|
+
// override is redacted, never the plaintext (qfg-r3a47).
|
|
478
|
+
const overrideValue = overrideRule.value?.confidential && !overrideRule.value?.decryptWith
|
|
479
|
+
? this.redactedValue(overrideRule.value)
|
|
480
|
+
: overrideRule.value.value;
|
|
467
481
|
json[envConfig.name].override = overrideValue;
|
|
468
482
|
}
|
|
469
483
|
}
|
|
@@ -473,6 +487,20 @@ Related commands:
|
|
|
473
487
|
this.log(contents.join('\n').trim());
|
|
474
488
|
return json;
|
|
475
489
|
}
|
|
490
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
491
|
+
redactedValue(value) {
|
|
492
|
+
if (!value || typeof value !== 'object')
|
|
493
|
+
return undefined;
|
|
494
|
+
if (value.type === 'provided' || value.provided)
|
|
495
|
+
return undefined;
|
|
496
|
+
const raw = value.value ?? value.string;
|
|
497
|
+
const text = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
498
|
+
if (value.decryptWith)
|
|
499
|
+
return redactedDisplay(redactedSummary({ ciphertext: text }));
|
|
500
|
+
if (value.confidential)
|
|
501
|
+
return redactedDisplay(redactedSummary({ plaintext: text }));
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
476
504
|
transformEvaluationStatsForJson(statsPerEnv, environments, startTime, endTime) {
|
|
477
505
|
const envResults = [];
|
|
478
506
|
let grandTotal = 0;
|
|
@@ -65,7 +65,10 @@ To target specific organizations or users, write rules on their KEY
|
|
|
65
65
|
'<%= config.bin %> pull && <%= config.bin %> config-schema',
|
|
66
66
|
];
|
|
67
67
|
static flags = {
|
|
68
|
-
confidential: Flags.boolean({
|
|
68
|
+
confidential: Flags.boolean({
|
|
69
|
+
allowNo: true,
|
|
70
|
+
description: 'mark the value as confidential. Auto-detected when the key already holds a confidential value; pass --no-confidential to store it in the clear',
|
|
71
|
+
}),
|
|
69
72
|
'env-var': Flags.string({
|
|
70
73
|
description: 'store no value: the SDK reads it from this env var at RUNTIME. To read a value from a local env var NOW, use --value-from-env',
|
|
71
74
|
}),
|
|
@@ -95,7 +98,7 @@ To target specific organizations or users, write rules on their KEY
|
|
|
95
98
|
if (!valueSource.ok) {
|
|
96
99
|
return this.err(valueSource.error ? valueSource.message : 'Value is required');
|
|
97
100
|
}
|
|
98
|
-
if (flags.confidential && secret.selected) {
|
|
101
|
+
if (flags.confidential === true && secret.selected) {
|
|
99
102
|
console.warn("Note: --confidential is implied when using --secret, so you don't need to specify both.");
|
|
100
103
|
}
|
|
101
104
|
// Validate required arguments before making API calls
|
|
@@ -152,18 +155,27 @@ To target specific organizations or users, write rules on their KEY
|
|
|
152
155
|
}
|
|
153
156
|
const currentConfig = detailRequest.json;
|
|
154
157
|
this.verboseLog('Config details:', currentConfig);
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
// Auto-detect from EVERY stored rule value, default and per-environment
|
|
159
|
+
// (qfg-r3a47): an update to an encrypted or confidential key must not
|
|
160
|
+
// silently store the new value in the clear.
|
|
161
|
+
const storedValues = [
|
|
162
|
+
...(currentConfig.default?.rules ?? []),
|
|
163
|
+
...(currentConfig.environments ?? []).flatMap((env) => env.rules ?? []),
|
|
164
|
+
].map((rule) => rule.value);
|
|
165
|
+
if (!secret.selected && storedValues.some((value) => value?.decryptWith)) {
|
|
166
|
+
this.verboseLog('Auto-detected encryption from existing config');
|
|
167
|
+
secret = {
|
|
168
|
+
keyName: secret.keyName,
|
|
169
|
+
selected: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
let confidential = flags.confidential === true;
|
|
173
|
+
if (flags.confidential === undefined &&
|
|
174
|
+
!secret.selected &&
|
|
175
|
+
storedValues.some((value) => value?.confidential && value.type !== 'provided')) {
|
|
176
|
+
confidential = true;
|
|
177
|
+
this.logToStderr(`Note: ${key} already holds a confidential value, so the new value is stored confidential too. ` +
|
|
178
|
+
'Pass --no-confidential to store it in the clear.');
|
|
167
179
|
}
|
|
168
180
|
// Get the environment
|
|
169
181
|
const environment = await getEnvironment({
|
|
@@ -190,9 +202,8 @@ To target specific organizations or users, write rules on their KEY
|
|
|
190
202
|
const keptPhrase = targetingRuleCount > 0 ? ` (${targetingRuleCount} targeting rule${targetingRuleCount === 1 ? '' : 's'} kept)` : '';
|
|
191
203
|
const deletePhrase = targetingRuleCount > 0 ? `, deleting ${targetingRuleCount} targeting rule(s)` : '';
|
|
192
204
|
const confirmMessage = (valuePhrase) => replaceTargeting
|
|
193
|
-
? `Confirm: set ${key} in ${environment.name} to ${valuePhrase} for EVERYONE${deletePhrase}
|
|
194
|
-
: `Confirm: set the fallback for ${key} in ${environment.name} to ${valuePhrase}?${keptPhrase}
|
|
195
|
-
const { confidential } = flags;
|
|
205
|
+
? `Confirm: set ${key} in ${environment.name} to ${valuePhrase} for EVERYONE${deletePhrase}?`
|
|
206
|
+
: `Confirm: set the fallback for ${key} in ${environment.name} to ${valuePhrase}?${keptPhrase}`;
|
|
196
207
|
// Get the value
|
|
197
208
|
if (flags['env-var']) {
|
|
198
209
|
if (!(await getConfirmation({ flags, message: confirmMessage(`be provided by \`${flags['env-var']}\``) }))) {
|
|
@@ -182,8 +182,8 @@ To inspect the current rollout:
|
|
|
182
182
|
const keptPhrase = targetingRuleCount > 0 ? ` (${targetingRuleCount} targeting rule${targetingRuleCount === 1 ? '' : 's'} kept)` : '';
|
|
183
183
|
const deletePhrase = targetingRuleCount > 0 ? `, deleting ${targetingRuleCount} targeting rule(s)` : '';
|
|
184
184
|
const message = replaceTargeting
|
|
185
|
-
? `Confirm: set rollout for ${key} in ${environment.name} to [${rolloutDescription}] for EVERYONE${deletePhrase}
|
|
186
|
-
: `Confirm: set rollout for ${key} in ${environment.name} to [${rolloutDescription}]?${keptPhrase}
|
|
185
|
+
? `Confirm: set rollout for ${key} in ${environment.name} to [${rolloutDescription}] for EVERYONE${deletePhrase}?`
|
|
186
|
+
: `Confirm: set rollout for ${key} in ${environment.name} to [${rolloutDescription}]?${keptPhrase}`;
|
|
187
187
|
if (!(await getConfirmation({ flags, message }))) {
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import type { PromptContext } from './get-string.js';
|
|
1
2
|
export declare const confirmFlag: {
|
|
2
3
|
confirm: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
3
4
|
};
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Ask a yes/no question; Enter alone answers no.
|
|
7
|
+
*
|
|
8
|
+
* Uses @inquirer/prompts' `confirm`, which reads in raw mode. oclif's
|
|
9
|
+
* `ux.confirm` read a cooked line, so under a pty the terminal echoed a stray
|
|
10
|
+
* `^D` before the success line (qfg-r3a47).
|
|
11
|
+
*/
|
|
12
|
+
declare const getConfirmation: ({ context, flags, message, }: {
|
|
13
|
+
context?: PromptContext;
|
|
5
14
|
flags: {
|
|
6
15
|
confirm?: boolean;
|
|
7
16
|
interactive?: boolean;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
2
3
|
import isInteractive from '../util/is-interactive.js';
|
|
3
4
|
export const confirmFlag = { confirm: Flags.boolean({ description: 'confirm without prompt' }) };
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Ask a yes/no question; Enter alone answers no.
|
|
7
|
+
*
|
|
8
|
+
* Uses @inquirer/prompts' `confirm`, which reads in raw mode. oclif's
|
|
9
|
+
* `ux.confirm` read a cooked line, so under a pty the terminal echoed a stray
|
|
10
|
+
* `^D` before the success line (qfg-r3a47).
|
|
11
|
+
*/
|
|
12
|
+
const getConfirmation = async ({ context, flags, message, }) => {
|
|
5
13
|
if (flags.confirm)
|
|
6
14
|
return true;
|
|
7
|
-
|
|
15
|
+
// A caller-supplied context (tests) brings its own streams instead of the TTY.
|
|
16
|
+
if (!context && !isInteractive(flags)) {
|
|
8
17
|
throw new Error('Cannot prompt for confirmation in a non-interactive context. Pass --confirm to skip the confirmation prompt.');
|
|
9
18
|
}
|
|
10
|
-
return
|
|
19
|
+
return confirm({ default: false, message }, context);
|
|
11
20
|
};
|
|
12
21
|
export default getConfirmation;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.
|
|
1
|
+
declare const _default: "0.6.0";
|
|
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.
|
|
2
|
+
export default '0.6.0';
|