@quonfig/cli 0.0.50 → 0.0.51
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 +50 -48
- package/dist/commands/generate.js +21 -5
- package/dist/commands/migrate.d.ts +1 -1
- package/dist/commands/migrate.js +16 -5
- package/dist/init/templates.js +59 -12
- package/dist/migrate/migration-report.js +72 -11
- package/dist/migrate/quonfig-target/report.d.ts +18 -1
- package/dist/migrate/sources/flagsmith/api.d.ts +115 -0
- package/dist/migrate/sources/flagsmith/api.js +402 -0
- package/dist/migrate/sources/flagsmith/translate.d.ts +72 -0
- package/dist/migrate/sources/flagsmith/translate.js +657 -0
- package/dist/migrate/sources/flagsmith/types.d.ts +282 -0
- package/dist/migrate/sources/flagsmith/types.js +18 -0
- package/dist/migrate/sources/flagsmith.d.ts +62 -0
- package/dist/migrate/sources/flagsmith.js +380 -23
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +3 -5
- package/package.json +2 -2
|
@@ -1,28 +1,385 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `qfg migrate --from flagsmith` — the MigrationSource implementation.
|
|
3
|
+
*
|
|
4
|
+
* Wires the Flagsmith fetcher (`flagsmith/api.ts` + `flagsmith/types.ts`) and
|
|
5
|
+
* converter (`flagsmith/translate.ts`) into the source-agnostic migration
|
|
6
|
+
* framework. Epic 1 implemented the read side (validateAuth, listEnvironments,
|
|
7
|
+
* fetchChanges yielding LegacyChange[] carrying raw Flagsmith JSON); Epic 3
|
|
8
|
+
* adds the translate() side — `translateFeature` and `translateSegment`
|
|
9
|
+
* dispatched by `raw.kind` (see `FlagsmithRaw` below). Report accumulators
|
|
10
|
+
* (`getSkippedConfigs`, `getCoercedSentinels`, `getConversionNotes`) thread
|
|
11
|
+
* the per-source notes into MIGRATION_REPORT.md.
|
|
12
|
+
*
|
|
13
|
+
* Design is frozen in `project/plans/migrator-flagsmith.md`. Notable choices
|
|
14
|
+
* for this epic — the contract Epic 3 will consume:
|
|
15
|
+
*
|
|
16
|
+
* - `LegacyChange.raw` is a discriminated union over `{kind: 'feature' | 'segment'}`.
|
|
17
|
+
* Features carry the full per-feature stitched bundle (env-default +
|
|
18
|
+
* segment-overrides + identity-overrides + feature-segment priority +
|
|
19
|
+
* inline MV options sorted by id asc). Segments are emitted separately
|
|
20
|
+
* (project-scoped, referenced from multiple features).
|
|
21
|
+
*
|
|
22
|
+
* - Tags are NOT separate LegacyChanges — they're inline `feature.tags[]` as
|
|
23
|
+
* integer IDs, plus a project-wide tag pool stashed in the source state for
|
|
24
|
+
* the converter to resolve id → label.
|
|
25
|
+
*
|
|
26
|
+
* - Edge identities are NOT separate LegacyChanges. Identity overrides are
|
|
27
|
+
* bundled inside the feature payload (`identity_overrides`); the identity
|
|
28
|
+
* `identifier` is already on each override row. Traits are runtime context
|
|
29
|
+
* the SDK sends — no server-side artifact to migrate (plan §5.5, D-F6).
|
|
30
|
+
*
|
|
31
|
+
* - Project metadata (`use_edge_identities`, `only_allow_lower_case_feature_names`,
|
|
32
|
+
* …) is captured during `validateAuth` and stashed in source state. The
|
|
33
|
+
* converter can read it via `getProjectMetadata()` (a Flagsmith-specific
|
|
34
|
+
* getter, not part of the MigrationSource interface).
|
|
35
|
+
*/
|
|
36
|
+
import { ConversionReport } from '../quonfig-target/report.js';
|
|
37
|
+
import { fetchProject, fetchSnapshot } from './flagsmith/api.js';
|
|
38
|
+
import { flagOutputPath, segmentOutputPath, translateFeature, translateSegment } from './flagsmith/translate.js';
|
|
12
39
|
const SOURCE_NAME = 'flagsmith';
|
|
40
|
+
const DEFAULT_PROJECT_ID = '1';
|
|
41
|
+
function resolveProjectId() {
|
|
42
|
+
return process.env.FLAGSMITH_PROJECT_ID ?? DEFAULT_PROJECT_ID;
|
|
43
|
+
}
|
|
44
|
+
const state = {
|
|
45
|
+
apiKey: null,
|
|
46
|
+
environments: [],
|
|
47
|
+
multivariateFeatures: [],
|
|
48
|
+
project: null,
|
|
49
|
+
projectId: resolveProjectId(),
|
|
50
|
+
report: new ConversionReport(),
|
|
51
|
+
segments: [],
|
|
52
|
+
tags: [],
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Override the Flagsmith project ID for this run. Called by `qfg migrate` from
|
|
56
|
+
* its `--project` flag (or the `FLAGSMITH_PROJECT_ID` env var) — mirrors
|
|
57
|
+
* `setLaunchDarklyProjectKey`. Flagsmith projects are numeric, but we accept
|
|
58
|
+
* either a number or a numeric-string and convert at the API boundary.
|
|
59
|
+
*/
|
|
60
|
+
export function setFlagsmithProjectId(projectId) {
|
|
61
|
+
state.projectId = String(projectId);
|
|
62
|
+
}
|
|
63
|
+
/** The project metadata captured by validateAuth, or null if we haven't authed yet. */
|
|
64
|
+
export function getFlagsmithProject() {
|
|
65
|
+
return state.project;
|
|
66
|
+
}
|
|
67
|
+
/** The tag pool from the last snapshot — Epic 3's converter resolves `feature.tags[]` IDs to labels via this. */
|
|
68
|
+
export function getFlagsmithTagPool() {
|
|
69
|
+
return [...state.tags];
|
|
70
|
+
}
|
|
71
|
+
class MissingAuthError extends Error {
|
|
72
|
+
constructor(operation) {
|
|
73
|
+
super(`flagsmith source ${operation} requires validateAuth(apiKey) to be called first (no API key configured).`);
|
|
74
|
+
this.name = 'MissingAuthError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function requireApiKey(operation) {
|
|
78
|
+
if (!state.apiKey)
|
|
79
|
+
throw new MissingAuthError(operation);
|
|
80
|
+
return state.apiKey;
|
|
81
|
+
}
|
|
82
|
+
/** Flagsmith env api_keys are random strings; we surface env `name` slugified. */
|
|
83
|
+
function slugifyEnvName(name) {
|
|
84
|
+
return name
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replaceAll(/[^\da-z]+/g, '-')
|
|
87
|
+
.replaceAll(/^-+|-+$/g, '');
|
|
88
|
+
}
|
|
89
|
+
async function validateAuthImpl(apiKey) {
|
|
90
|
+
// The cheapest authenticated probe is the project endpoint — 401s on a bad
|
|
91
|
+
// token, 404s on a bad project, and we need `use_edge_identities` for the
|
|
92
|
+
// snapshot walk anyway, so capture it here.
|
|
93
|
+
const project = await fetchProject(apiKey, state.projectId);
|
|
94
|
+
state.apiKey = apiKey;
|
|
95
|
+
state.project = project;
|
|
96
|
+
state.report = new ConversionReport();
|
|
97
|
+
state.environments = [];
|
|
98
|
+
state.multivariateFeatures = [];
|
|
99
|
+
state.segments = [];
|
|
100
|
+
state.tags = [];
|
|
101
|
+
}
|
|
102
|
+
async function listEnvironmentsImpl() {
|
|
103
|
+
const apiKey = requireApiKey('listEnvironments');
|
|
104
|
+
// We could do a dedicated fetchEnvironments call here, but fetchSnapshot
|
|
105
|
+
// will do that as part of fetchChanges anyway. To keep listEnvironments
|
|
106
|
+
// cheap (called by the migrate command before fetchChanges), we do a
|
|
107
|
+
// standalone env fetch and stash the results — fetchSnapshot will refresh
|
|
108
|
+
// them when it runs.
|
|
109
|
+
const { fetchEnvironments } = await import('./flagsmith/api.js');
|
|
110
|
+
const envs = await fetchEnvironments(apiKey, state.projectId);
|
|
111
|
+
state.environments = envs;
|
|
112
|
+
return [...new Set(envs.map((e) => slugifyEnvName(e.name)))];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Phase-1 config snapshot. Decision D2: always a full re-snapshot — we do not
|
|
116
|
+
* try to use Flagsmith's audit log for deltas (cursor stability across v2
|
|
117
|
+
* versioning rewrites is unverified). `sinceEpochMs` is currently unused;
|
|
118
|
+
* Epic 5 will use it for reporting "what changed since last run" without
|
|
119
|
+
* affecting the fetch itself.
|
|
120
|
+
*
|
|
121
|
+
* Yields one LegacyChange per feature (with the full per-env state bundle)
|
|
122
|
+
* followed by one LegacyChange per segment. Identity overrides are bundled
|
|
123
|
+
* inside the feature payload.
|
|
124
|
+
*/
|
|
125
|
+
async function* fetchChangesImpl() {
|
|
126
|
+
const apiKey = requireApiKey('fetchChanges');
|
|
127
|
+
const snapshot = await fetchSnapshot(apiKey, state.projectId);
|
|
128
|
+
state.project = snapshot.project;
|
|
129
|
+
state.environments = snapshot.environments;
|
|
130
|
+
state.segments = snapshot.segments;
|
|
131
|
+
state.tags = snapshot.tags;
|
|
132
|
+
state.multivariateFeatures = snapshot.features
|
|
133
|
+
.filter((bundle) => bundle.feature.type === 'MULTIVARIATE' && (bundle.feature.multivariate_options?.length ?? 0) > 0)
|
|
134
|
+
.map((bundle) => bundle.feature.name);
|
|
135
|
+
for (const featureBundle of snapshot.features) {
|
|
136
|
+
yield {
|
|
137
|
+
key: featureBundle.feature.name,
|
|
138
|
+
raw: { data: featureBundle, kind: 'feature' },
|
|
139
|
+
source: SOURCE_NAME,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
for (const segment of snapshot.segments) {
|
|
143
|
+
yield {
|
|
144
|
+
key: segment.name,
|
|
145
|
+
raw: { data: segment, kind: 'segment' },
|
|
146
|
+
source: SOURCE_NAME,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Dispatch by raw.kind to the right translator (plan §5.1). Last-resort
|
|
152
|
+
* safety net mirrors the LD source — one structurally broken feature must
|
|
153
|
+
* not abort the whole run, so conversion errors land as `skipped-config`
|
|
154
|
+
* report entries and translate returns `[]`.
|
|
155
|
+
*/
|
|
156
|
+
function translateImpl(change) {
|
|
157
|
+
const raw = change.raw;
|
|
158
|
+
if (!raw || typeof raw !== 'object' || !('kind' in raw))
|
|
159
|
+
return [];
|
|
160
|
+
try {
|
|
161
|
+
if (raw.kind === 'feature') {
|
|
162
|
+
const segmentNameById = new Map();
|
|
163
|
+
for (const seg of state.segments)
|
|
164
|
+
segmentNameById.set(seg.id, seg.name);
|
|
165
|
+
const envNameByApiKey = new Map();
|
|
166
|
+
for (const env of state.environments)
|
|
167
|
+
envNameByApiKey.set(env.api_key, env.name);
|
|
168
|
+
const out = translateFeature(raw.data, state.report, {
|
|
169
|
+
envNameByApiKey,
|
|
170
|
+
segmentNameById,
|
|
171
|
+
tags: state.tags,
|
|
172
|
+
});
|
|
173
|
+
return [{ contents: JSON.stringify(out, null, 2), path: flagOutputPath(raw.data.feature.name) }];
|
|
174
|
+
}
|
|
175
|
+
const out = translateSegment(raw.data, state.report);
|
|
176
|
+
return [{ contents: JSON.stringify(out, null, 2), path: segmentOutputPath(raw.data.name) }];
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
state.report.add('skipped-config', change.key ?? 'unknown', `conversion failed: ${message}`);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Configs that could not be converted at all (matches LD `getSkippedConfigs`). */
|
|
185
|
+
function getSkippedConfigsImpl() {
|
|
186
|
+
const skipped = state.report.byCategory('skipped-config');
|
|
187
|
+
if (skipped.length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
return {
|
|
190
|
+
entries: skipped.map((n) => ({ key: n.key, reason: n.detail })),
|
|
191
|
+
total: skipped.length,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Per-flag rule-value coercions Flagsmith forced — every `enabled-false-non-boolean`
|
|
196
|
+
* + `cross-env-value-type-coerced` note is counted here so the existing
|
|
197
|
+
* "rule values were coerced" rollup in MIGRATION_REPORT.md surfaces them.
|
|
198
|
+
*
|
|
199
|
+
* The bookkeeping is per-Quonfig-env (the env-name the customer sees in the
|
|
200
|
+
* report and in the file tree). D-F1 notes carry `env "<api_key>"` in their
|
|
201
|
+
* detail string from `translate.ts`; we regex it out and map api_key →
|
|
202
|
+
* slugified env name via the captured environments. Notes that don't carry an
|
|
203
|
+
* env reference (cross-env D-F5; rare malformed details) bucket under a
|
|
204
|
+
* synthetic `_cross-env` key so they still surface but are visually distinct.
|
|
205
|
+
*/
|
|
206
|
+
function getCoercedSentinelsImpl() {
|
|
207
|
+
const enabledFalse = state.report.byCategory('enabled-false-non-boolean');
|
|
208
|
+
const crossEnv = state.report.byCategory('cross-env-value-type-coerced');
|
|
209
|
+
if (enabledFalse.length === 0 && crossEnv.length === 0)
|
|
210
|
+
return null;
|
|
211
|
+
const envSlugByApiKey = new Map();
|
|
212
|
+
for (const env of state.environments)
|
|
213
|
+
envSlugByApiKey.set(env.api_key, slugifyEnvName(env.name));
|
|
214
|
+
const byEnv = {};
|
|
215
|
+
let total = 0;
|
|
216
|
+
const addToEnv = (envKey, outputPath) => {
|
|
217
|
+
const env = byEnv[envKey] ?? {};
|
|
218
|
+
env[outputPath] = (env[outputPath] ?? 0) + 1;
|
|
219
|
+
byEnv[envKey] = env;
|
|
220
|
+
total += 1;
|
|
221
|
+
};
|
|
222
|
+
// D-F1: detail strings look like `... in env "<api_key>" ...` (env-default
|
|
223
|
+
// path), `... identity override "<id>" ...`, or `... segment override "<name>" ...`.
|
|
224
|
+
// Only the env-default flavor includes an api_key; the override flavors are
|
|
225
|
+
// implicitly all-envs (they fire once per override row, which is always tied
|
|
226
|
+
// to an env, but the detail doesn't surface it). For override rows the
|
|
227
|
+
// bucketing reflects what we can know: "all envs" (`_all-envs`).
|
|
228
|
+
for (const note of enabledFalse) {
|
|
229
|
+
const path = flagOutputPath(note.key);
|
|
230
|
+
const match = /env "([^"]+)"/.exec(note.detail);
|
|
231
|
+
if (match) {
|
|
232
|
+
const apiKey = match[1];
|
|
233
|
+
const slug = envSlugByApiKey.get(apiKey) ?? apiKey;
|
|
234
|
+
addToEnv(slug, path);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// identity/segment override rows: surface under a synthetic bucket so
|
|
238
|
+
// the section still names the flag even when env attribution is lossy.
|
|
239
|
+
addToEnv('_all-envs', path);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// D-F5: cross-env divergence is a feature-wide finding — coerce target is
|
|
243
|
+
// string regardless of which env disagreed. Surface under a distinct bucket
|
|
244
|
+
// so the renderer doesn't show "0 from N envs" double-counting.
|
|
245
|
+
for (const note of crossEnv) {
|
|
246
|
+
addToEnv('_cross-env', flagOutputPath(note.key));
|
|
247
|
+
}
|
|
248
|
+
return { byEnv, total };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Currently no override sections are dropped due to env-id mismatch — Flagsmith
|
|
252
|
+
* has no equivalent of the Launch-source's archived/missing env case. Kept
|
|
253
|
+
* present so the report machinery's null-check still wires through.
|
|
254
|
+
*/
|
|
255
|
+
function getDroppedOverridesImpl() {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/** The full conversion report — every skip/drop/coerce note from this run. */
|
|
259
|
+
export function getConversionReport() {
|
|
260
|
+
return state.report;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Walk every segment rule (recursively across nested `rules[]`) and collect
|
|
264
|
+
* every `condition.property` reference. These are the trait/context attribute
|
|
265
|
+
* names that downstream Quonfig callers will need to send at eval time
|
|
266
|
+
* (plan §5.5 / D-F6). The `'user.key'` Quonfig built-in is excluded — it's the
|
|
267
|
+
* caller-identity slot that's already required for any flag eval.
|
|
268
|
+
*/
|
|
269
|
+
function collectIdentityTraits() {
|
|
270
|
+
const traitToSegments = new Map();
|
|
271
|
+
const visitRule = (rule, segmentName) => {
|
|
272
|
+
for (const cond of rule.conditions ?? []) {
|
|
273
|
+
const prop = cond.property;
|
|
274
|
+
if (!prop || prop.length === 0)
|
|
275
|
+
continue;
|
|
276
|
+
if (prop === 'user.key')
|
|
277
|
+
continue;
|
|
278
|
+
const set = traitToSegments.get(prop) ?? new Set();
|
|
279
|
+
set.add(segmentName);
|
|
280
|
+
traitToSegments.set(prop, set);
|
|
281
|
+
}
|
|
282
|
+
for (const child of rule.rules ?? [])
|
|
283
|
+
visitRule(child, segmentName);
|
|
284
|
+
};
|
|
285
|
+
for (const segment of state.segments) {
|
|
286
|
+
for (const rule of segment.rules ?? [])
|
|
287
|
+
visitRule(rule, segment.name);
|
|
288
|
+
}
|
|
289
|
+
const sorted = new Map();
|
|
290
|
+
for (const [trait, segs] of [...traitToSegments.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
291
|
+
sorted.set(trait, [...segs].sort((a, b) => a.localeCompare(b)));
|
|
292
|
+
}
|
|
293
|
+
return sorted;
|
|
294
|
+
}
|
|
295
|
+
function getConversionNotesImpl() {
|
|
296
|
+
// Convert raw report notes — translate.ts embeds env api_keys in its
|
|
297
|
+
// detail strings; substitute them with the slugified user-facing env name
|
|
298
|
+
// so the report is readable to anyone who hasn't memorized Flagsmith's
|
|
299
|
+
// 22-character env api_keys. No-op when the api_key isn't in the env map.
|
|
300
|
+
const envSlugByApiKey = new Map();
|
|
301
|
+
for (const env of state.environments)
|
|
302
|
+
envSlugByApiKey.set(env.api_key, slugifyEnvName(env.name));
|
|
303
|
+
const decorate = (detail) => {
|
|
304
|
+
let out = detail.replaceAll(/env "([^"]+)"/g, (full, apiKey) => {
|
|
305
|
+
const slug = envSlugByApiKey.get(apiKey);
|
|
306
|
+
return slug ? `env "${slug}"` : full;
|
|
307
|
+
});
|
|
308
|
+
// Cross-env divergence details list api_keys directly as `apk=type` pairs
|
|
309
|
+
// (e.g. `Qr6Q...=int, iwz7...=string`) without surrounding quotes. Decode
|
|
310
|
+
// every known api_key in the detail to its slugified name.
|
|
311
|
+
for (const [apiKey, slug] of envSlugByApiKey) {
|
|
312
|
+
if (apiKey === slug)
|
|
313
|
+
continue;
|
|
314
|
+
out = out.replaceAll(apiKey, slug);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
};
|
|
318
|
+
const notes = state.report.all().map((n) => ({ ...n, detail: decorate(n.detail) }));
|
|
319
|
+
// Synthesize rebucketed-rollout notes for every MULTIVARIATE feature so the
|
|
320
|
+
// dedicated "Users will be re-bucketed" section in MIGRATION_REPORT.md
|
|
321
|
+
// names each one (plan §5.4). Flagsmith and Quonfig hash buckets with
|
|
322
|
+
// different salts; identity-level assignments WILL change post-migration
|
|
323
|
+
// for every MV flag, regardless of whether the allocation has any per-env
|
|
324
|
+
// overrides.
|
|
325
|
+
for (const featureName of state.multivariateFeatures) {
|
|
326
|
+
notes.push({
|
|
327
|
+
category: 'rebucketed-rollout',
|
|
328
|
+
detail: 'multivariate feature; Flagsmith and Quonfig hash identities into buckets with different salts, so ' +
|
|
329
|
+
'individual identity → variation assignments will change after migration (the overall percentage allocation is preserved)',
|
|
330
|
+
key: featureName,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// Identity-trait references — pure FYI, no action required server-side, but
|
|
334
|
+
// every trait must be sent by the SDK caller at eval time or matching rules
|
|
335
|
+
// will silently miss. Plan §5.5 / D-F6.
|
|
336
|
+
const traitMap = collectIdentityTraits();
|
|
337
|
+
for (const [trait, segments] of traitMap) {
|
|
338
|
+
notes.push({
|
|
339
|
+
category: 'identity-traits-referenced',
|
|
340
|
+
detail: segments.length === 1
|
|
341
|
+
? `referenced by segment "${segments[0]}" — your SDK callers must send this attribute on the evaluation context`
|
|
342
|
+
: `referenced by ${segments.length} segments (${segments.slice(0, 3).join(', ')}${segments.length > 3 ? ', …' : ''}) — your SDK callers must send this attribute on the evaluation context`,
|
|
343
|
+
key: trait,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return notes.length === 0 ? null : notes;
|
|
347
|
+
}
|
|
348
|
+
function getEnvironmentMapImpl() {
|
|
349
|
+
if (state.environments.length === 0)
|
|
350
|
+
return null;
|
|
351
|
+
const seen = new Set();
|
|
352
|
+
const entries = [];
|
|
353
|
+
for (const env of state.environments) {
|
|
354
|
+
const quonfigName = slugifyEnvName(env.name);
|
|
355
|
+
if (seen.has(quonfigName))
|
|
356
|
+
continue;
|
|
357
|
+
seen.add(quonfigName);
|
|
358
|
+
entries.push({ quonfigName, sourceName: env.name });
|
|
359
|
+
}
|
|
360
|
+
return entries.length === 0 ? null : entries;
|
|
361
|
+
}
|
|
13
362
|
export const flagsmithSource = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new NotYetImplementedError(SOURCE_NAME, 'fetchChanges');
|
|
17
|
-
},
|
|
18
|
-
listEnvironments() {
|
|
19
|
-
throw new NotYetImplementedError(SOURCE_NAME, 'listEnvironments');
|
|
363
|
+
fetchChanges() {
|
|
364
|
+
return fetchChangesImpl();
|
|
20
365
|
},
|
|
366
|
+
getCoercedSentinels: getCoercedSentinelsImpl,
|
|
367
|
+
getConversionNotes: getConversionNotesImpl,
|
|
368
|
+
getDroppedOverrides: getDroppedOverridesImpl,
|
|
369
|
+
getEnvironmentMap: getEnvironmentMapImpl,
|
|
370
|
+
getSkippedConfigs: getSkippedConfigsImpl,
|
|
371
|
+
listEnvironments: listEnvironmentsImpl,
|
|
21
372
|
name: SOURCE_NAME,
|
|
22
|
-
translate
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
validateAuth() {
|
|
26
|
-
throw new NotYetImplementedError(SOURCE_NAME, 'validateAuth');
|
|
27
|
-
},
|
|
373
|
+
translate: translateImpl,
|
|
374
|
+
validateAuth: validateAuthImpl,
|
|
28
375
|
};
|
|
376
|
+
export function __resetFlagsmithSourceForTests() {
|
|
377
|
+
state.apiKey = null;
|
|
378
|
+
state.environments = [];
|
|
379
|
+
state.multivariateFeatures = [];
|
|
380
|
+
state.project = null;
|
|
381
|
+
state.projectId = resolveProjectId();
|
|
382
|
+
state.report = new ConversionReport();
|
|
383
|
+
state.segments = [];
|
|
384
|
+
state.tags = [];
|
|
385
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.0.
|
|
1
|
+
declare const _default: "0.0.51";
|
|
2
2
|
export default _default;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export default '0.0.
|
|
2
|
+
export default '0.0.51';
|
package/oclif.manifest.json
CHANGED
|
@@ -499,7 +499,7 @@
|
|
|
499
499
|
"type": "boolean"
|
|
500
500
|
},
|
|
501
501
|
"dir": {
|
|
502
|
-
"description": "Path to local QUONFIG_DIR (defaults to QUONFIG_DIR env var). When omitted, fetches the workspace from the server
|
|
502
|
+
"description": "Path to local QUONFIG_DIR (defaults to QUONFIG_DIR env var). When omitted, auto-detects a quonfig.json in the current directory; if none, fetches the workspace from the server.",
|
|
503
503
|
"env": "QUONFIG_DIR",
|
|
504
504
|
"name": "dir",
|
|
505
505
|
"hasDynamicHelp": false,
|
|
@@ -1390,10 +1390,8 @@
|
|
|
1390
1390
|
"type": "boolean"
|
|
1391
1391
|
},
|
|
1392
1392
|
"project": {
|
|
1393
|
-
"description": "
|
|
1394
|
-
"env": "LAUNCHDARKLY_PROJECT_KEY",
|
|
1393
|
+
"description": "Source-system project identifier. For --from launchdarkly this is the project KEY (default: \"default\"; env: LAUNCHDARKLY_PROJECT_KEY). For --from flagsmith this is the numeric project ID (env: FLAGSMITH_PROJECT_ID). Ignored by --from launch.",
|
|
1395
1394
|
"name": "project",
|
|
1396
|
-
"default": "default",
|
|
1397
1395
|
"hasDynamicHelp": false,
|
|
1398
1396
|
"multiple": false,
|
|
1399
1397
|
"type": "option"
|
|
@@ -3625,5 +3623,5 @@
|
|
|
3625
3623
|
]
|
|
3626
3624
|
}
|
|
3627
3625
|
},
|
|
3628
|
-
"version": "0.0.
|
|
3626
|
+
"version": "0.0.51"
|
|
3629
3627
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
|
|
3
3
|
"name": "@quonfig/cli",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.51",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/quonfig/cli/issues"
|
|
7
7
|
},
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"@oclif/core": "^3.18.2",
|
|
22
22
|
"@oclif/plugin-help": "^6",
|
|
23
23
|
"@oclif/plugin-plugins": "^4",
|
|
24
|
-
"@quonfig/node": "^0.0.
|
|
24
|
+
"@quonfig/node": "^0.0.28",
|
|
25
25
|
"acorn": "^8.10.0",
|
|
26
26
|
"acorn-walk": "^8.3.5",
|
|
27
27
|
"chalk": "^5.4.1",
|