@quonfig/cli 0.0.1

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