@quonfig/cli 0.0.16 → 0.0.17

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 CHANGED
@@ -14,7 +14,7 @@ $ npm install -g @quonfig/cli
14
14
  $ qfg COMMAND
15
15
  running command...
16
16
  $ qfg (--version)
17
- @quonfig/cli/0.0.16 linux-x64 node-v24.4.1
17
+ @quonfig/cli/0.0.17 linux-x64 node-v24.4.1
18
18
  $ qfg --help [COMMAND]
19
19
  USAGE
20
20
  $ qfg COMMAND
@@ -89,7 +89,7 @@ EXAMPLES
89
89
  $ qfg config-schema --json-schema # full JSON Schema document (copy into your editor)
90
90
  ```
91
91
 
92
- _See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/config-schema.ts)_
92
+ _See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/config-schema.ts)_
93
93
 
94
94
  ## `qfg create NAME`
95
95
 
@@ -161,7 +161,7 @@ EXAMPLES
161
161
  $ qfg set-rollout my.new.flag --environment production --true-percent 20
162
162
  ```
163
163
 
164
- _See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/create.ts)_
164
+ _See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/create.ts)_
165
165
 
166
166
  ## `qfg flag info [NAME]`
167
167
 
@@ -361,7 +361,7 @@ EXAMPLES
361
361
  $ qfg generate --targets node-ts -o ./dist # combine with targets
362
362
  ```
363
363
 
364
- _See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/generate.ts)_
364
+ _See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/generate.ts)_
365
365
 
366
366
  ## `qfg generate-new-hex-key`
367
367
 
@@ -384,7 +384,7 @@ EXAMPLES
384
384
  $ qfg generate-new-hex-key
385
385
  ```
386
386
 
387
- _See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/generate-new-hex-key.ts)_
387
+ _See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/generate-new-hex-key.ts)_
388
388
 
389
389
  ## `qfg get [NAME]`
390
390
 
@@ -416,7 +416,7 @@ EXAMPLES
416
416
  $ qfg get my.config.name --environment=production
417
417
  ```
418
418
 
419
- _See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/get.ts)_
419
+ _See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/get.ts)_
420
420
 
421
421
  ## `qfg info [NAME]`
422
422
 
@@ -464,7 +464,7 @@ EXAMPLES
464
464
  $ qfg info my.config.name --exclude-evaluations # skip 24h stats
465
465
  ```
466
466
 
467
- _See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/info.ts)_
467
+ _See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/info.ts)_
468
468
 
469
469
  ## `qfg init [DIRECTORY]`
470
470
 
@@ -504,7 +504,7 @@ EXAMPLES
504
504
  $ qfg init --dry-run
505
505
  ```
506
506
 
507
- _See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/init.ts)_
507
+ _See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/init.ts)_
508
508
 
509
509
  ## `qfg interactive`
510
510
 
@@ -534,7 +534,7 @@ EXAMPLES
534
534
  $ qfg
535
535
  ```
536
536
 
537
- _See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/interactive.ts)_
537
+ _See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/interactive.ts)_
538
538
 
539
539
  ## `qfg list`
540
540
 
@@ -574,7 +574,7 @@ EXAMPLES
574
574
  $ qfg list --feature-flags
575
575
  ```
576
576
 
577
- _See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/list.ts)_
577
+ _See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/list.ts)_
578
578
 
579
579
  ## `qfg login`
580
580
 
@@ -599,7 +599,7 @@ EXAMPLES
599
599
  $ qfg login --profile myprofile
600
600
  ```
601
601
 
602
- _See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/login.ts)_
602
+ _See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/login.ts)_
603
603
 
604
604
  ## `qfg logout`
605
605
 
@@ -622,7 +622,7 @@ EXAMPLES
622
622
  $ qfg logout
623
623
  ```
624
624
 
625
- _See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/logout.ts)_
625
+ _See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/logout.ts)_
626
626
 
627
627
  ## `qfg mcp`
628
628
 
@@ -655,7 +655,7 @@ EXAMPLES
655
655
  $ qfg mcp --url http://local-app.quonfig-staging.com:3003/api/v1/mcp
656
656
  ```
