@quonfig/cli 0.0.19 → 0.0.21

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.
@@ -4,6 +4,7 @@ import * as path from 'node:path';
4
4
  import { z } from 'zod';
5
5
  import { NodeTypeScriptGenerator } from '../codegen/code-generators/node-typescript-generator.js';
6
6
  import { ReactTypeScriptGenerator } from '../codegen/code-generators/react-typescript-generator.js';
7
+ import { fetchWorkspaceSnapshot } from '../codegen/fetch-workspace-snapshot.js';
7
8
  import { LocalConfigReader } from '../codegen/local-config-reader.js';
8
9
  import { SupportedLanguage } from '../codegen/types.js';
9
10
  import { BaseCommand } from '../index.js';
@@ -109,7 +110,7 @@ Example quonfig.config.json:
109
110
  ];
110
111
  static flags = {
111
112
  dir: Flags.string({
112
- description: 'Path to local QUONFIG_DIR (defaults to QUONFIG_DIR env var)',
113
+ description: 'Path to local QUONFIG_DIR (defaults to QUONFIG_DIR env var). When omitted, fetches the workspace from the server using your active credentials.',
113
114
  env: 'QUONFIG_DIR',
114
115
  }),
115
116
  'output-directory': Flags.string({
@@ -120,16 +121,25 @@ Example quonfig.config.json:
120
121
  default: SupportedLanguage.React,
121
122
  description: `Determines for language/framework to generate code for (${Object.values(SupportedLanguage).join(', ')})`,
122
123
  }),
124
+ workspace: Flags.string({
125
+ char: 'w',
126
+ description: 'Workspace slug or UUID for the remote-fetch path (defaults to QUONFIG_WORKSPACE env var or active profile). Only used when --dir is omitted.',
127
+ }),
123
128
  };
124
129
  static summary = 'Generate type definitions for your Quonfig configuration';
125
130
  async run() {
126
131
  const { flags } = await this.parse(Generate);
127
132
  this.verboseLog('=== GENERATE COMMAND START ===');
133
+ let snapshot;
128
134
  try {
129
- // Resolve workspace directory
130
- const dir = flags.dir;
135
+ // When --dir/QUONFIG_DIR is set, read the local checkout (so users
136
+ // who edit flag JSON in place keep that workflow). Otherwise clone
137
+ // the workspace into a tmp dir for codegen-only callers.
138
+ let dir = flags.dir;
131
139
  if (!dir) {
132
- this.error('No directory specified. Run `qfg pull` first or pass --dir <path>.');
140
+ this.verboseLog('No --dir/QUONFIG_DIR set; fetching workspace snapshot from server...');
141
+ snapshot = await fetchWorkspaceSnapshot(this, { workspace: flags.workspace });
142
+ dir = snapshot.dir;
133
143
  }
134
144
  // Look for and read local quonfig.config.json file
135
145
  const localConfig = await this.readLocalConfig(flags['output-directory']);
@@ -142,6 +152,7 @@ Example quonfig.config.json:
142
152
  const configFile = await reader.read();
143
153
  this.verboseLog(`Config read complete. Found ${configFile.configs.length} configs.`);
144
154
  const fileCreationPromises = [];
155
+ let needsMustache = false;
145
156
  for (const target of targets) {
146
157
  // Resolve the language input
147
158
  const language = this.resolveLanguage(target);
@@ -154,6 +165,9 @@ Example quonfig.config.json:
154
165
  console.log(`Generating ${language} code for configs...`);
155
166
  const generatedCode = generator.generate();
156
167
  this.verboseLog(`Code generation complete. Size: ${generatedCode.length}`);
168
+ if (generatedCode.includes("from 'mustache'")) {
169
+ needsMustache = true;
170
+ }
157
171
  const fileManager = createFileManager({ outputDirectory: outputDir, verboseLog: this.verboseLog.bind(this) });
158
172
  fileCreationPromises.push(fileManager.writeFile({ data: generatedCode, filename: targetConfig.clientFileName }));
159
173
  if ([SupportedLanguage.Node, SupportedLanguage.React].includes(language)) {
@@ -163,11 +177,26 @@ Example quonfig.config.json:
163
177
  }
164
178
  }
165
179
  await Promise.all(fileCreationPromises);
180
+ if (needsMustache) {
181
+ console.log('\nNote: generated code imports `mustache` for templated config values.\n' +
182
+ ' Add it to your project: `npm install mustache @types/mustache`\n' +
183
+ ' (or pnpm/yarn equivalent).');
184
+ }
166
185
  }
167
186
  catch (error) {
168
187
  console.error('ERROR:', error);
169
188
  this.error(error);
170
189
  }
190
+ finally {
191
+ if (snapshot) {
192
+ try {
193
+ await snapshot.cleanup();
194
+ }
195
+ catch (cleanupError) {
196
+ this.verboseLog('Generate', `Snapshot cleanup failed: ${String(cleanupError)}`);
197
+ }
198
+ }
199
+ }
171
200
  this.verboseLog('=== GENERATE COMMAND END ===');
172
201
  return { success: true };
173
202
  }
