@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.
- package/README.md +209 -161
- package/dist/codegen/local-config-reader.d.ts +6 -0
- package/dist/codegen/local-config-reader.js +113 -0
- package/dist/commands/create.js +3 -0
- package/dist/commands/generate.d.ts +3 -2
- package/dist/commands/generate.js +18 -10
- package/dist/commands/login.js +22 -15
- package/dist/commands/profile.d.ts +1 -0
- package/dist/commands/profile.js +6 -42
- package/dist/commands/sdk-key/create.d.ts +11 -0
- package/dist/commands/sdk-key/create.js +100 -0
- package/dist/commands/sdk-key/list.d.ts +10 -0
- package/dist/commands/sdk-key/list.js +48 -0
- package/dist/commands/sdk-key/revoke.d.ts +10 -0
- package/dist/commands/sdk-key/revoke.js +30 -0
- package/dist/commands/sdk-key.d.ts +6 -0
- package/dist/commands/sdk-key.js +15 -0
- package/dist/commands/whoami.js +8 -8
- package/dist/commands/workspace/switch.d.ts +7 -0
- package/dist/commands/workspace/switch.js +90 -0
- package/dist/commands/workspace.js +14 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -2
- package/dist/interactive-prompt.js +0 -4
- package/dist/ui/get-value.js +4 -1
- package/dist/util/get-client.js +34 -11
- package/dist/util/token-storage.d.ts +6 -0
- package/dist/util/token-storage.js +17 -0
- package/dist/verify/validate.js +28 -8
- package/oclif.manifest.json +433 -118
- package/package.json +1 -1
- package/dist/codegen/config-downloader.d.ts +0 -7
- package/dist/codegen/config-downloader.js +0 -26
- package/dist/commands/download.d.ts +0 -13
- package/dist/commands/download.js +0 -63
- package/dist/commands/serve.d.ts +0 -13
- package/dist/commands/serve.js +0 -101
|
@@ -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
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -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 {
|
|
3
|
-
export default class Generate extends
|
|
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 {
|
|
7
|
+
import { LocalConfigReader } from '../codegen/local-config-reader.js';
|
|
8
8
|
import { SupportedLanguage } from '../codegen/types.js';
|
|
9
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
//
|
|
132
|
-
const
|
|
133
|
-
this.verboseLog(
|
|
134
|
-
const configFile = await
|
|
135
|
-
this.verboseLog(
|
|
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
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
|
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
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,47 +1,11 @@
|
|
|
1
|
-
import { select } from '@inquirer/prompts';
|
|
2
1
|
import { BaseCommand } from '../index.js';
|
|
3
|
-
import
|
|
2
|
+
import WorkspaceSwitch from './workspace/switch.js';
|
|
4
3
|
export default class Profile extends BaseCommand {
|
|
5
|
-
static description = '
|
|
6
|
-
static
|
|
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
|
-
|
|
9
|
-
|
|
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,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
|
+
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -36,20 +36,20 @@ export default class Whoami extends BaseCommand {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
// Display user information
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const workspaceDisplay =
|
|
42
|
-
? `${
|
|
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(`
|
|
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
|
-
|
|
49
|
-
profile: activeProfile,
|
|
49
|
+
organizationName: profile.organizationName,
|
|
50
50
|
userId: tokens.userId,
|
|
51
51
|
workspace: profile.workspace,
|
|
52
|
-
|
|
52
|
+
workspaceSlug,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
}
|