@quonfig/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +707 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +9 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +8 -0
- package/dist/api/get-environments.d.ts +10 -0
- package/dist/api/get-environments.js +11 -0
- package/dist/codegen/code-generators/base-generator.d.ts +11 -0
- package/dist/codegen/code-generators/base-generator.js +8 -0
- package/dist/codegen/code-generators/base-typescript-generator.d.ts +17 -0
- package/dist/codegen/code-generators/base-typescript-generator.js +35 -0
- package/dist/codegen/code-generators/node-typescript-generator.d.ts +8 -0
- package/dist/codegen/code-generators/node-typescript-generator.js +96 -0
- package/dist/codegen/code-generators/react-typescript-generator.d.ts +11 -0
- package/dist/codegen/code-generators/react-typescript-generator.js +116 -0
- package/dist/codegen/config-downloader.d.ts +7 -0
- package/dist/codegen/config-downloader.js +26 -0
- package/dist/codegen/json-schema-to-zod.d.ts +3 -0
- package/dist/codegen/json-schema-to-zod.js +170 -0
- package/dist/codegen/language-mappers/json-to-zod-mapper.d.ts +4 -0
- package/dist/codegen/language-mappers/json-to-zod-mapper.js +55 -0
- package/dist/codegen/language-mappers/zod-base-mapper.d.ts +35 -0
- package/dist/codegen/language-mappers/zod-base-mapper.js +111 -0
- package/dist/codegen/language-mappers/zod-to-string-mapper.d.ts +25 -0
- package/dist/codegen/language-mappers/zod-to-string-mapper.js +87 -0
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.d.ts +35 -0
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +133 -0
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.d.ts +36 -0
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.js +150 -0
- package/dist/codegen/mustache-extractor.d.ts +6 -0
- package/dist/codegen/mustache-extractor.js +83 -0
- package/dist/codegen/schema-evaluator.d.ts +37 -0
- package/dist/codegen/schema-evaluator.js +283 -0
- package/dist/codegen/schema-extractor.d.ts +24 -0
- package/dist/codegen/schema-extractor.js +265 -0
- package/dist/codegen/types.d.ts +44 -0
- package/dist/codegen/types.js +5 -0
- package/dist/codegen/zod-introspection.d.ts +85 -0
- package/dist/codegen/zod-introspection.js +205 -0
- package/dist/commands/create.d.ts +19 -0
- package/dist/commands/create.js +175 -0
- package/dist/commands/download.d.ts +13 -0
- package/dist/commands/download.js +63 -0
- package/dist/commands/generate-new-hex-key.d.ts +7 -0
- package/dist/commands/generate-new-hex-key.js +10 -0
- package/dist/commands/generate.d.ts +16 -0
- package/dist/commands/generate.js +220 -0
- package/dist/commands/get.d.ts +13 -0
- package/dist/commands/get.js +141 -0
- package/dist/commands/info.d.ts +21 -0
- package/dist/commands/info.js +464 -0
- package/dist/commands/interactive.d.ts +5 -0
- package/dist/commands/interactive.js +8 -0
- package/dist/commands/list.d.ts +13 -0
- package/dist/commands/list.js +61 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/logout.d.ts +7 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +145 -0
- package/dist/commands/override.d.ts +17 -0
- package/dist/commands/override.js +170 -0
- package/dist/commands/profile.d.ts +7 -0
- package/dist/commands/profile.js +47 -0
- package/dist/commands/schema.d.ts +21 -0
- package/dist/commands/schema.js +168 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +99 -0
- package/dist/commands/set-default.d.ts +20 -0
- package/dist/commands/set-default.js +273 -0
- package/dist/commands/verify.d.ts +12 -0
- package/dist/commands/verify.js +48 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/commands/workspace.d.ts +7 -0
- package/dist/commands/workspace.js +38 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +10 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +112 -0
- package/dist/interactive-prompt.d.ts +2 -0
- package/dist/interactive-prompt.js +162 -0
- package/dist/quonfig.d.ts +15 -0
- package/dist/quonfig.js +75 -0
- package/dist/result.d.ts +35 -0
- package/dist/result.js +9 -0
- package/dist/services/mcp.d.ts +15 -0
- package/dist/services/mcp.js +59 -0
- package/dist/ui/get-confirmation.d.ts +10 -0
- package/dist/ui/get-confirmation.js +4 -0
- package/dist/ui/get-environment.d.ts +11 -0
- package/dist/ui/get-environment.js +42 -0
- package/dist/ui/get-key.d.ts +27 -0
- package/dist/ui/get-key.js +15 -0
- package/dist/ui/get-string.d.ts +5 -0
- package/dist/ui/get-string.js +8 -0
- package/dist/ui/get-value.d.ts +18 -0
- package/dist/ui/get-value.js +46 -0
- package/dist/util/autocomplete.d.ts +6 -0
- package/dist/util/autocomplete.js +25 -0
- package/dist/util/coerce.d.ts +7 -0
- package/dist/util/coerce.js +62 -0
- package/dist/util/color.d.ts +3 -0
- package/dist/util/color.js +4 -0
- package/dist/util/config-value-dto.d.ts +16 -0
- package/dist/util/config-value-dto.js +82 -0
- package/dist/util/context.d.ts +18 -0
- package/dist/util/context.js +10 -0
- package/dist/util/domain-urls.d.ts +3 -0
- package/dist/util/domain-urls.js +18 -0
- package/dist/util/encryption.d.ts +20 -0
- package/dist/util/encryption.js +153 -0
- package/dist/util/file-manager.d.ts +12 -0
- package/dist/util/file-manager.js +16 -0
- package/dist/util/get-client.d.ts +7 -0
- package/dist/util/get-client.js +127 -0
- package/dist/util/is-interactive.d.ts +5 -0
- package/dist/util/is-interactive.js +7 -0
- package/dist/util/json-maybe.d.ts +3 -0
- package/dist/util/json-maybe.js +14 -0
- package/dist/util/name-arg.d.ts +4 -0
- package/dist/util/name-arg.js +3 -0
- package/dist/util/oauth-client.d.ts +30 -0
- package/dist/util/oauth-client.js +99 -0
- package/dist/util/secret-flags.d.ts +13 -0
- package/dist/util/secret-flags.js +13 -0
- package/dist/util/token-storage.d.ts +25 -0
- package/dist/util/token-storage.js +90 -0
- package/dist/validations/value.d.ts +4 -0
- package/dist/validations/value.js +11 -0
- package/dist/verify/standalone.d.ts +14 -0
- package/dist/verify/standalone.js +133 -0
- package/dist/verify/validate.d.ts +43 -0
- package/dist/verify/validate.js +756 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/oclif.manifest.json +1429 -0
- package/package.json +104 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { getEnvironments } from '../api/get-environments.js';
|
|
3
|
+
import { APICommand } from '../index.js';
|
|
4
|
+
import autocomplete from '../util/autocomplete.js';
|
|
5
|
+
import { getAppUrl } from '../util/domain-urls.js';
|
|
6
|
+
import isInteractive from '../util/is-interactive.js';
|
|
7
|
+
import nameArg from '../util/name-arg.js';
|
|
8
|
+
export default class Info extends APICommand {
|
|
9
|
+
static args = { ...nameArg };
|
|
10
|
+
static description = 'Show details about the provided config/feature-flag/etc.';
|
|
11
|
+
static examples = ['<%= config.bin %> <%= command.id %> my.config.name'];
|
|
12
|
+
static flags = {
|
|
13
|
+
'exclude-evaluations': Flags.boolean({ default: false, description: 'Exclude evaluation data' }),
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { args, flags } = await this.parse(Info);
|
|
17
|
+
// Fetch all configs from metadata endpoint
|
|
18
|
+
const metadataRequest = await this.apiClient.post('/api/v1/metadata/list', { workspaceId: this.workspaceId });
|
|
19
|
+
if (!metadataRequest.ok) {
|
|
20
|
+
const errorMsg = metadataRequest.error?.error || `Failed to fetch configs: ${metadataRequest.status}`;
|
|
21
|
+
return this.err(errorMsg, { serverError: metadataRequest.error });
|
|
22
|
+
}
|
|
23
|
+
const metadataResponse = metadataRequest.json;
|
|
24
|
+
const configs = metadataResponse.configs;
|
|
25
|
+
// Get the key - from args or prompt
|
|
26
|
+
let key = args.name;
|
|
27
|
+
if (!key && isInteractive(flags)) {
|
|
28
|
+
const configKeys = configs.map((c) => c.key);
|
|
29
|
+
const selectedKey = await autocomplete({
|
|
30
|
+
message: 'Which item would you like to see?',
|
|
31
|
+
source: configKeys,
|
|
32
|
+
});
|
|
33
|
+
if (selectedKey) {
|
|
34
|
+
key = selectedKey;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!key) {
|
|
38
|
+
return this.err('Key is required');
|
|
39
|
+
}
|
|
40
|
+
// Fetch full config details
|
|
41
|
+
const configRequest = await this.apiClient.post('/api/v1/metadata/getByKey', { workspaceId: this.workspaceId, key });
|
|
42
|
+
if (!configRequest.ok) {
|
|
43
|
+
let errorMsg = configRequest.error?.error || `Failed to fetch config: ${configRequest.status}`;
|
|
44
|
+
// Customize "Not found" to include the key name
|
|
45
|
+
if (errorMsg === 'Not found' || configRequest.status === 404) {
|
|
46
|
+
errorMsg = `Key ${key} not found`;
|
|
47
|
+
}
|
|
48
|
+
return this.err(errorMsg, { error: errorMsg });
|
|
49
|
+
}
|
|
50
|
+
const fullConfig = configRequest.json;
|
|
51
|
+
this.verboseLog('Full config:', fullConfig);
|
|
52
|
+
// Get environments
|
|
53
|
+
const environments = await getEnvironments(this);
|
|
54
|
+
const appUrl = getAppUrl();
|
|
55
|
+
// Use new URL format: /workspaces/{workspaceId}/flags/{key}
|
|
56
|
+
if (!this.workspaceId) {
|
|
57
|
+
return this.err('Workspace ID not found. Please run `qfg login`.');
|
|
58
|
+
}
|
|
59
|
+
const url = `${appUrl}/workspaces/${this.workspaceId}/flags/${key}`;
|
|
60
|
+
this.log(url);
|
|
61
|
+
this.log('');
|
|
62
|
+
const json = { url };
|
|
63
|
+
json.values = this.parseConfig(fullConfig, environments, url);
|
|
64
|
+
this.log('');
|
|
65
|
+
// Fetch evaluation stats if needed
|
|
66
|
+
if (!flags['exclude-evaluations']) {
|
|
67
|
+
const evalStats = await this.fetchEvaluationStats(key, flags, environments);
|
|
68
|
+
if (evalStats) {
|
|
69
|
+
json.evaluations = evalStats;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { [key]: json };
|
|
73
|
+
}
|
|
74
|
+
displayEvaluationStats(statsPerEnv, environments) {
|
|
75
|
+
this.log('Evaluations over the last 24 hours:\n');
|
|
76
|
+
// Collect all environment stats first
|
|
77
|
+
const envStatsArray = [];
|
|
78
|
+
for (const env of environments) {
|
|
79
|
+
const envStats = statsPerEnv[env.name];
|
|
80
|
+
if (!envStats || !Array.isArray(envStats.intervals))
|
|
81
|
+
continue;
|
|
82
|
+
let totalEvaluations = 0;
|
|
83
|
+
const valueBreakdown = new Map();
|
|
84
|
+
// Aggregate data across all intervals for this environment
|
|
85
|
+
for (const interval of envStats.intervals) {
|
|
86
|
+
if (interval.data) {
|
|
87
|
+
for (const dataPoint of interval.data) {
|
|
88
|
+
totalEvaluations += dataPoint.count || 0;
|
|
89
|
+
let valueKey = 'unknown';
|
|
90
|
+
if (dataPoint.selectedValue) {
|
|
91
|
+
valueKey = this.extractSimpleValue(dataPoint.selectedValue);
|
|
92
|
+
}
|
|
93
|
+
valueBreakdown.set(valueKey, (valueBreakdown.get(valueKey) || 0) + (dataPoint.count || 0));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (totalEvaluations > 0) {
|
|
98
|
+
envStatsArray.push({
|
|
99
|
+
name: env.name,
|
|
100
|
+
total: totalEvaluations,
|
|
101
|
+
valueBreakdown,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Sort by total count descending
|
|
106
|
+
envStatsArray.sort((a, b) => b.total - a.total);
|
|
107
|
+
// Deduplicate by environment name (keep the one with more evaluations)
|
|
108
|
+
const seenNames = new Set();
|
|
109
|
+
const uniqueStats = envStatsArray.filter((envStat) => {
|
|
110
|
+
if (seenNames.has(envStat.name)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
seenNames.add(envStat.name);
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
// Display sorted stats
|
|
117
|
+
const contents = [];
|
|
118
|
+
for (const envStat of uniqueStats) {
|
|
119
|
+
contents.push(`${envStat.name}: ${envStat.total.toLocaleString()}`);
|
|
120
|
+
// Keep values in the order they were encountered (insertion order in Map)
|
|
121
|
+
for (const [value, count] of envStat.valueBreakdown.entries()) {
|
|
122
|
+
const percentage = Math.round((count / envStat.total) * 100);
|
|
123
|
+
contents.push(`- ${percentage}% - ${value}`);
|
|
124
|
+
}
|
|
125
|
+
contents.push('');
|
|
126
|
+
}
|
|
127
|
+
if (contents.length > 0) {
|
|
128
|
+
this.log(contents.join('\n').trim());
|
|
129
|
+
this.log('');
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.log('No evaluations found for the past 24 hours');
|
|
133
|
+
this.log('');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
extractSimpleValue(value) {
|
|
137
|
+
if (!value)
|
|
138
|
+
return 'null';
|
|
139
|
+
// Handle old format: {type: 'bool', value: false}
|
|
140
|
+
if (value.type && value.value !== undefined) {
|
|
141
|
+
// Format the value based on type
|
|
142
|
+
if (value.type === 'string') {
|
|
143
|
+
return `"${value.value}"`;
|
|
144
|
+
}
|
|
145
|
+
if (value.type === 'json') {
|
|
146
|
+
return JSON.stringify(value.value);
|
|
147
|
+
}
|
|
148
|
+
if (value.type === 'stringList') {
|
|
149
|
+
// Format as comma-separated values
|
|
150
|
+
if (Array.isArray(value.value)) {
|
|
151
|
+
return value.value.join(',');
|
|
152
|
+
}
|
|
153
|
+
return JSON.stringify(value.value);
|
|
154
|
+
}
|
|
155
|
+
return String(value.value);
|
|
156
|
+
}
|
|
157
|
+
// Handle new format: {bool: false}, {string: "test"}, etc.
|
|
158
|
+
if (value.bool !== undefined) {
|
|
159
|
+
return String(value.bool);
|
|
160
|
+
}
|
|
161
|
+
if (value.string !== undefined) {
|
|
162
|
+
return `"${value.string}"`;
|
|
163
|
+
}
|
|
164
|
+
if (value.int !== undefined || value.double !== undefined) {
|
|
165
|
+
return String(value.int || value.double);
|
|
166
|
+
}
|
|
167
|
+
if (value.stringList !== undefined) {
|
|
168
|
+
if (Array.isArray(value.stringList)) {
|
|
169
|
+
return value.stringList.join(',');
|
|
170
|
+
}
|
|
171
|
+
return JSON.stringify(value.stringList);
|
|
172
|
+
}
|
|
173
|
+
if (value.json !== undefined) {
|
|
174
|
+
return JSON.stringify(value.json);
|
|
175
|
+
}
|
|
176
|
+
return JSON.stringify(value);
|
|
177
|
+
}
|
|
178
|
+
async fetchEvaluationStats(key, flags, environments) {
|
|
179
|
+
// Fetch stats for all environments (similar to original implementation)
|
|
180
|
+
const endTime = Date.now();
|
|
181
|
+
const startTime = endTime - 24 * 60 * 60 * 1000; // 24 hours ago
|
|
182
|
+
const timeInterval = 'HOURLY';
|
|
183
|
+
const statsPerEnv = {};
|
|
184
|
+
// Fetch stats for each environment via oRPC
|
|
185
|
+
for (const env of environments) {
|
|
186
|
+
// eslint-disable-next-line no-await-in-loop
|
|
187
|
+
const request = await this.apiClient.post('/api/v1/analytics/evaluationStats', {
|
|
188
|
+
workspaceId: this.workspaceId,
|
|
189
|
+
environment: env.id,
|
|
190
|
+
configKey: key,
|
|
191
|
+
days: 1,
|
|
192
|
+
});
|
|
193
|
+
if (request.ok) {
|
|
194
|
+
// oRPC returns EvalStatRow[] directly — wrap for display compatibility
|
|
195
|
+
const rows = request.json;
|
|
196
|
+
statsPerEnv[env.name] = { intervals: [{ data: rows }] };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Check if we have any actual evaluation data
|
|
200
|
+
let hasData = false;
|
|
201
|
+
for (const envName of Object.keys(statsPerEnv)) {
|
|
202
|
+
const envStats = statsPerEnv[envName];
|
|
203
|
+
if (Array.isArray(envStats.intervals)) {
|
|
204
|
+
for (const interval of envStats.intervals) {
|
|
205
|
+
if (interval.data && interval.data.length > 0) {
|
|
206
|
+
hasData = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (hasData)
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
if (!hasData) {
|
|
215
|
+
this.log('No evaluations found for the past 24 hours');
|
|
216
|
+
return { error: 'No evaluations found for the past 24 hours' };
|
|
217
|
+
}
|
|
218
|
+
this.displayEvaluationStats(statsPerEnv, environments);
|
|
219
|
+
// Transform statsPerEnv into the expected JSON format
|
|
220
|
+
return this.transformEvaluationStatsForJson(statsPerEnv, environments, startTime, endTime);
|
|
221
|
+
}
|
|
222
|
+
formatValue(value, includeQuotes = true) {
|
|
223
|
+
if (!value)
|
|
224
|
+
return 'null';
|
|
225
|
+
// Handle encrypted values
|
|
226
|
+
if (value.decryptWith) {
|
|
227
|
+
return '[encrypted]';
|
|
228
|
+
}
|
|
229
|
+
// Handle confidential values
|
|
230
|
+
if (value.confidential) {
|
|
231
|
+
return '[confidential]';
|
|
232
|
+
}
|
|
233
|
+
// Handle weighted values (A/B testing)
|
|
234
|
+
if (Array.isArray(value.weightedValues)) {
|
|
235
|
+
const weights = value.weightedValues
|
|
236
|
+
.sort((a, b) => b.weight - a.weight)
|
|
237
|
+
.map((wv) => {
|
|
238
|
+
const percent = ((wv.weight / 1000) * 100).toFixed(1);
|
|
239
|
+
const val = this.extractSimpleValue(wv.value);
|
|
240
|
+
return `${percent}% ${val}`;
|
|
241
|
+
});
|
|
242
|
+
return weights.join(', ');
|
|
243
|
+
}
|
|
244
|
+
// Handle provided values (ENV_VAR)
|
|
245
|
+
if (value.provided) {
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
+
let str = `${value.provided.lookup}`;
|
|
248
|
+
if (value.confidential) {
|
|
249
|
+
str = `\`${str}\``;
|
|
250
|
+
}
|
|
251
|
+
str += ' via ENV';
|
|
252
|
+
return str;
|
|
253
|
+
}
|
|
254
|
+
// Handle simple values
|
|
255
|
+
// For config display, extract raw value without quotes
|
|
256
|
+
if (!includeQuotes) {
|
|
257
|
+
// Old format: {type: 'string', value: 'abc'}
|
|
258
|
+
if (value.type && value.value !== undefined) {
|
|
259
|
+
if (value.type === 'stringList' && Array.isArray(value.value)) {
|
|
260
|
+
return value.value.join(',');
|
|
261
|
+
}
|
|
262
|
+
return String(value.value);
|
|
263
|
+
}
|
|
264
|
+
// New format: {string: 'abc'}
|
|
265
|
+
if (value.string !== undefined)
|
|
266
|
+
return String(value.string);
|
|
267
|
+
if (value.bool !== undefined)
|
|
268
|
+
return String(value.bool);
|
|
269
|
+
if (value.int !== undefined || value.double !== undefined)
|
|
270
|
+
return String(value.int || value.double);
|
|
271
|
+
if (value.stringList !== undefined && Array.isArray(value.stringList)) {
|
|
272
|
+
return value.stringList.join(',');
|
|
273
|
+
}
|
|
274
|
+
if (value.json !== undefined)
|
|
275
|
+
return JSON.stringify(value.json);
|
|
276
|
+
}
|
|
277
|
+
return this.extractSimpleValue(value);
|
|
278
|
+
}
|
|
279
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
280
|
+
hasNonTrivialCriteria(rule) {
|
|
281
|
+
if (!rule.criteria || rule.criteria.length === 0)
|
|
282
|
+
return false;
|
|
283
|
+
// ALWAYS_TRUE is not a real criteria - treat it as unconditional
|
|
284
|
+
if (rule.criteria.length === 1 && rule.criteria[0].operator === 'ALWAYS_TRUE')
|
|
285
|
+
return false;
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
|
+
parseConfig(config, environments, url) {
|
|
290
|
+
const contents = [];
|
|
291
|
+
const json = {};
|
|
292
|
+
// Collect all environment configs including default
|
|
293
|
+
const allEnvConfigs = [];
|
|
294
|
+
// Add default config as a special environment
|
|
295
|
+
if (config.default) {
|
|
296
|
+
allEnvConfigs.push({
|
|
297
|
+
id: null,
|
|
298
|
+
name: 'Default',
|
|
299
|
+
rules: config.default.rules,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
// Add environment-specific configs
|
|
303
|
+
if (config.environments) {
|
|
304
|
+
for (const envConfig of config.environments) {
|
|
305
|
+
const env = environments.find((e) => e.id === envConfig.id);
|
|
306
|
+
allEnvConfigs.push({
|
|
307
|
+
id: envConfig.id,
|
|
308
|
+
name: env?.name || envConfig.id,
|
|
309
|
+
rules: envConfig.rules,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Display all configs
|
|
314
|
+
for (const envConfig of allEnvConfigs) {
|
|
315
|
+
let displayValue;
|
|
316
|
+
if (!envConfig.rules || envConfig.rules.length === 0) {
|
|
317
|
+
// No rules means inherit from default
|
|
318
|
+
displayValue = '[inherit]';
|
|
319
|
+
}
|
|
320
|
+
else if (envConfig.name === 'Default') {
|
|
321
|
+
// For Default environment, show the unconditional fallback value
|
|
322
|
+
// Find the ALWAYS_TRUE rule (usually the last rule)
|
|
323
|
+
const fallbackRule = envConfig.rules.find(
|
|
324
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
325
|
+
(r) => r.criteria?.length === 1 && r.criteria[0].operator === 'ALWAYS_TRUE');
|
|
326
|
+
displayValue = fallbackRule
|
|
327
|
+
? this.formatValue(fallbackRule.value, false)
|
|
328
|
+
: this.formatValue(envConfig.rules[0].value, false);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// For non-default environments with rules
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
333
|
+
const hasConditionalRules = envConfig.rules.some((r) => this.hasNonTrivialCriteria(r));
|
|
334
|
+
if (hasConditionalRules) {
|
|
335
|
+
// Has conditional rules - show [override] with the conditional value
|
|
336
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
337
|
+
const overrideRule = envConfig.rules.find((r) => this.hasNonTrivialCriteria(r));
|
|
338
|
+
if (overrideRule) {
|
|
339
|
+
const overrideValue = this.formatValue(overrideRule.value, false);
|
|
340
|
+
displayValue = `[override] \`${overrideValue}\``;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
displayValue = '[see rules]';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
// Only has unconditional rules (ALWAYS_TRUE) - show [see rules] to indicate env has custom config
|
|
348
|
+
displayValue = '[see rules]';
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
contents.push(`- ${envConfig.name}: ${displayValue}`);
|
|
352
|
+
const envUrl = envConfig.id ? `${url}?environment=${envConfig.id}` : `${url}?environment=undefined`;
|
|
353
|
+
json[envConfig.name] = {
|
|
354
|
+
url: envUrl,
|
|
355
|
+
};
|
|
356
|
+
// Add value to JSON based on rule structure
|
|
357
|
+
if (envConfig.rules && envConfig.rules.length > 0) {
|
|
358
|
+
if (envConfig.name === 'Default') {
|
|
359
|
+
// For Default, show the unconditional fallback value
|
|
360
|
+
const fallbackRule = envConfig.rules.find(
|
|
361
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
362
|
+
(r) => r.criteria?.length === 1 && r.criteria[0].operator === 'ALWAYS_TRUE');
|
|
363
|
+
const valueToShow = fallbackRule ? fallbackRule.value.value : envConfig.rules[0].value.value;
|
|
364
|
+
json[envConfig.name].value = valueToShow;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
// For non-default environments
|
|
368
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
369
|
+
const hasConditionalRules = envConfig.rules.some((r) => this.hasNonTrivialCriteria(r));
|
|
370
|
+
json[envConfig.name].value = '[see rules]';
|
|
371
|
+
// If there are conditional rules, also add override info
|
|
372
|
+
if (hasConditionalRules) {
|
|
373
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
374
|
+
const overrideRule = envConfig.rules.find((r) => this.hasNonTrivialCriteria(r));
|
|
375
|
+
if (overrideRule) {
|
|
376
|
+
// Extract just the value without formatting
|
|
377
|
+
const overrideValue = overrideRule.value.value;
|
|
378
|
+
json[envConfig.name].override = overrideValue;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
this.log(contents.join('\n').trim());
|
|
385
|
+
return json;
|
|
386
|
+
}
|
|
387
|
+
transformEvaluationStatsForJson(statsPerEnv, environments, startTime, endTime) {
|
|
388
|
+
const envResults = [];
|
|
389
|
+
let grandTotal = 0;
|
|
390
|
+
// Process each environment's stats
|
|
391
|
+
for (const env of environments) {
|
|
392
|
+
const envStats = statsPerEnv[env.name];
|
|
393
|
+
if (!envStats || !Array.isArray(envStats.intervals))
|
|
394
|
+
continue;
|
|
395
|
+
let totalEvaluations = 0;
|
|
396
|
+
const valueCounts = [];
|
|
397
|
+
const seenValues = new Map();
|
|
398
|
+
// Aggregate data across all intervals for this environment
|
|
399
|
+
for (const interval of envStats.intervals) {
|
|
400
|
+
if (interval.data) {
|
|
401
|
+
for (const dataPoint of interval.data) {
|
|
402
|
+
totalEvaluations += dataPoint.count || 0;
|
|
403
|
+
// Create a key for deduplication
|
|
404
|
+
const valueKey = JSON.stringify(dataPoint.selectedValue);
|
|
405
|
+
const existing = seenValues.get(valueKey);
|
|
406
|
+
if (existing) {
|
|
407
|
+
existing.count += dataPoint.count || 0;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const entry = {
|
|
411
|
+
configValue: this.transformValueForJson(dataPoint.selectedValue),
|
|
412
|
+
count: dataPoint.count || 0,
|
|
413
|
+
};
|
|
414
|
+
seenValues.set(valueKey, entry);
|
|
415
|
+
valueCounts.push(entry);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (totalEvaluations > 0) {
|
|
421
|
+
envResults.push({
|
|
422
|
+
counts: valueCounts,
|
|
423
|
+
envId: env.id,
|
|
424
|
+
name: env.name,
|
|
425
|
+
total: totalEvaluations,
|
|
426
|
+
});
|
|
427
|
+
grandTotal += totalEvaluations;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Sort by total descending
|
|
431
|
+
envResults.sort((a, b) => b.total - a.total);
|
|
432
|
+
// Deduplicate by environment name (keep the one with more evaluations)
|
|
433
|
+
const seenNames = new Set();
|
|
434
|
+
const uniqueResults = envResults.filter((env) => {
|
|
435
|
+
if (seenNames.has(env.name)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
seenNames.add(env.name);
|
|
439
|
+
return true;
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
end: endTime,
|
|
443
|
+
environments: uniqueResults,
|
|
444
|
+
start: startTime,
|
|
445
|
+
total: grandTotal,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
transformValueForJson(value) {
|
|
449
|
+
// If already in new format ({bool: true}, {string: "test"}, etc.), return as-is
|
|
450
|
+
if (value.bool !== undefined ||
|
|
451
|
+
value.string !== undefined ||
|
|
452
|
+
value.int !== undefined ||
|
|
453
|
+
value.double !== undefined ||
|
|
454
|
+
value.stringList !== undefined ||
|
|
455
|
+
value.json !== undefined) {
|
|
456
|
+
return value;
|
|
457
|
+
}
|
|
458
|
+
// Transform from old format {type: 'bool', value: true} to new format {bool: true}
|
|
459
|
+
if (value.type && value.value !== undefined) {
|
|
460
|
+
return { [value.type]: value.value };
|
|
461
|
+
}
|
|
462
|
+
return value;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
import { interactivePrompt } from '../interactive-prompt.js';
|
|
3
|
+
export default class Interactive extends BaseCommand {
|
|
4
|
+
static examples = ['<%= config.bin %>'];
|
|
5
|
+
async run() {
|
|
6
|
+
await interactivePrompt(this.config);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { APICommand } from '../index.js';
|
|
2
|
+
export default class List extends APICommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
configs: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
7
|
+
'feature-flags': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
8
|
+
'log-levels': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
9
|
+
schemas: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
segments: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<import("../result.js").JsonObj>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { APICommand } from '../index.js';
|
|
3
|
+
export default class List extends APICommand {
|
|
4
|
+
static description = `Show keys for your config/feature flags/etc.
|
|
5
|
+
|
|
6
|
+
All types are returned by default. If you pass one or more type flags (e.g. --configs), only those types will be returned`;
|
|
7
|
+
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --feature-flags'];
|
|
8
|
+
static flags = {
|
|
9
|
+
configs: Flags.boolean({ default: false, description: 'include configs' }),
|
|
10
|
+
'feature-flags': Flags.boolean({ default: false, description: 'include flags' }),
|
|
11
|
+
'log-levels': Flags.boolean({ default: false, description: 'include log levels' }),
|
|
12
|
+
schemas: Flags.boolean({ default: false, description: 'include schemas' }),
|
|
13
|
+
segments: Flags.boolean({ default: false, description: 'include segments' }),
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { flags } = await this.parse(List);
|
|
17
|
+
const request = await this.apiClient.post('/api/v1/metadata/list', { workspaceId: this.workspaceId });
|
|
18
|
+
if (!request.ok) {
|
|
19
|
+
const errorMsg = request.error?.error || `Failed to fetch configs: ${request.status}`;
|
|
20
|
+
return this.err(errorMsg, { serverError: request.error });
|
|
21
|
+
}
|
|
22
|
+
const response = request.json;
|
|
23
|
+
let configs = response.configs;
|
|
24
|
+
const selectedTypes = [];
|
|
25
|
+
if (flags.configs) {
|
|
26
|
+
selectedTypes.push('config');
|
|
27
|
+
}
|
|
28
|
+
if (flags['feature-flags']) {
|
|
29
|
+
selectedTypes.push('feature_flag');
|
|
30
|
+
}
|
|
31
|
+
if (flags['log-levels']) {
|
|
32
|
+
selectedTypes.push('log_level');
|
|
33
|
+
}
|
|
34
|
+
if (flags.schemas) {
|
|
35
|
+
selectedTypes.push('schema');
|
|
36
|
+
}
|
|
37
|
+
if (flags.segments) {
|
|
38
|
+
selectedTypes.push('segment');
|
|
39
|
+
}
|
|
40
|
+
if (selectedTypes.length > 0) {
|
|
41
|
+
configs = configs.filter((config) => selectedTypes.includes(config.type.toLowerCase()));
|
|
42
|
+
}
|
|
43
|
+
// Calculate column widths
|
|
44
|
+
const maxKeyLength = Math.max(10, ...configs.map((c) => c.key.length));
|
|
45
|
+
const maxTypeLength = Math.max(10, ...configs.map((c) => c.type.length));
|
|
46
|
+
const maxNameLength = Math.max(20, ...configs.map((c) => (c.name || '').length));
|
|
47
|
+
// Build output
|
|
48
|
+
const header = `${'KEY'.padEnd(maxKeyLength)} ${'TYPE'.padEnd(maxTypeLength)} ${'NAME'.padEnd(maxNameLength)} DESCRIPTION`;
|
|
49
|
+
const separator = `${'-'.repeat(maxKeyLength)} ${'-'.repeat(maxTypeLength)} ${'-'.repeat(maxNameLength)} ${'-'.repeat(20)}`;
|
|
50
|
+
const rows = configs.map((config) => {
|
|
51
|
+
const key = config.key.padEnd(maxKeyLength);
|
|
52
|
+
const type = config.type.padEnd(maxTypeLength);
|
|
53
|
+
const name = (config.name || '').padEnd(maxNameLength);
|
|
54
|
+
const description = config.description || '';
|
|
55
|
+
return `${key} ${type} ${name} ${description}`;
|
|
56
|
+
});
|
|
57
|
+
const output = [header, separator, ...rows].join('\n');
|
|
58
|
+
const keys = configs.map((config) => config.key);
|
|
59
|
+
return this.ok(output, { keys });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { JsonObj } from '../result.js';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
export default class Login extends BaseCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
profile: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<JsonObj | void>;
|
|
10
|
+
}
|