@quonfig/cli 0.0.12 → 0.0.14
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 +91 -33
- package/dist/codegen/json-schema-to-zod.js +7 -5
- package/dist/codegen/language-mappers/zod-base-mapper.d.ts +2 -2
- package/dist/codegen/language-mappers/zod-to-string-mapper.d.ts +2 -2
- package/dist/codegen/language-mappers/zod-to-string-mapper.js +6 -6
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.d.ts +2 -2
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +6 -6
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.d.ts +2 -2
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.js +5 -5
- package/dist/codegen/local-config-reader.js +0 -1
- package/dist/codegen/schema-extractor.js +5 -2
- package/dist/commands/info.js +4 -1
- package/dist/commands/init.js +12 -2
- package/dist/commands/interactive.d.ts +1 -0
- package/dist/commands/interactive.js +8 -0
- package/dist/commands/login.js +9 -3
- package/dist/commands/migrate/doctor.js +1 -1
- package/dist/commands/migrate/my-code.d.ts +11 -0
- package/dist/commands/migrate/my-code.js +80 -0
- package/dist/commands/migrate/status.js +3 -16
- package/dist/commands/migrate.js +29 -8
- package/dist/commands/override.js +6 -3
- package/dist/commands/profile.d.ts +1 -1
- package/dist/commands/profile.js +1 -1
- package/dist/commands/pull.d.ts +2 -2
- package/dist/commands/pull.js +45 -47
- package/dist/commands/schema.d.ts +3 -3
- package/dist/commands/schema.js +37 -37
- package/dist/commands/sdk-key/create.js +12 -14
- package/dist/commands/sdk-key/revoke.d.ts +1 -1
- package/dist/commands/sdk-key/revoke.js +2 -4
- package/dist/commands/set-default.js +7 -5
- package/dist/commands/set-rollout.d.ts +1 -1
- package/dist/commands/set-rollout.js +16 -13
- package/dist/commands/sync.d.ts +3 -3
- package/dist/commands/sync.js +30 -26
- package/dist/commands/whoami.js +1 -5
- package/dist/commands/workspace/bootstrap.js +7 -7
- package/dist/commands/workspace/switch.js +2 -2
- package/dist/init/init-workspace.d.ts +2 -2
- package/dist/init/init-workspace.js +32 -18
- package/dist/init/samples.d.ts +1 -1
- package/dist/init/samples.js +2 -2
- package/dist/init/schema.js +24 -15
- package/dist/interactive-prompt.js +1 -10
- package/dist/migrate/doctor.js +3 -1
- package/dist/migrate/local-write.d.ts +26 -0
- package/dist/migrate/local-write.js +85 -0
- package/dist/migrate/migration-report.js +1 -9
- package/dist/migrate/push-strategy.js +1 -7
- package/dist/migrate/push-to-cloud.js +1 -1
- package/dist/migrate/sources/flagsmith.js +10 -0
- package/dist/quonfig.d.ts +1 -1
- package/dist/quonfig.js +5 -9
- package/dist/ui/get-value.js +1 -1
- package/dist/util/config-value-dto.js +17 -1
- package/dist/util/encryption.js +4 -1
- package/dist/util/friction-log.d.ts +1 -1
- package/dist/util/friction-log.js +5 -1
- package/dist/util/get-client.js +51 -3
- package/dist/util/get-valid-token.d.ts +4 -0
- package/dist/util/get-valid-token.js +17 -2
- package/dist/util/git-ops.js +7 -7
- package/dist/util/gitea-api.d.ts +2 -2
- package/dist/util/gitea-api.js +5 -3
- package/dist/util/gitea-token-storage.d.ts +2 -2
- package/dist/util/gitea-token-storage.js +1 -1
- package/dist/util/oauth-client.js +1 -1
- package/dist/util/token-storage.js +2 -2
- package/dist/verify/standalone.d.ts +0 -1
- package/dist/verify/standalone.js +22 -23
- package/dist/verify/validate.d.ts +8 -8
- package/dist/verify/validate.js +252 -246
- package/oclif.manifest.json +79 -3
- package/package.json +6 -6
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { APICommand } from '../../index.js';
|
|
3
3
|
export default class SdkKeyRevoke extends APICommand {
|
|
4
|
-
static description = 'Revoke an SDK key by its ID';
|
|
5
4
|
static args = {
|
|
6
5
|
keyId: Args.string({
|
|
7
6
|
description: 'ID of the SDK key to revoke',
|
|
8
7
|
required: true,
|
|
9
8
|
}),
|
|
10
9
|
};
|
|
11
|
-
static
|
|
12
|
-
|
|
13
|
-
];
|
|
10
|
+
static description = 'Revoke an SDK key by its ID';
|
|
11
|
+
static examples = ['<%= config.bin %> <%= command.id %> a1b2c3d4-e5f6-7890-abcd-ef1234567890'];
|
|
14
12
|
async run() {
|
|
15
13
|
const { args } = await this.parse(SdkKeyRevoke);
|
|
16
14
|
const request = await this.apiClient.post('/api/v1/sdkKeys/delete', {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { ProvidedSource } from '@quonfig/node';
|
|
2
|
+
import { ProvidedSource, ConfigValueType } from '@quonfig/node';
|
|
3
3
|
import { APICommand } from '../index.js';
|
|
4
|
-
import { ConfigValueType } from '@quonfig/node';
|
|
5
4
|
import getConfirmation, { confirmFlag } from '../ui/get-confirmation.js';
|
|
6
5
|
import getEnvironment from '../ui/get-environment.js';
|
|
7
6
|
import getString from '../ui/get-string.js';
|
|
@@ -38,6 +37,8 @@ To see all current values and rules for a flag:
|
|
|
38
37
|
'<%= config.bin %> <%= command.id %> my.config.name --env-var=MY_ENV_VAR_NAME --environment=production',
|
|
39
38
|
'# For a percentage rollout, use set-rollout instead:',
|
|
40
39
|
'<%= config.bin %> set-rollout my.flag.name --environment production --true-percent 20',
|
|
40
|
+
'# For per-user / per-property targeting (e.g. user.email == X), edit JSON directly:',
|
|
41
|
+
'<%= config.bin %> pull && <%= config.bin %> config-schema',
|
|
41
42
|
];
|
|
42
43
|
static flags = {
|
|
43
44
|
confidential: Flags.boolean({ default: false, description: 'mark the value as confidential' }),
|
|
@@ -96,7 +97,10 @@ To see all current values and rules for a flag:
|
|
|
96
97
|
this.verboseLog('Selected config:', config);
|
|
97
98
|
// Check if existing config has encrypted values
|
|
98
99
|
if (!secret.selected) {
|
|
99
|
-
const configDetailsRequest = await this.apiClient.post('/api/v1/metadata/getByKey', {
|
|
100
|
+
const configDetailsRequest = await this.apiClient.post('/api/v1/metadata/getByKey', {
|
|
101
|
+
workspaceId: this.workspaceId,
|
|
102
|
+
key,
|
|
103
|
+
});
|
|
100
104
|
if (configDetailsRequest.ok) {
|
|
101
105
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
106
|
const configDetails = configDetailsRequest.json;
|
|
@@ -170,7 +174,6 @@ To see all current values and rules for a flag:
|
|
|
170
174
|
});
|
|
171
175
|
}
|
|
172
176
|
async submitChange({ confidential, config, envVar, environment, key, secret, value, }) {
|
|
173
|
-
/* eslint-disable camelcase */
|
|
174
177
|
const typeMapping = {
|
|
175
178
|
bool: 'bool',
|
|
176
179
|
string: 'string',
|
|
@@ -182,7 +185,6 @@ To see all current values and rules for a flag:
|
|
|
182
185
|
duration: 'duration',
|
|
183
186
|
int_range: 'intRange',
|
|
184
187
|
};
|
|
185
|
-
/* eslint-enable camelcase */
|
|
186
188
|
const type = typeMapping[config.valueType.toLowerCase()] || config.valueType;
|
|
187
189
|
let configValue;
|
|
188
190
|
let successMessage;
|
|
@@ -14,8 +14,8 @@ export default class SetRollout extends APICommand {
|
|
|
14
14
|
confirm: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<JsonObj | void>;
|
|
17
|
-
private parseWeights;
|
|
18
17
|
private coerceValue;
|
|
18
|
+
private parseWeights;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Pick a short, stable variant name from a rollout value. Uses the first
|
|
@@ -115,15 +115,7 @@ To inspect the current rollout:
|
|
|
115
115
|
return;
|
|
116
116
|
// Build weighted values
|
|
117
117
|
let weightedValues;
|
|
118
|
-
if (flags['true-percent']
|
|
119
|
-
const truePercent = flags['true-percent'];
|
|
120
|
-
const falsePercent = 100 - truePercent;
|
|
121
|
-
weightedValues = [
|
|
122
|
-
{ value: { type: 'bool', value: true }, weight: truePercent * (WEIGHT_TOTAL / 100) },
|
|
123
|
-
{ value: { type: 'bool', value: false }, weight: falsePercent * (WEIGHT_TOTAL / 100) },
|
|
124
|
-
];
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
118
|
+
if (flags['true-percent'] === undefined) {
|
|
127
119
|
// Parse --weights "value1:N,value2:M,..."
|
|
128
120
|
const parsed = this.parseWeights(flags.weights, config.valueType);
|
|
129
121
|
if (!parsed.ok) {
|
|
@@ -131,6 +123,14 @@ To inspect the current rollout:
|
|
|
131
123
|
}
|
|
132
124
|
weightedValues = parsed.weights;
|
|
133
125
|
}
|
|
126
|
+
else {
|
|
127
|
+
const truePercent = flags['true-percent'];
|
|
128
|
+
const falsePercent = 100 - truePercent;
|
|
129
|
+
weightedValues = [
|
|
130
|
+
{ value: { type: 'bool', value: true }, weight: truePercent * (WEIGHT_TOTAL / 100) },
|
|
131
|
+
{ value: { type: 'bool', value: false }, weight: falsePercent * (WEIGHT_TOTAL / 100) },
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
134
|
const totalWeight = weightedValues.reduce((sum, wv) => sum + wv.weight, 0);
|
|
135
135
|
if (totalWeight !== WEIGHT_TOTAL) {
|
|
136
136
|
return this.err(`Weights must sum to 100. Got: ${weightedValues.map((wv) => `${wv.weight / 1000}%`).join(' + ')} = ${totalWeight / 1000}%`);
|
|
@@ -209,7 +209,13 @@ To inspect the current rollout:
|
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
this.verboseLog(request.error);
|
|
212
|
-
return this.err(`Failed to set rollout: ${request.status} | ${JSON.stringify(request.error)}`, {
|
|
212
|
+
return this.err(`Failed to set rollout: ${request.status} | ${JSON.stringify(request.error)}`, {
|
|
213
|
+
key,
|
|
214
|
+
serverError: request.error,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
coerceValue(raw, valueType) {
|
|
218
|
+
return coerceValue(raw, valueType);
|
|
213
219
|
}
|
|
214
220
|
parseWeights(weightsStr, valueType) {
|
|
215
221
|
const pairs = weightsStr.split(',').map((s) => s.trim());
|
|
@@ -241,9 +247,6 @@ To inspect the current rollout:
|
|
|
241
247
|
}
|
|
242
248
|
return { ok: true, weights };
|
|
243
249
|
}
|
|
244
|
-
coerceValue(raw, valueType) {
|
|
245
|
-
return coerceValue(raw, valueType);
|
|
246
|
-
}
|
|
247
250
|
}
|
|
248
251
|
/**
|
|
249
252
|
* Pick a short, stable variant name from a rollout value. Uses the first
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -8,11 +8,11 @@ export default class Sync extends BaseCommand {
|
|
|
8
8
|
interval: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
9
|
watch: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
10
|
};
|
|
11
|
-
private workspaceId;
|
|
12
11
|
private resolvedDir;
|
|
12
|
+
private workspaceId;
|
|
13
13
|
run(): Promise<JsonObj | void>;
|
|
14
|
-
private runSync;
|
|
15
14
|
private ensureRemoteUrl;
|
|
16
|
-
private refreshToken;
|
|
17
15
|
private looksLike401;
|
|
16
|
+
private refreshToken;
|
|
17
|
+
private runSync;
|
|
18
18
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -5,7 +5,9 @@ import { getActiveProfile, loadAuthConfig } from '../util/token-storage.js';
|
|
|
5
5
|
import { loadGiteaToken, isGiteaTokenExpired, saveGiteaToken } from '../util/gitea-token-storage.js';
|
|
6
6
|
import { mintGiteaToken } from '../util/gitea-api.js';
|
|
7
7
|
import { isGitRepo, isWorkingTreeClean, gitFetch, canFastForward, hasDivergedFromRemote, gitMergeFfOnly, gitSetRemote, } from '../util/git-ops.js';
|
|
8
|
-
const sleep = (ms) => new Promise((resolve) =>
|
|
8
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
9
|
+
setTimeout(resolve, ms);
|
|
10
|
+
});
|
|
9
11
|
export default class Sync extends BaseCommand {
|
|
10
12
|
static description = 'Continuously poll for workspace config updates and apply them locally';
|
|
11
13
|
static examples = [
|
|
@@ -26,8 +28,8 @@ export default class Sync extends BaseCommand {
|
|
|
26
28
|
required: true,
|
|
27
29
|
}),
|
|
28
30
|
};
|
|
29
|
-
workspaceId;
|
|
30
31
|
resolvedDir;
|
|
32
|
+
workspaceId;
|
|
31
33
|
async run() {
|
|
32
34
|
const { flags } = await this.parse(Sync);
|
|
33
35
|
// Resolve directory
|
|
@@ -61,10 +63,31 @@ export default class Sync extends BaseCommand {
|
|
|
61
63
|
// Poll loop
|
|
62
64
|
// eslint-disable-next-line no-constant-condition
|
|
63
65
|
while (true) {
|
|
66
|
+
// eslint-disable-next-line no-await-in-loop
|
|
64
67
|
await sleep(intervalSec * 1000);
|
|
68
|
+
// eslint-disable-next-line no-await-in-loop
|
|
65
69
|
await this.runSync();
|
|
66
70
|
}
|
|
67
71
|
}
|
|
72
|
+
async ensureRemoteUrl() {
|
|
73
|
+
const entry = await loadGiteaToken(this.workspaceId);
|
|
74
|
+
if (!entry || isGiteaTokenExpired(entry)) {
|
|
75
|
+
await this.refreshToken();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await gitSetRemote(this.resolvedDir, entry.repoUrl);
|
|
79
|
+
}
|
|
80
|
+
looksLike401(err) {
|
|
81
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
82
|
+
return msg.includes('401') || msg.toLowerCase().includes('authentication failed');
|
|
83
|
+
}
|
|
84
|
+
async refreshToken() {
|
|
85
|
+
const data = await mintGiteaToken(this.workspaceId, 'read', 'pull');
|
|
86
|
+
const entry = { token: data.token, repoUrl: data.repoUrl, expiresAt: data.expiresAt };
|
|
87
|
+
await saveGiteaToken(this.workspaceId, entry);
|
|
88
|
+
await gitSetRemote(this.resolvedDir, entry.repoUrl);
|
|
89
|
+
this.verboseLog('Sync', 'Token refreshed.');
|
|
90
|
+
}
|
|
68
91
|
async runSync() {
|
|
69
92
|
const timestamp = new Date().toISOString();
|
|
70
93
|
try {
|
|
@@ -103,16 +126,16 @@ export default class Sync extends BaseCommand {
|
|
|
103
126
|
this.log(`[${timestamp}] Updated.`);
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
|
-
catch (
|
|
107
|
-
const msg =
|
|
108
|
-
if (this.looksLike401(
|
|
129
|
+
catch (error) {
|
|
130
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
131
|
+
if (this.looksLike401(error)) {
|
|
109
132
|
this.log(`[${timestamp}] Token expired, refreshing...`);
|
|
110
133
|
try {
|
|
111
134
|
await this.refreshToken();
|
|
112
135
|
await this.ensureRemoteUrl();
|
|
113
136
|
}
|
|
114
|
-
catch (
|
|
115
|
-
this.log(`[${timestamp}] Warning: could not refresh token: ${String(
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.log(`[${timestamp}] Warning: could not refresh token: ${String(error)}`);
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
141
|
else {
|
|
@@ -120,23 +143,4 @@ export default class Sync extends BaseCommand {
|
|
|
120
143
|
}
|
|
121
144
|
}
|
|
122
145
|
}
|
|
123
|
-
async ensureRemoteUrl() {
|
|
124
|
-
const entry = await loadGiteaToken(this.workspaceId);
|
|
125
|
-
if (!entry || isGiteaTokenExpired(entry)) {
|
|
126
|
-
await this.refreshToken();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
await gitSetRemote(this.resolvedDir, entry.repoUrl);
|
|
130
|
-
}
|
|
131
|
-
async refreshToken() {
|
|
132
|
-
const data = await mintGiteaToken(this.workspaceId, 'read', 'pull');
|
|
133
|
-
const entry = { token: data.token, repoUrl: data.repoUrl, expiresAt: data.expiresAt };
|
|
134
|
-
await saveGiteaToken(this.workspaceId, entry);
|
|
135
|
-
await gitSetRemote(this.resolvedDir, entry.repoUrl);
|
|
136
|
-
this.verboseLog('Sync', 'Token refreshed.');
|
|
137
|
-
}
|
|
138
|
-
looksLike401(err) {
|
|
139
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
-
return msg.includes('401') || msg.toLowerCase().includes('authentication failed');
|
|
141
|
-
}
|
|
142
146
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -15,12 +15,10 @@ export default class Whoami extends BaseCommand {
|
|
|
15
15
|
}
|
|
16
16
|
// Get email from stored user info or decode JWT
|
|
17
17
|
let userEmail = tokens.userEmail;
|
|
18
|
-
let orgId;
|
|
19
18
|
if (!userEmail) {
|
|
20
19
|
try {
|
|
21
20
|
const payload = decodeJWT(tokens.accessToken);
|
|
22
21
|
userEmail = payload.email;
|
|
23
|
-
orgId = payload.org_id;
|
|
24
22
|
}
|
|
25
23
|
catch {
|
|
26
24
|
// If we can't decode the token, continue without email
|
|
@@ -38,9 +36,7 @@ export default class Whoami extends BaseCommand {
|
|
|
38
36
|
// Display user information
|
|
39
37
|
const workspaceSlug = profile.workspaceSlug || profile.workspaceName;
|
|
40
38
|
const orgPrefix = profile.organizationName ? `${profile.organizationName} / ` : '';
|
|
41
|
-
const workspaceDisplay = workspaceSlug
|
|
42
|
-
? `${orgPrefix}${workspaceSlug} (${profile.workspace})`
|
|
43
|
-
: profile.workspace;
|
|
39
|
+
const workspaceDisplay = workspaceSlug ? `${orgPrefix}${workspaceSlug} (${profile.workspace})` : profile.workspace;
|
|
44
40
|
this.log(`Logged in as: ${userEmail || 'Unknown'}`);
|
|
45
41
|
this.log(`Workspace: ${workspaceDisplay}`);
|
|
46
42
|
return {
|
|
@@ -6,7 +6,7 @@ import { getActiveProfile, loadAuthConfig } from '../../util/token-storage.js';
|
|
|
6
6
|
import { mintGiteaToken } from '../../util/gitea-api.js';
|
|
7
7
|
import { isGitRepo, hasAtLeastOneCommit, gitSetRemote, gitPushForceLease, gitPushForce, getRemoteUrl, displayUrl, } from '../../util/git-ops.js';
|
|
8
8
|
export default class WorkspaceBootstrap extends BaseCommand {
|
|
9
|
-
static description =
|
|
9
|
+
static description = "Push a local git repo to Gitea as this workspace's config repository";
|
|
10
10
|
static examples = [
|
|
11
11
|
'<%= config.bin %> workspace bootstrap --dir ./our-config',
|
|
12
12
|
'<%= config.bin %> workspace bootstrap --dir ./launch-migrator/output',
|
|
@@ -70,8 +70,8 @@ export default class WorkspaceBootstrap extends BaseCommand {
|
|
|
70
70
|
}
|
|
71
71
|
this.log('Validation passed.\n');
|
|
72
72
|
}
|
|
73
|
-
catch (
|
|
74
|
-
this.verboseLog('Validation error', String(
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.verboseLog('Validation error', String(error));
|
|
75
75
|
this.log('Warning: could not run validation. Proceeding anyway.');
|
|
76
76
|
this.log('Use --skip-validate to suppress this warning.\n');
|
|
77
77
|
}
|
|
@@ -99,8 +99,8 @@ export default class WorkspaceBootstrap extends BaseCommand {
|
|
|
99
99
|
try {
|
|
100
100
|
tokenData = await mintGiteaToken(workspaceId, 'write', 'bootstrap');
|
|
101
101
|
}
|
|
102
|
-
catch (
|
|
103
|
-
return this.err(`Could not get Gitea credentials: ${String(
|
|
102
|
+
catch (error) {
|
|
103
|
+
return this.err(`Could not get Gitea credentials: ${String(error)}\n\nMake sure the workspace is fully provisioned in the Quonfig app before bootstrapping.`);
|
|
104
104
|
}
|
|
105
105
|
const { repoUrl } = tokenData;
|
|
106
106
|
this.verboseLog('WorkspaceBootstrap', { repoUrl: displayUrl(repoUrl) });
|
|
@@ -132,8 +132,8 @@ export default class WorkspaceBootstrap extends BaseCommand {
|
|
|
132
132
|
await gitPushForceLease(resolvedDir);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
-
catch (
|
|
136
|
-
return this.err(`Push failed: ${String(
|
|
135
|
+
catch (error) {
|
|
136
|
+
return this.err(`Push failed: ${String(error)}`);
|
|
137
137
|
}
|
|
138
138
|
this.log(`\nBootstrap complete.`);
|
|
139
139
|
this.log(`Workspace "${workspaceName}" is now connected to your local directory.`);
|
|
@@ -26,7 +26,7 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
26
26
|
const res = await fetch(`${apiUrl}/api/v1/userWorkspaces/list`, {
|
|
27
27
|
method: 'POST',
|
|
28
28
|
headers: {
|
|
29
|
-
|
|
29
|
+
Authorization: `Bearer ${accessToken}`,
|
|
30
30
|
'Content-Type': 'application/json',
|
|
31
31
|
},
|
|
32
32
|
body: JSON.stringify({ json: {} }),
|
|
@@ -34,7 +34,7 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
34
34
|
if (!res.ok) {
|
|
35
35
|
return this.err(`Failed to fetch workspaces (${res.status}). Try \`qfg login\` to re-authenticate.`);
|
|
36
36
|
}
|
|
37
|
-
const body = await res.json();
|
|
37
|
+
const body = (await res.json());
|
|
38
38
|
const all = (body.json ?? body);
|
|
39
39
|
candidates = Array.isArray(all) ? all : [];
|
|
40
40
|
}
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export interface InitOptions {
|
|
8
8
|
dir: string;
|
|
9
|
+
dryRun: boolean;
|
|
9
10
|
/** true = force include samples, false = force skip, undefined = auto-detect */
|
|
10
11
|
samples: boolean | undefined;
|
|
11
|
-
dryRun: boolean;
|
|
12
12
|
}
|
|
13
13
|
export type ActionKind = 'git-init' | 'create-dir' | 'write-file' | 'skip-file' | 'create-hook' | 'update-hook' | 'skip-hook';
|
|
14
14
|
export interface InitAction {
|
|
15
|
+
description: string;
|
|
15
16
|
kind: ActionKind;
|
|
16
17
|
path: string;
|
|
17
|
-
description: string;
|
|
18
18
|
}
|
|
19
19
|
export interface InitPlan {
|
|
20
20
|
actions: InitAction[];
|
|
@@ -11,13 +11,7 @@ import { SAMPLE_FILES } from './samples.js';
|
|
|
11
11
|
import { storedConfigJsonSchema } from './schema.js';
|
|
12
12
|
import { PRE_COMMIT_MARKER, agentsMdTemplate, claudeMdTemplate, preCommitHookContent, readmeTemplate, } from './templates.js';
|
|
13
13
|
// ── Directories that define a workspace ────────────────────────────────
|
|
14
|
-
const WORKSPACE_DIRS = [
|
|
15
|
-
'configs',
|
|
16
|
-
'feature-flags',
|
|
17
|
-
'segments',
|
|
18
|
-
'log-levels',
|
|
19
|
-
'schemas',
|
|
20
|
-
];
|
|
14
|
+
const WORKSPACE_DIRS = ['configs', 'feature-flags', 'segments', 'log-levels', 'schemas'];
|
|
21
15
|
// ── Plan ───────────────────────────────────────────────────────────────
|
|
22
16
|
export function planInit(options) {
|
|
23
17
|
const { dir } = options;
|
|
@@ -49,7 +43,7 @@ export function planInit(options) {
|
|
|
49
43
|
{ file: 'CLAUDE.md', content: claudeMdTemplate() },
|
|
50
44
|
{ file: 'AGENTS.md', content: agentsMdTemplate() },
|
|
51
45
|
];
|
|
52
|
-
for (const { file
|
|
46
|
+
for (const { file } of managedFiles) {
|
|
53
47
|
const filePath = path.join(dir, file);
|
|
54
48
|
const verb = fs.existsSync(filePath) ? 'Update' : 'Create';
|
|
55
49
|
actions.push({ kind: 'write-file', path: file, description: `${verb} ${file}` });
|
|
@@ -77,25 +71,45 @@ export function planInit(options) {
|
|
|
77
71
|
// 5. Git pre-commit hook (always runs — git repo guaranteed by step 0)
|
|
78
72
|
if (needsGitInit) {
|
|
79
73
|
// Fresh git init — hook will be created
|
|
80
|
-
actions.push({
|
|
74
|
+
actions.push({
|
|
75
|
+
kind: 'create-hook',
|
|
76
|
+
path: '.git/hooks/pre-commit',
|
|
77
|
+
description: 'Install pre-commit hook (qfg verify)',
|
|
78
|
+
});
|
|
81
79
|
}
|
|
82
80
|
else {
|
|
83
81
|
const hooksDir = path.join(gitDir, 'hooks');
|
|
84
82
|
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
85
83
|
if (fs.existsSync(hookPath)) {
|
|
86
|
-
const existing = fs.readFileSync(hookPath, '
|
|
84
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
87
85
|
if (existing.includes(PRE_COMMIT_MARKER)) {
|
|
88
|
-
actions.push({
|
|
86
|
+
actions.push({
|
|
87
|
+
kind: 'skip-hook',
|
|
88
|
+
path: '.git/hooks/pre-commit',
|
|
89
|
+
description: 'Pre-commit hook already installed',
|
|
90
|
+
});
|
|
89
91
|
}
|
|
90
92
|
else if (existing.includes('qfg verify')) {
|
|
91
|
-
actions.push({
|
|
93
|
+
actions.push({
|
|
94
|
+
kind: 'skip-hook',
|
|
95
|
+
path: '.git/hooks/pre-commit',
|
|
96
|
+
description: 'Pre-commit hook already runs qfg verify',
|
|
97
|
+
});
|
|
92
98
|
}
|
|
93
99
|
else {
|
|
94
|
-
actions.push({
|
|
100
|
+
actions.push({
|
|
101
|
+
kind: 'update-hook',
|
|
102
|
+
path: '.git/hooks/pre-commit',
|
|
103
|
+
description: 'Append qfg verify to existing pre-commit hook',
|
|
104
|
+
});
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
else {
|
|
98
|
-
actions.push({
|
|
108
|
+
actions.push({
|
|
109
|
+
kind: 'create-hook',
|
|
110
|
+
path: '.git/hooks/pre-commit',
|
|
111
|
+
description: 'Install pre-commit hook (qfg verify)',
|
|
112
|
+
});
|
|
99
113
|
}
|
|
100
114
|
}
|
|
101
115
|
return { actions, isFirstTime, samplesIncluded: includeSamples };
|
|
@@ -134,7 +148,7 @@ export function executeInit(plan, dir) {
|
|
|
134
148
|
}
|
|
135
149
|
const content = managedContent[action.path] ?? sampleContent[action.path];
|
|
136
150
|
if (content) {
|
|
137
|
-
fs.writeFileSync(fullPath, content, '
|
|
151
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
138
152
|
}
|
|
139
153
|
break;
|
|
140
154
|
}
|
|
@@ -143,14 +157,14 @@ export function executeInit(plan, dir) {
|
|
|
143
157
|
if (!fs.existsSync(hooksDir)) {
|
|
144
158
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
145
159
|
}
|
|
146
|
-
fs.writeFileSync(fullPath, preCommitHookContent(), '
|
|
160
|
+
fs.writeFileSync(fullPath, preCommitHookContent(), 'utf8');
|
|
147
161
|
fs.chmodSync(fullPath, 0o755);
|
|
148
162
|
break;
|
|
149
163
|
}
|
|
150
164
|
case 'update-hook': {
|
|
151
|
-
const existing = fs.readFileSync(fullPath, '
|
|
165
|
+
const existing = fs.readFileSync(fullPath, 'utf8');
|
|
152
166
|
const appended = existing.trimEnd() + '\n\n' + preCommitHookContent().split('\n').slice(1).join('\n');
|
|
153
|
-
fs.writeFileSync(fullPath, appended, '
|
|
167
|
+
fs.writeFileSync(fullPath, appended, 'utf8');
|
|
154
168
|
break;
|
|
155
169
|
}
|
|
156
170
|
case 'skip-file':
|
package/dist/init/samples.d.ts
CHANGED
package/dist/init/samples.js
CHANGED
|
@@ -95,9 +95,9 @@ export const SAMPLE_FILES = [
|
|
|
95
95
|
},
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
|
-
path: 'log-levels/example.app.json',
|
|
98
|
+
path: 'log-levels/log-level.example.app.json',
|
|
99
99
|
content: {
|
|
100
|
-
key: 'example.app',
|
|
100
|
+
key: 'log-level.example.app',
|
|
101
101
|
type: 'log_level',
|
|
102
102
|
valueType: 'log_level',
|
|
103
103
|
sendToClientSdk: false,
|
package/dist/init/schema.js
CHANGED
|
@@ -11,17 +11,27 @@ const valueTypes = ['bool', 'string', 'int', 'double', 'json', 'string_list', 'd
|
|
|
11
11
|
const logLevels = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];
|
|
12
12
|
const operators = [
|
|
13
13
|
'ALWAYS_TRUE',
|
|
14
|
-
'PROP_IS_ONE_OF',
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
14
|
+
'PROP_IS_ONE_OF',
|
|
15
|
+
'PROP_IS_NOT_ONE_OF',
|
|
16
|
+
'PROP_STARTS_WITH_ONE_OF',
|
|
17
|
+
'PROP_DOES_NOT_START_WITH_ONE_OF',
|
|
18
|
+
'PROP_ENDS_WITH_ONE_OF',
|
|
19
|
+
'PROP_DOES_NOT_END_WITH_ONE_OF',
|
|
20
|
+
'PROP_CONTAINS_ONE_OF',
|
|
21
|
+
'PROP_DOES_NOT_CONTAIN_ONE_OF',
|
|
22
|
+
'PROP_LESS_THAN',
|
|
23
|
+
'PROP_LESS_THAN_OR_EQUAL',
|
|
24
|
+
'PROP_GREATER_THAN',
|
|
25
|
+
'PROP_GREATER_THAN_OR_EQUAL',
|
|
26
|
+
'PROP_BEFORE',
|
|
27
|
+
'PROP_AFTER',
|
|
28
|
+
'PROP_MATCHES',
|
|
29
|
+
'PROP_DOES_NOT_MATCH',
|
|
30
|
+
'IN_SEG',
|
|
31
|
+
'NOT_IN_SEG',
|
|
23
32
|
'IN_INT_RANGE',
|
|
24
|
-
'LOOKUP_KEY_IN',
|
|
33
|
+
'LOOKUP_KEY_IN',
|
|
34
|
+
'LOOKUP_KEY_NOT_IN',
|
|
25
35
|
];
|
|
26
36
|
// ── Value sub-schemas ──────────────────────────────────────────────────
|
|
27
37
|
const boolValue = {
|
|
@@ -173,10 +183,7 @@ const weightedValuesValue = {
|
|
|
173
183
|
additionalProperties: false,
|
|
174
184
|
};
|
|
175
185
|
const ruleValue = {
|
|
176
|
-
oneOf: [
|
|
177
|
-
...simpleValue.oneOf,
|
|
178
|
-
weightedValuesValue,
|
|
179
|
-
],
|
|
186
|
+
oneOf: [...simpleValue.oneOf, weightedValuesValue],
|
|
180
187
|
};
|
|
181
188
|
// ── Criterion ──────────────────────────────────────────────────────────
|
|
182
189
|
const criterion = {
|
|
@@ -310,10 +317,11 @@ export function storedConfigJsonSchema() {
|
|
|
310
317
|
},
|
|
311
318
|
required: ['key', 'type', 'valueType', 'default'],
|
|
312
319
|
additionalProperties: true,
|
|
313
|
-
// Type-specific constraints
|
|
320
|
+
// Type-specific constraints (JSON Schema if/then/else — not Promise thenables)
|
|
314
321
|
allOf: [
|
|
315
322
|
{
|
|
316
323
|
if: { properties: { type: { const: 'segment' } } },
|
|
324
|
+
// eslint-disable-next-line unicorn/no-thenable
|
|
317
325
|
then: {
|
|
318
326
|
properties: {
|
|
319
327
|
valueType: { const: 'bool' },
|
|
@@ -323,6 +331,7 @@ export function storedConfigJsonSchema() {
|
|
|
323
331
|
},
|
|
324
332
|
{
|
|
325
333
|
if: { properties: { type: { const: 'log_level' } } },
|
|
334
|
+
// eslint-disable-next-line unicorn/no-thenable
|
|
326
335
|
then: {
|
|
327
336
|
properties: {
|
|
328
337
|
valueType: { const: 'log_level' },
|
|
@@ -15,16 +15,7 @@ const convertToKebabCase = (text) => text
|
|
|
15
15
|
.trim()
|
|
16
16
|
.replaceAll(' ', '-')
|
|
17
17
|
.toLowerCase();
|
|
18
|
-
const commands = [
|
|
19
|
-
SetDefault,
|
|
20
|
-
GenerateNewHexKey,
|
|
21
|
-
Create,
|
|
22
|
-
Get,
|
|
23
|
-
Info,
|
|
24
|
-
Mcp,
|
|
25
|
-
Override,
|
|
26
|
-
List,
|
|
27
|
-
].map((command) => {
|
|
18
|
+
const commands = [SetDefault, GenerateNewHexKey, Create, Get, Info, Mcp, Override, List].map((command) => {
|
|
28
19
|
const id = convertToKebabCase(command.name);
|
|
29
20
|
return {
|
|
30
21
|
command,
|
package/dist/migrate/doctor.js
CHANGED
|
@@ -124,7 +124,9 @@ const checkSdkDatadirSupport = (ctx) => {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
return {
|
|
127
|
-
message: language
|
|
127
|
+
message: language
|
|
128
|
+
? `SDK language "${language}" supports datadir mode.`
|
|
129
|
+
: 'No SDK language specified; assuming datadir-capable.',
|
|
128
130
|
name: 'sdk-datadir-support',
|
|
129
131
|
passed: true,
|
|
130
132
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ImportState } from './import-state.js';
|
|
2
|
+
import { type MigrationReportData } from './migration-report.js';
|
|
3
|
+
import { type PushIdentity } from './push-strategy.js';
|
|
4
|
+
import type { LegacyChange, MigrationSource } from './source.js';
|
|
5
|
+
export interface ApplyLocalMigrationOptions {
|
|
6
|
+
/** Commit author. Defaults to the migrator identity. */
|
|
7
|
+
author?: PushIdentity;
|
|
8
|
+
/** Initial branch name when initializing a fresh repo. Defaults to `main`. */
|
|
9
|
+
branch?: string;
|
|
10
|
+
changes: LegacyChange[];
|
|
11
|
+
commitMessage: string;
|
|
12
|
+
/** Environments discovered from the source (slugified). Written to `quonfig.json` if missing. */
|
|
13
|
+
environments: string[];
|
|
14
|
+
importState: ImportState;
|
|
15
|
+
localDir: string;
|
|
16
|
+
reportData: MigrationReportData;
|
|
17
|
+
source: MigrationSource;
|
|
18
|
+
}
|
|
19
|
+
export interface ApplyLocalMigrationResult {
|
|
20
|
+
/** `'initialized'` if we had to `git init` the target, `'reused'` if it was already a repo. */
|
|
21
|
+
action: 'initialized' | 'reused';
|
|
22
|
+
commitSha: string | null;
|
|
23
|
+
/** `false` if translate produced no net changes — nothing was committed. */
|
|
24
|
+
committed: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare const applyLocalMigration: (opts: ApplyLocalMigrationOptions) => Promise<ApplyLocalMigrationResult>;
|