@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,7 @@
1
+ import type { RequestResult } from '../result.js';
2
+ import { APICommand } from '../index.js';
3
+ import { Client } from '@quonfig/node';
4
+ export declare const resetClientCache: () => void;
5
+ declare const getClient: (command: APICommand, sdkKey?: string, profile?: string) => Promise<Client>;
6
+ export declare const unwrapRequest: (command: APICommand, promise: Promise<Response>) => Promise<RequestResult>;
7
+ export default getClient;
@@ -0,0 +1,127 @@
1
+ import { Client } from '@quonfig/node';
2
+ import { getApiUrl } from '../util/domain-urls.js';
3
+ import jsonMaybe from '../util/json-maybe.js';
4
+ import { decodeJWT, refreshAccessToken } from '../util/oauth-client.js';
5
+ import { getActiveProfile, loadAuthConfig, loadTokens, saveTokens } from '../util/token-storage.js';
6
+ import version from '../version.js';
7
+ let clientInstance;
8
+ let cachedWorkspaceId;
9
+ export const resetClientCache = () => {
10
+ clientInstance = undefined;
11
+ cachedWorkspaceId = undefined;
12
+ };
13
+ const getClient = async (command, sdkKey, profile) => {
14
+ // If client exists, still set workspaceId on command if available
15
+ if (clientInstance) {
16
+ if (cachedWorkspaceId) {
17
+ command.workspaceId = cachedWorkspaceId;
18
+ }
19
+ return clientInstance;
20
+ }
21
+ let jwt;
22
+ let workspaceId;
23
+ // If no SDK key provided, use WorkOS OAuth tokens
24
+ if (!sdkKey) {
25
+ const authConfig = await loadAuthConfig();
26
+ let tokens = await loadTokens();
27
+ command.verboseLog('OAuth auth', {
28
+ hasAuthConfig: Boolean(authConfig),
29
+ hasAccessToken: Boolean(tokens?.accessToken),
30
+ });
31
+ if (!authConfig || !tokens?.accessToken) {
32
+ command.error('No authentication found. Please run `qfg login`.', { exit: 401 });
33
+ }
34
+ // Check if token is expired and refresh if needed.
35
+ // Prefer the JWT's actual exp claim over the stored expiresAt.
36
+ let tokenExpired = tokens.expiresAt ? tokens.expiresAt < Date.now() : false;
37
+ try {
38
+ const payload = decodeJWT(tokens.accessToken);
39
+ if (typeof payload.exp === 'number') {
40
+ tokenExpired = payload.exp * 1000 < Date.now();
41
+ }
42
+ }
43
+ catch {
44
+ // If we can't decode, fall back to stored expiresAt
45
+ }
46
+ if (tokenExpired && tokens.refreshToken) {
47
+ command.verboseLog('Token expired, refreshing...');
48
+ try {
49
+ const refreshed = await refreshAccessToken(tokens.refreshToken);
50
+ // Use the JWT's actual exp claim for expiry, not a hardcoded duration
51
+ let expiresAt = Date.now() + 300 * 1000; // fallback: 5 minutes
52
+ try {
53
+ const payload = decodeJWT(refreshed.access_token);
54
+ if (typeof payload.exp === 'number') {
55
+ expiresAt = payload.exp * 1000; // convert seconds to ms
56
+ }
57
+ }
58
+ catch {
59
+ // Use fallback
60
+ }
61
+ tokens = {
62
+ ...tokens,
63
+ accessToken: refreshed.access_token,
64
+ expiresAt,
65
+ refreshToken: refreshed.refresh_token,
66
+ };
67
+ await saveTokens(tokens);
68
+ command.verboseLog('Token refreshed successfully');
69
+ }
70
+ catch (error) {
71
+ command.error('Session expired. Please run `qfg login` to re-authenticate.', { exit: 401 });
72
+ }
73
+ }
74
+ // Get the active profile to find the workspace
75
+ const activeProfile = getActiveProfile(profile);
76
+ const profileData = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
77
+ command.verboseLog('Profile lookup', {
78
+ activeProfile,
79
+ hasProfileData: Boolean(profileData),
80
+ workspaceId: profileData?.workspace,
81
+ });
82
+ if (profileData) {
83
+ workspaceId = profileData.workspace;
84
+ }
85
+ // Use the WorkOS access token directly as the JWT
86
+ jwt = tokens.accessToken;
87
+ // If still no JWT, user needs to login
88
+ if (!jwt) {
89
+ command.error('No authentication found. Please run `qfg login`.', { exit: 401 });
90
+ }
91
+ }
92
+ // Store workspaceId on the command for use in building URLs and cache it
93
+ if (workspaceId) {
94
+ command.workspaceId = workspaceId;
95
+ cachedWorkspaceId = workspaceId;
96
+ }
97
+ clientInstance = new Client({
98
+ jwt,
99
+ sdkKey,
100
+ apiUrl: getApiUrl(),
101
+ clientIdentifier: `cli-${version}`,
102
+ log: command.verboseLog,
103
+ });
104
+ return clientInstance;
105
+ };
106
+ export const unwrapRequest = async (command, promise) => {
107
+ const request = await promise;
108
+ if (request.status.toString().startsWith('2')) {
109
+ const json = (await request.json());
110
+ command.verboseLog('ApiClient', { response: json });
111
+ return { json, ok: true, status: request.status };
112
+ }
113
+ // Handle 403 with a user-friendly message
114
+ if (request.status === 403) {
115
+ return {
116
+ error: { error: 'You do not have permission to perform this action. Please check your workspace permissions.' },
117
+ ok: false,
118
+ status: request.status,
119
+ };
120
+ }
121
+ const error = jsonMaybe(await request.text());
122
+ if (typeof error === 'string') {
123
+ return { error: { error }, ok: false, status: request.status };
124
+ }
125
+ return { error, ok: false, status: request.status };
126
+ };
127
+ export default getClient;
@@ -0,0 +1,5 @@
1
+ type Flags = {
2
+ interactive?: boolean;
3
+ };
4
+ declare const isInteractive: (flags: Flags) => boolean | undefined;
5
+ export default isInteractive;
@@ -0,0 +1,7 @@
1
+ const isInteractive = (flags) => {
2
+ if (flags.interactive === false) {
3
+ return false;
4
+ }
5
+ return (process.stdout.isTTY && process.stdin.isTTY) || flags.interactive;
6
+ };
7
+ export default isInteractive;
@@ -0,0 +1,3 @@
1
+ import type { JsonObj } from '../result.js';
2
+ declare const jsonMaybe: (json: JsonObj | string) => JsonObj | string;
3
+ export default jsonMaybe;
@@ -0,0 +1,14 @@
1
+ // Sometimes we get JSON from the server but if there's an exception, we might get a string.
2
+ // This function will try to parse the string as JSON and return the parsed object or the original string.
3
+ const jsonMaybe = (json) => {
4
+ if (typeof json === 'string') {
5
+ try {
6
+ return JSON.parse(json);
7
+ }
8
+ catch {
9
+ return json;
10
+ }
11
+ }
12
+ return json;
13
+ };
14
+ export default jsonMaybe;
@@ -0,0 +1,4 @@
1
+ declare const nameArg: {
2
+ name: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
3
+ };
4
+ export default nameArg;
@@ -0,0 +1,3 @@
1
+ import { Args } from '@oclif/core';
2
+ const nameArg = { name: Args.string({ description: 'config/feature-flag/etc. name' }) };
3
+ export default nameArg;
@@ -0,0 +1,30 @@
1
+ export interface DeviceCodeResponse {
2
+ device_code: string;
3
+ expires_in: number;
4
+ interval: number;
5
+ user_code: string;
6
+ verification_uri: string;
7
+ verification_uri_complete: string;
8
+ }
9
+ export interface WorkOSUser {
10
+ email: string;
11
+ email_verified: boolean;
12
+ first_name: string | null;
13
+ id: string;
14
+ last_name: string | null;
15
+ organization_id?: string;
16
+ }
17
+ export interface DeviceTokenResponse {
18
+ access_token: string;
19
+ authentication_method: string;
20
+ refresh_token: string;
21
+ user: WorkOSUser;
22
+ }
23
+ export interface TokenRefreshResponse {
24
+ access_token: string;
25
+ refresh_token: string;
26
+ }
27
+ export declare const requestDeviceCode: () => Promise<DeviceCodeResponse>;
28
+ export declare const pollForToken: (deviceCode: string, interval: number, expiresIn: number, verbose?: boolean) => Promise<DeviceTokenResponse>;
29
+ export declare const refreshAccessToken: (refreshToken: string) => Promise<TokenRefreshResponse>;
30
+ export declare const decodeJWT: (token: string) => Record<string, unknown>;
@@ -0,0 +1,99 @@
1
+ import { getDomain } from './domain-urls.js';
2
+ const WORKOS_BASE_URL = 'https://api.workos.com';
3
+ // WorkOS Client IDs for the CLI (public client — no secret needed)
4
+ const WORKOS_CLIENT_ID_PRODUCTION = 'client_01KKCCWMNB2K8HYQZQ96G1MXE0';
5
+ const WORKOS_CLIENT_ID_STAGING = 'client_01KKCCWM4K87JS0MKS8PHZ0ZY1';
6
+ const getClientId = () => {
7
+ if (process.env.WORKOS_CLIENT_ID) {
8
+ return process.env.WORKOS_CLIENT_ID;
9
+ }
10
+ const domain = getDomain();
11
+ if (domain === 'quonfig-staging.com') {
12
+ return WORKOS_CLIENT_ID_STAGING;
13
+ }
14
+ return WORKOS_CLIENT_ID_PRODUCTION;
15
+ };
16
+ // --- Device Code Flow ---
17
+ export const requestDeviceCode = async () => {
18
+ const response = await fetch(`${WORKOS_BASE_URL}/user_management/authorize/device`, {
19
+ body: new URLSearchParams({
20
+ client_id: getClientId(),
21
+ }),
22
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
23
+ method: 'POST',
24
+ });
25
+ if (!response.ok) {
26
+ const errorText = await response.text();
27
+ throw new Error(`Failed to request device code: ${errorText}`);
28
+ }
29
+ return response.json();
30
+ };
31
+ export const pollForToken = async (deviceCode, interval, expiresIn, verbose) => {
32
+ const deadline = Date.now() + expiresIn * 1000;
33
+ let pollInterval = interval * 1000;
34
+ while (Date.now() < deadline) {
35
+ // eslint-disable-next-line no-await-in-loop
36
+ await new Promise((resolve) => {
37
+ setTimeout(resolve, pollInterval);
38
+ });
39
+ if (verbose) {
40
+ console.error('[pollForToken] Polling for device authorization...');
41
+ }
42
+ // eslint-disable-next-line no-await-in-loop
43
+ const response = await fetch(`${WORKOS_BASE_URL}/user_management/authenticate`, {
44
+ body: new URLSearchParams({
45
+ client_id: getClientId(),
46
+ device_code: deviceCode,
47
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
48
+ }),
49
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
50
+ method: 'POST',
51
+ });
52
+ if (response.ok) {
53
+ return response.json();
54
+ }
55
+ // eslint-disable-next-line no-await-in-loop
56
+ const error = await response.json().catch(() => ({}));
57
+ const errorCode = error.error || error.code;
58
+ if (errorCode === 'authorization_pending') {
59
+ continue;
60
+ }
61
+ if (errorCode === 'slow_down') {
62
+ pollInterval += 1000;
63
+ continue;
64
+ }
65
+ if (errorCode === 'access_denied') {
66
+ throw new Error('Authorization was denied by the user.');
67
+ }
68
+ if (errorCode === 'expired_token') {
69
+ throw new Error('Device code expired. Please try logging in again.');
70
+ }
71
+ throw new Error(`Authentication failed: ${JSON.stringify(error)}`);
72
+ }
73
+ throw new Error('Device code expired. Please try logging in again.');
74
+ };
75
+ // --- Token Refresh ---
76
+ export const refreshAccessToken = async (refreshToken) => {
77
+ const response = await fetch(`${WORKOS_BASE_URL}/user_management/authenticate`, {
78
+ body: new URLSearchParams({
79
+ client_id: getClientId(),
80
+ grant_type: 'refresh_token',
81
+ refresh_token: refreshToken,
82
+ }),
83
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
84
+ method: 'POST',
85
+ });
86
+ if (!response.ok) {
87
+ const errorText = await response.text();
88
+ throw new Error(`Failed to refresh token: ${errorText}`);
89
+ }
90
+ return response.json();
91
+ };
92
+ // --- JWT Utilities ---
93
+ export const decodeJWT = (token) => {
94
+ const parts = token.split('.');
95
+ if (parts.length !== 3) {
96
+ throw new Error('Invalid JWT format');
97
+ }
98
+ return JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
99
+ };
@@ -0,0 +1,13 @@
1
+ declare const secretFlags: (secretDescription: string) => {
2
+ secret: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
3
+ 'secret-key-name': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
4
+ };
5
+ export type Secret = {
6
+ keyName: string;
7
+ selected: boolean;
8
+ };
9
+ export declare const parsedSecretFlags: (flags: {
10
+ secret: boolean;
11
+ "secret-key-name": string;
12
+ }) => Secret;
13
+ export default secretFlags;
@@ -0,0 +1,13 @@
1
+ import { Flags } from '@oclif/core';
2
+ const secretFlags = (secretDescription) => ({
3
+ secret: Flags.boolean({ default: false, description: secretDescription }),
4
+ 'secret-key-name': Flags.string({
5
+ default: 'quonfig.secrets.encryption.key',
6
+ description: 'name of the secret key to use for encryption/decryption',
7
+ }),
8
+ });
9
+ export const parsedSecretFlags = (flags) => ({
10
+ keyName: flags['secret-key-name'],
11
+ selected: flags.secret,
12
+ });
13
+ export default secretFlags;
@@ -0,0 +1,25 @@
1
+ export interface TokenStorageOptions {
2
+ quonfigDir?: string;
3
+ }
4
+ export interface TokenData {
5
+ accessToken: string;
6
+ expiresAt: number;
7
+ refreshToken: string;
8
+ userEmail?: string;
9
+ userId?: string;
10
+ }
11
+ export interface AuthConfig {
12
+ defaultProfile?: string;
13
+ profiles: {
14
+ [profileName: string]: {
15
+ workspace: string;
16
+ workspaceName?: string;
17
+ };
18
+ };
19
+ }
20
+ export declare const saveTokens: (tokens: TokenData, options?: TokenStorageOptions) => Promise<void>;
21
+ export declare const loadTokens: (options?: TokenStorageOptions) => Promise<TokenData | null>;
22
+ export declare const saveAuthConfig: (config: AuthConfig, options?: TokenStorageOptions) => Promise<void>;
23
+ export declare const loadAuthConfig: (options?: TokenStorageOptions) => Promise<AuthConfig | null>;
24
+ export declare const getActiveProfile: (profileArg?: string) => string;
25
+ export declare const clearAuth: (options?: TokenStorageOptions) => Promise<void>;
@@ -0,0 +1,90 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ const getQuonfigDir = (options) => options?.quonfigDir || path.join(os.homedir(), '.quonfig');
5
+ const getTokenFile = (options) => path.join(getQuonfigDir(options), 'tokens.json');
6
+ const getConfigFile = (options) => path.join(getQuonfigDir(options), 'config');
7
+ const ensureQuonfigDir = async (options) => {
8
+ const quonfigDir = getQuonfigDir(options);
9
+ try {
10
+ await fs.promises.access(quonfigDir, fs.constants.F_OK);
11
+ }
12
+ catch {
13
+ await fs.promises.mkdir(quonfigDir, { recursive: true });
14
+ }
15
+ };
16
+ export const saveTokens = async (tokens, options) => {
17
+ await ensureQuonfigDir(options);
18
+ await fs.promises.writeFile(getTokenFile(options), JSON.stringify(tokens, null, 2), 'utf8');
19
+ };
20
+ export const loadTokens = async (options) => {
21
+ try {
22
+ const data = await fs.promises.readFile(getTokenFile(options), 'utf8');
23
+ return JSON.parse(data);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ };
29
+ export const saveAuthConfig = async (config, options) => {
30
+ await ensureQuonfigDir(options);
31
+ let configContent = '';
32
+ // Write default profile if specified
33
+ if (config.defaultProfile) {
34
+ configContent += `default_profile = ${config.defaultProfile}\n\n`;
35
+ }
36
+ // Write each profile
37
+ for (const [profileName, profileData] of Object.entries(config.profiles)) {
38
+ configContent += `[profile ${profileName}]\n`;
39
+ configContent += `workspace = ${profileData.workspace}`;
40
+ if (profileData.workspaceName) {
41
+ configContent += ` # ${profileData.workspaceName}`;
42
+ }
43
+ configContent += '\n\n';
44
+ }
45
+ await fs.promises.writeFile(getConfigFile(options), configContent, 'utf8');
46
+ };
47
+ export const loadAuthConfig = async (options) => {
48
+ try {
49
+ const data = await fs.promises.readFile(getConfigFile(options), 'utf8');
50
+ const config = {
51
+ profiles: {},
52
+ };
53
+ // Parse default profile
54
+ const defaultMatch = data.match(/default_profile\s*=\s*(.+)/);
55
+ if (defaultMatch && defaultMatch[1]) {
56
+ config.defaultProfile = defaultMatch[1].trim();
57
+ }
58
+ // Parse profiles
59
+ const profileRegex = /\[profile\s+(\w+)]\s*\n\s*workspace\s*=\s*([^\s#]+)(?:\s*#\s*(.+))?/g;
60
+ let match;
61
+ while ((match = profileRegex.exec(data)) !== null) {
62
+ const profileName = match[1];
63
+ const workspace = match[2];
64
+ const workspaceName = match[3]?.trim();
65
+ config.profiles[profileName] = {
66
+ workspace,
67
+ workspaceName,
68
+ };
69
+ }
70
+ return Object.keys(config.profiles).length > 0 ? config : null;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ };
76
+ export const getActiveProfile = (profileArg) => profileArg || process.env.QUONFIG_PROFILE || 'default';
77
+ export const clearAuth = async (options) => {
78
+ try {
79
+ await fs.promises.unlink(getTokenFile(options));
80
+ }
81
+ catch {
82
+ // Ignore if file doesn't exist
83
+ }
84
+ try {
85
+ await fs.promises.unlink(getConfigFile(options));
86
+ }
87
+ catch {
88
+ // Ignore if file doesn't exist
89
+ }
90
+ };
@@ -0,0 +1,4 @@
1
+ import { Quonfig } from '@quonfig/node';
2
+ import { Result } from '../result.js';
3
+ declare const validateValue: (quonfig: Quonfig, key: string, value: string) => Result<string>;
4
+ export default validateValue;
@@ -0,0 +1,11 @@
1
+ import { failure, success } from '../result.js';
2
+ const validateValue = (quonfig, key, value) => {
3
+ const config = quonfig.rawConfig(key);
4
+ if (!config) {
5
+ return failure(`Could not find config named ${key}`);
6
+ }
7
+ // In the new quonfig format, allowableValues are not yet supported at the config level.
8
+ // This validation is a placeholder — the server enforces schema constraints.
9
+ return success(value);
10
+ };
11
+ export default validateValue;
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone workspace validator.
4
+ *
5
+ * Usage:
6
+ * qfg-verify <workspace-dir> Validate a workspace directory on disk
7
+ * qfg-verify --git-hook Run as git pre-receive hook (reads stdin for refs,
8
+ * uses git commands to read files from the pushed commit)
9
+ *
10
+ * Exit codes:
11
+ * 0 All checks pass
12
+ * 1 Validation errors found
13
+ */
14
+ export {};
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone workspace validator.
4
+ *
5
+ * Usage:
6
+ * qfg-verify <workspace-dir> Validate a workspace directory on disk
7
+ * qfg-verify --git-hook Run as git pre-receive hook (reads stdin for refs,
8
+ * uses git commands to read files from the pushed commit)
9
+ *
10
+ * Exit codes:
11
+ * 0 All checks pass
12
+ * 1 Validation errors found
13
+ */
14
+ import { execSync } from "node:child_process";
15
+ import * as readline from "node:readline";
16
+ import { formatResult, validateFileMap, validateWorkspace } from "./validate.js";
17
+ async function main() {
18
+ const args = process.argv.slice(2);
19
+ if (args.includes("--git-hook")) {
20
+ await runGitHook();
21
+ }
22
+ else if (args.includes("--help") || args.includes("-h")) {
23
+ printUsage();
24
+ }
25
+ else {
26
+ const dir = args[0] || ".";
27
+ runDiskValidation(dir);
28
+ }
29
+ }
30
+ function printUsage() {
31
+ console.log(`qfg-verify - Quonfig workspace validator
32
+
33
+ Usage:
34
+ qfg-verify [path] Validate workspace directory (default: .)
35
+ qfg-verify --git-hook Run as git pre-receive hook
36
+ qfg-verify --help Show this help
37
+
38
+ Exit codes:
39
+ 0 All checks pass
40
+ 1 Validation errors found`);
41
+ }
42
+ function runDiskValidation(dir) {
43
+ const result = validateWorkspace(dir);
44
+ console.log(formatResult(result));
45
+ process.exit(result.valid ? 0 : 1);
46
+ }
47
+ /**
48
+ * Git pre-receive hook mode.
49
+ *
50
+ * Reads stdin for pushed refs (one line per ref: <old-oid> <new-oid> <ref-name>).
51
+ * For each new commit, lists all config files and validates them.
52
+ */
53
+ async function runGitHook() {
54
+ const refs = [];
55
+ // Read all of stdin (ref lines). Use callback API for bun compatibility.
56
+ await new Promise((resolve) => {
57
+ const rl = readline.createInterface({ input: process.stdin });
58
+ rl.on("line", (line) => {
59
+ const parts = line.trim().split(" ");
60
+ if (parts.length >= 3) {
61
+ refs.push({ oldOid: parts[0], newOid: parts[1], refName: parts[2] });
62
+ }
63
+ });
64
+ rl.on("close", resolve);
65
+ });
66
+ if (refs.length === 0) {
67
+ console.log("qfg-verify: no refs received");
68
+ process.exit(0);
69
+ }
70
+ // Validate the latest pushed commit for each ref
71
+ let hasErrors = false;
72
+ for (const ref of refs) {
73
+ // Skip deletions
74
+ if (ref.newOid === "0000000000000000000000000000000000000000")
75
+ continue;
76
+ console.log(`qfg-verify: validating ${ref.refName} (${ref.newOid.slice(0, 8)})`);
77
+ try {
78
+ const files = readFilesFromCommit(ref.newOid);
79
+ const result = validateFileMap(files);
80
+ console.log(formatResult(result));
81
+ if (!result.valid) {
82
+ hasErrors = true;
83
+ }
84
+ }
85
+ catch (err) {
86
+ console.error(`qfg-verify: error reading commit ${ref.newOid}: ${err.message}`);
87
+ hasErrors = true;
88
+ }
89
+ }
90
+ process.exit(hasErrors ? 1 : 0);
91
+ }
92
+ /**
93
+ * Read all JSON config files from a git commit using `git show`.
94
+ * Works in bare repos (no working tree needed).
95
+ */
96
+ function readFilesFromCommit(commitOid) {
97
+ const files = new Map();
98
+ const dirs = ["configs", "feature-flags", "segments", "log-levels", "schemas", "schemas-protected"];
99
+ for (const dir of dirs) {
100
+ // List files in this directory at the given commit
101
+ let listing;
102
+ try {
103
+ listing = execSync(`git ls-tree --name-only ${commitOid} ${dir}/`, {
104
+ encoding: "utf-8",
105
+ stdio: ["pipe", "pipe", "pipe"],
106
+ });
107
+ }
108
+ catch {
109
+ // Directory doesn't exist in this commit
110
+ continue;
111
+ }
112
+ const filenames = listing.trim().split("\n").filter(Boolean);
113
+ for (const filePath of filenames) {
114
+ if (!filePath.endsWith(".json") || filePath.includes("/."))
115
+ continue;
116
+ try {
117
+ const content = execSync(`git show ${commitOid}:${filePath}`, {
118
+ encoding: "utf-8",
119
+ stdio: ["pipe", "pipe", "pipe"],
120
+ });
121
+ files.set(filePath, content);
122
+ }
123
+ catch {
124
+ // File can't be read, skip
125
+ }
126
+ }
127
+ }
128
+ return files;
129
+ }
130
+ main().catch((err) => {
131
+ console.error(`qfg-verify: fatal: ${err.message}`);
132
+ process.exit(1);
133
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Workspace validation logic.
3
+ *
4
+ * This module validates a Quonfig workspace directory against the StoredConfig
5
+ * schema. It can be used from both the oclif CLI (`qfg verify`) and the
6
+ * standalone compiled binary (git pre-receive hook).
7
+ *
8
+ * Validation checks:
9
+ * - Valid JSON
10
+ * - Schema compliance (StoredConfigSchema from Zod)
11
+ * - Key matches filename
12
+ * - Key constraints (1-512 chars, no slashes, not "new")
13
+ * - Config type matches directory
14
+ * - Segment constraints (valueType=bool, sendToClientSdk=false)
15
+ * - Log level constraints (valueType=log_level)
16
+ * - Referential integrity (IN_SEG/NOT_IN_SEG reference existing segments)
17
+ * - schemaKey references existing schemas
18
+ * - Rule structure (criteria + value present)
19
+ * - Value type consistency
20
+ * - Weighted values non-empty and consistent
21
+ */
22
+ export type Severity = "error" | "warning";
23
+ export interface ValidationIssue {
24
+ file: string;
25
+ message: string;
26
+ severity: Severity;
27
+ suggestion?: string;
28
+ }
29
+ export interface ValidationResult {
30
+ issues: ValidationIssue[];
31
+ filesChecked: number;
32
+ valid: boolean;
33
+ }
34
+ /**
35
+ * Validate an entire workspace directory on disk.
36
+ */
37
+ export declare function validateWorkspace(workspaceDir: string): ValidationResult;
38
+ /**
39
+ * Validate configs provided as a map of { "dir/file.json": jsonString }.
40
+ * Used by the pre-receive hook which reads files from git objects.
41
+ */
42
+ export declare function validateFileMap(files: Map<string, string>): ValidationResult;
43
+ export declare function formatResult(result: ValidationResult): string;