@quonfig/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +707 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +9 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +8 -0
- package/dist/api/get-environments.d.ts +10 -0
- package/dist/api/get-environments.js +11 -0
- package/dist/codegen/code-generators/base-generator.d.ts +11 -0
- package/dist/codegen/code-generators/base-generator.js +8 -0
- package/dist/codegen/code-generators/base-typescript-generator.d.ts +17 -0
- package/dist/codegen/code-generators/base-typescript-generator.js +35 -0
- package/dist/codegen/code-generators/node-typescript-generator.d.ts +8 -0
- package/dist/codegen/code-generators/node-typescript-generator.js +96 -0
- package/dist/codegen/code-generators/react-typescript-generator.d.ts +11 -0
- package/dist/codegen/code-generators/react-typescript-generator.js +116 -0
- package/dist/codegen/config-downloader.d.ts +7 -0
- package/dist/codegen/config-downloader.js +26 -0
- package/dist/codegen/json-schema-to-zod.d.ts +3 -0
- package/dist/codegen/json-schema-to-zod.js +170 -0
- package/dist/codegen/language-mappers/json-to-zod-mapper.d.ts +4 -0
- package/dist/codegen/language-mappers/json-to-zod-mapper.js +55 -0
- package/dist/codegen/language-mappers/zod-base-mapper.d.ts +35 -0
- package/dist/codegen/language-mappers/zod-base-mapper.js +111 -0
- package/dist/codegen/language-mappers/zod-to-string-mapper.d.ts +25 -0
- package/dist/codegen/language-mappers/zod-to-string-mapper.js +87 -0
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.d.ts +35 -0
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +133 -0
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.d.ts +36 -0
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.js +150 -0
- package/dist/codegen/mustache-extractor.d.ts +6 -0
- package/dist/codegen/mustache-extractor.js +83 -0
- package/dist/codegen/schema-evaluator.d.ts +37 -0
- package/dist/codegen/schema-evaluator.js +283 -0
- package/dist/codegen/schema-extractor.d.ts +24 -0
- package/dist/codegen/schema-extractor.js +265 -0
- package/dist/codegen/types.d.ts +44 -0
- package/dist/codegen/types.js +5 -0
- package/dist/codegen/zod-introspection.d.ts +85 -0
- package/dist/codegen/zod-introspection.js +205 -0
- package/dist/commands/create.d.ts +19 -0
- package/dist/commands/create.js +175 -0
- package/dist/commands/download.d.ts +13 -0
- package/dist/commands/download.js +63 -0
- package/dist/commands/generate-new-hex-key.d.ts +7 -0
- package/dist/commands/generate-new-hex-key.js +10 -0
- package/dist/commands/generate.d.ts +16 -0
- package/dist/commands/generate.js +220 -0
- package/dist/commands/get.d.ts +13 -0
- package/dist/commands/get.js +141 -0
- package/dist/commands/info.d.ts +21 -0
- package/dist/commands/info.js +464 -0
- package/dist/commands/interactive.d.ts +5 -0
- package/dist/commands/interactive.js +8 -0
- package/dist/commands/list.d.ts +13 -0
- package/dist/commands/list.js +61 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/logout.d.ts +7 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +145 -0
- package/dist/commands/override.d.ts +17 -0
- package/dist/commands/override.js +170 -0
- package/dist/commands/profile.d.ts +7 -0
- package/dist/commands/profile.js +47 -0
- package/dist/commands/schema.d.ts +21 -0
- package/dist/commands/schema.js +168 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +99 -0
- package/dist/commands/set-default.d.ts +20 -0
- package/dist/commands/set-default.js +273 -0
- package/dist/commands/verify.d.ts +12 -0
- package/dist/commands/verify.js +48 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/commands/workspace.d.ts +7 -0
- package/dist/commands/workspace.js +38 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +10 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +112 -0
- package/dist/interactive-prompt.d.ts +2 -0
- package/dist/interactive-prompt.js +162 -0
- package/dist/quonfig.d.ts +15 -0
- package/dist/quonfig.js +75 -0
- package/dist/result.d.ts +35 -0
- package/dist/result.js +9 -0
- package/dist/services/mcp.d.ts +15 -0
- package/dist/services/mcp.js +59 -0
- package/dist/ui/get-confirmation.d.ts +10 -0
- package/dist/ui/get-confirmation.js +4 -0
- package/dist/ui/get-environment.d.ts +11 -0
- package/dist/ui/get-environment.js +42 -0
- package/dist/ui/get-key.d.ts +27 -0
- package/dist/ui/get-key.js +15 -0
- package/dist/ui/get-string.d.ts +5 -0
- package/dist/ui/get-string.js +8 -0
- package/dist/ui/get-value.d.ts +18 -0
- package/dist/ui/get-value.js +46 -0
- package/dist/util/autocomplete.d.ts +6 -0
- package/dist/util/autocomplete.js +25 -0
- package/dist/util/coerce.d.ts +7 -0
- package/dist/util/coerce.js +62 -0
- package/dist/util/color.d.ts +3 -0
- package/dist/util/color.js +4 -0
- package/dist/util/config-value-dto.d.ts +16 -0
- package/dist/util/config-value-dto.js +82 -0
- package/dist/util/context.d.ts +18 -0
- package/dist/util/context.js +10 -0
- package/dist/util/domain-urls.d.ts +3 -0
- package/dist/util/domain-urls.js +18 -0
- package/dist/util/encryption.d.ts +20 -0
- package/dist/util/encryption.js +153 -0
- package/dist/util/file-manager.d.ts +12 -0
- package/dist/util/file-manager.js +16 -0
- package/dist/util/get-client.d.ts +7 -0
- package/dist/util/get-client.js +127 -0
- package/dist/util/is-interactive.d.ts +5 -0
- package/dist/util/is-interactive.js +7 -0
- package/dist/util/json-maybe.d.ts +3 -0
- package/dist/util/json-maybe.js +14 -0
- package/dist/util/name-arg.d.ts +4 -0
- package/dist/util/name-arg.js +3 -0
- package/dist/util/oauth-client.d.ts +30 -0
- package/dist/util/oauth-client.js +99 -0
- package/dist/util/secret-flags.d.ts +13 -0
- package/dist/util/secret-flags.js +13 -0
- package/dist/util/token-storage.d.ts +25 -0
- package/dist/util/token-storage.js +90 -0
- package/dist/validations/value.d.ts +4 -0
- package/dist/validations/value.js +11 -0
- package/dist/verify/standalone.d.ts +14 -0
- package/dist/verify/standalone.js +133 -0
- package/dist/verify/validate.d.ts +43 -0
- package/dist/verify/validate.js +756 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/oclif.manifest.json +1429 -0
- package/package.json +104 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
import { decodeJWT, pollForToken, requestDeviceCode } from '../util/oauth-client.js';
|
|
4
|
+
import { getApiUrl } from '../util/domain-urls.js';
|
|
5
|
+
import { loadAuthConfig, saveAuthConfig, saveTokens } from '../util/token-storage.js';
|
|
6
|
+
export default class Login extends BaseCommand {
|
|
7
|
+
static description = 'Log in to Quonfig via WorkOS device authorization';
|
|
8
|
+
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --profile myprofile'];
|
|
9
|
+
static flags = {
|
|
10
|
+
profile: Flags.string({
|
|
11
|
+
char: 'p',
|
|
12
|
+
description: 'Profile name to create or update (defaults to "default")',
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { flags } = await this.parse(Login);
|
|
17
|
+
const profileName = flags.profile || 'default';
|
|
18
|
+
// Step 1: Request device code from WorkOS
|
|
19
|
+
this.verboseLog('Requesting device authorization code...');
|
|
20
|
+
const deviceAuth = await requestDeviceCode();
|
|
21
|
+
// Step 2: Display code and URL to user
|
|
22
|
+
this.log(`\nTo authenticate, visit:\n`);
|
|
23
|
+
this.log(` ${deviceAuth.verification_uri_complete}\n`);
|
|
24
|
+
this.log(`Or go to ${deviceAuth.verification_uri} and enter code: ${deviceAuth.user_code}\n`);
|
|
25
|
+
this.log('Waiting for authentication...');
|
|
26
|
+
// Step 3: Poll for token
|
|
27
|
+
const tokenResponse = await pollForToken(deviceAuth.device_code, deviceAuth.interval, deviceAuth.expires_in, this.isVerbose);
|
|
28
|
+
const user = tokenResponse.user;
|
|
29
|
+
const userEmail = user.email;
|
|
30
|
+
// Decode JWT to inspect claims
|
|
31
|
+
if (this.isVerbose) {
|
|
32
|
+
const payload = decodeJWT(tokenResponse.access_token);
|
|
33
|
+
this.verboseLog('\n=== Decoded JWT Payload ===');
|
|
34
|
+
this.verboseLog(JSON.stringify(payload, null, 2));
|
|
35
|
+
this.verboseLog('===========================\n');
|
|
36
|
+
}
|
|
37
|
+
// Extract org_id from JWT (WorkOS scopes the token to the selected org)
|
|
38
|
+
const jwtPayload = decodeJWT(tokenResponse.access_token);
|
|
39
|
+
const orgId = jwtPayload.org_id || user.organization_id;
|
|
40
|
+
// Use the JWT's actual exp claim for expiry
|
|
41
|
+
let expiresAt = Date.now() + 300 * 1000; // fallback: 5 minutes
|
|
42
|
+
if (typeof jwtPayload.exp === 'number') {
|
|
43
|
+
expiresAt = jwtPayload.exp * 1000;
|
|
44
|
+
}
|
|
45
|
+
// Save tokens
|
|
46
|
+
await saveTokens({
|
|
47
|
+
accessToken: tokenResponse.access_token,
|
|
48
|
+
expiresAt,
|
|
49
|
+
refreshToken: tokenResponse.refresh_token,
|
|
50
|
+
userEmail,
|
|
51
|
+
userId: user.id,
|
|
52
|
+
});
|
|
53
|
+
// Resolve org_id → workspace UUID via the API
|
|
54
|
+
let workspaceId;
|
|
55
|
+
let workspaceName;
|
|
56
|
+
try {
|
|
57
|
+
const apiUrl = getApiUrl();
|
|
58
|
+
this.verboseLog('Resolving workspace...', { apiUrl, orgId });
|
|
59
|
+
const res = await fetch(`${apiUrl}/api/v1/userWorkspaces/list`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Authorization': `Bearer ${tokenResponse.access_token}`,
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ json: {} }),
|
|
66
|
+
});
|
|
67
|
+
if (res.ok) {
|
|
68
|
+
const body = await res.json();
|
|
69
|
+
const workspaces = body.json ?? body;
|
|
70
|
+
const match = Array.isArray(workspaces)
|
|
71
|
+
? workspaces.find((w) => w.workosOrgId === orgId) ?? workspaces[0]
|
|
72
|
+
: undefined;
|
|
73
|
+
if (match) {
|
|
74
|
+
workspaceId = match.workspaceId;
|
|
75
|
+
workspaceName = match.organizationName;
|
|
76
|
+
this.verboseLog('Resolved workspace', { workspaceId, workspaceName });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.verboseLog('Failed to resolve workspace', { status: res.status });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.verboseLog('Failed to resolve workspace', { error: String(error) });
|
|
85
|
+
}
|
|
86
|
+
// Get or create config with profile
|
|
87
|
+
const existingConfig = await loadAuthConfig();
|
|
88
|
+
// Save auth config
|
|
89
|
+
const isFirstProfile = !existingConfig || Object.keys(existingConfig.profiles).length === 0;
|
|
90
|
+
const shouldSetDefault = isFirstProfile || profileName === 'default' || !existingConfig?.defaultProfile;
|
|
91
|
+
await saveAuthConfig({
|
|
92
|
+
defaultProfile: shouldSetDefault ? profileName : existingConfig?.defaultProfile,
|
|
93
|
+
profiles: {
|
|
94
|
+
...existingConfig?.profiles,
|
|
95
|
+
[profileName]: {
|
|
96
|
+
workspace: workspaceId || orgId || 'unknown',
|
|
97
|
+
workspaceName: workspaceName || (orgId ? `org:${orgId}` : undefined),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
this.log(`\nProfile '${profileName}' configured.`);
|
|
102
|
+
if (shouldSetDefault) {
|
|
103
|
+
this.log('Set as default profile.');
|
|
104
|
+
}
|
|
105
|
+
this.log('\nSuccessfully logged in!');
|
|
106
|
+
if (userEmail) {
|
|
107
|
+
this.log(`Logged in as: ${userEmail}`);
|
|
108
|
+
}
|
|
109
|
+
if (workspaceName) {
|
|
110
|
+
this.log(`Workspace: ${workspaceName} (${workspaceId})`);
|
|
111
|
+
}
|
|
112
|
+
else if (orgId) {
|
|
113
|
+
this.log(`Organization: ${orgId}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
email: userEmail,
|
|
117
|
+
organizationId: orgId,
|
|
118
|
+
profile: profileName,
|
|
119
|
+
success: true,
|
|
120
|
+
userId: user.id,
|
|
121
|
+
workspaceId,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
import { clearAuth } from '../util/token-storage.js';
|
|
3
|
+
export default class Logout extends BaseCommand {
|
|
4
|
+
static description = 'Log out and clear stored authentication tokens';
|
|
5
|
+
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
6
|
+
async run() {
|
|
7
|
+
await clearAuth();
|
|
8
|
+
this.log('Successfully logged out!');
|
|
9
|
+
this.log('Authentication tokens have been cleared.');
|
|
10
|
+
return { success: true };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
export default class Mcp extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
editor: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
url: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
interactive: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
9
|
+
'no-color': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
verbose: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { parse as parseJSON, stringify as stringifyJSON } from 'comment-json';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, relative } from 'node:path';
|
|
5
|
+
import { BaseCommand } from '../index.js';
|
|
6
|
+
import { ConfigPaths, SupportedEditors, getServersConfig, resolveConfigPath } from '../services/mcp.js';
|
|
7
|
+
import autocomplete from '../util/autocomplete.js';
|
|
8
|
+
import { green } from '../util/color.js';
|
|
9
|
+
import isInteractive from '../util/is-interactive.js';
|
|
10
|
+
export default class Mcp extends BaseCommand {
|
|
11
|
+
static description = 'Configure Quonfig MCP server for your AI assistant';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %>',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --editor cursor',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --url http://local-app.goatsofquonfig.com:3003/api/v1/mcp',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...BaseCommand.baseFlags,
|
|
19
|
+
editor: Flags.string({
|
|
20
|
+
description: 'Editor to configure (cursor, vscode, claude, windsurf)',
|
|
21
|
+
options: [...SupportedEditors],
|
|
22
|
+
}),
|
|
23
|
+
url: Flags.string({
|
|
24
|
+
description: 'Internal URL for testing (defaults to https://app.quonfig.com/api/v1/mcp)',
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { flags } = await this.parse(Mcp);
|
|
29
|
+
// Select Editor
|
|
30
|
+
let selectedEditor;
|
|
31
|
+
if (flags.editor) {
|
|
32
|
+
selectedEditor = flags.editor;
|
|
33
|
+
}
|
|
34
|
+
else if (isInteractive(flags)) {
|
|
35
|
+
const editorChoice = await autocomplete({
|
|
36
|
+
message: green('Which editor do you want to configure?'),
|
|
37
|
+
source: SupportedEditors.map((editor) => ConfigPaths[editor].name),
|
|
38
|
+
});
|
|
39
|
+
if (!editorChoice) {
|
|
40
|
+
return this.error('Editor selection is required');
|
|
41
|
+
}
|
|
42
|
+
// Find the editor key by name
|
|
43
|
+
selectedEditor = Object.keys(ConfigPaths).find((key) => ConfigPaths[key].name === editorChoice);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return this.error('--editor flag is required when interactive mode is not available');
|
|
47
|
+
}
|
|
48
|
+
// Determine config paths
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
const globalPath = resolveConfigPath(selectedEditor, false);
|
|
51
|
+
const localPath = resolveConfigPath(selectedEditor, true);
|
|
52
|
+
const fullLocalPath = localPath ? join(cwd, localPath) : undefined;
|
|
53
|
+
if (!globalPath) {
|
|
54
|
+
return this.error(`Unsupported platform for editor: ${selectedEditor}`);
|
|
55
|
+
}
|
|
56
|
+
// Determine scope (global vs local)
|
|
57
|
+
let configPathType = 'global';
|
|
58
|
+
if (fullLocalPath && isInteractive(flags)) {
|
|
59
|
+
const scopeChoice = await autocomplete({
|
|
60
|
+
message: green('Configure global or project-local settings?'),
|
|
61
|
+
source: [`Local (${relative(cwd, fullLocalPath)})`, `Global (${globalPath})`],
|
|
62
|
+
});
|
|
63
|
+
if (!scopeChoice) {
|
|
64
|
+
return this.error('Scope selection is required');
|
|
65
|
+
}
|
|
66
|
+
configPathType = scopeChoice.startsWith('Local') ? 'local' : 'global';
|
|
67
|
+
}
|
|
68
|
+
const configPath = configPathType === 'local' ? fullLocalPath : globalPath;
|
|
69
|
+
const displayConfigPath = configPathType === 'local' ? relative(cwd, configPath) : configPath;
|
|
70
|
+
this.log(`Configuring ${ConfigPaths[selectedEditor].name} at ${displayConfigPath}`);
|
|
71
|
+
// Read/Parse existing config
|
|
72
|
+
let editorConfig = {};
|
|
73
|
+
if (existsSync(configPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const content = readFileSync(configPath, 'utf8');
|
|
76
|
+
const parsed = parseJSON(content);
|
|
77
|
+
editorConfig = (parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {});
|
|
78
|
+
this.log(`✓ Read existing configuration`);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return this.error(`Failed to parse configuration file: ${error}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.log(`Configuration file not found, creating new one`);
|
|
86
|
+
}
|
|
87
|
+
// Get servers config section
|
|
88
|
+
const serversConfig = getServersConfig(editorConfig);
|
|
89
|
+
// Check for existing Quonfig entries
|
|
90
|
+
const existingQuonfigEntries = Object.keys(serversConfig).filter((key) => /quonfig/i.test(key));
|
|
91
|
+
// Determine entry key
|
|
92
|
+
let targetEntryKey = 'quonfig';
|
|
93
|
+
if (existingQuonfigEntries.length > 0 && isInteractive(flags)) {
|
|
94
|
+
const choices = [`Add: ${targetEntryKey}`, ...existingQuonfigEntries.map((key) => `Update: ${key}`)];
|
|
95
|
+
const choice = await autocomplete({
|
|
96
|
+
message: green('Add a new MCP server or update an existing one?'),
|
|
97
|
+
source: choices,
|
|
98
|
+
});
|
|
99
|
+
if (!choice) {
|
|
100
|
+
return this.error('MCP server selection is required');
|
|
101
|
+
}
|
|
102
|
+
if (choice.startsWith('Update:')) {
|
|
103
|
+
targetEntryKey = choice.replace('Update: ', '');
|
|
104
|
+
this.log(`Updating existing MCP server entry: ${targetEntryKey}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.log(`Adding new MCP server entry: ${targetEntryKey}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (existingQuonfigEntries.length > 0) {
|
|
111
|
+
// Non-interactive mode with existing entries - update the first one
|
|
112
|
+
targetEntryKey = existingQuonfigEntries[0];
|
|
113
|
+
this.log(`Updating existing MCP server entry: ${targetEntryKey}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
this.log(`Adding new MCP server entry: ${targetEntryKey}`);
|
|
117
|
+
}
|
|
118
|
+
// Construct MCP endpoint URL and config
|
|
119
|
+
const mcpUrl = flags.url || 'https://app.quonfig.com/api/v1/mcp';
|
|
120
|
+
if (selectedEditor === 'claude-code') {
|
|
121
|
+
serversConfig[targetEntryKey] = {
|
|
122
|
+
type: 'http',
|
|
123
|
+
url: mcpUrl,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
serversConfig[targetEntryKey] = {
|
|
128
|
+
url: mcpUrl,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Write config file
|
|
132
|
+
try {
|
|
133
|
+
// Ensure directory exists
|
|
134
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
135
|
+
const configString = stringifyJSON(editorConfig, null, 2);
|
|
136
|
+
writeFileSync(configPath, configString);
|
|
137
|
+
this.log(`✓ Configuration updated successfully`);
|
|
138
|
+
this.log('');
|
|
139
|
+
this.log('You may need to restart your editor for changes to take effect.');
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return this.error(`Failed to write configuration file: ${error}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { APICommand } from '../index.js';
|
|
2
|
+
import { JsonObj } from '../result.js';
|
|
3
|
+
export default class Override extends APICommand {
|
|
4
|
+
static args: {
|
|
5
|
+
name: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
environment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
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
|
+
run(): Promise<JsonObj | void>;
|
|
15
|
+
private removeOverride;
|
|
16
|
+
private setOverride;
|
|
17
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
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
|
+
import { checkmark } from '../util/color.js';
|
|
7
|
+
import isInteractive from '../util/is-interactive.js';
|
|
8
|
+
import nameArg from '../util/name-arg.js';
|
|
9
|
+
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';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %> # will prompt for name and value',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> my.flag.name --value=true',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> my.flag.name --remove',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> my.double.config --value=3.14159',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
environment: Flags.string({ description: 'environment to override in' }),
|
|
20
|
+
remove: Flags.boolean({ default: false, description: 'remove your override (if present)' }),
|
|
21
|
+
value: Flags.string({ description: 'value to use for your override' }),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { args, flags } = await this.parse(Override);
|
|
25
|
+
if (flags.remove && flags.value) {
|
|
26
|
+
this.err('remove and value flags are mutually exclusive');
|
|
27
|
+
}
|
|
28
|
+
// Fetch all configs from metadata endpoint
|
|
29
|
+
const metadataRequest = await this.apiClient.post('/api/v1/metadata/list', { workspaceId: this.workspaceId });
|
|
30
|
+
if (!metadataRequest.ok) {
|
|
31
|
+
const errorMsg = metadataRequest.error?.error || `Failed to fetch configs: ${metadataRequest.status}`;
|
|
32
|
+
return this.err(errorMsg, { serverError: metadataRequest.error });
|
|
33
|
+
}
|
|
34
|
+
const metadataResponse = metadataRequest.json;
|
|
35
|
+
const configs = metadataResponse.configs;
|
|
36
|
+
// Get the environment
|
|
37
|
+
const environment = await getEnvironment({
|
|
38
|
+
command: this,
|
|
39
|
+
flags,
|
|
40
|
+
message: 'Which environment would you like to override in?',
|
|
41
|
+
providedEnvironment: flags.environment,
|
|
42
|
+
});
|
|
43
|
+
if (!environment) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Get the key - from args or prompt
|
|
47
|
+
let key = args.name;
|
|
48
|
+
if (!key && isInteractive(flags)) {
|
|
49
|
+
const configKeys = configs.map((c) => c.key);
|
|
50
|
+
const selectedKey = await autocomplete({
|
|
51
|
+
message: 'Which item would you like to override?',
|
|
52
|
+
source: configKeys,
|
|
53
|
+
});
|
|
54
|
+
if (selectedKey) {
|
|
55
|
+
key = selectedKey;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!key) {
|
|
59
|
+
return this.err('Key is required');
|
|
60
|
+
}
|
|
61
|
+
const config = configs.find((c) => c.key === key);
|
|
62
|
+
if (!config) {
|
|
63
|
+
return this.err(`Could not find config named ${key}`);
|
|
64
|
+
}
|
|
65
|
+
this.verboseLog('Selected config:', config);
|
|
66
|
+
if (flags.remove) {
|
|
67
|
+
return this.removeOverride(key, environment.id, config.version);
|
|
68
|
+
}
|
|
69
|
+
// Get the value
|
|
70
|
+
let value = flags.value;
|
|
71
|
+
if (!value && isInteractive(flags)) {
|
|
72
|
+
value = await getString({
|
|
73
|
+
allowBlank: false,
|
|
74
|
+
message: 'Override value',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!value) {
|
|
78
|
+
return this.err('Value is required');
|
|
79
|
+
}
|
|
80
|
+
return this.setOverride(config, value, environment.id);
|
|
81
|
+
}
|
|
82
|
+
async removeOverride(key, environmentId, currentVersionId) {
|
|
83
|
+
const request = await this.apiClient.post('/internal/ops/v1/remove-variant', {
|
|
84
|
+
configKey: key,
|
|
85
|
+
currentVersionId,
|
|
86
|
+
environmentId,
|
|
87
|
+
});
|
|
88
|
+
if (request.ok) {
|
|
89
|
+
this.log(`${checkmark} Override removed`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Handle 404 case - no override exists to remove
|
|
93
|
+
if (request.status === 404) {
|
|
94
|
+
this.log(`No override found for ${key}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.err(`Failed to remove override: ${request.status}`, { key, serverError: request.error });
|
|
98
|
+
}
|
|
99
|
+
async setOverride(config, value, environmentId) {
|
|
100
|
+
const { key, valueType, version } = config;
|
|
101
|
+
// Map the valueType to the format expected by the API
|
|
102
|
+
/* eslint-disable camelcase */
|
|
103
|
+
const typeMapping = {
|
|
104
|
+
bool: 'bool',
|
|
105
|
+
string: 'string',
|
|
106
|
+
int: 'int',
|
|
107
|
+
double: 'double',
|
|
108
|
+
string_list: 'stringList',
|
|
109
|
+
json: 'json',
|
|
110
|
+
limit_definition: 'limitDefinition',
|
|
111
|
+
duration: 'duration',
|
|
112
|
+
int_range: 'intRange',
|
|
113
|
+
};
|
|
114
|
+
/* eslint-enable camelcase */
|
|
115
|
+
const type = typeMapping[valueType.toLowerCase()] || valueType;
|
|
116
|
+
// Parse the value based on type and build variant object
|
|
117
|
+
let variantValue = value;
|
|
118
|
+
let variantType = type;
|
|
119
|
+
switch (type) {
|
|
120
|
+
case 'stringList': {
|
|
121
|
+
variantValue = value.split(',');
|
|
122
|
+
variantType = 'string_list';
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'bool': {
|
|
126
|
+
variantValue = value.toLowerCase() === 'true';
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'int': {
|
|
130
|
+
variantValue = Number.parseInt(value, 10);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'double': {
|
|
134
|
+
variantValue = Number.parseFloat(value);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'json': {
|
|
138
|
+
try {
|
|
139
|
+
variantValue = JSON.parse(value);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return this.err(`Invalid JSON value: ${value}`);
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'string': {
|
|
147
|
+
variantValue = value;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
// No default
|
|
151
|
+
}
|
|
152
|
+
const request = await this.apiClient.post('/internal/ops/v1/assign-variant', {
|
|
153
|
+
configKey: key,
|
|
154
|
+
currentVersionId: version,
|
|
155
|
+
environmentId,
|
|
156
|
+
variant: {
|
|
157
|
+
type: variantType,
|
|
158
|
+
value: variantValue,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
if (request.ok) {
|
|
162
|
+
this.log(`${checkmark} Override set`);
|
|
163
|
+
return { key, success: true };
|
|
164
|
+
}
|
|
165
|
+
const errMsg = request.status === 400
|
|
166
|
+
? `Failed to override value: ${request.status} -- is ${value} a valid ${type}?`
|
|
167
|
+
: `Failed to override value: ${request.status}`;
|
|
168
|
+
this.err(errMsg, { key, serverError: request.error });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
import { loadAuthConfig, saveAuthConfig } from '../util/token-storage.js';
|
|
4
|
+
export default class Profile extends BaseCommand {
|
|
5
|
+
static description = 'Manage profiles and set default profile';
|
|
6
|
+
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
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
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { APICommand } from '../index.js';
|
|
2
|
+
import { JsonObj } from '../result.js';
|
|
3
|
+
export default class Schema extends APICommand {
|
|
4
|
+
static args: {
|
|
5
|
+
name: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
get: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
|
+
protected: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
12
|
+
'set-json-schema': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
'set-zod': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<JsonObj | void>;
|
|
16
|
+
private getSchema;
|
|
17
|
+
private upsertSchema;
|
|
18
|
+
private fetchSchema;
|
|
19
|
+
private createSchema;
|
|
20
|
+
private updateSchema;
|
|
21
|
+
}
|