@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,168 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { APICommand } from '../index.js';
|
|
4
|
+
import { checkmark } from '../util/color.js';
|
|
5
|
+
const isSchemaDocument = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
const loadSchemaDocument = (input) => {
|
|
7
|
+
const source = input.trim().startsWith('@') ? readFileSync(input.trim().slice(1), 'utf8') : input;
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = JSON.parse(source);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
throw new Error(`Schema documents must be valid JSON Schema objects: ${message}`);
|
|
15
|
+
}
|
|
16
|
+
if (!isSchemaDocument(parsed)) {
|
|
17
|
+
throw new Error('Schema documents must be JSON objects.');
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
};
|
|
21
|
+
export default class Schema extends APICommand {
|
|
22
|
+
static args = {
|
|
23
|
+
name: Args.string({ description: 'schema key', required: true }),
|
|
24
|
+
};
|
|
25
|
+
static description = 'Get or update first-class schema documents';
|
|
26
|
+
static examples = [
|
|
27
|
+
'<%= config.bin %> <%= command.id %> my-schema --get',
|
|
28
|
+
'<%= config.bin %> <%= command.id %> my-schema --set-json-schema=\'{"type":"object","properties":{}}\'',
|
|
29
|
+
'<%= config.bin %> <%= command.id %> my-schema --set-json-schema=@schemas/my-schema.json --protected',
|
|
30
|
+
];
|
|
31
|
+
static flags = {
|
|
32
|
+
get: Flags.boolean({ description: 'get the schema document' }),
|
|
33
|
+
protected: Flags.boolean({ default: false, description: 'store the schema in protected storage' }),
|
|
34
|
+
'set-json-schema': Flags.string({
|
|
35
|
+
description: 'set a plain JSON Schema document (inline JSON or @file path)',
|
|
36
|
+
}),
|
|
37
|
+
'set-zod': Flags.string({
|
|
38
|
+
description: 'compatibility alias for --set-json-schema; now expects plain JSON Schema',
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
async run() {
|
|
42
|
+
const { args, flags } = await this.parse(Schema);
|
|
43
|
+
if (!this.workspaceId) {
|
|
44
|
+
return this.err('Workspace ID not found. Please run `qfg login`.');
|
|
45
|
+
}
|
|
46
|
+
const schemaKey = args.name;
|
|
47
|
+
const schemaInput = flags['set-json-schema'] ?? flags['set-zod'];
|
|
48
|
+
if (flags.get && schemaInput) {
|
|
49
|
+
return this.err('Use either --get or --set-json-schema, not both.');
|
|
50
|
+
}
|
|
51
|
+
if (!flags.get && !schemaInput) {
|
|
52
|
+
return this.err('Use --get or --set-json-schema.');
|
|
53
|
+
}
|
|
54
|
+
if (flags.get) {
|
|
55
|
+
return this.getSchema(schemaKey);
|
|
56
|
+
}
|
|
57
|
+
return this.upsertSchema(schemaKey, schemaInput, flags.protected);
|
|
58
|
+
}
|
|
59
|
+
async getSchema(schemaKey) {
|
|
60
|
+
const request = await this.apiClient.post('/api/v1/schemas/getByKey', {
|
|
61
|
+
workspaceId: this.workspaceId,
|
|
62
|
+
schemaKey,
|
|
63
|
+
});
|
|
64
|
+
if (!request.ok) {
|
|
65
|
+
if (request.status === 404) {
|
|
66
|
+
return this.err(`Schema ${schemaKey} not found`, {
|
|
67
|
+
key: schemaKey,
|
|
68
|
+
phase: 'lookup',
|
|
69
|
+
serverError: request.error,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return this.err(`Failed to fetch schema ${schemaKey}: ${request.status} | ${JSON.stringify(request.error)}`, {
|
|
73
|
+
key: schemaKey,
|
|
74
|
+
phase: 'lookup',
|
|
75
|
+
serverError: request.error,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const response = request.json;
|
|
79
|
+
if (!isSchemaDocument(response.schema)) {
|
|
80
|
+
return this.err(`Schema ${schemaKey} returned an invalid document`, {
|
|
81
|
+
key: schemaKey,
|
|
82
|
+
phase: 'lookup',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return this.ok(JSON.stringify(response.schema, null, 2), {
|
|
86
|
+
commitSha: response.commitSha,
|
|
87
|
+
key: response.key,
|
|
88
|
+
protected: response.protected,
|
|
89
|
+
schema: response.schema,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async upsertSchema(schemaKey, schemaInput, protectedSchema) {
|
|
93
|
+
const schema = loadSchemaDocument(schemaInput);
|
|
94
|
+
const existing = await this.fetchSchema(schemaKey);
|
|
95
|
+
if (!existing) {
|
|
96
|
+
return this.createSchema(schemaKey, schema, protectedSchema);
|
|
97
|
+
}
|
|
98
|
+
return this.updateSchema(schemaKey, schema, existing, protectedSchema);
|
|
99
|
+
}
|
|
100
|
+
async fetchSchema(schemaKey) {
|
|
101
|
+
const request = await this.apiClient.post('/api/v1/schemas/getByKey', {
|
|
102
|
+
workspaceId: this.workspaceId,
|
|
103
|
+
schemaKey,
|
|
104
|
+
});
|
|
105
|
+
if (request.ok) {
|
|
106
|
+
const response = request.json;
|
|
107
|
+
if (isSchemaDocument(response.schema)) {
|
|
108
|
+
return response;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
if (request.status === 404) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`Failed to fetch schema ${schemaKey}: ${request.status} | ${JSON.stringify(request.error)}`);
|
|
116
|
+
}
|
|
117
|
+
async createSchema(schemaKey, schema, protectedSchema) {
|
|
118
|
+
const request = await this.apiClient.post('/api/v1/schemas/create', {
|
|
119
|
+
workspaceId: this.workspaceId,
|
|
120
|
+
schemaKey,
|
|
121
|
+
protected: protectedSchema,
|
|
122
|
+
schema,
|
|
123
|
+
});
|
|
124
|
+
if (!request.ok) {
|
|
125
|
+
if (request.status === 409) {
|
|
126
|
+
const current = await this.fetchSchema(schemaKey);
|
|
127
|
+
if (current) {
|
|
128
|
+
return this.updateSchema(schemaKey, schema, current, protectedSchema);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return this.err(`Failed to create schema: ${schemaKey} | ${request.status} | ${JSON.stringify(request.error)}`, {
|
|
132
|
+
key: schemaKey,
|
|
133
|
+
phase: 'creation',
|
|
134
|
+
serverError: request.error,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const response = request.json;
|
|
138
|
+
return this.ok(`${checkmark} Created schema: ${schemaKey}`, {
|
|
139
|
+
commitSha: response.commitSha,
|
|
140
|
+
key: response.key,
|
|
141
|
+
protected: response.protected,
|
|
142
|
+
schema: response.schema,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async updateSchema(schemaKey, schema, existing, protectedSchema) {
|
|
146
|
+
const request = await this.apiClient.post('/api/v1/schemas/update', {
|
|
147
|
+
workspaceId: this.workspaceId,
|
|
148
|
+
schemaKey,
|
|
149
|
+
protected: protectedSchema,
|
|
150
|
+
schema,
|
|
151
|
+
expectedCommitSha: existing.commitSha,
|
|
152
|
+
});
|
|
153
|
+
if (!request.ok) {
|
|
154
|
+
return this.err(`Failed to update schema: ${schemaKey} | ${request.status} | ${JSON.stringify(request.error)}`, {
|
|
155
|
+
key: schemaKey,
|
|
156
|
+
phase: 'update',
|
|
157
|
+
serverError: request.error,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const response = request.json;
|
|
161
|
+
return this.ok(`${checkmark} Updated schema: ${schemaKey}`, {
|
|
162
|
+
commitSha: response.commitSha,
|
|
163
|
+
key: response.key,
|
|
164
|
+
protected: response.protected,
|
|
165
|
+
schema: response.schema,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
export default class Serve extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
'data-file': import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
private requestHandler;
|
|
13
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { ConfigType } from '@quonfig/node';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import { BaseCommand } from '../index.js';
|
|
6
|
+
import { initQuonfig } from '../quonfig.js';
|
|
7
|
+
import { valueTypeStringForConfig } from '@quonfig/node';
|
|
8
|
+
import { javaScriptClientFormattedContextToContext } from '../util/context.js';
|
|
9
|
+
const allowCORSPreflight = (res) => {
|
|
10
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
11
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
12
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-quonfig-client-version, authorization');
|
|
13
|
+
};
|
|
14
|
+
const ALWAYS_SEND_CONFIG_TYPES = new Set([ConfigType.FeatureFlag, ConfigType.LogLevel]);
|
|
15
|
+
const base64ToBytes = (base64) => {
|
|
16
|
+
const binString = Buffer.from(base64, 'base64').toString('binary');
|
|
17
|
+
return Uint8Array.from(binString, (m) => m.codePointAt(0));
|
|
18
|
+
};
|
|
19
|
+
export default class Serve extends BaseCommand {
|
|
20
|
+
static args = {
|
|
21
|
+
'data-file': Args.string({ description: 'file to read', required: true }),
|
|
22
|
+
};
|
|
23
|
+
static description = `Serve a datafile on a local port
|
|
24
|
+
|
|
25
|
+
You can download a datafile using the \`download\` command.
|
|
26
|
+
|
|
27
|
+
You'll need to update your JavaScript (or React) client to point to this server.
|
|
28
|
+
|
|
29
|
+
e.g. \`endpoints: ["http://localhost:3099"],\`
|
|
30
|
+
`;
|
|
31
|
+
static examples = ['<%= config.bin %> <%= command.id %> ./quonfig.test.588.config.json --port=3099 '];
|
|
32
|
+
static flags = {
|
|
33
|
+
port: Flags.integer({ default: 3099, description: 'port to serve on' }),
|
|
34
|
+
};
|
|
35
|
+
async run() {
|
|
36
|
+
const { args, flags } = await this.parse(Serve);
|
|
37
|
+
const file = args['data-file'];
|
|
38
|
+
if (!fs.existsSync(file)) {
|
|
39
|
+
return this.error(`File not found: ${file}`);
|
|
40
|
+
}
|
|
41
|
+
let quonfig;
|
|
42
|
+
try {
|
|
43
|
+
quonfig = await initQuonfig(this, file);
|
|
44
|
+
if (!quonfig) {
|
|
45
|
+
throw new Error('Quonfig not initialized');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error_) {
|
|
49
|
+
const error = error_;
|
|
50
|
+
if (/No projectEnvId found in config/.test(error.message) || /Unexpected end of JSON input/.test(error.message)) {
|
|
51
|
+
return this.error(`${error.message}\nYour download file seems invalid or corrupt. Please redownload your datafile.`);
|
|
52
|
+
}
|
|
53
|
+
return this.error(error);
|
|
54
|
+
}
|
|
55
|
+
const { port } = flags;
|
|
56
|
+
const server = http.createServer(this.requestHandler(quonfig));
|
|
57
|
+
server.listen(port, (err) => {
|
|
58
|
+
if (err) {
|
|
59
|
+
return this.err(err);
|
|
60
|
+
}
|
|
61
|
+
console.log(`Server is listening on ${port}. Press ctrl-c to stop.`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
requestHandler(quonfig) {
|
|
65
|
+
return (req, res) => {
|
|
66
|
+
allowCORSPreflight(res);
|
|
67
|
+
// Handle CORS preflight request
|
|
68
|
+
if (req.method === 'OPTIONS') {
|
|
69
|
+
res.writeHead(204);
|
|
70
|
+
res.end();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const match = req.url?.match(/^\/?\/configs\/eval-with-context\/(.+)$/);
|
|
74
|
+
if (req.method === 'GET' && match) {
|
|
75
|
+
const encodedContext = match[1].split('?')[0];
|
|
76
|
+
console.log(`Received context: ${encodedContext}`);
|
|
77
|
+
console.log(`Decoded context: ${decodeURIComponent(encodedContext)}`);
|
|
78
|
+
const decoded = new TextDecoder().decode(base64ToBytes(decodeURIComponent(encodedContext)));
|
|
79
|
+
const context = javaScriptClientFormattedContextToContext(JSON.parse(decoded));
|
|
80
|
+
this.log(`${new Date().toISOString()}: Provided context: ${JSON.stringify(context)}`);
|
|
81
|
+
const config = {};
|
|
82
|
+
for (const key of quonfig.keys()) {
|
|
83
|
+
const raw = quonfig.rawConfig(key);
|
|
84
|
+
if (raw && (ALWAYS_SEND_CONFIG_TYPES.has(raw.type) || raw.sendToClientSdk)) {
|
|
85
|
+
const valueType = valueTypeStringForConfig(raw) ?? '?';
|
|
86
|
+
config[key] = { value: { [valueType]: quonfig.get(key, context) } };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
90
|
+
res.end(JSON.stringify({ evaluations: config }));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.verboseLog(`No handler for ${req.method} ${req.url}`);
|
|
94
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
95
|
+
res.end('Not Found. You can only GET /configs/eval-with-context/<context>');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { APICommand } from '../index.js';
|
|
2
|
+
import { JsonObj } from '../result.js';
|
|
3
|
+
export default class SetDefault 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
|
+
secret: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
|
+
'secret-key-name': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
confirm: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
13
|
+
confidential: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
14
|
+
'env-var': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
15
|
+
environment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
16
|
+
value: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<JsonObj | void>;
|
|
19
|
+
private submitChange;
|
|
20
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { ProvidedSource } from '@quonfig/node';
|
|
3
|
+
import { APICommand } from '../index.js';
|
|
4
|
+
import { ConfigValueType } from '@quonfig/node';
|
|
5
|
+
import getConfirmation, { confirmFlag } from '../ui/get-confirmation.js';
|
|
6
|
+
import getEnvironment from '../ui/get-environment.js';
|
|
7
|
+
import getString from '../ui/get-string.js';
|
|
8
|
+
import autocomplete from '../util/autocomplete.js';
|
|
9
|
+
import { checkmark } from '../util/color.js';
|
|
10
|
+
import { mapConfigValueToDto } from '../util/config-value-dto.js';
|
|
11
|
+
import { makeConfidentialValue } from '../util/encryption.js';
|
|
12
|
+
import isInteractive from '../util/is-interactive.js';
|
|
13
|
+
import nameArg from '../util/name-arg.js';
|
|
14
|
+
import secretFlags, { parsedSecretFlags } from '../util/secret-flags.js';
|
|
15
|
+
export default class SetDefault extends APICommand {
|
|
16
|
+
static args = { ...nameArg };
|
|
17
|
+
static description = 'Set/update the default value for an environment (other rules still apply)';
|
|
18
|
+
static examples = [
|
|
19
|
+
'<%= config.bin %> <%= command.id %> my.flag.name # will prompt for value and env',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> my.flag.name --value=true --environment=staging',
|
|
21
|
+
'<%= config.bin %> <%= command.id %> my.flag.name --value=true --secret',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> my.config.name --env-var=MY_ENV_VAR_NAME --environment=production',
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
confidential: Flags.boolean({ default: false, description: 'mark the value as confidential' }),
|
|
26
|
+
'env-var': Flags.string({ description: 'environment variable to use as default value' }),
|
|
27
|
+
environment: Flags.string({ description: 'environment to change' }),
|
|
28
|
+
value: Flags.string({ description: 'new default value' }),
|
|
29
|
+
...confirmFlag,
|
|
30
|
+
...secretFlags('encrypt the value of this item'),
|
|
31
|
+
};
|
|
32
|
+
async run() {
|
|
33
|
+
const { args, flags } = await this.parse(SetDefault);
|
|
34
|
+
let secret = parsedSecretFlags(flags);
|
|
35
|
+
if (flags['env-var'] && flags.value) {
|
|
36
|
+
return this.err('cannot specify both --env-var and --value');
|
|
37
|
+
}
|
|
38
|
+
if (flags['env-var'] && secret.selected) {
|
|
39
|
+
return this.err('cannot specify both --env-var and --secret');
|
|
40
|
+
}
|
|
41
|
+
if (flags.confidential && secret.selected) {
|
|
42
|
+
console.warn("Note: --confidential is implied when using --secret, so you don't need to specify both.");
|
|
43
|
+
}
|
|
44
|
+
// Validate required arguments before making API calls
|
|
45
|
+
if (!args.name && !isInteractive(flags)) {
|
|
46
|
+
return this.err("'name' argument is required when interactive mode isn't available.");
|
|
47
|
+
}
|
|
48
|
+
if (!flags.environment && !isInteractive(flags)) {
|
|
49
|
+
return this.err("'environment' is required when interactive mode isn't available.");
|
|
50
|
+
}
|
|
51
|
+
// Fetch all configs from metadata endpoint
|
|
52
|
+
const metadataRequest = await this.apiClient.post('/api/v1/metadata/list', { workspaceId: this.workspaceId });
|
|
53
|
+
if (!metadataRequest.ok) {
|
|
54
|
+
const errorMsg = metadataRequest.error?.error || `Failed to fetch configs: ${metadataRequest.status}`;
|
|
55
|
+
return this.err(errorMsg, { serverError: metadataRequest.error });
|
|
56
|
+
}
|
|
57
|
+
const metadataResponse = metadataRequest.json;
|
|
58
|
+
const configs = metadataResponse.configs;
|
|
59
|
+
// Get the key - from args or prompt
|
|
60
|
+
let key = args.name;
|
|
61
|
+
if (!key && isInteractive(flags)) {
|
|
62
|
+
const configKeys = configs.map((c) => c.key);
|
|
63
|
+
const selectedKey = await autocomplete({
|
|
64
|
+
message: 'Which item would you like to change the default for?',
|
|
65
|
+
source: configKeys,
|
|
66
|
+
});
|
|
67
|
+
if (selectedKey) {
|
|
68
|
+
key = selectedKey;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!key) {
|
|
72
|
+
return this.err("'name' argument is required when interactive mode isn't available.");
|
|
73
|
+
}
|
|
74
|
+
const config = configs.find((c) => c.key === key);
|
|
75
|
+
if (!config) {
|
|
76
|
+
return this.err(`Could not find config named ${key}`);
|
|
77
|
+
}
|
|
78
|
+
this.verboseLog('Selected config:', config);
|
|
79
|
+
// Check if existing config has encrypted values
|
|
80
|
+
if (!secret.selected) {
|
|
81
|
+
const configDetailsRequest = await this.apiClient.post('/api/v1/metadata/getByKey', { workspaceId: this.workspaceId, key });
|
|
82
|
+
if (configDetailsRequest.ok) {
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
const configDetails = configDetailsRequest.json;
|
|
85
|
+
this.verboseLog('Config details:', configDetails);
|
|
86
|
+
// Check if any existing rules have encrypted values
|
|
87
|
+
if (configDetails.default?.rules) {
|
|
88
|
+
for (const rule of configDetails.default.rules) {
|
|
89
|
+
if (rule.value?.decryptWith) {
|
|
90
|
+
this.verboseLog('Auto-detected encryption from existing config');
|
|
91
|
+
secret = {
|
|
92
|
+
keyName: secret.keyName,
|
|
93
|
+
selected: true,
|
|
94
|
+
};
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Get the environment
|
|
102
|
+
const environment = await getEnvironment({
|
|
103
|
+
command: this,
|
|
104
|
+
flags,
|
|
105
|
+
message: 'Which environment would you like to change the default for?',
|
|
106
|
+
providedEnvironment: flags.environment,
|
|
107
|
+
});
|
|
108
|
+
this.verboseLog({ environment });
|
|
109
|
+
if (!environment) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { confidential } = flags;
|
|
113
|
+
// Get the value
|
|
114
|
+
if (flags['env-var']) {
|
|
115
|
+
if (!(await getConfirmation({
|
|
116
|
+
flags,
|
|
117
|
+
message: `Confirm: change the default for ${key} in ${environment.name} to be provided by \`${flags['env-var']}\`? yes/no`,
|
|
118
|
+
}))) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
return this.submitChange({
|
|
122
|
+
confidential,
|
|
123
|
+
config,
|
|
124
|
+
envVar: flags['env-var'],
|
|
125
|
+
environment,
|
|
126
|
+
environmentId: environment.id,
|
|
127
|
+
key,
|
|
128
|
+
secret,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
let value = flags.value;
|
|
132
|
+
if (!value && isInteractive(flags)) {
|
|
133
|
+
value = await getString({
|
|
134
|
+
allowBlank: true,
|
|
135
|
+
message: 'Default value',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (value === undefined) {
|
|
139
|
+
return this.err('Value is required');
|
|
140
|
+
}
|
|
141
|
+
const secretMaybe = secret.selected ? ' (encrypted)' : '';
|
|
142
|
+
const message = `Confirm: change the default for ${key} in ${environment.name} to \`${value}\`${secretMaybe}? yes/no`;
|
|
143
|
+
if (!(await getConfirmation({ flags, message }))) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
return this.submitChange({
|
|
147
|
+
confidential,
|
|
148
|
+
config,
|
|
149
|
+
environment,
|
|
150
|
+
environmentId: environment.id,
|
|
151
|
+
key,
|
|
152
|
+
secret,
|
|
153
|
+
value,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async submitChange({ confidential, config, envVar, environment, environmentId, key, secret, value, }) {
|
|
157
|
+
/* eslint-disable camelcase */
|
|
158
|
+
const typeMapping = {
|
|
159
|
+
bool: 'bool',
|
|
160
|
+
string: 'string',
|
|
161
|
+
int: 'int',
|
|
162
|
+
double: 'double',
|
|
163
|
+
string_list: 'stringList',
|
|
164
|
+
json: 'json',
|
|
165
|
+
limit_definition: 'limitDefinition',
|
|
166
|
+
duration: 'duration',
|
|
167
|
+
int_range: 'intRange',
|
|
168
|
+
};
|
|
169
|
+
/* eslint-enable camelcase */
|
|
170
|
+
const type = typeMapping[config.valueType.toLowerCase()] || config.valueType;
|
|
171
|
+
let configValue;
|
|
172
|
+
let successMessage;
|
|
173
|
+
if (envVar === undefined) {
|
|
174
|
+
successMessage = `Successfully changed default to \`${value}\``;
|
|
175
|
+
if (secret.selected) {
|
|
176
|
+
// Handle encrypted values using shared utility
|
|
177
|
+
const encryptedValueResult = await makeConfidentialValue(this, value, secret, environmentId);
|
|
178
|
+
if (!encryptedValueResult.ok) {
|
|
179
|
+
return this.err(encryptedValueResult.message || 'Failed to encrypt value');
|
|
180
|
+
}
|
|
181
|
+
configValue = mapConfigValueToDto(encryptedValueResult.value, ConfigValueType.String);
|
|
182
|
+
successMessage += ' (encrypted)';
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Parse the value based on type and build value object
|
|
186
|
+
let variantValue = value;
|
|
187
|
+
let variantType = type;
|
|
188
|
+
switch (type) {
|
|
189
|
+
case 'stringList': {
|
|
190
|
+
variantValue = value.split(',');
|
|
191
|
+
variantType = 'string_list';
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case 'bool': {
|
|
195
|
+
const lowerValue = value.toLowerCase();
|
|
196
|
+
if (lowerValue !== 'true' && lowerValue !== 'false') {
|
|
197
|
+
return this.err(`'${value}' is not a valid value for ${key}`);
|
|
198
|
+
}
|
|
199
|
+
variantValue = lowerValue === 'true';
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case 'int': {
|
|
203
|
+
variantValue = Number.parseInt(value, 10);
|
|
204
|
+
if (Number.isNaN(variantValue)) {
|
|
205
|
+
return this.err(`Invalid default value for int: ${value}`);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case 'double': {
|
|
210
|
+
variantValue = Number.parseFloat(value);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'json': {
|
|
214
|
+
try {
|
|
215
|
+
variantValue = JSON.parse(value);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return this.err(`Invalid JSON value: ${value}`);
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'string': {
|
|
223
|
+
variantValue = value;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
// No default
|
|
227
|
+
}
|
|
228
|
+
configValue = {
|
|
229
|
+
type: variantType,
|
|
230
|
+
value: variantValue,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
configValue = {
|
|
236
|
+
provided: {
|
|
237
|
+
lookup: envVar,
|
|
238
|
+
source: ProvidedSource.EnvVar,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
successMessage = `Successfully changed default to be provided by \`${envVar}\``;
|
|
242
|
+
}
|
|
243
|
+
if (confidential && !secret.selected) {
|
|
244
|
+
configValue.confidential = true;
|
|
245
|
+
successMessage += ' (confidential)';
|
|
246
|
+
}
|
|
247
|
+
const payload = {
|
|
248
|
+
configKey: key,
|
|
249
|
+
currentVersionId: config.version,
|
|
250
|
+
environmentId: environmentId ? Number.parseInt(environmentId, 10) : 0,
|
|
251
|
+
value: configValue,
|
|
252
|
+
};
|
|
253
|
+
this.verboseLog('Payload:', payload);
|
|
254
|
+
const request = await this.apiClient.post('/internal/ops/v1/set-default', payload);
|
|
255
|
+
if (request.ok) {
|
|
256
|
+
this.log(`${checkmark} ${successMessage}`);
|
|
257
|
+
return {
|
|
258
|
+
environment: {
|
|
259
|
+
id: environmentId,
|
|
260
|
+
name: environment.name,
|
|
261
|
+
},
|
|
262
|
+
key,
|
|
263
|
+
success: true,
|
|
264
|
+
value,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
this.verboseLog(request.error);
|
|
268
|
+
const errMsg = request.status === 400
|
|
269
|
+
? `Failed to change default: ${request.status} -- is ${value || envVar} a valid value?`
|
|
270
|
+
: `Failed to change default: ${request.status}`;
|
|
271
|
+
return this.err(errMsg, { key, serverError: request.error });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
export default class Verify extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
path: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
strict: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<object>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
import { formatResult, validateWorkspace } from '../verify/validate.js';
|
|
4
|
+
export default class Verify extends BaseCommand {
|
|
5
|
+
static args = {
|
|
6
|
+
path: Args.string({
|
|
7
|
+
default: '.',
|
|
8
|
+
description: 'Path to workspace directory',
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Validate a Quonfig workspace directory';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> verify',
|
|
14
|
+
'<%= config.bin %> verify ./my-workspace',
|
|
15
|
+
'<%= config.bin %> verify --json',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
strict: Flags.boolean({
|
|
19
|
+
default: false,
|
|
20
|
+
description: 'Treat warnings as errors',
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { args, flags } = await this.parse(Verify);
|
|
25
|
+
const dir = args.path;
|
|
26
|
+
const result = validateWorkspace(dir);
|
|
27
|
+
if (flags.strict) {
|
|
28
|
+
// Promote warnings to errors
|
|
29
|
+
for (const issue of result.issues) {
|
|
30
|
+
if (issue.severity === 'warning') {
|
|
31
|
+
issue.severity = 'error';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
result.valid = !result.issues.some((i) => i.severity === 'error');
|
|
35
|
+
}
|
|
36
|
+
if (this.jsonEnabled()) {
|
|
37
|
+
if (!result.valid) {
|
|
38
|
+
this.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
this.log(formatResult(result));
|
|
43
|
+
if (!result.valid) {
|
|
44
|
+
this.exit(1);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
}
|