@@ -3,7 +3,7 @@ import { select } from '@inquirer/prompts';
3
3
  import { BaseCommand } from '../index.js';
4
4
  import { decodeJWT, pollForToken, requestDeviceCode } from '../util/oauth-client.js';
5
5
  import { getApiUrl } from '../util/domain-urls.js';
6
- import { loadAuthConfig, saveAuthConfig, saveTokens } from '../util/token-storage.js';
6
+ import { getAuthConfigFilePath, getTokenFilePath, loadAuthConfig, saveAuthConfig, saveTokens, } from '../util/token-storage.js';
7
7
  export default class Login extends BaseCommand {
8
8
  static description = 'Log in to Quonfig via WorkOS device authorization';
9
9
  static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --profile myprofile'];
@@ -47,14 +47,22 @@ export default class Login extends BaseCommand {
47
47
  if (typeof jwtPayload.exp === 'number') {
48
48
  expiresAt = jwtPayload.exp * 1000;
49
49
  }
50
- // Save tokens
51
- await saveTokens({
52
- accessToken: tokenResponse.access_token,
53
- expiresAt,
54
- refreshToken: tokenResponse.refresh_token,
55
- userEmail,
56
- userId: user.id,
57
- });
50
+ // Save tokens — verifies the file actually persisted before returning.
51
+ this.verboseLog('Saving tokens to', getTokenFilePath());
52
+ let tokensPath;
53
+ try {
54
+ tokensPath = await saveTokens({
55
+ accessToken: tokenResponse.access_token,
56
+ expiresAt,
57
+ refreshToken: tokenResponse.refresh_token,
58
+ userEmail,
59
+ userId: user.id,
60
+ });
61
+ }
62
+ catch (error) {
63
+ return this.err(`Login failed: could not save tokens. ${error.message}`);
64
+ }
65
+ this.verboseLog('Tokens saved at', tokensPath);
58
66
  // Resolve org_id → workspace UUID via the API
59
67
  let workspaceId;
60
68
  let workspaceName;
@@ -113,18 +121,26 @@ export default class Login extends BaseCommand {
113
121
  // Save auth config
114
122
  const isFirstProfile = !existingConfig || Object.keys(existingConfig.profiles).length === 0;
115
123
  const shouldSetDefault = isFirstProfile || profileName === 'default' || !existingConfig?.defaultProfile;
116
- await saveAuthConfig({
117
- defaultProfile: shouldSetDefault ? profileName : existingConfig?.defaultProfile,
118
- profiles: {
119
- ...existingConfig?.profiles,
120
- [profileName]: {
121
- workspace: workspaceId || orgId || 'unknown',
122
- workspaceName: workspaceName || (orgId ? `org:${orgId}` : undefined),
123
- workspaceSlug,
124
- organizationName,
124
+ this.verboseLog('Saving auth config to', getAuthConfigFilePath());
125
+ let configPath;
126
+ try {
127
+ configPath = await saveAuthConfig({
128
+ defaultProfile: shouldSetDefault ? profileName : existingConfig?.defaultProfile,
129
+ profiles: {
130
+ ...existingConfig?.profiles,
131
+ [profileName]: {
132
+ workspace: workspaceId || orgId || 'unknown',
133
+ workspaceName: workspaceName || (orgId ? `org:${orgId}` : undefined),
134
+ workspaceSlug,
135
+ organizationName,
136
+ },
125
137
  },
126
- },
127
- });
138
+ });
139
+ }
140
+ catch (error) {
141
+ return this.err(`Login failed: could not save auth config. ${error.message}`);
142
+ }
143
+ this.verboseLog('Auth config saved at', configPath);
128
144
  this.log('\nSuccessfully logged in!');
129
145
  if (userEmail) {
130
146
  this.log(`Logged in as: ${userEmail}`);
@@ -1,17 +1,23 @@
1
1
  import { APICommand } from '../index.js';
2
- import { JsonObj } from '../result.js';
2
+ import type { JsonObj } from '../result.js';
3
3
  export default class Override extends APICommand {
4
4
  static args: {
5
5
  name: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
6
+ value: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
6
7
  };
7
8
  static description: string;
8
9
  static examples: string[];
9
10
  static flags: {
10
- environment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ clear: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ env: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
13
  remove: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
- value: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
14
  };
14
15
  run(): Promise<JsonObj | void>;
15
- private removeOverride;
16
- private setOverride;
16
+ private callFindOrCreate;
17
+ private callRemove;
18
+ private fetchFlag;
19
+ private runClear;
20
+ private runList;
21
+ private runRemove;
22
+ private runSet;
17
23
  }
@@ -1,173 +1,269 @@
1
- import { Flags } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import { APICommand } from '../index.js';
3
- import getEnvironment from '../ui/get-environment.js';
4
- import getString from '../ui/get-string.js';
5
- import autocomplete from '../util/autocomplete.js';
6
3
  import { checkmark } from '../util/color.js';
7
- import isInteractive from '../util/is-interactive.js';
8
- import nameArg from '../util/name-arg.js';
4
+ import { loadTokens } from '../util/token-storage.js';
5
+ const OVERRIDE_PROP = 'quonfig-user.email';
6
+ const OVERRIDE_OP = 'PROP_IS_ONE_OF';
7
+ /**
8
+ * Parse the fresh SHA out of a 409 conflict message of the form
9
+ * "<path> was modified (expected <oldSha>, got <newSha>)"
10
+ * Returns undefined if the message doesn't match.
11
+ */
12
+ function parseFreshShaFromConflict(message) {
13
+ if (!message)
14
+ return undefined;
15
+ const match = message.match(/got\s+([\w.-]+)\)/);
16
+ return match?.[1];
17
+ }
18
+ /**
19
+ * Coerce a raw CLI value string into the typed Value the server expects.
20
+ * Order matters: bool → int → double → json → string.
21
+ */
22
+ function inferValue(raw) {
23
+ if (raw === 'true' || raw === 'false') {
24
+ return { type: 'bool', value: raw === 'true' };
25
+ }
26
+ if (/^-?\d+$/.test(raw)) {
27
+ return { type: 'int', value: Number.parseInt(raw, 10) };
28
+ }
29
+ if (/^-?\d+\.\d+$/.test(raw)) {
30
+ return { type: 'double', value: Number.parseFloat(raw) };
31
+ }
32
+ const trimmed = raw.trim();
33
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
34
+ try {
35
+ return { type: 'json', value: JSON.parse(trimmed) };
36
+ }
37
+ catch {
38
+ // Fall through to string — looked like JSON but didn't parse.
39
+ }
40
+ }
41
+ return { type: 'string', value: raw };
42
+ }
43
+ /**
44
+ * Find the existing override rule for a user in a specific env. Returns the
45
+ * value the user is currently overridden to, or undefined.
46
+ */
47
+ function findExistingOverride(flag, env, userEmail) {
48
+ const envEntry = flag.environments?.find((e) => e.id === env);
49
+ for (const rule of envEntry?.rules ?? []) {
50
+ if (!rule.criteria || rule.criteria.length !== 1)
51
+ continue;
52
+ const c = rule.criteria[0];
53
+ if (c.propertyName !== OVERRIDE_PROP || c.operator !== OVERRIDE_OP)
54
+ continue;
55
+ const emails = c.valueToMatch?.value ?? [];
56
+ if (emails.includes(userEmail) && rule.value) {
57
+ return rule.value;
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+ function valuesEqual(a, b) {
63
+ if (a.type !== b.type)
64
+ return false;
65
+ return JSON.stringify(a.value) === JSON.stringify(b.value);
66
+ }
9
67
  export default class Override extends APICommand {
10
- static args = { ...nameArg };
11
- static description = `Override the value of an item for your user/SDK key combo.
68
+ static args = {
69
+ name: Args.string({ description: 'flag/config key to override' }),
70
+ value: Args.string({ description: 'new value (type inferred: bool/int/double/json/string)' }),
71
+ };
72
+ static description = `Override a flag value for your dev user.
12
73
 
13
- To target users other than yourself, or to express rules like "user.email == X → value Y",
14
- you cannot use override — see:
15
- qfg config-schema # operator reference + worked multi-rule example
16
- qfg pull # clone the workspace so you can edit JSON directly`;
74
+ Writes a top-priority rule keyed on the dev-only quonfig-user.email property,
75
+ so the override only fires for SDK clients that set quonfig-user.email in
76
+ their context. Production SDKs typically don't set this property, which makes
77
+ overrides effectively inert in production.
78
+
79
+ Examples
80
+ qfg override # list flags where you have an override
81
+ qfg override my.flag true # set bool override
82
+ qfg override my.flag 42 # set int override
83
+ qfg override my.flag '{"a":1}' # set json override
84
+ qfg override my.flag --remove # remove your override on my.flag
85
+ qfg override --clear # remove ALL of your overrides in this env`;
17
86
  static examples = [
18
- '<%= config.bin %> <%= command.id %> # will prompt for name and value',
19
- '<%= config.bin %> <%= command.id %> my.flag.name --value=true',
20
- '<%= config.bin %> <%= command.id %> my.flag.name --remove',
21
- '<%= config.bin %> <%= command.id %> my.double.config --value=3.14159',
87
+ '<%= config.bin %> <%= command.id %>',
88
+ '<%= config.bin %> <%= command.id %> my.flag true',
89
+ '<%= config.bin %> <%= command.id %> my.flag --remove',
90
+ '<%= config.bin %> <%= command.id %> --clear',
91
+ '<%= config.bin %> <%= command.id %> my.flag true --env=staging',
22
92
  ];
23
93
  static flags = {
24
- environment: Flags.string({ description: 'environment to override in' }),
25
- remove: Flags.boolean({ default: false, description: 'remove your override (if present)' }),
26
- value: Flags.string({ description: 'value to use for your override' }),
94
+ clear: Flags.boolean({ default: false, description: 'remove ALL of your overrides in this env' }),
95
+ env: Flags.string({ description: 'environment to operate in (default: $QUONFIG_ENVIRONMENT)' }),
96
+ remove: Flags.boolean({ default: false, description: 'remove your override on this key' }),
27
97
  };
28
98
  async run() {
29
99
  const { args, flags } = await this.parse(Override);
30
- if (flags.remove && flags.value) {
31
- this.err('remove and value flags are mutually exclusive');
32
- }
33
- // Fetch all configs from metadata endpoint
34
- const metadataRequest = await this.apiClient.post('/api/v1/metadata/list', { workspaceId: this.workspaceId });
35
- if (!metadataRequest.ok) {
36
- const errorMsg = metadataRequest.error?.error || `Failed to fetch configs: ${metadataRequest.status}`;
37
- return this.err(errorMsg, { serverError: metadataRequest.error });
38
- }
39
- const metadataResponse = metadataRequest.json;
40
- const configs = metadataResponse.configs;
41
- // Get the environment
42
- const environment = await getEnvironment({
43
- command: this,
44
- flags,
45
- message: 'Which environment would you like to override in?',
46
- providedEnvironment: flags.environment,
47
- });
48
- if (!environment) {
49
- return;
50
- }
51
- // Get the key - from args or prompt
52
- let key = args.name;
53
- if (!key && isInteractive(flags)) {
54
- const configKeys = configs.map((c) => c.key);
55
- const selectedKey = await autocomplete({
56
- message: 'Which item would you like to override?',
57
- source: configKeys,
58
- });
59
- if (selectedKey) {
60
- key = selectedKey;
61
- }
100
+ const tokens = await loadTokens();
101
+ const userEmail = tokens?.userEmail;
102
+ if (!userEmail) {
103
+ return this.err('Not logged in. Run `qfg login` first.');
62
104
  }
63
- if (!key) {
64
- return this.err('Key is required');
105
+ const env = flags.env || process.env.QUONFIG_ENVIRONMENT;
106
+ if (!env) {
107
+ return this.err('No environment specified. Pass --env=<env> or set QUONFIG_ENVIRONMENT.');
65
108
  }
66
- const config = configs.find((c) => c.key === key);
67
- if (!config) {
68
- return this.err(`Could not find config named ${key}`);
109
+ if (env === 'production') {
110
+ this.warn('Overrides on production are inert for SDK clients that do not set quonfig-user.email (most production SDKs do not). Continuing anyway.');
69
111
  }
70
- this.verboseLog('Selected config:', config);
71
- if (flags.remove) {
72
- return this.removeOverride(key, environment.id, config.version);
73
- }
74
- // Get the value
75
- let value = flags.value;
76
- if (!value && isInteractive(flags)) {
77
- value = await getString({
78
- allowBlank: false,
79
- message: 'Override value',
80
- });
112
+ if (flags.clear) {
113
+ return this.runClear({ env, userEmail });
81
114
  }
82
- if (!value) {
83
- return this.err('Value is required');
115
+ if (!args.name) {
116
+ return this.runList({ env, userEmail });
84
117
  }
85
- return this.setOverride(config, value, environment.id);
86
- }
87
- async removeOverride(key, environmentId, currentVersionId) {
88
- const request = await this.apiClient.post('/internal/ops/v1/remove-variant', {
89
- configKey: key,
90
- currentVersionId,
91
- environmentId,
92
- });
93
- if (request.ok) {
94
- this.log(`${checkmark} Override removed`);
95
- return;
118
+ if (flags.remove) {
119
+ return this.runRemove({ env, key: args.name, userEmail });
96
120
  }
97
- // Handle 404 case - no override exists to remove
98
- if (request.status === 404) {
99
- this.log(`No override found for ${key}`);
100
- return;
121
+ if (args.value === undefined) {
122
+ return this.err('Missing value. Usage: qfg override <key> <value> [--env=<env>]');
101
123
  }
102
- this.err(`Failed to remove override: ${request.status}`, { key, serverError: request.error });
124
+ return this.runSet({ env, key: args.name, rawValue: args.value, userEmail });
103
125
  }
104
- async setOverride(config, value, environmentId) {
105
- const { key, valueType, version } = config;
106
- // Map the valueType to the format expected by the API
107
- const typeMapping = {
108
- bool: 'bool',
109
- string: 'string',
110
- int: 'int',
111
- double: 'double',
112
- string_list: 'stringList',
113
- json: 'json',
114
- limit_definition: 'limitDefinition',
115
- duration: 'duration',
116
- int_range: 'intRange',
117
- };
118
- const type = typeMapping[valueType.toLowerCase()] || valueType;
119
- // Parse the value based on type and build variant object
120
- let variantValue = value;
121
- let variantType = type;
122
- switch (type) {
123
- case 'stringList': {
124
- variantValue = value.split(',');
125
- variantType = 'string_list';
126
- break;
127
- }
128
- case 'bool': {
129
- variantValue = value.toLowerCase() === 'true';
130
- break;
131
- }
132
- case 'int': {
133
- variantValue = Number.parseInt(value, 10);
134
- break;
135
- }
136
- case 'double': {
137
- variantValue = Number.parseFloat(value);
138
- break;
126
+ async callFindOrCreate(opts) {
127
+ let sha = opts.currentSha;
128
+ let attempt = 0;
129
+ // One stale-SHA retry: parse the fresh SHA from the 409 message and try again.
130
+ while (attempt < 2) {
131
+ attempt += 1;
132
+ // eslint-disable-next-line no-await-in-loop
133
+ const res = await this.apiClient.post('/api/v1/flags/findOrCreateOverride', {
134
+ currentSha: sha,
135
+ env: opts.env,
136
+ flagKey: opts.key,
137
+ userEmail: opts.userEmail,
138
+ value: opts.value,
139
+ workspaceId: this.workspaceId,
140
+ });
141
+ if (res.ok) {
142
+ const commitSha = res.json?.commitSha;
143
+ return this.ok(`${checkmark} Set override ${opts.key}=${JSON.stringify(opts.value.value)} (${opts.value.type}) for ${opts.userEmail} in env=${opts.env} (commit ${commitSha}).`, { commitSha, env: opts.env, key: opts.key, value: opts.value });
139
144
  }
140
- case 'json': {
141
- try {
142
- variantValue = JSON.parse(value);
143
- }
144
- catch {
145
- return this.err(`Invalid JSON value: ${value}`);
145
+ if (res.status === 409 && attempt === 1) {
146
+ const fresh = parseFreshShaFromConflict(res.error?.message);
147
+ if (fresh) {
148
+ this.verboseLog(`Stale SHA — retrying once with fresh SHA ${fresh}`);
149
+ sha = fresh;
150
+ continue;
146
151
  }
147
- break;
148
152
  }
149
- case 'string': {
150
- variantValue = value;
151
- break;
153
+ return this.err(`Failed to set override on ${opts.key}: ${res.status} ${JSON.stringify(res.error)}`);
154
+ }
155
+ }
156
+ async callRemove(opts) {
157
+ let sha = opts.flag.commitSha ?? '';
158
+ let attempt = 0;
159
+ while (attempt < 2) {
160
+ attempt += 1;
161
+ // eslint-disable-next-line no-await-in-loop
162
+ const res = await this.apiClient.post('/api/v1/flags/removeOverride', {
163
+ currentSha: sha,
164
+ env: opts.env,
165
+ flagKey: opts.key,
166
+ userEmail: opts.userEmail,
167
+ workspaceId: this.workspaceId,
168
+ });
169
+ if (res.ok)
170
+ return true;
171
+ if (res.status === 409 && attempt === 1) {
172
+ const fresh = parseFreshShaFromConflict(res.error?.message);
173
+ if (fresh) {
174
+ this.verboseLog(`Stale SHA — retrying remove with fresh SHA ${fresh}`);
175
+ sha = fresh;
176
+ continue;
177
+ }
152
178
  }
153
- // No default
154
- }
155
- const request = await this.apiClient.post('/internal/ops/v1/assign-variant', {
156
- configKey: key,
157
- currentVersionId: version,
158
- environmentId,
159
- variant: {
160
- type: variantType,
161
- value: variantValue,
162
- },
179
+ this.err(`Failed to remove override on ${opts.key}: ${res.status} ${JSON.stringify(res.error)}`);
180
+ return false;
181
+ }
182
+ return false;
183
+ }
184
+ async fetchFlag(key) {
185
+ const res = await this.apiClient.post('/api/v1/flags/getByKey', {
186
+ flagKey: key,
187
+ workspaceId: this.workspaceId,
188
+ });
189
+ if (!res.ok) {
190
+ if (res.status === 404)
191
+ return undefined;
192
+ this.err(`Failed to fetch flag ${key}: ${res.status} ${JSON.stringify(res.error)}`);
193
+ return undefined;
194
+ }
195
+ return res.json;
196
+ }
197
+ async runClear(opts) {
198
+ const res = await this.apiClient.post('/api/v1/flags/list', { workspaceId: this.workspaceId });
199
+ if (!res.ok) {
200
+ return this.err(`Failed to list flags: ${res.status} ${JSON.stringify(res.error)}`);
201
+ }
202
+ const flags = res.json ?? [];
203
+ const targets = flags.filter((f) => findExistingOverride(f, opts.env, opts.userEmail) !== undefined);
204
+ if (targets.length === 0) {
205
+ return this.ok(`No overrides to clear in env=${opts.env}.`);
206
+ }
207
+ const cleared = [];
208
+ for (const f of targets) {
209
+ // eslint-disable-next-line no-await-in-loop
210
+ const removed = await this.callRemove({
211
+ env: opts.env,
212
+ flag: f,
213
+ key: f.key,
214
+ userEmail: opts.userEmail,
215
+ });
216
+ if (removed)
217
+ cleared.push(f.key);
218
+ }
219
+ return this.ok(`${checkmark} Cleared ${cleared.length} override(s) in env=${opts.env}: ${cleared.join(', ')}`);
220
+ }
221
+ async runList(opts) {
222
+ const res = await this.apiClient.post('/api/v1/flags/list', { workspaceId: this.workspaceId });
223
+ if (!res.ok) {
224
+ return this.err(`Failed to list flags: ${res.status} ${JSON.stringify(res.error)}`);
225
+ }
226
+ const flags = res.json ?? [];
227
+ const overridden = flags.filter((f) => findExistingOverride(f, opts.env, opts.userEmail) !== undefined);
228
+ if (overridden.length === 0) {
229
+ return this.ok(`You have no overrides in env=${opts.env}.`);
230
+ }
231
+ this.log(`Your overrides in env=${opts.env}:`);
232
+ for (const f of overridden) {
233
+ const v = findExistingOverride(f, opts.env, opts.userEmail);
234
+ this.log(` ${f.key} = ${JSON.stringify(v?.value)} (${v?.type})`);
235
+ }
236
+ return { env: opts.env, overrides: overridden.map((f) => f.key) };
237
+ }
238
+ async runRemove(opts) {
239
+ const flag = await this.fetchFlag(opts.key);
240
+ if (!flag) {
241
+ return this.err(`Flag ${opts.key} not found.`);
242
+ }
243
+ const existing = findExistingOverride(flag, opts.env, opts.userEmail);
244
+ if (!existing) {
245
+ return this.ok(`You have no override on ${opts.key} in env=${opts.env}.`);
246
+ }
247
+ const removed = await this.callRemove({ env: opts.env, flag, key: opts.key, userEmail: opts.userEmail });
248
+ return removed ? this.ok(`${checkmark} Removed override on ${opts.key} in env=${opts.env}.`) : undefined;
249
+ }
250
+ async runSet(opts) {
251
+ const value = inferValue(opts.rawValue);
252
+ const flag = await this.fetchFlag(opts.key);
253
+ if (!flag) {
254
+ return this.err(`Flag ${opts.key} not found.`);
255
+ }
256
+ const existing = findExistingOverride(flag, opts.env, opts.userEmail);
257
+ if (existing && valuesEqual(existing, value)) {
258
+ return this.ok(`${opts.key} is already set to ${JSON.stringify(value.value)} (${value.type}) for ${opts.userEmail} in env=${opts.env}.`);
259
+ }
260
+ const sha = flag.commitSha ?? '';
261
+ return this.callFindOrCreate({
262
+ currentSha: sha,
263
+ env: opts.env,
264
+ key: opts.key,
265
+ userEmail: opts.userEmail,
266
+ value,
163
267
  });
164
- if (request.ok) {
165
- this.log(`${checkmark} Override set`);
166
- return { key, success: true };
167
- }
168
- const errMsg = request.status === 400
169
- ? `Failed to override value: ${request.status} -- is ${value} a valid ${type}?`
170
- : `Failed to override value: ${request.status}`;
171
- this.err(errMsg, { key, serverError: request.error });
172
268
  }
173
269
  }
@@ -192,7 +192,7 @@ To see all current values and rules for a flag:
192
192
  successMessage = `Successfully changed default to \`${value}\``;
193
193
  if (secret.selected) {
194
194
  // Handle encrypted values using shared utility
195
- const encryptedValueResult = await makeConfidentialValue(this, value, secret, environment.id);
195
+ const encryptedValueResult = await makeConfidentialValue(this, value, secret, environment.name);
196
196
  if (!encryptedValueResult.ok) {
197
197
  return this.err(encryptedValueResult.message || 'Failed to encrypt value');
198
198
  }