657
657
 
658
- _See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/mcp.ts)_
658
+ _See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/mcp.ts)_
659
659
 
660
660
  ## `qfg migrate`
661
661
 
@@ -709,7 +709,7 @@ EXAMPLES
709
709
  $ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-config --staging
710
710
  ```
711
711
 
712
- _See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/migrate.ts)_
712
+ _See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate.ts)_
713
713
 
714
714
  ## `qfg migrate doctor`
715
715
 
@@ -747,7 +747,7 @@ EXAMPLES
747
747
  $ qfg migrate doctor --json
748
748
  ```
749
749
 
750
- _See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/migrate/doctor.ts)_
750
+ _See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate/doctor.ts)_
751
751
 
752
752
  ## `qfg migrate my-code`
753
753
 
@@ -779,7 +779,7 @@ EXAMPLES
779
779
  $ qfg migrate my-code --dry-run
780
780
  ```
781
781
 
782
- _See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/migrate/my-code.ts)_
782
+ _See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate/my-code.ts)_
783
783
 
784
784
  ## `qfg migrate status`
785
785
 
@@ -810,7 +810,7 @@ EXAMPLES
810
810
  $ qfg migrate status --json
811
811
  ```
812
812
 
813
- _See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/migrate/status.ts)_
813
+ _See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate/status.ts)_
814
814
 
815
815
  ## `qfg override [NAME]`
816
816
 
@@ -854,7 +854,7 @@ EXAMPLES
854
854
  $ qfg override my.double.config --value=3.14159
855
855
  ```
856
856
 
857
- _See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/override.ts)_
857
+ _See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/override.ts)_
858
858
 
859
859
  ## `qfg pull`
860
860
 
@@ -898,7 +898,7 @@ EXAMPLES
898
898
  $ qfg pull # uses QUONFIG_DIR env var
899
899
  ```
900
900
 
901
- _See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/pull.ts)_
901
+ _See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/pull.ts)_
902
902
 
903
903
  ## `qfg push`
904
904
 
@@ -946,7 +946,7 @@ EXAMPLES
946
946
  $ qfg push --dir ./our-config --yes
947
947
  ```
948
948
 
949
- _See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/push.ts)_
949
+ _See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/push.ts)_
950
950
 
951
951
  ## `qfg schema NAME`
952
952
 
@@ -984,7 +984,7 @@ EXAMPLES
984
984
  $ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
985
985
  ```
986
986
 
987
- _See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/schema.ts)_
987
+ _See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/schema.ts)_
988
988
 
989
989
  ## `qfg sdk-key`
990
990
 
@@ -1011,7 +1011,7 @@ EXAMPLES
1011
1011
  $ qfg sdk-key revoke <key-id>
1012
1012
  ```
1013
1013
 
1014
- _See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/sdk-key.ts)_
1014
+ _See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sdk-key.ts)_
1015
1015
 
1016
1016
  ## `qfg sdk-key create`
1017
1017
 
@@ -1043,7 +1043,7 @@ EXAMPLES
1043
1043
  $ qfg sdk-key create --environment staging --type browser
1044
1044
  ```
1045
1045
 
1046
- _See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/sdk-key/create.ts)_
1046
+ _See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sdk-key/create.ts)_
1047
1047
 
1048
1048
  ## `qfg sdk-key list`
1049
1049
 
@@ -1072,7 +1072,7 @@ EXAMPLES
1072
1072
  $ qfg sdk-key list --environment production
1073
1073
  ```
1074
1074
 
1075
- _See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/sdk-key/list.ts)_
1075
+ _See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sdk-key/list.ts)_
1076
1076
 
1077
1077
  ## `qfg sdk-key revoke KEYID`
1078
1078
 
@@ -1099,7 +1099,7 @@ EXAMPLES
1099
1099
  $ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
1100
1100
  ```
1101
1101
 
