@quonfig/cli 0.0.49 → 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.
Files changed (35) hide show
  1. package/README.md +50 -48
  2. package/dist/commands/generate.js +21 -5
  3. package/dist/commands/info.d.ts +1 -0
  4. package/dist/commands/info.js +28 -1
  5. package/dist/commands/migrate.d.ts +1 -1
  6. package/dist/commands/migrate.js +16 -5
  7. package/dist/commands/pull.js +2 -2
  8. package/dist/init/templates.js +59 -12
  9. package/dist/migrate/local-write.js +11 -1
  10. package/dist/migrate/migration-report.d.ts +16 -5
  11. package/dist/migrate/migration-report.js +351 -20
  12. package/dist/migrate/push-to-cloud.js +6 -1
  13. package/dist/migrate/quonfig-target/report.d.ts +45 -4
  14. package/dist/migrate/quonfig-target/report.js +5 -2
  15. package/dist/migrate/quonfig-target/ruleset.js +8 -6
  16. package/dist/migrate/source.d.ts +27 -0
  17. package/dist/migrate/sources/flagsmith/api.d.ts +115 -0
  18. package/dist/migrate/sources/flagsmith/api.js +402 -0
  19. package/dist/migrate/sources/flagsmith/translate.d.ts +72 -0
  20. package/dist/migrate/sources/flagsmith/translate.js +657 -0
  21. package/dist/migrate/sources/flagsmith/types.d.ts +282 -0
  22. package/dist/migrate/sources/flagsmith/types.js +18 -0
  23. package/dist/migrate/sources/flagsmith.d.ts +62 -0
  24. package/dist/migrate/sources/flagsmith.js +380 -23
  25. package/dist/migrate/sources/launchdarkly/api.d.ts +22 -1
  26. package/dist/migrate/sources/launchdarkly/api.js +23 -1
  27. package/dist/migrate/sources/launchdarkly/translate.js +42 -2
  28. package/dist/migrate/sources/launchdarkly/types.d.ts +19 -0
  29. package/dist/migrate/sources/launchdarkly.js +75 -4
  30. package/dist/util/friction-log.d.ts +1 -0
  31. package/dist/util/friction-log.js +6 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/oclif.manifest.json +158 -160
  35. package/package.json +2 -2
@@ -12,7 +12,7 @@ import * as fs from 'node:fs';
12
12
  import * as os from 'node:os';
13
13
  import * as path from 'node:path';
14
14
  import { ConversionReport } from '../quonfig-target/report.js';
15
- import { fetchProjectEnvironments, fetchSegmentsForEnv, fetchSnapshot } from './launchdarkly/api.js';
15
+ import { fetchMembers, fetchProjectEnvironments, fetchProjectEnvironmentsDetailed, fetchSegmentsForEnv, fetchSnapshot, } from './launchdarkly/api.js';
16
16
  import { auditEntryToLegacyChange, buildFlagAuditSpec, getCommitMetaForAuditEntry, probeRetentionHorizon, walkAuditLog, } from './launchdarkly/audit.js';
17
17
  import { flagOutputPath, segmentOutputPath, translateFlag, translateSegment } from './launchdarkly/translate.js';
18
18
  const SOURCE_NAME = 'launchdarkly';
