@quonfig/cli 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -20
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +83 -0
- package/dist/init/init-workspace.d.ts +25 -0
- package/dist/init/init-workspace.js +162 -0
- package/dist/init/samples.d.ts +11 -0
- package/dist/init/samples.js +118 -0
- package/dist/init/schema.d.ts +9 -0
- package/dist/init/schema.js +334 -0
- package/dist/init/templates.d.ts +12 -0
- package/dist/init/templates.js +161 -0
- package/dist/util/token-storage.js +6 -1
- package/dist/verify/validate.d.ts +13 -0
- package/dist/verify/validate.js +89 -14
- package/oclif.manifest.json +76 -1
- package/package.json +1 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema for Quonfig StoredConfig files.
|
|
3
|
+
*
|
|
4
|
+
* This is the machine-readable schema for files in configs/, feature-flags/,
|
|
5
|
+
* segments/, and log-levels/. Written to `quonfig.schema.json` by `qfg init`.
|
|
6
|
+
*
|
|
7
|
+
* Derived from the Zod schemas in verify/validate.ts — keep in sync.
|
|
8
|
+
*/
|
|
9
|
+
const configTypes = ['config', 'feature_flag', 'log_level', 'segment'];
|
|
10
|
+
const valueTypes = ['bool', 'string', 'int', 'double', 'json', 'string_list', 'duration', 'log_level'];
|
|
11
|
+
const logLevels = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];
|
|
12
|
+
const operators = [
|
|
13
|
+
'ALWAYS_TRUE',
|
|
14
|
+
'PROP_IS_ONE_OF', 'PROP_IS_NOT_ONE_OF',
|
|
15
|
+
'PROP_STARTS_WITH_ONE_OF', 'PROP_DOES_NOT_START_WITH_ONE_OF',
|
|
16
|
+
'PROP_ENDS_WITH_ONE_OF', 'PROP_DOES_NOT_END_WITH_ONE_OF',
|
|
17
|
+
'PROP_CONTAINS_ONE_OF', 'PROP_DOES_NOT_CONTAIN_ONE_OF',
|
|
18
|
+
'PROP_LESS_THAN', 'PROP_LESS_THAN_OR_EQUAL',
|
|
19
|
+
'PROP_GREATER_THAN', 'PROP_GREATER_THAN_OR_EQUAL',
|
|
20
|
+
'PROP_BEFORE', 'PROP_AFTER',
|
|
21
|
+
'PROP_MATCHES', 'PROP_DOES_NOT_MATCH',
|
|
22
|
+
'IN_SEG', 'NOT_IN_SEG',
|
|
23
|
+
'IN_INT_RANGE',
|
|
24
|
+
'LOOKUP_KEY_IN', 'LOOKUP_KEY_NOT_IN',
|
|
25
|
+
];
|
|
26
|
+
// ── Value sub-schemas ──────────────────────────────────────────────────
|
|
27
|
+
const boolValue = {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
type: { const: 'bool' },
|
|
31
|
+
value: { type: 'boolean' },
|
|
32
|
+
},
|
|
33
|
+
required: ['type', 'value'],
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
};
|
|
36
|
+
const stringValue = {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
type: { const: 'string' },
|
|
40
|
+
value: { type: 'string' },
|
|
41
|
+
},
|
|
42
|
+
required: ['type', 'value'],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
};
|
|
45
|
+
const intValue = {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
type: { const: 'int' },
|
|
49
|
+
value: { oneOf: [{ type: 'number' }, { type: 'string' }] },
|
|
50
|
+
},
|
|
51
|
+
required: ['type', 'value'],
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
};
|
|
54
|
+
const doubleValue = {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
type: { const: 'double' },
|
|
58
|
+
value: { oneOf: [{ type: 'number' }, { type: 'string' }] },
|
|
59
|
+
},
|
|
60
|
+
required: ['type', 'value'],
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
};
|
|
63
|
+
const jsonValue = {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
type: { const: 'json' },
|
|
67
|
+
value: {},
|
|
68
|
+
},
|
|
69
|
+
required: ['type', 'value'],
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
};
|
|
72
|
+
const stringListValue = {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
type: { const: 'string_list' },
|
|
76
|
+
value: { type: 'array', items: { type: 'string' } },
|
|
77
|
+
},
|
|
78
|
+
required: ['type', 'value'],
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
};
|
|
81
|
+
const durationValue = {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
type: { const: 'duration' },
|
|
85
|
+
value: { type: 'string', description: 'Go-style duration (e.g. "30s", "5m", "1h30m")' },
|
|
86
|
+
},
|
|
87
|
+
required: ['type', 'value'],
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
};
|
|
90
|
+
const logLevelValue = {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
type: { const: 'log_level' },
|
|
94
|
+
value: { enum: [...logLevels] },
|
|
95
|
+
},
|
|
96
|
+
required: ['type', 'value'],
|
|
97
|
+
additionalProperties: false,
|
|
98
|
+
};
|
|
99
|
+
const schemaValue = {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
type: { const: 'schema' },
|
|
103
|
+
value: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
schemaType: { type: 'string' },
|
|
107
|
+
schema: { type: 'string' },
|
|
108
|
+
},
|
|
109
|
+
required: ['schemaType', 'schema'],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['type', 'value'],
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
};
|
|
115
|
+
const providedValue = {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
type: { const: 'provided' },
|
|
119
|
+
value: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
source: { type: 'string' },
|
|
123
|
+
lookup: { type: 'string' },
|
|
124
|
+
},
|
|
125
|
+
required: ['source', 'lookup'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: ['type', 'value'],
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
};
|
|
131
|
+
const simpleValue = {
|
|
132
|
+
oneOf: [
|
|
133
|
+
boolValue,
|
|
134
|
+
stringValue,
|
|
135
|
+
intValue,
|
|
136
|
+
doubleValue,
|
|
137
|
+
jsonValue,
|
|
138
|
+
stringListValue,
|
|
139
|
+
durationValue,
|
|
140
|
+
logLevelValue,
|
|
141
|
+
schemaValue,
|
|
142
|
+
providedValue,
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
const weightedValuesValue = {
|
|
146
|
+
type: 'object',
|
|
147
|
+
description: 'Weighted rollout for A/B testing. Weights are out of 100,000.',
|
|
148
|
+
properties: {
|
|
149
|
+
type: { const: 'weighted_values' },
|
|
150
|
+
value: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
weightedValues: {
|
|
154
|
+
type: 'array',
|
|
155
|
+
items: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
value: simpleValue,
|
|
159
|
+
weight: { type: 'integer', minimum: 0, maximum: 100_000 },
|
|
160
|
+
},
|
|
161
|
+
required: ['value', 'weight'],
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
hashByPropertyName: { type: 'string', default: 'user.key' },
|
|
166
|
+
splitEvenly: { type: 'boolean' },
|
|
167
|
+
},
|
|
168
|
+
required: ['weightedValues'],
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
required: ['type', 'value'],
|
|
173
|
+
additionalProperties: false,
|
|
174
|
+
};
|
|
175
|
+
const ruleValue = {
|
|
176
|
+
oneOf: [
|
|
177
|
+
...simpleValue.oneOf,
|
|
178
|
+
weightedValuesValue,
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
// ── Criterion ──────────────────────────────────────────────────────────
|
|
182
|
+
const criterion = {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
operator: {
|
|
186
|
+
enum: [...operators],
|
|
187
|
+
description: 'ALWAYS_TRUE needs no extra fields. PROP_* operators require propertyName and valueToMatch. IN_SEG/NOT_IN_SEG require valueToMatch with segment key.',
|
|
188
|
+
},
|
|
189
|
+
propertyName: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
description: 'Required for PROP_* operators.',
|
|
192
|
+
},
|
|
193
|
+
valueToMatch: {
|
|
194
|
+
...simpleValue,
|
|
195
|
+
description: 'Required for all operators except ALWAYS_TRUE.',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
required: ['operator'],
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
};
|
|
201
|
+
// ── Rule ───────────────────────────────────────────────────────────────
|
|
202
|
+
const configRule = {
|
|
203
|
+
type: 'object',
|
|
204
|
+
description: 'Rules are evaluated in order; first match wins. All criteria within a rule are ANDed.',
|
|
205
|
+
properties: {
|
|
206
|
+
criteria: {
|
|
207
|
+
type: 'array',
|
|
208
|
+
items: criterion,
|
|
209
|
+
},
|
|
210
|
+
value: ruleValue,
|
|
211
|
+
},
|
|
212
|
+
required: ['criteria', 'value'],
|
|
213
|
+
additionalProperties: false,
|
|
214
|
+
};
|
|
215
|
+
// ── Environment ────────────────────────────────────────────────────────
|
|
216
|
+
const configEnvironment = {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
id: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$',
|
|
222
|
+
description: 'Lowercase slug (e.g. "production", "staging"). Must NOT be a UUID.',
|
|
223
|
+
},
|
|
224
|
+
rules: {
|
|
225
|
+
type: 'array',
|
|
226
|
+
items: configRule,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
required: ['id', 'rules'],
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
};
|
|
232
|
+
// ── Variant ────────────────────────────────────────────────────────────
|
|
233
|
+
const variant = {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
id: { type: 'string' },
|
|
237
|
+
key: { type: 'string' },
|
|
238
|
+
value: simpleValue,
|
|
239
|
+
description: { type: 'string' },
|
|
240
|
+
},
|
|
241
|
+
required: ['value'],
|
|
242
|
+
additionalProperties: false,
|
|
243
|
+
};
|
|
244
|
+
// ── Top-level StoredConfig ─────────────────────────────────────────────
|
|
245
|
+
export function storedConfigJsonSchema() {
|
|
246
|
+
return {
|
|
247
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
248
|
+
$id: 'https://quonfig.com/schemas/stored-config.json',
|
|
249
|
+
title: 'Quonfig StoredConfig',
|
|
250
|
+
description: 'Schema for config files in a Quonfig workspace (configs/, feature-flags/, segments/, log-levels/).',
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
key: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
minLength: 1,
|
|
256
|
+
maxLength: 512,
|
|
257
|
+
not: { const: 'new' },
|
|
258
|
+
pattern: '^[^/\\\\]+$',
|
|
259
|
+
description: 'Unique identifier. Must match the filename (without .json).',
|
|
260
|
+
},
|
|
261
|
+
type: {
|
|
262
|
+
enum: [...configTypes],
|
|
263
|
+
description: 'Determines which directory the file lives in: config→configs/, feature_flag→feature-flags/, segment→segments/, log_level→log-levels/.',
|
|
264
|
+
},
|
|
265
|
+
valueType: {
|
|
266
|
+
enum: [...valueTypes],
|
|
267
|
+
description: 'The value type. Must match the "type" field in rule values. Segments must use "bool". Log levels must use "log_level".',
|
|
268
|
+
},
|
|
269
|
+
default: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
rules: {
|
|
273
|
+
type: 'array',
|
|
274
|
+
items: configRule,
|
|
275
|
+
minItems: 1,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
required: ['rules'],
|
|
279
|
+
additionalProperties: false,
|
|
280
|
+
},
|
|
281
|
+
id: { type: 'string' },
|
|
282
|
+
projectId: { type: 'string' },
|
|
283
|
+
name: { type: 'string' },
|
|
284
|
+
description: { type: 'string' },
|
|
285
|
+
sendToClientSdk: {
|
|
286
|
+
type: 'boolean',
|
|
287
|
+
default: false,
|
|
288
|
+
description: 'Whether to expose this config to client-side SDKs. Must be false for segments.',
|
|
289
|
+
},
|
|
290
|
+
schemaKey: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
description: 'References a schema in schemas/ or schemas-protected/. The referenced file must exist.',
|
|
293
|
+
},
|
|
294
|
+
accessLevel: { type: 'string' },
|
|
295
|
+
protection: { type: 'string' },
|
|
296
|
+
tags: {
|
|
297
|
+
type: 'array',
|
|
298
|
+
items: { type: 'string' },
|
|
299
|
+
},
|
|
300
|
+
environments: {
|
|
301
|
+
type: 'array',
|
|
302
|
+
items: configEnvironment,
|
|
303
|
+
default: [],
|
|
304
|
+
},
|
|
305
|
+
variants: {
|
|
306
|
+
type: 'array',
|
|
307
|
+
items: variant,
|
|
308
|
+
default: [],
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
required: ['key', 'type', 'valueType', 'default'],
|
|
312
|
+
additionalProperties: true,
|
|
313
|
+
// Type-specific constraints
|
|
314
|
+
allOf: [
|
|
315
|
+
{
|
|
316
|
+
if: { properties: { type: { const: 'segment' } } },
|
|
317
|
+
then: {
|
|
318
|
+
properties: {
|
|
319
|
+
valueType: { const: 'bool' },
|
|
320
|
+
sendToClientSdk: { const: false },
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
if: { properties: { type: { const: 'log_level' } } },
|
|
326
|
+
then: {
|
|
327
|
+
properties: {
|
|
328
|
+
valueType: { const: 'log_level' },
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed document templates for `qfg init`.
|
|
3
|
+
*
|
|
4
|
+
* These are overwritten on every run of `qfg init` — they are not
|
|
5
|
+
* user-editable. Each starts with a comment so users know not to
|
|
6
|
+
* hand-edit them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readmeTemplate(): string;
|
|
9
|
+
export declare function claudeMdTemplate(): string;
|
|
10
|
+
export declare function agentsMdTemplate(): string;
|
|
11
|
+
export declare const PRE_COMMIT_MARKER = "# --- qfg-verify ---";
|
|
12
|
+
export declare function preCommitHookContent(): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed document templates for `qfg init`.
|
|
3
|
+
*
|
|
4
|
+
* These are overwritten on every run of `qfg init` — they are not
|
|
5
|
+
* user-editable. Each starts with a comment so users know not to
|
|
6
|
+
* hand-edit them.
|
|
7
|
+
*/
|
|
8
|
+
const MANAGED_HEADER = '<!-- Managed by `qfg init` — do not edit manually. Run `qfg init` to update. -->';
|
|
9
|
+
// ── README.md ──────────────────────────────────────────────────────────
|
|
10
|
+
export function readmeTemplate() {
|
|
11
|
+
return `${MANAGED_HEADER}
|
|
12
|
+
|
|
13
|
+
# Quonfig Workspace
|
|
14
|
+
|
|
15
|
+
This is a [Quonfig](https://quonfig.com) workspace repository. All configuration — feature flags, configs, segments, log levels, and schemas — is stored as JSON files tracked in git.
|
|
16
|
+
|
|
17
|
+
## Directory layout
|
|
18
|
+
|
|
19
|
+
| Directory | What goes here |
|
|
20
|
+
|-----------|---------------|
|
|
21
|
+
| \`configs/\` | Application configuration (strings, ints, JSON, etc.) |
|
|
22
|
+
| \`feature-flags/\` | Boolean or multi-variant feature flags |
|
|
23
|
+
| \`segments/\` | Audience segments for targeting rules |
|
|
24
|
+
| \`log-levels/\` | Dynamic log level configuration |
|
|
25
|
+
| \`schemas/\` | JSON Schemas that configs can reference for validation |
|
|
26
|
+
| \`schemas-protected/\` | Admin-only schemas |
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
\`\`\`bash
|
|
31
|
+
# Validate the workspace
|
|
32
|
+
qfg verify
|
|
33
|
+
|
|
34
|
+
# Create a new feature flag
|
|
35
|
+
qfg create my.new.flag --type=boolean-flag
|
|
36
|
+
|
|
37
|
+
# List all configs
|
|
38
|
+
qfg list
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
## Schema
|
|
42
|
+
|
|
43
|
+
\`quonfig.schema.json\` in the repo root defines the config file format. Editors that support JSON Schema will provide autocomplete and validation automatically.
|
|
44
|
+
|
|
45
|
+
## Validation
|
|
46
|
+
|
|
47
|
+
Run \`qfg verify\` after every change. A git pre-commit hook is installed to do this automatically.
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
// ── CLAUDE.md ──────────────────────────────────────────────────────────
|
|
51
|
+
export function claudeMdTemplate() {
|
|
52
|
+
return `${MANAGED_HEADER}
|
|
53
|
+
|
|
54
|
+
# Quonfig Workspace — Agent Reference
|
|
55
|
+
|
|
56
|
+
This is a Quonfig workspace repository. Configuration is stored as JSON files in git.
|
|
57
|
+
|
|
58
|
+
**After every change to this repo, run \`qfg verify\` to validate.**
|
|
59
|
+
|
|
60
|
+
## JSON Schema
|
|
61
|
+
|
|
62
|
+
The canonical schema for config files is \`quonfig.schema.json\` in the repo root. Use it as the source of truth for field names, types, enums, and constraints.
|
|
63
|
+
|
|
64
|
+
Config files can reference it for editor support:
|
|
65
|
+
|
|
66
|
+
\`\`\`json
|
|
67
|
+
{ "$schema": "./quonfig.schema.json", "key": "my.config", ... }
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
## Directory Structure
|
|
71
|
+
|
|
72
|
+
Files must live in the directory matching their \`type\`. The filename (without \`.json\`) must exactly match the \`key\` field.
|
|
73
|
+
|
|
74
|
+
| Directory | Config \`type\` field | Description |
|
|
75
|
+
|-----------|---------------------|-------------|
|
|
76
|
+
| \`configs/\` | \`"config"\` | Application configuration values |
|
|
77
|
+
| \`feature-flags/\` | \`"feature_flag"\` | Boolean or multi-variant feature flags |
|
|
78
|
+
| \`segments/\` | \`"segment"\` | Audience segments for targeting rules |
|
|
79
|
+
| \`log-levels/\` | \`"log_level"\` | Dynamic log level configuration |
|
|
80
|
+
| \`schemas/\` | \`"schema"\` | JSON Schemas that configs can reference |
|
|
81
|
+
| \`schemas-protected/\` | \`"schema"\` | Protected schemas (admin-only) |
|
|
82
|
+
|
|
83
|
+
## Minimal Example
|
|
84
|
+
|
|
85
|
+
\`\`\`json
|
|
86
|
+
{
|
|
87
|
+
"$schema": "./quonfig.schema.json",
|
|
88
|
+
"key": "my-app.timeout",
|
|
89
|
+
"type": "config",
|
|
90
|
+
"valueType": "int",
|
|
91
|
+
"default": {
|
|
92
|
+
"rules": [
|
|
93
|
+
{
|
|
94
|
+
"criteria": [{ "operator": "ALWAYS_TRUE" }],
|
|
95
|
+
"value": { "type": "int", "value": 30 }
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"environments": [],
|
|
100
|
+
"variants": []
|
|
101
|
+
}
|
|
102
|
+
\`\`\`
|
|
103
|
+
|
|
104
|
+
## Rules and Criteria
|
|
105
|
+
|
|
106
|
+
The \`default\` block (and each environment override) contains a \`rules\` array. Rules are evaluated in order; the first rule whose criteria all match wins. All criteria within one rule are ANDed. Use multiple rules for OR logic.
|
|
107
|
+
|
|
108
|
+
### Operator quick reference
|
|
109
|
+
|
|
110
|
+
| Category | Operators | Required fields |
|
|
111
|
+
|----------|-----------|----------------|
|
|
112
|
+
| Always match | \`ALWAYS_TRUE\` | (none) |
|
|
113
|
+
| Property string | \`PROP_IS_ONE_OF\`, \`PROP_IS_NOT_ONE_OF\`, \`PROP_STARTS_WITH_ONE_OF\`, \`PROP_ENDS_WITH_ONE_OF\`, \`PROP_CONTAINS_ONE_OF\` + negative variants | \`propertyName\`, \`valueToMatch\` |
|
|
114
|
+
| Property comparison | \`PROP_LESS_THAN\`, \`PROP_LESS_THAN_OR_EQUAL\`, \`PROP_GREATER_THAN\`, \`PROP_GREATER_THAN_OR_EQUAL\` | \`propertyName\`, \`valueToMatch\` |
|
|
115
|
+
| Property date | \`PROP_BEFORE\`, \`PROP_AFTER\` | \`propertyName\`, \`valueToMatch\` |
|
|
116
|
+
| Property regex | \`PROP_MATCHES\`, \`PROP_DOES_NOT_MATCH\` | \`propertyName\`, \`valueToMatch\` |
|
|
117
|
+
| Segment | \`IN_SEG\`, \`NOT_IN_SEG\` | \`valueToMatch\` (segment key) |
|
|
118
|
+
| Other | \`IN_INT_RANGE\`, \`LOOKUP_KEY_IN\`, \`LOOKUP_KEY_NOT_IN\` | \`valueToMatch\` |
|
|
119
|
+
|
|
120
|
+
See \`quonfig.schema.json\` for the full operator enum and value type definitions.
|
|
121
|
+
|
|
122
|
+
## Environments
|
|
123
|
+
|
|
124
|
+
Environment overrides go in the \`environments\` array. Each has an \`id\` (lowercase slug like \`production\`, never a UUID) and its own \`rules\` array. Environment IDs are defined in \`environments.json\` at the repo root.
|
|
125
|
+
|
|
126
|
+
## Cross-File Validation Rules
|
|
127
|
+
|
|
128
|
+
These constraints span multiple files and are enforced by \`qfg verify\` (not expressible in the JSON Schema alone):
|
|
129
|
+
|
|
130
|
+
1. **Filename = key**: the JSON filename (without .json) must exactly match the \`key\` field
|
|
131
|
+
2. **No duplicate keys** across the entire workspace
|
|
132
|
+
3. **Segment references** (\`IN_SEG\`/\`NOT_IN_SEG\`) must point to existing segments in \`segments/\`
|
|
133
|
+
4. **Schema references** (\`schemaKey\`) must point to existing files in \`schemas/\` or \`schemas-protected/\`
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
// ── AGENTS.md ──────────────────────────────────────────────────────────
|
|
137
|
+
export function agentsMdTemplate() {
|
|
138
|
+
return `${MANAGED_HEADER}
|
|
139
|
+
|
|
140
|
+
# Agent Instructions
|
|
141
|
+
|
|
142
|
+
This is a Quonfig workspace repository. See \`CLAUDE.md\` for the complete schema reference and validation rules.
|
|
143
|
+
|
|
144
|
+
## Key rules
|
|
145
|
+
|
|
146
|
+
1. **Always validate**: run \`qfg verify\` after any change to this repo.
|
|
147
|
+
2. Filename must match the \`key\` field (e.g. \`my.flag.json\` has \`"key": "my.flag"\`).
|
|
148
|
+
3. Files must be in the correct directory for their \`type\`.
|
|
149
|
+
4. See \`CLAUDE.md\` for the full list of value types, operators, and constraints.
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
// ── Pre-commit hook ────────────────────────────────────────────────────
|
|
153
|
+
export const PRE_COMMIT_MARKER = '# --- qfg-verify ---';
|
|
154
|
+
export function preCommitHookContent() {
|
|
155
|
+
return `#!/bin/sh
|
|
156
|
+
${PRE_COMMIT_MARKER}
|
|
157
|
+
# Validate workspace before committing.
|
|
158
|
+
# Installed by \`qfg init\`. Safe to remove if you prefer manual validation.
|
|
159
|
+
qfg verify .
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { getDomain } from './domain-urls.js';
|
|
4
5
|
const getQuonfigDir = (options) => options?.quonfigDir || path.join(os.homedir(), '.quonfig');
|
|
5
|
-
const getTokenFile = (options) =>
|
|
6
|
+
const getTokenFile = (options) => {
|
|
7
|
+
const domain = getDomain();
|
|
8
|
+
const filename = domain !== 'quonfig.com' ? `tokens-${domain.replaceAll('.', '-')}.json` : 'tokens.json';
|
|
9
|
+
return path.join(getQuonfigDir(options), filename);
|
|
10
|
+
};
|
|
6
11
|
const getConfigFile = (options) => path.join(getQuonfigDir(options), 'config');
|
|
7
12
|
const ensureQuonfigDir = async (options) => {
|
|
8
13
|
const quonfigDir = getQuonfigDir(options);
|
|
@@ -26,10 +26,23 @@ export interface ValidationIssue {
|
|
|
26
26
|
severity: Severity;
|
|
27
27
|
suggestion?: string;
|
|
28
28
|
}
|
|
29
|
+
export interface ValidationStats {
|
|
30
|
+
configs: number;
|
|
31
|
+
featureFlags: number;
|
|
32
|
+
segments: number;
|
|
33
|
+
logLevels: number;
|
|
34
|
+
schemas: number;
|
|
35
|
+
environmentOverrides: number;
|
|
36
|
+
rules: number;
|
|
37
|
+
segmentRefsChecked: number;
|
|
38
|
+
schemaRefsChecked: number;
|
|
39
|
+
uniqueKeysVerified: number;
|
|
40
|
+
}
|
|
29
41
|
export interface ValidationResult {
|
|
30
42
|
issues: ValidationIssue[];
|
|
31
43
|
filesChecked: number;
|
|
32
44
|
valid: boolean;
|
|
45
|
+
stats: ValidationStats;
|
|
33
46
|
}
|
|
34
47
|
/**
|
|
35
48
|
* Validate an entire workspace directory on disk.
|