@quonfig/cli 0.0.5 → 0.0.7

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 (37) hide show
  1. package/README.md +209 -161
  2. package/dist/codegen/local-config-reader.d.ts +6 -0
  3. package/dist/codegen/local-config-reader.js +113 -0
  4. package/dist/commands/create.js +3 -0
  5. package/dist/commands/generate.d.ts +3 -2
  6. package/dist/commands/generate.js +18 -10
  7. package/dist/commands/login.js +22 -15
  8. package/dist/commands/profile.d.ts +1 -0
  9. package/dist/commands/profile.js +6 -42
  10. package/dist/commands/sdk-key/create.d.ts +11 -0
  11. package/dist/commands/sdk-key/create.js +100 -0
  12. package/dist/commands/sdk-key/list.d.ts +10 -0
  13. package/dist/commands/sdk-key/list.js +48 -0
  14. package/dist/commands/sdk-key/revoke.d.ts +10 -0
  15. package/dist/commands/sdk-key/revoke.js +30 -0
  16. package/dist/commands/sdk-key.d.ts +6 -0
  17. package/dist/commands/sdk-key.js +15 -0
  18. package/dist/commands/whoami.js +8 -8
  19. package/dist/commands/workspace/switch.d.ts +7 -0
  20. package/dist/commands/workspace/switch.js +90 -0
  21. package/dist/commands/workspace.js +14 -23
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +9 -2
  24. package/dist/interactive-prompt.js +0 -4
  25. package/dist/ui/get-value.js +4 -1
  26. package/dist/util/get-client.js +34 -11
  27. package/dist/util/token-storage.d.ts +6 -0
  28. package/dist/util/token-storage.js +17 -0
  29. package/dist/verify/validate.js +28 -8
  30. package/oclif.manifest.json +433 -118
  31. package/package.json +1 -1
  32. package/dist/codegen/config-downloader.d.ts +0 -7
  33. package/dist/codegen/config-downloader.js +0 -26
  34. package/dist/commands/download.d.ts +0 -13
  35. package/dist/commands/download.js +0 -63
  36. package/dist/commands/serve.d.ts +0 -13
  37. package/dist/commands/serve.js +0 -101