1102
- _See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/sdk-key/revoke.ts)_
1102
+ _See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sdk-key/revoke.ts)_
1103
1103
 
1104
1104
  ## `qfg set-default [NAME]`
1105
1105
 
@@ -1170,7 +1170,7 @@ EXAMPLES
1170
1170
  $ qfg pull && qfg config-schema
1171
1171
  ```
1172
1172
 
1173
- _See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/set-default.ts)_
1173
+ _See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/set-default.ts)_
1174
1174
 
1175
1175
  ## `qfg set-rollout [NAME]`
1176
1176
 
@@ -1247,7 +1247,7 @@ EXAMPLES
1247
1247
  $ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
1248
1248
  ```
1249
1249
 
1250
- _See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/set-rollout.ts)_
1250
+ _See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/set-rollout.ts)_
1251
1251
 
1252
1252
  ## `qfg sync`
1253
1253
 
@@ -1279,7 +1279,7 @@ EXAMPLES
1279
1279
  $ qfg sync --watch --dir ./our-config --interval 30
1280
1280
  ```
1281
1281
 
1282
- _See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/sync.ts)_
1282
+ _See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sync.ts)_
1283
1283
 
1284
1284
  ## `qfg toggle [NAME]`
1285
1285
 
@@ -1381,7 +1381,7 @@ EXAMPLES
1381
1381
  $ qfg verify --json
1382
1382
  ```
1383
1383
 
1384
- _See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/verify.ts)_
1384
+ _See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/verify.ts)_
1385
1385
 
1386
1386
  ## `qfg whoami`
1387
1387
 
@@ -1404,7 +1404,7 @@ EXAMPLES
1404
1404
  $ qfg whoami
1405
1405
  ```
1406
1406
 
1407
- _See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/whoami.ts)_
1407
+ _See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/whoami.ts)_
1408
1408
 
1409
1409
  ## `qfg workspace`
1410
1410
 
@@ -1427,7 +1427,7 @@ EXAMPLES
1427
1427
  $ qfg workspace
1428
1428
  ```
1429
1429
 
1430
- _See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/workspace.ts)_
1430
+ _See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/workspace.ts)_
1431
1431
 
1432
1432
  ## `qfg workspace bootstrap`
1433
1433
 
@@ -1460,7 +1460,7 @@ EXAMPLES
1460
1460
  $ qfg workspace bootstrap --dir ./our-config --skip-validate
1461
1461
  ```
1462
1462
 
1463
- _See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/workspace/bootstrap.ts)_
1463
+ _See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/workspace/bootstrap.ts)_
1464
1464
 
1465
1465
  ## `qfg workspace switch [SLUG]`
1466
1466
 
@@ -1488,7 +1488,7 @@ EXAMPLES
1488
1488
  $ qfg workspace switch my-workspace-slug
1489
1489
  ```
1490
1490
 
1491
- _See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.16/src/commands/workspace/switch.ts)_
1491
+ _See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/workspace/switch.ts)_
1492
1492
  <!-- commandsstop -->
1493
1493
 
1494
1494
  ## Local Development
@@ -7,7 +7,7 @@ import { applyLocalMigration } from '../migrate/local-write.js';
7
7
  import { PushConflictError } from '../util/clone-and-stack-push.js';
8
8
  import { pushMigrationToCloud } from '../migrate/push-to-cloud.js';
9
9
  import { UnknownSourceError, getSource } from '../migrate/registry.js';
10
- import { NotYetImplementedError } from '../migrate/source.js';
10
+ import { NotYetImplementedError, } from '../migrate/source.js';
11
11
  import { applyLaunchBaseUrl } from '../migrate/sources/launch/api.js';
12
12
  import { mintGiteaToken } from '../util/gitea-api.js';
13
13
  import { displayUrl } from '../util/git-ops.js';
@@ -104,6 +104,7 @@ export default class Migrate extends BaseCommand {
104
104
  return this.err(error.message);
105
105
  throw error;
106
106
  }