@@ -39,7 +39,9 @@ function resolveProjectKey() {
39
39
  const state = {
40
40
  apiKey: null,
41
41
  environments: [],
42
+ environmentSourceNames: [],
42
43
  fullSummary: false,
44
+ maintainerMap: null,
43
45
  projectKey: resolveProjectKey(),
44
46
  report: new ConversionReport(),
45
47
  retentionHorizon: null,
@@ -89,11 +91,15 @@ function slugifyEnvKey(key) {
89
91
  }
90
92
  async function validateAuthImpl(apiKey) {
91
93
  // A cheap authenticated call — 401s on a bad token, 404s on a bad project.
92
- await fetchProjectEnvironments(apiKey, state.projectKey);
94
+ // Use the detailed call so we can stash {key, name} for the report's env
95
+ // mapping table (qfg-1t7m) without a second round-trip.
96
+ const detailed = await fetchProjectEnvironmentsDetailed(apiKey, state.projectKey);
93
97
  state.apiKey = apiKey;
94
98
  state.report = new ConversionReport();
95
99
  state.environments = [];
100
+ state.environmentSourceNames = detailed;
96
101
  state.retentionHorizon = null;
102
+ state.maintainerMap = null;
97
103
  // Plan §4.1.1: under --full-summary, probe the audit-log retention window
98
104
  // here — the earliest authenticated hook — so the command can tell the user
99
105
  // the real history horizon BEFORE the slow Phase-2 walk starts. The walk can
@@ -103,10 +109,33 @@ async function validateAuthImpl(apiKey) {
103
109
  state.retentionHorizon = await probeRetentionHorizon(apiKey, { spec: buildFlagAuditSpec(state.projectKey) });
104
110
  }
105
111
  }
112
+ /**
113
+ * qfg-l8uz: best-effort `/members` lookup so MIGRATION_REPORT.md can render
114
+ * `maintainerId → email`. Project-scoped tokens or accounts that hide member
115
+ * data will 403 here — that's fine, the migration must not fail just because
116
+ * the rollup loses a label. Returns null on any error, the empty map when the
117
+ * endpoint returns no members, and a stripped map otherwise (members without
118
+ * an email are skipped — we have nothing useful to render for them).
119
+ */
120
+ async function loadMaintainerMap(apiKey) {
121
+ try {
122
+ const members = await fetchMembers(apiKey);
123
+ const map = {};
124
+ for (const member of members) {
125
+ if (member.email)
126
+ map[member._id] = member.email;
127
+ }
128
+ return map;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
106
134
  async function listEnvironmentsImpl() {
107
135
  const apiKey = requireApiKey('listEnvironments');
108
- const envKeys = await fetchProjectEnvironments(apiKey, state.projectKey);
109
- state.environments = envKeys.map((k) => slugifyEnvKey(k));
136
+ const detailed = await fetchProjectEnvironmentsDetailed(apiKey, state.projectKey);
137
+ state.environmentSourceNames = detailed;
138
+ state.environments = detailed.map((e) => slugifyEnvKey(e.key));
110
139
  return [...new Set(state.environments)];
111
140
  }
112
141
  /**
@@ -120,6 +149,11 @@ async function listEnvironmentsImpl() {
120
149
  */
121
150
  async function* fetchChangesImpl() {
122
151
  const apiKey = requireApiKey('fetchChanges');
152
+ // qfg-l8uz: load the account-wide member directory once so the report can
153
+ // render maintainerId → email pairs. Deliberately best-effort — a failed
154
+ // call (project-scoped token, hidden member data) just leaves the rollup
155
+ // showing opaque hex IDs.
156
+ state.maintainerMap = await loadMaintainerMap(apiKey);
123
157
  if (state.fullSummary) {
124
158
  yield* fetchAuditHistory(apiKey);
125
159
  return;
@@ -243,6 +277,39 @@ function getSkippedConfigsImpl() {
243
277
  export function getConversionReport() {
244
278
  return state.report;
245
279
  }
280
+ /**
281
+ * qfg-l8uz: maintainerId → email map produced by `/members` during
282
+ * validateAuth, or null when the call failed (project-scoped token, account
283
+ * hides members, etc.). The report renderer surfaces this as a sub-table on
284
+ * `## Identifier map` and decorates `dropped-maintainer` rollups so a reader
285
+ * sees real emails alongside the opaque hex IDs.
286
+ */
287
+ function getMaintainerMapImpl() {
288
+ if (state.maintainerMap === null)
289
+ return null;
290
+ return Object.keys(state.maintainerMap).length === 0 ? null : { ...state.maintainerMap };
291
+ }
292
+ /**
293
+ * qfg-1t7m: source-name → Quonfig-name pairs for the report's "Environment
294
+ * mapping table". The source name is LD's user-facing env display name; the
295
+ * Quonfig name is the slugified env key. De-duped on the slugified key — two
296
+ * source envs that collapse to the same slug get one row (the first wins),
297
+ * since that is the row that will exist in quonfig.json.
298
+ */
299
+ function getEnvironmentMapImpl() {
300
+ if (state.environmentSourceNames.length === 0)
301
+ return null;
302
+ const seen = new Set();
303
+ const entries = [];
304
+ for (const env of state.environmentSourceNames) {
305
+ const quonfigName = slugifyEnvKey(env.key);
306
+ if (seen.has(quonfigName))
307
+ continue;
308
+ seen.add(quonfigName);
309
+ entries.push({ quonfigName, sourceName: env.name });
310
+ }
311
+ return entries.length === 0 ? null : entries;
312
+ }
246
313
  /**
247
314
  * Every structured conversion note from this run — re-bucketed rollouts,
248
315
  * dropped prerequisites, lossy individual-target conversions, etc. The write
@@ -275,6 +342,8 @@ export const launchdarklySource = {
275
342
  },
276
343
  getCommitMeta: getCommitMetaImpl,
277
344
  getConversionNotes: getConversionNotesImpl,
345
+ getEnvironmentMap: getEnvironmentMapImpl,
346
+ getMaintainerMap: getMaintainerMapImpl,
278
347
  getSkippedConfigs: getSkippedConfigsImpl,
279
348
  listEnvironments: listEnvironmentsImpl,
280
349
  name: SOURCE_NAME,
@@ -284,7 +353,9 @@ export const launchdarklySource = {
284
353
  export function __resetLaunchDarklySourceForTests() {
285
354
  state.apiKey = null;
286
355
  state.environments = [];
356
+ state.environmentSourceNames = [];
287
357
  state.fullSummary = false;
358
+ state.maintainerMap = null;
288
359
  state.projectKey = resolveProjectKey();
289
360
  state.report = new ConversionReport();
290
361
  state.retentionHorizon = null;
@@ -4,6 +4,7 @@ export interface FrictionEntry {
4
4
  exitCode: number;
5
5
  ts: string;
6
6
  }
7
+ export declare function getDefaultFrictionLogPath(home?: string): string;
7
8
  export declare function getFrictionLogPath(envValue?: string | undefined, cwd?: string): null | string;
8
9
  export declare function buildFrictionEntry(opts: {
9
10
  argv?: string[];
@@ -1,5 +1,9 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
4
+ export function getDefaultFrictionLogPath(home = os.homedir()) {
5
+ return path.join(home, '.qfg', 'friction.log');
6
+ }
3
7
  export function getFrictionLogPath(envValue, cwd = process.cwd()) {
4
8
  if (envValue === undefined)
5
9
  return null;
@@ -7,7 +11,7 @@ export function getFrictionLogPath(envValue, cwd = process.cwd()) {
7
11
  if (trimmed === '' || trimmed === 'false' || trimmed === '0')
8
12
  return null;
9
13
  if (trimmed === 'true' || trimmed === '1') {
10
- return path.join(cwd, '.qfg-friction.log');
14
+ return getDefaultFrictionLogPath();
11
15
  }
12
16
  return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
13
17
  }
@@ -23,6 +27,7 @@ export function buildFrictionEntry(opts) {
23
27
  };
24
28
  }
25
29
  export function appendFrictionEntry(logPath, entry) {
30
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
26
31
  fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf8');
27
32
  }
28
33
  const MAX_STDERR_CAPTURE = 4000;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "0.0.49";
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.49';
2
+ export default '0.0.51';