@@ -0,0 +1,6 @@
1
+ import type { ConfigFile } from './types.js';
2
+ export declare class LocalConfigReader {
3
+ private dir;
4
+ constructor(dir: string);
5
+ read(): Promise<ConfigFile>;
6
+ }
@@ -0,0 +1,113 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ function mapGitValue(gitValue) {
4
+ const { type, value } = gitValue;
5
+ switch (type) {
6
+ case 'bool': {
7
+ return { value: { bool: value } };
8
+ }
9
+ case 'int': {
10
+ return { value: { int: value } };
11
+ }
12
+ case 'json': {
13
+ return { value: { json: { json: value } } };
14
+ }
15
+ case 'log_level': {
16
+ return { value: { logLevel: value } };
17
+ }
18
+ case 'string_list': {
19
+ return { value: { string: JSON.stringify(value) } };
20
+ }
21
+ case 'string':
22
+ default: {
23
+ return { value: { string: value } };
24
+ }
25
+ }
26
+ }
27
+ function buildRows(gitConfig) {
28
+ const allRules = [
29
+ ...(gitConfig.default?.rules ?? []),
30
+ ...(gitConfig.environments ?? []).flatMap((env) => env.rules),
31
+ ];
32
+ return allRules.map((rule) => ({
33
+ values: [mapGitValue(rule.value)],
34
+ }));
35
+ }
36
+ function mapConfigType(gitType) {
37
+ return gitType === 'feature_flag' ? 'FEATURE_FLAG' : 'CONFIG';
38
+ }
39
+ function mapValueType(gitValueType) {
40
+ return gitValueType.toUpperCase();
41
+ }
42
+ async function readJsonFiles(dir) {
43
+ try {
44
+ const entries = await fs.promises.readdir(dir);
45
+ const jsonFiles = entries.filter((f) => f.endsWith('.json'));
46
+ return Promise.all(jsonFiles.map(async (file) => {
47
+ const filePath = path.join(dir, file);
48
+ const content = await fs.promises.readFile(filePath, 'utf8');
49
+ return { filePath, data: JSON.parse(content) };
50
+ }));
51
+ }
52
+ catch (error) {
53
+ if (error.code === 'ENOENT') {
54
+ return [];
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+ export class LocalConfigReader {
60
+ dir;
61
+ constructor(dir) {
62
+ this.dir = dir;
63
+ }
64
+ async read() {
65
+ // Validate dir exists
66
+ try {
67
+ await fs.promises.access(this.dir, fs.constants.F_OK);
68
+ }
69
+ catch {
70
+ throw new Error(`Directory not found: ${this.dir}. Run \`qfg pull --dir ${this.dir}\` to clone it.`);
71
+ }
72
+ // Validate it's a quonfig workspace
73
+ const quonfigJsonPath = path.join(this.dir, 'quonfig.json');
74
+ try {
75
+ await fs.promises.access(quonfigJsonPath, fs.constants.F_OK);
76
+ }
77
+ catch {
78
+ throw new Error(`${this.dir} does not look like a Quonfig workspace. Is this the right directory?`);
79
+ }
80
+ // Read configs and feature-flags
81
+ const [configFiles, flagFiles] = await Promise.all([
82
+ readJsonFiles(path.join(this.dir, 'configs')),
83
+ readJsonFiles(path.join(this.dir, 'feature-flags')),
84
+ ]);
85
+ const configs = [];
86
+ for (const { filePath, data } of [...configFiles, ...flagFiles]) {
87
+ const gitConfig = data;
88
+ if (!gitConfig.key || !gitConfig.type || !gitConfig.valueType) {
89
+ console.warn(`Skipping malformed config file: ${filePath}`);
90
+ continue;
91
+ }
92
+ const rows = buildRows(gitConfig);
93
+ configs.push({
94
+ configType: mapConfigType(gitConfig.type),
95
+ key: gitConfig.key,
96
+ rows,
97
+ schemaKey: gitConfig.schemaKey,
98
+ sendToClientSdk: gitConfig.sendToClientSdk,
99
+ valueType: mapValueType(gitConfig.valueType),
100
+ });
101
+ }
102
+ // Read schemas
103
+ const schemaFiles = await readJsonFiles(path.join(this.dir, 'schemas'));
104
+ const schemas = schemaFiles.map(({ filePath, data }) => ({
105
+ path: filePath,
106
+ schema: data,
107
+ }));
108
+ if (configs.length === 0) {
109
+ console.warn('No configs or feature-flags found in workspace. Generated types will be empty.');
110
+ }
111
+ return { configs, schemas };
112
+ }
113
+ }
@@ -105,6 +105,9 @@ Or edit the JSON config file directly for complex targeting rules:
105
105
  configValue = confidentialValueResult.value;
106
106
  }
107
107
  }
108
+ else if (valueInput.error) {
109
+ return this.err(valueInput.message, valueInput.json);
110
+ }
108
111
  else {
109
112
  return;
110
113
  }
@@ -1,9 +1,10 @@
1
1
  import type { JsonObj } from '../result.js';
2
- import { APICommand } from '../index.js';
3
- export default class Generate extends APICommand {
2
+ import { BaseCommand } from '../index.js';
3
+ export default class Generate extends BaseCommand {
4
4
  static description: string;
5
5
  static examples: string[];
6
6
  static flags: {
7
+ dir: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
7
8
  'output-directory': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
9
  targets: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
10
  };
@@ -4,9 +4,9 @@ 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 { ConfigDownloader } from '../codegen/config-downloader.js';
7
+ import { LocalConfigReader } from '../codegen/local-config-reader.js';
8
8
  import { SupportedLanguage } from '../codegen/types.js';
9
- import { APICommand } from '../index.js';
9
+ import { BaseCommand } from '../index.js';
10
10
  import { createFileManager } from '../util/file-manager.js';
11
11
  // base types
12
12
  const nodeOrReactLanguageSpecificSchema = z.object({
@@ -66,7 +66,7 @@ const DEFAULT_CONFIG = {
66
66
  },
67
67
  },
68
68
  };
69
- export default class Generate extends APICommand {
69
+ export default class Generate extends BaseCommand {
70
70
  /* eslint-disable no-irregular-whitespace */
71
71
  static description = `You can use the default type-generation configuration, or by provide your own via a quonfig.config.json file:
72
72
 
@@ -108,6 +108,10 @@ Example quonfig.config.json:
108
108
  '<%= config.bin %> <%= command.id %> --targets node-ts -o ./dist # combine with targets',
109
109
  ];
110
110
  static flags = {
111
+ dir: Flags.string({
112
+ description: 'Path to local QUONFIG_DIR (defaults to QUONFIG_DIR env var)',
113
+ env: 'QUONFIG_DIR',
114
+ }),
111
115
  'output-directory': Flags.string({
112
116
  char: 'o',
113
117
  description: 'Override the output directory for generated files',
@@ -121,18 +125,22 @@ Example quonfig.config.json:
121
125
  async run() {
122
126
  const { flags } = await this.parse(Generate);
123
127
  this.verboseLog('=== GENERATE COMMAND START ===');
124
- this.verboseLog(`Base API URL: ${process.env.QUONFIG_API_URL || 'Default'}`);
125
128
  try {
129
+ // Resolve workspace directory
130
+ const dir = flags.dir;
131
+ if (!dir) {
132
+ this.error('No directory specified. Run `qfg pull` first or pass --dir <path>.');
133
+ }
126
134
  // Look for and read local quonfig.config.json file
127
135
  const localConfig = await this.readLocalConfig(flags['output-directory']);
128
- // Use tarets flag override, otherwise fall back to local config
136
+ // Use targets flag override, otherwise fall back to local config
129
137
  const targets = flags.targets.split(',') || Object.keys(localConfig);
130
138
  this.verboseLog(`Language(s): ${targets.join(', ')}`);
131
- // Download the configuration using the APICommand's client
132
- const downloader = new ConfigDownloader(this);
133
- this.verboseLog('Downloading config...');
134
- const configFile = await downloader.downloadConfig();
135
- this.verboseLog('Config download complete.');
139
+ // Read configuration from local directory
140
+ const reader = new LocalConfigReader(dir);
141
+ this.verboseLog(`Reading config from ${dir}...`);
142
+ const configFile = await reader.read();
143
+ this.verboseLog(`Config read complete. Found ${configFile.configs.length} configs.`);
136
144
  const fileCreationPromises = [];
137
145
  for (const target of targets) {
138
146
  // Resolve the language input
@@ -10,7 +10,8 @@ export default class Login extends BaseCommand {
10
10
  static flags = {
11
11
  profile: Flags.string({
12
12
  char: 'p',
13
- description: 'Profile name to create or update (defaults to "default")',
13
+ description: 'Profile name to save credentials under (advanced; defaults to "default")',
14
+ hidden: true,
14
15
  }),
15
16
  };
16
17
  async run() {
@@ -56,6 +57,8 @@ export default class Login extends BaseCommand {
56
57
  let workspaceName;
57
58
  let workspaceSlug;
58
59
  let organizationName;
60
+ let organizationId;
61
+ let multipleWorkspacesAvailable = false;
59
62
  try {
60
63
  const apiUrl = getApiUrl();
61
64
  this.verboseLog('Resolving workspace...', { apiUrl, orgId });
@@ -70,17 +73,15 @@ export default class Login extends BaseCommand {
70
73
  if (res.ok) {
71
74
  const body = await res.json();
72
75
  const allWorkspaces = (body.json ?? body);
73
- const orgWorkspaces = Array.isArray(allWorkspaces)
74
- ? allWorkspaces.filter((w) => w.workosOrgId === orgId)
75
- : [];
76
- const candidates = orgWorkspaces.length > 0 ? orgWorkspaces : (Array.isArray(allWorkspaces) ? allWorkspaces : []);
76
+ const candidates = Array.isArray(allWorkspaces) ? allWorkspaces : [];
77
+ multipleWorkspacesAvailable = candidates.length > 1;
77
78
  let match;
78
79
  if (candidates.length === 1) {
79
80
  match = candidates[0];
80
81
  }
81
82
  else if (candidates.length > 1) {
82
83
  const chosen = await select({
83
- choices: candidates.map((w) => ({ name: w.workspaceSlug, value: w.workspaceId })),
84
+ choices: candidates.map((w) => ({ name: `${w.organizationName} / ${w.workspaceSlug}`, value: w.workspaceId })),
84
85
  message: 'Select workspace:',
85
86
  });
86
87
  match = candidates.find((w) => w.workspaceId === chosen);
@@ -90,6 +91,7 @@ export default class Login extends BaseCommand {
90
91
  workspaceName = match.workspaceSlug;
91
92
  workspaceSlug = match.workspaceSlug;
92
93
  organizationName = match.organizationName;
94
+ organizationId = match.workosOrgId;
93
95
  this.verboseLog('Resolved workspace', { workspaceId, workspaceName, workspaceSlug, organizationName });
94
96
  }
95
97
  }
@@ -113,33 +115,38 @@ export default class Login extends BaseCommand {
113
115
  workspace: workspaceId || orgId || 'unknown',
114
116
  workspaceName: workspaceName || (orgId ? `org:${orgId}` : undefined),
115
117
  workspaceSlug,
118
+ organizationName,
116
119
  },
117
120
  },
118
121
  });
119
- this.log(`\nProfile '${profileName}' configured.`);
120
- if (shouldSetDefault) {
121
- this.log('Set as default profile.');
122
- }
123
122
  this.log('\nSuccessfully logged in!');
124
123
  if (userEmail) {
125
124
  this.log(`Logged in as: ${userEmail}`);
126
125
  }
127
126
  if (workspaceName) {
128
- this.log(`Workspace: ${workspaceName} (${workspaceId})`);
129
- if (organizationName) {
130
- this.log(`Organization: ${organizationName}`);
127
+ const orgPrefix = organizationName ? `${organizationName} / ` : '';
128
+ this.log(`Workspace: ${orgPrefix}${workspaceName}`);
129
+ if (workspaceId) {
130
+ this.log(` (${workspaceId})`);
131
131
  }
132
132
  }
133
133
  else if (orgId) {
134
134
  this.log(`Organization: ${orgId}`);
135
135
  }
136
+ if (multipleWorkspacesAvailable && workspaceSlug) {
137
+ this.log(`\nYou have multiple workspaces. To pin this project to ${workspaceSlug},`);
138
+ this.log(`add this to your project's .env file:`);
139
+ this.log(` QUONFIG_WORKSPACE=${workspaceSlug}`);
140
+ this.log(`\nTo switch your default workspace: qfg workspace switch`);
141
+ }
136
142
  return {
137
143
  email: userEmail,
138
- organizationId: orgId,
139
- profile: profileName,
144
+ organizationId: organizationId || orgId,
145
+ organizationName,
140
146
  success: true,
141
147
  userId: user.id,
142
148
  workspaceId,
149
+ workspaceSlug,
143
150
  };
144
151
  }
145
152
  }
@@ -2,6 +2,7 @@ import type { JsonObj } from '../result.js';
2
2
  import { BaseCommand } from '../index.js';
3
3
  export default class Profile extends BaseCommand {
4
4
  static description: string;
5
+ static hidden: boolean;
5
6
  static examples: string[];
6
7
  run(): Promise<JsonObj | void>;
7
8
  }
@@ -1,47 +1,11 @@
1
- import { select } from '@inquirer/prompts';
2
1
  import { BaseCommand } from '../index.js';
3
- import { loadAuthConfig, saveAuthConfig } from '../util/token-storage.js';
2
+ import WorkspaceSwitch from './workspace/switch.js';
4
3
  export default class Profile extends BaseCommand {
5
- static description = 'Manage profiles and set default profile';
6
- static examples = ['<%= config.bin %> <%= command.id %>'];
4
+ static description = 'Deprecated. Use `qfg workspace switch` instead.';
5
+ static hidden = true;
6
+ static examples = ['<%= config.bin %> workspace switch'];
7
7
  async run() {
8
- const authConfig = await loadAuthConfig();
9
- if (!authConfig || Object.keys(authConfig.profiles).length === 0) {
10
- return this.err('Not logged in. Please run `qfg login` first.');
11
- }
12
- const currentDefault = authConfig.defaultProfile || 'default';
13
- // If only one profile, just show it
14
- if (Object.keys(authConfig.profiles).length === 1) {
15
- const profileName = Object.keys(authConfig.profiles)[0];
16
- const profile = authConfig.profiles[profileName];
17
- this.log(`Current default profile: ${profileName}`);
18
- this.log(`Workspace: ${profile.workspaceName || profile.workspace}`);
19
- this.log('\nYou only have one profile.');
20
- return {
21
- defaultProfile: profileName,
22
- profiles: Object.keys(authConfig.profiles),
23
- };
24
- }
25
- // Show current default
26
- this.log(`Current default profile: ${currentDefault}\n`);
27
- // Let user select a new default profile
28
- const newDefault = await select({
29
- choices: Object.entries(authConfig.profiles).map(([name, profile]) => ({
30
- name: `${name} (${profile.workspaceName || profile.workspace})`,
31
- value: name,
32
- })),
33
- message: 'Select default profile:',
34
- });
35
- // Update the default profile
36
- await saveAuthConfig({
37
- ...authConfig,
38
- defaultProfile: newDefault,
39
- });
40
- this.log(`\nDefault profile set to: ${newDefault}`);
41
- return {
42
- defaultProfile: newDefault,
43
- previousDefault: currentDefault,
44
- success: true,
45
- };
8
+ this.log('Note: `qfg profile` is deprecated. Use `qfg workspace switch` instead.\n');
9
+ return WorkspaceSwitch.run([], this.config);
46
10
  }
47
11
  }
@@ -0,0 +1,11 @@
1
+ import type { JsonObj } from '../../result.js';
2
+ import { APICommand } from '../../index.js';
3
+ export default class SdkKeyCreate extends APICommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ environment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ type: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
+ };
10
+ run(): Promise<JsonObj | void>;
11
+ }
@@ -0,0 +1,100 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { select } from '@inquirer/prompts';
3
+ import { APICommand } from '../../index.js';
4
+ import { getEnvironments } from '../../api/get-environments.js';
5
+ export default class SdkKeyCreate extends APICommand {
6
+ static description = 'Create a new SDK key';
7
+ static examples = [
8
+ '<%= config.bin %> <%= command.id %> --environment production --type server',
9
+ '<%= config.bin %> <%= command.id %> --environment staging --type browser',
10
+ ];
11
+ static flags = {
12
+ environment: Flags.string({
13
+ char: 'e',
14
+ description: 'Environment name (e.g. production, staging)',
15
+ required: false,
16
+ }),
17
+ type: Flags.string({
18
+ char: 't',
19
+ description: 'Key type: server (backend) or browser (frontend)',
20
+ options: ['server', 'browser'],
21
+ required: false,
22
+ }),
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(SdkKeyCreate);
26
+ // Fetch available environments
27
+ const environments = await getEnvironments(this);
28
+ if (environments.length === 0) {
29
+ return this.err('No environments found for this workspace.');
30
+ }
31
+ // Resolve environment
32
+ let environmentId;
33
+ let environmentName;
34
+ if (flags.environment) {
35
+ const match = environments.find((e) => e.name.toLowerCase() === flags.environment.toLowerCase());
36
+ if (!match) {
37
+ const names = environments.map((e) => e.name).join(', ');
38
+ return this.err(`Environment "${flags.environment}" not found. Available: ${names}`);
39
+ }
40
+ environmentId = match.id;
41
+ environmentName = match.name;
42
+ }
43
+ else {
44
+ if (environments.length === 1) {
45
+ environmentId = environments[0].id;
46
+ environmentName = environments[0].name;
47
+ this.log(`Using environment: ${environmentName}`);
48
+ }
49
+ else {
50
+ const chosen = await select({
51
+ choices: environments.map((e) => ({ name: e.name, value: e.id })),
52
+ message: 'Select environment:',
53
+ });
54
+ const match = environments.find((e) => e.id === chosen);
55
+ environmentId = match.id;
56
+ environmentName = match.name;
57
+ }
58
+ }
59
+ // Resolve key type
60
+ let keyType;
61
+ if (flags.type) {
62
+ keyType = flags.type === 'server' ? 'backend' : 'frontend';
63
+ }
64
+ else {
65
+ const chosen = await select({
66
+ choices: [
67
+ { name: 'server (backend SDK — Node.js, Go, etc.)', value: 'backend' },
68
+ { name: 'browser (frontend SDK — JavaScript/React)', value: 'frontend' },
69
+ ],
70
+ message: 'Select key type:',
71
+ });
72
+ keyType = chosen;
73
+ }
74
+ const request = await this.apiClient.post('/api/v1/sdkKeys/create', {
75
+ workspaceId: this.workspaceId,
76
+ environmentId,
77
+ keyType,
78
+ });
79
+ if (!request.ok) {
80
+ const errorMsg = request.error?.error || `Failed to create SDK key: ${request.status}`;
81
+ return this.err(errorMsg, { serverError: request.error });
82
+ }
83
+ const key = request.json;
84
+ const typeLabel = keyType === 'backend' ? 'server' : 'browser';
85
+ this.log(`\nSDK key created successfully!\n`);
86
+ this.log(`Environment: ${environmentName}`);
87
+ this.log(`Type: ${typeLabel}`);
88
+ this.log(`Key ID: ${key.id}`);
89
+ this.log(`\nYour SDK key (shown only once — copy it now):\n`);
90
+ this.log(` ${key.rawKey}\n`);
91
+ this.log(`To revoke this key: qfg sdk-key revoke ${key.id}`);
92
+ return {
93
+ environmentId: key.environmentId,
94
+ environmentName: key.environmentName,
95
+ id: key.id,
96
+ keyType: typeLabel,
97
+ rawKey: key.rawKey,
98
+ };
99
+ }
100
+ }
@@ -0,0 +1,10 @@
1
+ import type { JsonObj } from '../../result.js';
2
+ import { APICommand } from '../../index.js';
3
+ export default class SdkKeyList extends APICommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ environment: 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
+ }
@@ -0,0 +1,48 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { APICommand } from '../../index.js';
3
+ export default class SdkKeyList extends APICommand {
4
+ static description = 'List SDK keys for your workspace';
5
+ static examples = [
6
+ '<%= config.bin %> <%= command.id %>',
7
+ '<%= config.bin %> <%= command.id %> --environment production',
8
+ ];
9
+ static flags = {
10
+ environment: Flags.string({
11
+ char: 'e',
12
+ description: 'Filter by environment name',
13
+ required: false,
14
+ }),
15
+ };
16
+ async run() {
17
+ const { flags } = await this.parse(SdkKeyList);
18
+ const request = await this.apiClient.post('/api/v1/sdkKeys/list', { workspaceId: this.workspaceId });
19
+ if (!request.ok) {
20
+ const errorMsg = request.error?.error || `Failed to fetch SDK keys: ${request.status}`;
21
+ return this.err(errorMsg, { serverError: request.error });
22
+ }
23
+ let keys = (Array.isArray(request.json) ? request.json : []);
24
+ if (flags.environment) {
25
+ const envFilter = flags.environment.toLowerCase();
26
+ keys = keys.filter((k) => k.environmentName.toLowerCase() === envFilter);
27
+ }
28
+ if (keys.length === 0) {
29
+ const envSuffix = flags.environment ? ` for environment "${flags.environment}"` : '';
30
+ this.log(`No SDK keys found${envSuffix}.`);
31
+ this.log(`\nCreate one with: qfg sdk-key create --environment <name> --type server|browser`);
32
+ return { keys: [] };
33
+ }
34
+ const maxEnvLength = Math.max(11, ...keys.map((k) => k.environmentName.length));
35
+ const maxTypeLength = 7; // "browser" is longest
36
+ const maxIdLength = Math.max(2, ...keys.map((k) => k.id.length));
37
+ const header = `${'ID'.padEnd(maxIdLength)} ${'ENVIRONMENT'.padEnd(maxEnvLength)} ${'TYPE'.padEnd(maxTypeLength)} ${'PREFIX'.padEnd(28)} CREATED`;
38
+ const separator = `${'-'.repeat(maxIdLength)} ${'-'.repeat(maxEnvLength)} ${'-'.repeat(maxTypeLength)} ${'-'.repeat(28)} ${'-'.repeat(24)}`;
39
+ const rows = keys.map((k) => {
40
+ const typeLabel = k.keyType === 'backend' ? 'server' : 'browser';
41
+ const prefix = k.keyType === 'backend' ? `qf_sk_${k.environmentName}_...` : `qf_pk_${k.environmentName}_...`;
42
+ const created = k.createdAt ? new Date(k.createdAt).toISOString().slice(0, 10) : 'unknown';
43
+ return `${k.id.padEnd(maxIdLength)} ${k.environmentName.padEnd(maxEnvLength)} ${typeLabel.padEnd(maxTypeLength)} ${prefix.padEnd(28)} ${created}`;
44
+ });
45
+ this.log([header, separator, ...rows].join('\n'));
46
+ return { keys };
47
+ }
48
+ }
@@ -0,0 +1,10 @@
1
+ import type { JsonObj } from '../../result.js';
2
+ import { APICommand } from '../../index.js';
3
+ export default class SdkKeyRevoke extends APICommand {
4
+ static description: string;
5
+ static args: {
6
+ keyId: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static examples: string[];
9
+ run(): Promise<JsonObj | void>;
10
+ }
@@ -0,0 +1,30 @@
1
+ import { Args } from '@oclif/core';
2
+ import { APICommand } from '../../index.js';
3
+ export default class SdkKeyRevoke extends APICommand {
4
+ static description = 'Revoke an SDK key by its ID';
5
+ static args = {
6
+ keyId: Args.string({
7
+ description: 'ID of the SDK key to revoke',
8
+ required: true,
9
+ }),
10
+ };
11
+ static examples = [
12
+ '<%= config.bin %> <%= command.id %> a1b2c3d4-e5f6-7890-abcd-ef1234567890',
13
+ ];
14
+ async run() {
15
+ const { args } = await this.parse(SdkKeyRevoke);
16
+ const request = await this.apiClient.post('/api/v1/sdkKeys/delete', {
17
+ workspaceId: this.workspaceId,
18
+ keyId: args.keyId,
19
+ });
20
+ if (!request.ok) {
21
+ if (request.status === 404) {
22
+ return this.err(`SDK key "${args.keyId}" not found or already revoked.`);
23
+ }
24
+ const errorMsg = request.error?.error || `Failed to revoke SDK key: ${request.status}`;
25
+ return this.err(errorMsg, { serverError: request.error });
26
+ }
27
+ this.log(`SDK key ${args.keyId} has been revoked.`);
28
+ return { keyId: args.keyId, revoked: true };
29
+ }
30
+ }
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from '../index.js';
2
+ export default class SdkKey extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '../index.js';
2
+ export default class SdkKey extends BaseCommand {
3
+ static description = 'Manage SDK keys for your workspace';
4
+ static examples = [
5
+ '<%= config.bin %> sdk-key list',
6
+ '<%= config.bin %> sdk-key create --environment production --type server',
7
+ '<%= config.bin %> sdk-key revoke <key-id>',
8
+ ];
9
+ async run() {
10
+ this.log('Use one of the sdk-key subcommands:');
11
+ this.log(' qfg sdk-key list');
12
+ this.log(' qfg sdk-key create --environment <name> --type server|browser');
13
+ this.log(' qfg sdk-key revoke <key-id>');
14
+ }
15
+ }
@@ -36,20 +36,20 @@ export default class Whoami extends BaseCommand {
36
36
  };
37
37
  }