107
+ let duplicateResolutionsForWarn = null;
107
108
  try {
108
109
  await source.validateAuth(flags['api-key']);
109
110
  const environments = await source.listEnvironments();
@@ -148,12 +149,14 @@ export default class Migrate extends BaseCommand {
148
149
  const result = await pushMigrationToCloud({
149
150
  changes: toProcess,
150
151
  commitMessage: buildCommitMessage(flags.from, toProcess.length),
152
+ environments,
151
153
  importState,
152
154
  localDir: dir,
153
155
  remoteUrl: repoUrl,
154
156
  reportData: buildReportData(flags.from, toProcess.length),
155
157
  source,
156
158
  });
159
+ duplicateResolutionsForWarn = result.duplicateResolutions;
157
160
  this.log(result.committed
158
161
  ? `Pushed ${toProcess.length} change(s). commit=${result.commitSha?.slice(0, 8) ?? ''} action=${result.action}`
159
162
  : `No net changes produced by this run. Nothing to commit or push.`);
@@ -188,6 +191,7 @@ export default class Migrate extends BaseCommand {
188
191
  reportData: buildReportData(flags.from, toProcess.length),
189
192
  source,
190
193
  });
194
+ duplicateResolutionsForWarn = localResult.duplicateResolutions;
191
195
  this.log(localResult.committed
192
196
  ? `Committed ${toProcess.length} change(s). commit=${localResult.commitSha?.slice(0, 8) ?? ''} action=${localResult.action}`
193
197
  : `No net changes produced by this run. Nothing to commit.`);
@@ -203,6 +207,13 @@ export default class Migrate extends BaseCommand {
203
207
  return this.err(error.message);
204
208
  throw error;
205
209
  }
210
+ finally {
211
+ // Surface source-level warnings even if apply/push threw (e.g.
212
+ // detectDuplicateKeys). The source accumulator holds the data regardless.
213
+ warnAboutDroppedOverrides(this, source.getDroppedOverrides?.() ?? null);
214
+ warnAboutSkippedConfigs(this, source.getSkippedConfigs?.() ?? null);
215
+ warnAboutDuplicateResolutions(this, duplicateResolutionsForWarn);
216
+ }
206
217
  }
207
218
  async resolvePushWorkspace(slugOrId) {
208
219
  const authConfig = await loadAuthConfig();
@@ -250,6 +261,41 @@ function buildReportData(source, count) {
250
261
  unsupportedFeatures: [],
251
262
  };
252
263
  }
