@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.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +707 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +9 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +8 -0
  7. package/dist/api/get-environments.d.ts +10 -0
  8. package/dist/api/get-environments.js +11 -0
  9. package/dist/codegen/code-generators/base-generator.d.ts +11 -0
  10. package/dist/codegen/code-generators/base-generator.js +8 -0
  11. package/dist/codegen/code-generators/base-typescript-generator.d.ts +17 -0
  12. package/dist/codegen/code-generators/base-typescript-generator.js +35 -0
  13. package/dist/codegen/code-generators/node-typescript-generator.d.ts +8 -0
  14. package/dist/codegen/code-generators/node-typescript-generator.js +96 -0
  15. package/dist/codegen/code-generators/react-typescript-generator.d.ts +11 -0
  16. package/dist/codegen/code-generators/react-typescript-generator.js +116 -0
  17. package/dist/codegen/config-downloader.d.ts +7 -0
  18. package/dist/codegen/config-downloader.js +26 -0
  19. package/dist/codegen/json-schema-to-zod.d.ts +3 -0
  20. package/dist/codegen/json-schema-to-zod.js +170 -0
  21. package/dist/codegen/language-mappers/json-to-zod-mapper.d.ts +4 -0
  22. package/dist/codegen/language-mappers/json-to-zod-mapper.js +55 -0
  23. package/dist/codegen/language-mappers/zod-base-mapper.d.ts +35 -0
  24. package/dist/codegen/language-mappers/zod-base-mapper.js +111 -0
  25. package/dist/codegen/language-mappers/zod-to-string-mapper.d.ts +25 -0
  26. package/dist/codegen/language-mappers/zod-to-string-mapper.js +87 -0
  27. package/dist/codegen/language-mappers/zod-to-typescript-mapper.d.ts +35 -0
  28. package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +133 -0
  29. package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.d.ts +36 -0
  30. package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.js +150 -0
  31. package/dist/codegen/mustache-extractor.d.ts +6 -0
  32. package/dist/codegen/mustache-extractor.js +83 -0
  33. package/dist/codegen/schema-evaluator.d.ts +37 -0
  34. package/dist/codegen/schema-evaluator.js +283 -0
  35. package/dist/codegen/schema-extractor.d.ts +24 -0
  36. package/dist/codegen/schema-extractor.js +265 -0
  37. package/dist/codegen/types.d.ts +44 -0
  38. package/dist/codegen/types.js +5 -0
  39. package/dist/codegen/zod-introspection.d.ts +85 -0
  40. package/dist/codegen/zod-introspection.js +205 -0
  41. package/dist/commands/create.d.ts +19 -0
  42. package/dist/commands/create.js +175 -0
  43. package/dist/commands/download.d.ts +13 -0
  44. package/dist/commands/download.js +63 -0
  45. package/dist/commands/generate-new-hex-key.d.ts +7 -0
  46. package/dist/commands/generate-new-hex-key.js +10 -0
  47. package/dist/commands/generate.d.ts +16 -0
  48. package/dist/commands/generate.js +220 -0
  49. package/dist/commands/get.d.ts +13 -0
  50. package/dist/commands/get.js +141 -0
  51. package/dist/commands/info.d.ts +21 -0
  52. package/dist/commands/info.js +464 -0
  53. package/dist/commands/interactive.d.ts +5 -0
  54. package/dist/commands/interactive.js +8 -0
  55. package/dist/commands/list.d.ts +13 -0
  56. package/dist/commands/list.js +61 -0
  57. package/dist/commands/login.d.ts +10 -0
  58. package/dist/commands/login.js +124 -0
  59. package/dist/commands/logout.d.ts +7 -0
  60. package/dist/commands/logout.js +12 -0
  61. package/dist/commands/mcp.d.ts +13 -0
  62. package/dist/commands/mcp.js +145 -0
  63. package/dist/commands/override.d.ts +17 -0
  64. package/dist/commands/override.js +170 -0
  65. package/dist/commands/profile.d.ts +7 -0
  66. package/dist/commands/profile.js +47 -0
  67. package/dist/commands/schema.d.ts +21 -0
  68. package/dist/commands/schema.js +168 -0
  69. package/dist/commands/serve.d.ts +13 -0
  70. package/dist/commands/serve.js +99 -0
  71. package/dist/commands/set-default.d.ts +20 -0
  72. package/dist/commands/set-default.js +273 -0
  73. package/dist/commands/verify.d.ts +12 -0
  74. package/dist/commands/verify.js +48 -0
  75. package/dist/commands/whoami.d.ts +7 -0
  76. package/dist/commands/whoami.js +52 -0
  77. package/dist/commands/workspace.d.ts +7 -0
  78. package/dist/commands/workspace.js +38 -0
  79. package/dist/constants.d.ts +2 -0
  80. package/dist/constants.js +2 -0
  81. package/dist/help.d.ts +4 -0
  82. package/dist/help.js +10 -0
  83. package/dist/index.d.ts +40 -0
  84. package/dist/index.js +112 -0
  85. package/dist/interactive-prompt.d.ts +2 -0
  86. package/dist/interactive-prompt.js +162 -0
  87. package/dist/quonfig.d.ts +15 -0
  88. package/dist/quonfig.js +75 -0
  89. package/dist/result.d.ts +35 -0
  90. package/dist/result.js +9 -0
  91. package/dist/services/mcp.d.ts +15 -0
  92. package/dist/services/mcp.js +59 -0
  93. package/dist/ui/get-confirmation.d.ts +10 -0
  94. package/dist/ui/get-confirmation.js +4 -0
  95. package/dist/ui/get-environment.d.ts +11 -0
  96. package/dist/ui/get-environment.js +42 -0
  97. package/dist/ui/get-key.d.ts +27 -0
  98. package/dist/ui/get-key.js +15 -0
  99. package/dist/ui/get-string.d.ts +5 -0
  100. package/dist/ui/get-string.js +8 -0
  101. package/dist/ui/get-value.d.ts +18 -0
  102. package/dist/ui/get-value.js +46 -0
  103. package/dist/util/autocomplete.d.ts +6 -0
  104. package/dist/util/autocomplete.js +25 -0
  105. package/dist/util/coerce.d.ts +7 -0
  106. package/dist/util/coerce.js +62 -0
  107. package/dist/util/color.d.ts +3 -0
  108. package/dist/util/color.js +4 -0
  109. package/dist/util/config-value-dto.d.ts +16 -0
  110. package/dist/util/config-value-dto.js +82 -0
  111. package/dist/util/context.d.ts +18 -0
  112. package/dist/util/context.js +10 -0
  113. package/dist/util/domain-urls.d.ts +3 -0
  114. package/dist/util/domain-urls.js +18 -0
  115. package/dist/util/encryption.d.ts +20 -0
  116. package/dist/util/encryption.js +153 -0
  117. package/dist/util/file-manager.d.ts +12 -0
  118. package/dist/util/file-manager.js +16 -0
  119. package/dist/util/get-client.d.ts +7 -0
  120. package/dist/util/get-client.js +127 -0
  121. package/dist/util/is-interactive.d.ts +5 -0
  122. package/dist/util/is-interactive.js +7 -0
  123. package/dist/util/json-maybe.d.ts +3 -0
  124. package/dist/util/json-maybe.js +14 -0
  125. package/dist/util/name-arg.d.ts +4 -0
  126. package/dist/util/name-arg.js +3 -0
  127. package/dist/util/oauth-client.d.ts +30 -0
  128. package/dist/util/oauth-client.js +99 -0
  129. package/dist/util/secret-flags.d.ts +13 -0
  130. package/dist/util/secret-flags.js +13 -0
  131. package/dist/util/token-storage.d.ts +25 -0
  132. package/dist/util/token-storage.js +90 -0
  133. package/dist/validations/value.d.ts +4 -0
  134. package/dist/validations/value.js +11 -0
  135. package/dist/verify/standalone.d.ts +14 -0
  136. package/dist/verify/standalone.js +133 -0
  137. package/dist/verify/validate.d.ts +43 -0
  138. package/dist/verify/validate.js +756 -0
  139. package/dist/version.d.ts +2 -0
  140. package/dist/version.js +2 -0
  141. package/oclif.manifest.json +1429 -0
  142. 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,5 @@
1
+ import { BaseCommand } from '../index.js';
2
+ export default class Interactive extends BaseCommand {
3
+ static examples: string[];
4
+ run(): Promise<void>;
5
+ }
@@ -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
+ }