@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.
- package/README.md +157 -21
- package/bin/dev.js +5 -9
- package/bin/dev.mjs +6 -0
- package/dist/codegen/zod-introspection.js +13 -4
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +83 -0
- package/dist/commands/login.js +27 -6
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/pull.d.ts +15 -0
- package/dist/commands/pull.js +176 -0
- package/dist/commands/serve.js +3 -1
- package/dist/commands/sync.d.ts +18 -0
- package/dist/commands/sync.js +142 -0
- package/dist/commands/whoami.js +4 -1
- package/dist/commands/workspace/bootstrap.d.ts +13 -0
- package/dist/commands/workspace/bootstrap.js +155 -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/coerce.js +2 -1
- package/dist/util/get-client.js +6 -44
- package/dist/util/get-valid-token.d.ts +5 -0
- package/dist/util/get-valid-token.js +44 -0
- package/dist/util/git-ops.d.ts +32 -0
- package/dist/util/git-ops.js +164 -0
- package/dist/util/gitea-api.d.ts +11 -0
- package/dist/util/gitea-api.js +45 -0
- package/dist/util/gitea-token-storage.d.ts +8 -0
- package/dist/util/gitea-token-storage.js +47 -0
- package/dist/util/token-storage.d.ts +1 -0
- package/dist/util/token-storage.js +30 -11
- package/dist/verify/validate.d.ts +14 -0
- package/dist/verify/validate.js +224 -19
- package/oclif.manifest.json +298 -2
- 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;
|