264
+ function warnAboutDroppedOverrides(cmd, dropped) {
265
+ if (!dropped || dropped.total === 0)
266
+ return;
267
+ const envIds = Object.keys(dropped.byEnv).sort();
268
+ cmd.warn(`Dropped ${dropped.total} override section(s) referencing ${envIds.length} env ID(s) not present in the source's env list (likely archived/deleted):`);
269
+ for (const envId of envIds) {
270
+ const perFlag = dropped.byEnv[envId];
271
+ const totalForEnv = Object.values(perFlag).reduce((s, n) => s + n, 0);
272
+ cmd.warn(` env-${envId}: ${totalForEnv} dropped from ${Object.keys(perFlag).length} flag(s)`);
273
+ for (const flagPath of Object.keys(perFlag).sort()) {
274
+ cmd.warn(` - ${flagPath} (${perFlag[flagPath]})`);
275
+ }
276
+ }
277
+ cmd.warn(`If any of these envs are still in use, restore them in the source system and re-run the migration. Full detail is also written to MIGRATION_REPORT.md.`);
278
+ }
279
+ function warnAboutDuplicateResolutions(cmd, resolved) {
280
+ if (!resolved || resolved.total === 0)
281
+ return;
282
+ cmd.warn(`Resolved ${resolved.total} cross-type key collision(s) in the source data by keeping the config side and deleting the other type(s). Review each and clean up the source system so the collision stops recurring:`);
283
+ const sorted = [...resolved.entries].sort((a, b) => a.key.localeCompare(b.key));
284
+ for (const entry of sorted) {
285
+ cmd.warn(` ${entry.key} (${entry.collisionTypes.join(', ')}): kept ${entry.kept}, deleted ${entry.deleted.join(', ')}`);
286
+ }
287
+ cmd.warn('Full detail is also written to MIGRATION_REPORT.md.');
288
+ }
289
+ function warnAboutSkippedConfigs(cmd, skipped) {
290
+ if (!skipped || skipped.total === 0)
291
+ return;
292
+ cmd.warn(`Skipped ${skipped.total} invalid config(s) from the source system — these were not written to the workspace. Fix each in the source system and re-run:`);
293
+ const sorted = [...skipped.entries].sort((a, b) => a.key.localeCompare(b.key));
294
+ for (const entry of sorted) {
295
+ cmd.warn(` ${entry.key}: ${entry.reason}`);
296
+ }
297
+ cmd.warn('Full detail is also written to MIGRATION_REPORT.md.');
298
+ }
253
299
  function resolveTargetDir(dirFlag, cwd) {
254
300
  if (dirFlag)
255
301
  return path.resolve(cwd, dirFlag);
@@ -1,7 +1,7 @@
1
1
  import { type ImportState } from './import-state.js';
2
2
  import { type MigrationReportData } from './migration-report.js';
3
3
  import { type PushIdentity } from '../util/clone-and-stack-push.js';
4
- import type { LegacyChange, MigrationSource } from './source.js';
4
+ import type { DroppedOverrideSummary, DuplicateResolutionSummary, LegacyChange, MigrationSource, SkippedConfigSummary } from './source.js';
5
5
  export interface ApplyLocalMigrationOptions {
6
6
  /** Commit author. Defaults to the migrator identity. */
7
7
  author?: PushIdentity;
@@ -22,5 +22,11 @@ export interface ApplyLocalMigrationResult {
22
22
  commitSha: string | null;
23
23
  /** `false` if translate produced no net changes — nothing was committed. */
24
24
  committed: boolean;
25
+ /** Override sections dropped during translate because env.id was unknown. Null if none. */
26
+ droppedOverrides: DroppedOverrideSummary | null;
27
+ /** Cross-type key collisions resolved by preferring the config side. Null if none. */
28
+ duplicateResolutions: DuplicateResolutionSummary | null;
29
+ /** Configs soft-skipped during translate due to invalid source data. Null if none. */
30
+ skippedConfigs: SkippedConfigSummary | null;
25
31
  }
26
32
  export declare const applyLocalMigration: (opts: ApplyLocalMigrationOptions) => Promise<ApplyLocalMigrationResult>;
@@ -4,6 +4,7 @@ import * as path from 'node:path';
4
4
  import * as util from 'node:util';
5
5
  import { removeQfFromGitignore, writeImportState } from './import-state.js';
6
6
  import { writeMigrationReport } from './migration-report.js';
7
+ import { detectDuplicateKeys } from './sources/launch/translate.js';
7
8
  import { MIGRATOR_IDENTITY } from '../util/clone-and-stack-push.js';
8
9
  const execFile = util.promisify(execFileCb);
9
10
  const isGitRepo = async (dir) => {
@@ -49,14 +50,45 @@ const ensureLocalRepo = async (localDir, branch) => {
49
50
  return 'initialized';
50
51
  };
51
52
  const writeQuonfigFiles = (dir, changes, source) => {
53
+ const livePaths = new Map();
52
54
  for (const change of changes) {
53
55
  const files = source.translate(change);
54
56
  for (const file of files) {
55
57
  const full = path.join(dir, file.path);
56
- fs.mkdirSync(path.dirname(full), { recursive: true });
57
- fs.writeFileSync(full, file.contents);
58
+ if (file.deleted) {
59
+ try {
60
+ fs.unlinkSync(full);
61
+ }
62
+ catch (error) {
63
+ if (error.code !== 'ENOENT')
64
+ throw error;
65
+ }
66
+ livePaths.delete(file.path);
67
+ }
68
+ else {
69
+ if (file.contents === undefined) {
70
+ throw new Error(`translate() emitted a write op for ${file.path} without contents`);
71
+ }
72
+ fs.mkdirSync(path.dirname(full), { recursive: true });
73
+ fs.writeFileSync(full, file.contents);
74
+ livePaths.set(file.path, true);
75
+ }
58
76
  }
59
77
  }
78
+ const resolutions = detectDuplicateKeys([...livePaths.keys()].map((p) => ({ path: p })));
79
+ for (const resolution of resolutions) {
80
+ for (const toDelete of resolution.deleted) {
81
+ try {
82
+ fs.unlinkSync(path.join(dir, toDelete));
83
+ }
84
+ catch (error) {
85
+ if (error.code !== 'ENOENT')
86
+ throw error;
87
+ }
88
+ livePaths.delete(toDelete);
89
+ }
90
+ }
91
+ return resolutions;
60
92
  };
61
93
  export const applyLocalMigration = async (opts) => {
62
94
  const branch = opts.branch ?? 'main';
@@ -65,13 +97,22 @@ export const applyLocalMigration = async (opts) => {
65
97
  await execFile('git', ['-C', opts.localDir, 'config', 'user.name', author.name]);
66
98
  await execFile('git', ['-C', opts.localDir, 'config', 'user.email', author.email]);
67
99
  ensureQuonfigJson(opts.localDir, opts.environments);
68
- writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
100
+ const resolutionEntries = writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
69
101
  removeQfFromGitignore(opts.localDir);
70
102
  writeImportState(opts.localDir, opts.importState);
71
- writeMigrationReport(opts.localDir, opts.reportData);
103
+ const droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
104
+ const skippedConfigs = opts.source.getSkippedConfigs?.() ?? null;
105
+ const duplicateResolutions = resolutionEntries.length > 0 ? { entries: resolutionEntries, total: resolutionEntries.length } : null;
106
+ const reportData = {
107
+ ...opts.reportData,
108
+ ...(droppedOverrides ? { droppedOverrides } : {}),
109
+ ...(duplicateResolutions ? { duplicateResolutions } : {}),
110
+ ...(skippedConfigs ? { skippedConfigs } : {}),
111
+ };
112
+ writeMigrationReport(opts.localDir, reportData);
72
113
  await execFile('git', ['-C', opts.localDir, 'add', '--all']);
73
114
  if (!(await hasStagedChanges(opts.localDir))) {
74
- return { action, committed: false, commitSha: null };
115
+ return { action, committed: false, commitSha: null, droppedOverrides, duplicateResolutions, skippedConfigs };
75
116
  }
76
117
  await gitCommitFromStdin(opts.localDir, opts.commitMessage, {
77
118
  ...process.env,
@@ -81,5 +122,12 @@ export const applyLocalMigration = async (opts) => {
81
122
  GIT_COMMITTER_NAME: author.name,
82
123
  });
83
124
  const { stdout: sha } = await execFile('git', ['-C', opts.localDir, 'rev-parse', 'HEAD']);
84
- return { action, commitSha: sha.trim(), committed: true };
125
+ return {
126
+ action,
127
+ commitSha: sha.trim(),
128
+ committed: true,
129
+ droppedOverrides,
130
+ duplicateResolutions,
131
+ skippedConfigs,
132
+ };
85
133
  };
@@ -1,4 +1,5 @@
1
1
  import type { IdentifierMap } from './identifier-map.js';
2
+ import type { DroppedOverrideSummary, DuplicateResolutionSummary, SkippedConfigSummary } from './source.js';
2
3
  export interface MigrationReportCounts {
3
4
  environmentsMapped: number;
4
5
  flagsMigrated: number;
@@ -29,11 +30,28 @@ export interface FollowUpChecklist {
29
30
  export interface MigrationReportData {
30
31
  cleanMappings: CleanMapping[];
31
32
  counts: MigrationReportCounts;
33
+ /**
34
+ * Override sections that were dropped during translate() because the env.id was not
35
+ * present in the source's env map (e.g. archived/deleted Reforge envs). Null when
36
+ * nothing was dropped.
37
+ */
38
+ droppedOverrides?: DroppedOverrideSummary | null;
32
39
  dryRun: boolean;
40
+ /**
41
+ * Cross-type key collisions resolved by keeping the config side and deleting
42
+ * the non-config type(s). Null when none were detected. Customers should
43
+ * review each entry and clean up the source data to avoid the collision.
44
+ */
45
+ duplicateResolutions?: DuplicateResolutionSummary | null;
33
46
  environmentMap: EnvironmentMapEntry[];
34
47
  followUp: FollowUpChecklist;
35
48
  identifierMap: IdentifierMap;
36
49
  lossyMappings: LossyMapping[];
50
+ /**
51
+ * Configs soft-skipped by translate() because the source data was invalid
52
+ * (e.g. variant/valueType mismatch). Null when nothing was skipped.
53
+ */
54
+ skippedConfigs?: SkippedConfigSummary | null;
37
55
  source: string;
38
56
  unsupportedFeatures: UnsupportedFeature[];
39
57
  }
@@ -49,6 +49,59 @@ const renderIdentifierMap = (map) => {
49
49
  const rows = sorted.map((k) => `| \`${k}\` | \`${map[k]}\` |`);
50
50
  return [header, '', '| Legacy key | Quonfig key |', '| --- | --- |', ...rows].join('\n');
51
51
  };
52
+ const renderDroppedOverrides = (dropped) => {
53
+ if (!dropped || dropped.total === 0)
54
+ return null;
55
+ const envIds = Object.keys(dropped.byEnv).sort();
56
+ const lines = [
57
+ '## Dropped override sections',
58
+ '',
59
+ `Dropped **${dropped.total}** override section(s) referencing **${envIds.length}** env ID(s) not present in the source's env list (likely archived/deleted). If any of these envs are still in use, restore them in the source system and re-run the migration.`,
60
+ ];
61
+ for (const envId of envIds) {
62
+ const perFlag = dropped.byEnv[envId];
63
+ const totalForEnv = Object.values(perFlag).reduce((s, n) => s + n, 0);
64
+ const flagCount = Object.keys(perFlag).length;
65
+ lines.push('', `### env-${envId} — ${totalForEnv} dropped from ${flagCount} flag(s)`, '');
66
+ const sorted = Object.keys(perFlag).sort();
67
+ for (const flagPath of sorted) {
68
+ lines.push(`- \`${flagPath}\` (${perFlag[flagPath]})`);
69
+ }
70
+ }
71
+ return lines.join('\n');
72
+ };
73
+ const renderDuplicateResolutions = (resolved) => {
74
+ if (!resolved || resolved.total === 0)
75
+ return null;
76
+ const lines = [
77
+ '## Resolved cross-type duplicates',
78
+ '',
79
+ `Reforge had **${resolved.total}** key(s) present as both a config and a feature_flag (or other types) at the same time. qfg requires globally-unique keys, so the migrator kept the **config** side and deleted the other type(s). Review each and clean up the source system so the collision stops recurring.`,
80
+ '',
81
+ ];
82
+ const sorted = [...resolved.entries].sort((a, b) => a.key.localeCompare(b.key));
83
+ for (const entry of sorted) {
84
+ lines.push(`- \`${entry.key}\` (${entry.collisionTypes.join(', ')}): kept \`${entry.kept}\`, deleted ${entry.deleted
85
+ .map((p) => `\`${p}\``)
86
+ .join(', ')}`);
87
+ }
88
+ return lines.join('\n');
89
+ };
90
+ const renderSkippedConfigs = (skipped) => {
91
+ if (!skipped || skipped.total === 0)
92
+ return null;
93
+ const lines = [
94
+ '## Skipped invalid configs',
95
+ '',
96
+ `Skipped **${skipped.total}** config(s) with structurally invalid source data — the migrator refused to emit them rather than ship broken data. Fix each in the source system and re-run.`,
97
+ '',
98
+ ];
99
+ const sorted = [...skipped.entries].sort((a, b) => a.key.localeCompare(b.key));
100
+ for (const entry of sorted) {
101
+ lines.push(`- \`${entry.key}\` — ${entry.reason}`);
102
+ }
103
+ return lines.join('\n');
104
+ };
52
105
  const renderFollowUp = (followUp) => {
53
106
  const must = followUp.mustFixBeforeCutover.length === 0
54
107
  ? NONE
@@ -73,7 +126,17 @@ export const buildMigrationReport = (data) => {
73
126
  if (data.dryRun) {
74
127
  sections.push('> **DRY RUN** — no files were written to the workspace during this run.');
75
128
  }
76
- sections.push(`# Migration Report (from ${data.source})`, `_Reflects only the changes produced by this run. Re-running overwrites this file._`, renderCounts(data.counts), renderCleanMappings(data.cleanMappings), renderLossyMappings(data.lossyMappings), renderUnsupported(data.unsupportedFeatures), renderEnvironmentMap(data.environmentMap), renderIdentifierMap(data.identifierMap), renderFollowUp(data.followUp));
129
+ sections.push(`# Migration Report (from ${data.source})`, `_Reflects only the changes produced by this run. Re-running overwrites this file._`, renderCounts(data.counts), renderCleanMappings(data.cleanMappings), renderLossyMappings(data.lossyMappings), renderUnsupported(data.unsupportedFeatures), renderEnvironmentMap(data.environmentMap), renderIdentifierMap(data.identifierMap));
130
+ const dropped = renderDroppedOverrides(data.droppedOverrides);
131
+ if (dropped !== null)
132
+ sections.push(dropped);
133
+ const resolved = renderDuplicateResolutions(data.duplicateResolutions);
134
+ if (resolved !== null)
135
+ sections.push(resolved);
136
+ const skipped = renderSkippedConfigs(data.skippedConfigs);
137
+ if (skipped !== null)
138
+ sections.push(skipped);
139
+ sections.push(renderFollowUp(data.followUp));
77
140
  return sections.join('\n\n') + '\n';
78
141
  };
79
142
  export const migrationReportPath = (outputDir) => path.join(outputDir, 'MIGRATION_REPORT.md');
@@ -1,15 +1,26 @@
1
1
  import { type ImportState } from './import-state.js';
2
2
  import { type MigrationReportData } from './migration-report.js';
3
3
  import { type CloneAndStackPushResult } from '../util/clone-and-stack-push.js';
4
- import type { LegacyChange, MigrationSource } from './source.js';
4
+ import type { DroppedOverrideSummary, DuplicateResolutionSummary, LegacyChange, MigrationSource, SkippedConfigSummary } from './source.js';
5
5
  export interface PushMigrationToCloudOptions {
6
6
  branch?: string;
7
7
  changes: LegacyChange[];
8
8
  commitMessage: string;
9
+ /**
10
+ * Source-side environments (slugified). Additively merged into the target
11
+ * workspace's quonfig.json so flag files that reference these envs verify
12
+ * cleanly. Existing target-only envs are preserved.
13
+ */
14
+ environments?: string[];
9
15
  importState: ImportState;
10
16
  localDir: string;
11
17
  remoteUrl: string;
12
18
  reportData: MigrationReportData;
13
19
  source: MigrationSource;
14
20
  }
15
- export declare const pushMigrationToCloud: (opts: PushMigrationToCloudOptions) => Promise<CloneAndStackPushResult>;
21
+ export interface PushMigrationToCloudResult extends CloneAndStackPushResult {
22
+ droppedOverrides: DroppedOverrideSummary | null;
23
+ duplicateResolutions: DuplicateResolutionSummary | null;
24
+ skippedConfigs: SkippedConfigSummary | null;
25
+ }
26
+ export declare const pushMigrationToCloud: (opts: PushMigrationToCloudOptions) => Promise<PushMigrationToCloudResult>;