@quonfig/cli 0.0.1 → 0.0.4

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 (40) hide show
  1. package/README.md +157 -21
  2. package/bin/dev.js +5 -9
  3. package/bin/dev.mjs +6 -0
  4. package/dist/codegen/zod-introspection.js +13 -4
  5. package/dist/commands/init.d.ts +13 -0
  6. package/dist/commands/init.js +83 -0
  7. package/dist/commands/login.js +27 -6
  8. package/dist/commands/mcp.js +1 -1
  9. package/dist/commands/pull.d.ts +15 -0
  10. package/dist/commands/pull.js +176 -0
  11. package/dist/commands/serve.js +3 -1
  12. package/dist/commands/sync.d.ts +18 -0
  13. package/dist/commands/sync.js +142 -0
  14. package/dist/commands/whoami.js +4 -1
  15. package/dist/commands/workspace/bootstrap.d.ts +13 -0
  16. package/dist/commands/workspace/bootstrap.js +155 -0
  17. package/dist/init/init-workspace.d.ts +25 -0
  18. package/dist/init/init-workspace.js +162 -0
  19. package/dist/init/samples.d.ts +11 -0
  20. package/dist/init/samples.js +118 -0
  21. package/dist/init/schema.d.ts +9 -0
  22. package/dist/init/schema.js +334 -0
  23. package/dist/init/templates.d.ts +12 -0
  24. package/dist/init/templates.js +161 -0
  25. package/dist/util/coerce.js +2 -1
  26. package/dist/util/get-client.js +6 -44
  27. package/dist/util/get-valid-token.d.ts +5 -0
  28. package/dist/util/get-valid-token.js +44 -0
  29. package/dist/util/git-ops.d.ts +32 -0
  30. package/dist/util/git-ops.js +164 -0
  31. package/dist/util/gitea-api.d.ts +11 -0
  32. package/dist/util/gitea-api.js +45 -0
  33. package/dist/util/gitea-token-storage.d.ts +8 -0
  34. package/dist/util/gitea-token-storage.js +47 -0
  35. package/dist/util/token-storage.d.ts +1 -0
  36. package/dist/util/token-storage.js +30 -11
  37. package/dist/verify/validate.d.ts +14 -0
  38. package/dist/verify/validate.js +224 -19
  39. package/oclif.manifest.json +298 -2
  40. package/package.json +2 -2
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Core logic for `qfg init`.
3
+ *
4
+ * Uses a plan/execute pattern so --dry-run can show what *would* happen
5
+ * without writing anything.
6
+ */
7
+ import { execSync } from 'node:child_process';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { SAMPLE_FILES } from './samples.js';
11
+ import { storedConfigJsonSchema } from './schema.js';
12
+ import { PRE_COMMIT_MARKER, agentsMdTemplate, claudeMdTemplate, preCommitHookContent, readmeTemplate, } from './templates.js';
13
+ // ── Directories that define a workspace ────────────────────────────────
14
+ const WORKSPACE_DIRS = [
15
+ 'configs',
16
+ 'feature-flags',
17
+ 'segments',
18
+ 'log-levels',
19
+ 'schemas',
20
+ ];
21
+ // ── Plan ───────────────────────────────────────────────────────────────
22
+ export function planInit(options) {
23
+ const { dir } = options;
24
+ const actions = [];
25
+ // Detect first-time vs update: do any workspace dirs already exist?
26
+ const isFirstTime = !WORKSPACE_DIRS.some((d) => fs.existsSync(path.join(dir, d)));
27
+ // Resolve samples flag: first-time → default on, update → default off
28
+ const includeSamples = options.samples ?? isFirstTime;
29
+ // 0. Ensure it's a git repo
30
+ const gitDir = path.join(dir, '.git');
31
+ const needsGitInit = !fs.existsSync(gitDir);
32
+ if (needsGitInit) {
33
+ actions.push({ kind: 'git-init', path: '.git', description: 'Initialize git repository' });
34
+ }
35
+ // 1. Ensure directories exist
36
+ for (const d of WORKSPACE_DIRS) {
37
+ const dirPath = path.join(dir, d);
38
+ if (fs.existsSync(dirPath)) {
39
+ // directory already exists, nothing to do
40
+ }
41
+ else {
42
+ actions.push({ kind: 'create-dir', path: d, description: `Create ${d}/` });
43
+ }
44
+ }
45
+ // 2. Managed files — always overwrite
46
+ const managedFiles = [
47
+ { file: 'quonfig.schema.json', content: JSON.stringify(storedConfigJsonSchema(), null, 2) + '\n' },
48
+ { file: 'README.md', content: readmeTemplate() },
49
+ { file: 'CLAUDE.md', content: claudeMdTemplate() },
50
+ { file: 'AGENTS.md', content: agentsMdTemplate() },
51
+ ];
52
+ for (const { file, content: _content } of managedFiles) {
53
+ const filePath = path.join(dir, file);
54
+ const verb = fs.existsSync(filePath) ? 'Update' : 'Create';
55
+ actions.push({ kind: 'write-file', path: file, description: `${verb} ${file}` });
56
+ }
57
+ // 3. quonfig.json — create if missing
58
+ const envsPath = path.join(dir, 'quonfig.json');
59
+ if (fs.existsSync(envsPath)) {
60
+ // leave it alone
61
+ }
62
+ else {
63
+ actions.push({ kind: 'write-file', path: 'quonfig.json', description: 'Create quonfig.json' });
64
+ }
65
+ // 4. Sample data
66
+ if (includeSamples) {
67
+ for (const sample of SAMPLE_FILES) {
68
+ const samplePath = path.join(dir, sample.path);
69
+ if (fs.existsSync(samplePath)) {
70
+ actions.push({ kind: 'skip-file', path: sample.path, description: `Skip ${sample.path} (already exists)` });
71
+ }
72
+ else {
73
+ actions.push({ kind: 'write-file', path: sample.path, description: `Create ${sample.path}` });
74
+ }
75
+ }
76
+ }
77
+ // 5. Git pre-commit hook (always runs — git repo guaranteed by step 0)
78
+ if (needsGitInit) {
79
+ // Fresh git init — hook will be created
80
+ actions.push({ kind: 'create-hook', path: '.git/hooks/pre-commit', description: 'Install pre-commit hook (qfg verify)' });
81
+ }
82
+ else {
83
+ const hooksDir = path.join(gitDir, 'hooks');
84
+ const hookPath = path.join(hooksDir, 'pre-commit');
85
+ if (fs.existsSync(hookPath)) {
86
+ const existing = fs.readFileSync(hookPath, 'utf-8');
87
+ if (existing.includes(PRE_COMMIT_MARKER)) {
88
+ actions.push({ kind: 'skip-hook', path: '.git/hooks/pre-commit', description: 'Pre-commit hook already installed' });
89
+ }
90
+ else if (existing.includes('qfg verify')) {
91
+ actions.push({ kind: 'skip-hook', path: '.git/hooks/pre-commit', description: 'Pre-commit hook already runs qfg verify' });
92
+ }
93
+ else {
94
+ actions.push({ kind: 'update-hook', path: '.git/hooks/pre-commit', description: 'Append qfg verify to existing pre-commit hook' });
95
+ }
96
+ }
97
+ else {
98
+ actions.push({ kind: 'create-hook', path: '.git/hooks/pre-commit', description: 'Install pre-commit hook (qfg verify)' });
99
+ }
100
+ }
101
+ return { actions, isFirstTime, samplesIncluded: includeSamples };
102
+ }
103
+ // ── Execute ────────────────────────────────────────────────────────────
104
+ export function executeInit(plan, dir) {
105
+ // Build a lookup for managed file content
106
+ const managedContent = {
107
+ 'quonfig.schema.json': JSON.stringify(storedConfigJsonSchema(), null, 2) + '\n',
108
+ 'README.md': readmeTemplate(),
109
+ 'CLAUDE.md': claudeMdTemplate(),
110
+ 'AGENTS.md': agentsMdTemplate(),
111
+ 'quonfig.json': JSON.stringify({ environments: [] }, null, 2) + '\n',
112
+ };
113
+ // Build a lookup for sample content
114
+ const sampleContent = {};
115
+ for (const sample of SAMPLE_FILES) {
116
+ sampleContent[sample.path] = JSON.stringify(sample.content, null, 2) + '\n';
117
+ }
118
+ for (const action of plan.actions) {
119
+ const fullPath = path.join(dir, action.path);
120
+ switch (action.kind) {
121
+ case 'git-init': {
122
+ execSync('git init', { cwd: dir, stdio: 'pipe' });
123
+ break;
124
+ }
125
+ case 'create-dir': {
126
+ fs.mkdirSync(fullPath, { recursive: true });
127
+ break;
128
+ }
129
+ case 'write-file': {
130
+ // Ensure parent directory exists
131
+ const parentDir = path.dirname(fullPath);
132
+ if (!fs.existsSync(parentDir)) {
133
+ fs.mkdirSync(parentDir, { recursive: true });
134
+ }
135
+ const content = managedContent[action.path] ?? sampleContent[action.path];
136
+ if (content) {
137
+ fs.writeFileSync(fullPath, content, 'utf-8');
138
+ }
139
+ break;
140
+ }
141
+ case 'create-hook': {
142
+ const hooksDir = path.dirname(fullPath);
143
+ if (!fs.existsSync(hooksDir)) {
144
+ fs.mkdirSync(hooksDir, { recursive: true });
145
+ }
146
+ fs.writeFileSync(fullPath, preCommitHookContent(), 'utf-8');
147
+ fs.chmodSync(fullPath, 0o755);
148
+ break;
149
+ }
150
+ case 'update-hook': {
151
+ const existing = fs.readFileSync(fullPath, 'utf-8');
152
+ const appended = existing.trimEnd() + '\n\n' + preCommitHookContent().split('\n').slice(1).join('\n');
153
+ fs.writeFileSync(fullPath, appended, 'utf-8');
154
+ break;
155
+ }
156
+ case 'skip-file':
157
+ case 'skip-hook':
158
+ // Nothing to do
159
+ break;
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Sample config data for `qfg init --samples`.
3
+ *
4
+ * Each entry maps a relative path (e.g. "configs/example.greeting.json")
5
+ * to the JSON object that should be written there.
6
+ */
7
+ export interface SampleFile {
8
+ path: string;
9
+ content: object;
10
+ }
11
+ export declare const SAMPLE_FILES: SampleFile[];
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Sample config data for `qfg init --samples`.
3
+ *
4
+ * Each entry maps a relative path (e.g. "configs/example.greeting.json")
5
+ * to the JSON object that should be written there.
6
+ */
7
+ export const SAMPLE_FILES = [
8
+ {
9
+ path: 'configs/example.greeting.json',
10
+ content: {
11
+ key: 'example.greeting',
12
+ type: 'config',
13
+ valueType: 'string',
14
+ sendToClientSdk: true,
15
+ description: 'A greeting message shown to users.',
16
+ default: {
17
+ rules: [
18
+ {
19
+ criteria: [{ operator: 'ALWAYS_TRUE' }],
20
+ value: { type: 'string', value: 'Hello, world!' },
21
+ },
22
+ ],
23
+ },
24
+ environments: [
25
+ {
26
+ id: 'production',
27
+ rules: [
28
+ {
29
+ criteria: [{ operator: 'ALWAYS_TRUE' }],
30
+ value: { type: 'string', value: 'Welcome to our app!' },
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ variants: [],
36
+ },
37
+ },
38
+ {
39
+ path: 'feature-flags/example.dark-mode.json',
40
+ content: {
41
+ key: 'example.dark-mode',
42
+ type: 'feature_flag',
43
+ valueType: 'bool',
44
+ sendToClientSdk: true,
45
+ description: 'Enable dark mode UI.',
46
+ default: {
47
+ rules: [
48
+ {
49
+ criteria: [{ operator: 'ALWAYS_TRUE' }],
50
+ value: { type: 'bool', value: false },
51
+ },
52
+ ],
53
+ },
54
+ environments: [
55
+ {
56
+ id: 'staging',
57
+ rules: [
58
+ {
59
+ criteria: [{ operator: 'ALWAYS_TRUE' }],
60
+ value: { type: 'bool', value: true },
61
+ },
62
+ ],
63
+ },
64
+ ],
65
+ variants: [],
66
+ },
67
+ },
68
+ {
69
+ path: 'segments/example.beta-users.json',
70
+ content: {
71
+ key: 'example.beta-users',
72
+ type: 'segment',
73
+ valueType: 'bool',
74
+ sendToClientSdk: false,
75
+ description: 'Users who opted in to the beta program.',
76
+ default: {
77
+ rules: [
78
+ {
79
+ criteria: [
80
+ {
81
+ operator: 'PROP_IS_ONE_OF',
82
+ propertyName: 'user.beta',
83
+ valueToMatch: { type: 'string_list', value: ['true'] },
84
+ },
85
+ ],
86
+ value: { type: 'bool', value: true },
87
+ },
88
+ {
89
+ criteria: [{ operator: 'ALWAYS_TRUE' }],
90
+ value: { type: 'bool', value: false },
91
+ },
92
+ ],
93
+ },
94
+ environments: [],
95
+ variants: [],
96
+ },
97
+ },
98
+ {
99
+ path: 'log-levels/example.app.json',
100
+ content: {
101
+ key: 'example.app',
102
+ type: 'log_level',
103
+ valueType: 'log_level',
104
+ sendToClientSdk: false,
105
+ description: 'Application log level.',
106
+ default: {
107
+ rules: [
108
+ {
109
+ criteria: [{ operator: 'ALWAYS_TRUE' }],
110
+ value: { type: 'log_level', value: 'INFO' },
111
+ },
112
+ ],
113
+ },
114
+ environments: [],
115
+ variants: [],
116
+ },
117
+ },
118
+ ];
@@ -0,0 +1,9 @@
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
+ export declare function storedConfigJsonSchema(): object;
@@ -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;