38
38
  // Display user information
39
- this.log(`Logged in as: ${userEmail || 'Unknown'}`);
40
- this.log(`Active profile: ${activeProfile}`);
41
- const workspaceDisplay = profile.workspaceName
42
- ? `${profile.workspaceName} (${profile.workspace})`
39
+ const workspaceSlug = profile.workspaceSlug || profile.workspaceName;
40
+ const orgPrefix = profile.organizationName ? `${profile.organizationName} / ` : '';
41
+ const workspaceDisplay = workspaceSlug
42
+ ? `${orgPrefix}${workspaceSlug} (${profile.workspace})`
43
43
  : profile.workspace;
44
- this.log(`Active workspace: ${workspaceDisplay}`);
44
+ this.log(`Logged in as: ${userEmail || 'Unknown'}`);
45
+ this.log(`Workspace: ${workspaceDisplay}`);
45
46
  return {
46
47
  email: userEmail,
47
48
  loggedIn: true,
48
- organizationId: orgId || profile.workspace,
49
- profile: activeProfile,
49
+ organizationName: profile.organizationName,
50
50
  userId: tokens.userId,
51
51
  workspace: profile.workspace,
52
- workspaceName: profile.workspaceName,
52
+ workspaceSlug,
53
53
  };
54
54
  }
55
55
  }
@@ -0,0 +1,7 @@
1
+ import type { JsonObj } from '../../result.js';
2
+ import { BaseCommand } from '../../index.js';
3
+ export default class WorkspaceSwitch extends BaseCommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ run(): Promise<JsonObj | void>;
7
+ }