@quonfig/cli 0.0.34 → 0.0.35

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.34 linux-x64 node-v24.4.1
17
+ @quonfig/cli/0.0.35 linux-x64 node-v24.4.1
18
18
  $ qfg --help [COMMAND]
19
19
  USAGE
20
20
  $ qfg COMMAND
@@ -91,7 +91,7 @@ EXAMPLES
91
91
  $ qfg config-schema --json-schema # full JSON Schema document (copy into your editor)
92
92
  ```
93
93
 
94
- _See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/config-schema.ts)_
94
+ _See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/config-schema.ts)_
95
95
 
96
96
  ## `qfg create NAME`
97
97
 
@@ -176,7 +176,7 @@ EXAMPLES
176
176
  $ qfg set-rollout my.new.flag --environment production --true-percent 20
177
177
  ```
178
178
 
179
- _See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/create.ts)_
179
+ _See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/create.ts)_
180
180
 
181
181
  ## `qfg delete NAME`
182
182
 
@@ -222,7 +222,7 @@ EXAMPLES
222
222
  $ qfg delete my.config --yes
223
223
  ```
224
224
 
225
- _See code: [src/commands/delete.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/delete.ts)_
225
+ _See code: [src/commands/delete.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/delete.ts)_
226
226
 
227
227
  ## `qfg flag info [NAME]`
228
228
 
@@ -425,7 +425,7 @@ EXAMPLES
425
425
  $ qfg generate --targets node-ts -o ./dist # combine with targets
426
426
  ```
427
427
 
428
- _See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/generate.ts)_
428
+ _See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/generate.ts)_
429
429
 
430
430
  ## `qfg generate-new-hex-key`
431
431
 
@@ -448,7 +448,7 @@ EXAMPLES
448
448
  $ qfg generate-new-hex-key
449
449
  ```
450
450
 
451
- _See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/generate-new-hex-key.ts)_
451
+ _See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/generate-new-hex-key.ts)_
452
452
 
453
453
  ## `qfg get [NAME]`
454
454
 
@@ -480,7 +480,7 @@ EXAMPLES
480
480
  $ qfg get my.config.name --environment=production
481
481
  ```
482
482
 
483
- _See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/get.ts)_
483
+ _See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/get.ts)_
484
484
 
485
485
  ## `qfg info [NAME]`
486
486
 
@@ -528,7 +528,7 @@ EXAMPLES
528
528
  $ qfg info my.config.name --exclude-evaluations # skip 24h stats
529
529
  ```
530
530
 
531
- _See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/info.ts)_
531
+ _See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/info.ts)_
532
532
 
533
533
  ## `qfg init [DIRECTORY]`
534
534
 
@@ -568,7 +568,7 @@ EXAMPLES
568
568
  $ qfg init --dry-run
569
569
  ```
570
570
 
571
- _See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/init.ts)_
571
+ _See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/init.ts)_
572
572
 
573
573
  ## `qfg interactive`
574
574
 
@@ -598,7 +598,7 @@ EXAMPLES
598
598
  $ qfg
599
599
  ```
600
600
 
601
- _See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/interactive.ts)_
601
+ _See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/interactive.ts)_
602
602
 
603
603
  ## `qfg list`
604
604
 
@@ -638,7 +638,7 @@ EXAMPLES
638
638
  $ qfg list --feature-flags
639
639
  ```
640
640
 
641
- _See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/list.ts)_
641
+ _See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/list.ts)_
642
642
 
643
643
  ## `qfg log-level NAME`
644
644
 
@@ -703,7 +703,7 @@ EXAMPLES
703
703
  $ qfg log-level log-level.my-app --target=Chatty --value=INFO --environment=production
704
704
  ```
705
705
 
706
- _See code: [src/commands/log-level.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/log-level.ts)_
706
+ _See code: [src/commands/log-level.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/log-level.ts)_
707
707
 
708
708
  ## `qfg login`
709
709
 
@@ -728,7 +728,7 @@ EXAMPLES
728
728
  $ qfg login --profile myprofile
729
729
  ```
730
730
 
731
- _See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/login.ts)_
731
+ _See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/login.ts)_
732
732
 
733
733
  ## `qfg logout`
734
734
 
@@ -751,7 +751,7 @@ EXAMPLES
751
751
  $ qfg logout
752
752
  ```
753
753
 
754
- _See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/logout.ts)_
754
+ _See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/logout.ts)_
755
755
 
756
756
  ## `qfg migrate`
757
757
 
@@ -805,7 +805,7 @@ EXAMPLES
805
805
  $ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --staging
806
806
  ```
807
807
 
808
- _See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/migrate.ts)_
808
+ _See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/migrate.ts)_
809
809
 
810
810
  ## `qfg migrate doctor`
811
811
 
@@ -843,7 +843,7 @@ EXAMPLES
843
843
  $ qfg migrate doctor --json
844
844
  ```
845
845
 
846
- _See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/migrate/doctor.ts)_
846
+ _See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/migrate/doctor.ts)_
847
847
 
848
848
  ## `qfg migrate my-code`
849
849
 
@@ -875,7 +875,7 @@ EXAMPLES
875
875
  $ qfg migrate my-code --dry-run
876
876
  ```
877
877
 
878
- _See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/migrate/my-code.ts)_
878
+ _See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/migrate/my-code.ts)_
879
879
 
880
880
  ## `qfg migrate status`
881
881
 
@@ -906,7 +906,7 @@ EXAMPLES
906
906
  $ qfg migrate status --json
907
907
  ```
908
908
 
909
- _See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/migrate/status.ts)_
909
+ _See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/migrate/status.ts)_
910
910
 
911
911
  ## `qfg override [NAME] [VALUE]`
912
912
 
@@ -961,7 +961,7 @@ EXAMPLES
961
961
  $ qfg override my.flag true --env=staging
962
962
  ```
963
963
 
964
- _See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/override.ts)_
964
+ _See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/override.ts)_
965
965
 
966
966
  ## `qfg pull`
967
967
 
@@ -1008,7 +1008,7 @@ EXAMPLES
1008
1008
  $ qfg pull # uses QUONFIG_DIR env var
1009
1009
  ```
1010
1010
 
1011
- _See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/pull.ts)_
1011
+ _See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/pull.ts)_
1012
1012
 
1013
1013
  ## `qfg push`
1014
1014
 
@@ -1056,7 +1056,7 @@ EXAMPLES
1056
1056
  $ qfg push --dir ./our-config --yes
1057
1057
  ```
1058
1058
 
1059
- _See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/push.ts)_
1059
+ _See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/push.ts)_
1060
1060
 
1061
1061
  ## `qfg schema NAME`
1062
1062
 
@@ -1094,7 +1094,7 @@ EXAMPLES
1094
1094
  $ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
1095
1095
  ```
1096
1096
 
1097
- _See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/schema.ts)_
1097
+ _See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/schema.ts)_
1098
1098
 
1099
1099
  ## `qfg sdk-key`
1100
1100
 
@@ -1121,7 +1121,7 @@ EXAMPLES
1121
1121
  $ qfg sdk-key revoke <key-id>
1122
1122
  ```
1123
1123
 
1124
- _See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/sdk-key.ts)_
1124
+ _See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sdk-key.ts)_
1125
1125
 
1126
1126
  ## `qfg sdk-key create`
1127
1127
 
@@ -1153,7 +1153,7 @@ EXAMPLES
1153
1153
  $ qfg sdk-key create --environment staging --type browser
1154
1154
  ```
1155
1155
 
1156
- _See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/sdk-key/create.ts)_
1156
+ _See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sdk-key/create.ts)_
1157
1157
 
1158
1158
  ## `qfg sdk-key list`
1159
1159
 
@@ -1182,7 +1182,7 @@ EXAMPLES
1182
1182
  $ qfg sdk-key list --environment production
1183
1183
  ```
1184
1184
 
1185
- _See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/sdk-key/list.ts)_
1185
+ _See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sdk-key/list.ts)_
1186
1186
 
1187
1187
  ## `qfg sdk-key revoke KEYID`
1188
1188
 
@@ -1209,7 +1209,7 @@ EXAMPLES
1209
1209
  $ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
1210
1210
  ```
1211
1211
 
1212
- _See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/sdk-key/revoke.ts)_
1212
+ _See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sdk-key/revoke.ts)_
1213
1213
 
1214
1214
  ## `qfg set-default [NAME]`
1215
1215
 
@@ -1280,7 +1280,7 @@ EXAMPLES
1280
1280
  $ qfg pull && qfg config-schema
1281
1281
  ```
1282
1282
 
1283
- _See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/set-default.ts)_
1283
+ _See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/set-default.ts)_
1284
1284
 
1285
1285
  ## `qfg set-rollout [NAME]`
1286
1286
 
@@ -1357,7 +1357,7 @@ EXAMPLES
1357
1357
  $ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
1358
1358
  ```
1359
1359
 
1360
- _See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/set-rollout.ts)_
1360
+ _See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/set-rollout.ts)_
1361
1361
 
1362
1362
  ## `qfg sync`
1363
1363
 
@@ -1389,7 +1389,7 @@ EXAMPLES
1389
1389
  $ qfg sync --watch --dir ./our-config --interval 30
1390
1390
  ```
1391
1391
 
1392
- _See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/sync.ts)_
1392
+ _See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sync.ts)_
1393
1393
 
1394
1394
  ## `qfg toggle [NAME]`
1395
1395
 
@@ -1491,7 +1491,7 @@ EXAMPLES
1491
1491
  $ qfg verify --json
1492
1492
  ```
1493
1493
 
1494
- _See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/verify.ts)_
1494
+ _See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/verify.ts)_
1495
1495
 
1496
1496
  ## `qfg whoami`
1497
1497
 
@@ -1514,7 +1514,7 @@ EXAMPLES
1514
1514
  $ qfg whoami
1515
1515
  ```
1516
1516
 
1517
- _See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/whoami.ts)_
1517
+ _See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/whoami.ts)_
1518
1518
 
1519
1519
  ## `qfg workspace`
1520
1520
 
@@ -1537,7 +1537,7 @@ EXAMPLES
1537
1537
  $ qfg workspace
1538
1538
  ```
1539
1539
 
1540
- _See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/workspace.ts)_
1540
+ _See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/workspace.ts)_
1541
1541
 
1542
1542
  ## `qfg workspace bootstrap`
1543
1543
 
@@ -1570,7 +1570,7 @@ EXAMPLES
1570
1570
  $ qfg workspace bootstrap --dir ./our-config --skip-validate
1571
1571
  ```
1572
1572
 
1573
- _See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/workspace/bootstrap.ts)_
1573
+ _See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/workspace/bootstrap.ts)_
1574
1574
 
1575
1575
  ## `qfg workspace create SLUG`
1576
1576
 
@@ -1604,7 +1604,7 @@ EXAMPLES
1604
1604
  $ qfg workspace create my-team --org 11111111-1111-1111-1111-111111111111
1605
1605
  ```
1606
1606
 
1607
- _See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/workspace/create.ts)_
1607
+ _See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/workspace/create.ts)_
1608
1608
 
1609
1609
  ## `qfg workspace switch [SLUG]`
1610
1610
 
@@ -1632,7 +1632,7 @@ EXAMPLES
1632
1632
  $ qfg workspace switch acme/production
1633
1633
  ```
1634
1634
 
1635
- _See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.34/src/commands/workspace/switch.ts)_
1635
+ _See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/workspace/switch.ts)_
1636
1636
  <!-- commandsstop -->
1637
1637
 
1638
1638
  ## Local Development
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { BaseCommand } from '../index.js';
5
5
  import { CrossSourceError, assertSourceMatches, readImportState } from '../migrate/import-state.js';
6
- import { applyLocalMigration } from '../migrate/local-write.js';
6
+ import { applyLocalMigration, MigratorKeyCollisionError } from '../migrate/local-write.js';
7
7
  import { buildPushConflictSuggestion } from '../migrate/migrate-suggestion.js';
8
8
  import { PushConflictError, PushHookRejectedError } from '../util/clone-and-stack-push.js';
9
9
  import { MigratorVerifyError, pushMigrationToCloud } from '../migrate/push-to-cloud.js';
@@ -150,12 +150,11 @@ export default class Migrate extends BaseCommand {
150
150
  try {
151
151
  const result = await pushMigrationToCloud({
152
152
  changes: toProcess,
153
- commitMessage: buildCommitMessage(flags.from, toProcess.length),
154
153
  environments,
155
154
  importState,
156
155
  localDir: dir,
157
156
  remoteUrl: repoUrl,
158
- reportData: buildReportData(flags.from, toProcess.length),
157
+ reportData: buildReportData(flags.from),
159
158
  source,
160
159
  });
161
160
  duplicateResolutionsForWarn = result.duplicateResolutions;
@@ -183,6 +182,9 @@ export default class Migrate extends BaseCommand {
183
182
  `Fix the issues above (typically in the source system or by editing the local files), then re-run with --push. ` +
184
183
  `You can re-run \`qfg verify ${dir}\` locally to confirm the fixes before pushing.`);
185
184
  }
185
+ if (error instanceof MigratorKeyCollisionError) {
186
+ return this.err(error.message);
187
+ }
186
188
  throw error;
187
189
  }
188
190
  }
@@ -194,11 +196,10 @@ export default class Migrate extends BaseCommand {
194
196
  this.log(`Fetched ${changes.length} change(s) from ${flags.from}; processing ${toProcess.length} into ${dir}.`);
195
197
  const localResult = await applyLocalMigration({
196
198
  changes: toProcess,
197
- commitMessage: buildCommitMessage(flags.from, toProcess.length),
198
199
  environments,
199
200
  importState: localImportState,
200
201
  localDir: dir,
201
- reportData: buildReportData(flags.from, toProcess.length),
202
+ reportData: buildReportData(flags.from),
202
203
  source,
203
204
  });
204
205
  duplicateResolutionsForWarn = localResult.duplicateResolutions;
@@ -215,6 +216,8 @@ export default class Migrate extends BaseCommand {
215
216
  catch (error) {
216
217
  if (error instanceof NotYetImplementedError)
217
218
  return this.err(error.message);
219
+ if (error instanceof MigratorKeyCollisionError)
220
+ return this.err(error.message);
218
221
  throw error;
219
222
  }
220
223
  finally {
@@ -247,16 +250,20 @@ function latestChangedAtOf(changes, fallback) {
247
250
  }
248
251
  return latest ?? fallback;
249
252
  }
250
- function buildCommitMessage(source, count) {
251
- return `migrator: import ${count} change(s) from ${source}`;
252
- }
253
- function buildReportData(source, count) {
253
+ function buildReportData(source) {
254
+ // Counts are placeholders; applyLocalMigration / pushMigrationToCloud
255
+ // override `counts` after writing files so they reflect what actually
256
+ // landed on disk (qfg-7eig). The commit message is also derived from those
257
+ // counts inside the apply function — it is not built here.
254
258
  return {
255
259
  cleanMappings: [],
256
260
  counts: {
261
+ configsMigrated: 0,
257
262
  environmentsMapped: 0,
258
- flagsMigrated: count,
263
+ flagsMigrated: 0,
259
264
  itemsSkipped: 0,
265
+ logLevelsMigrated: 0,
266
+ schemasMigrated: 0,
260
267
  segmentsMigrated: 0,
261
268
  },
262
269
  dryRun: false,
@@ -1,18 +1,21 @@
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 { CoercedSentinelSummary, DroppedOverrideSummary, DuplicateResolutionSummary, LegacyChange, MigrationSource, SkippedConfigSummary } from './source.js';
4
+ import type { CoercedSentinelSummary, DroppedOverrideSummary, DuplicateResolution, 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;
8
8
  /** Initial branch name when initializing a fresh repo. Defaults to `main`. */
9
9
  branch?: string;
10
10
  changes: LegacyChange[];
11
- commitMessage: string;
12
11
  /** Environments discovered from the source (slugified). Written to `quonfig.json` if missing. */
13
12
  environments: string[];
14
13
  importState: ImportState;
15
14
  localDir: string;
15
+ /**
16
+ * Base report data. The migrator overrides `counts` after writing files,
17
+ * since counts must reflect what was actually written to disk (qfg-7eig).
18
+ */
16
19
  reportData: MigrationReportData;
17
20
  source: MigrationSource;
18
21
  }
@@ -31,4 +34,18 @@ export interface ApplyLocalMigrationResult {
31
34
  /** Configs soft-skipped during translate due to invalid source data. Null if none. */
32
35
  skippedConfigs: SkippedConfigSummary | null;
33
36
  }
37
+ export declare class MigratorKeyCollisionError extends Error {
38
+ readonly destPath: string;
39
+ readonly firstSourceKey: string;
40
+ readonly secondSourceKey: string;
41
+ constructor(destPath: string, firstSourceKey: string, secondSourceKey: string);
42
+ }
43
+ export declare const writeQuonfigFiles: (dir: string, changes: LegacyChange[], source: MigrationSource) => WriteQuonfigFilesResult;
44
+ interface WriteQuonfigFilesResult {
45
+ livePaths: string[];
46
+ resolutions: DuplicateResolution[];
47
+ }
48
+ export declare const buildMigrationCounts: (livePaths: string[], environmentsMapped: number, itemsSkipped: number) => import("./migration-report.js").MigrationReportCounts;
49
+ export declare const buildMigrationCommitMessage: (source: string, counts: import("./migration-report.js").MigrationReportCounts) => string;
34
50
  export declare const applyLocalMigration: (opts: ApplyLocalMigrationOptions) => Promise<ApplyLocalMigrationResult>;
51
+ export {};
@@ -32,11 +32,54 @@ const ensureLocalRepo = async (localDir, branch) => {
32
32
  await runGit(['init', `--initial-branch=${branch}`, localDir]);
33
33
  return 'initialized';
34
34
  };
35
- const writeQuonfigFiles = (dir, changes, source) => {
35
+ export class MigratorKeyCollisionError extends Error {
36
+ destPath;
37
+ firstSourceKey;
38
+ secondSourceKey;
39
+ constructor(destPath, firstSourceKey, secondSourceKey) {
40
+ super(`Two source keys collide on destination ${destPath} after key normalization: ` +
41
+ `"${firstSourceKey}" and "${secondSourceKey}". Quonfig keys must be globally unique within a type tree. ` +
42
+ `Rename one in the source system and re-run.`);
43
+ this.destPath = destPath;
44
+ this.firstSourceKey = firstSourceKey;
45
+ this.secondSourceKey = secondSourceKey;
46
+ this.name = 'MigratorKeyCollisionError';
47
+ }
48
+ }
49
+ const ensureNoNestedPath = (relativePath) => {
50
+ // qfg-qhk1: every output must be exactly `<type-dir>/<flat-key>.json` —
51
+ // never nested. If we ever emit a nested path, refuse to write so we surface
52
+ // it loudly rather than silently producing dirs that lose the flat-file
53
+ // contract (and break tombstone cleanup on subsequent runs).
54
+ const parts = relativePath.split('/');
55
+ if (parts.length !== 2) {
56
+ throw new Error(`Migrator refusing to write nested path "${relativePath}" — outputs must be flat <type-dir>/<key>.json. ` +
57
+ `This is a bug in the source's translate() — it should normalize key separators before computing the path.`);
58
+ }
59
+ };
60
+ const rmdirIfEmpty = (dir) => {
61
+ try {
62
+ fs.rmdirSync(dir);
63
+ }
64
+ catch (error) {
65
+ const e = error;
66
+ // ENOTEMPTY: still has contents (expected, leave alone). ENOENT: already gone.
67
+ if (e.code !== 'ENOTEMPTY' && e.code !== 'ENOENT')
68
+ throw error;
69
+ }
70
+ };
71
+ export const writeQuonfigFiles = (dir, changes, source) => {
36
72
  const livePaths = new Map();
73
+ // qfg-qhk1: track which source key wrote each destination path. If a second,
74
+ // different source key tries to write to the same destination, that is a
75
+ // post-normalization collision (e.g. source had keys `foo/bar` and `foo.bar`,
76
+ // both normalize to `foo.bar`). Throw with a clear message.
77
+ const pathOwners = new Map();
37
78
  for (const change of changes) {
38
79
  const files = source.translate(change);
80
+ const sourceKey = change.key ?? '(unknown)';
39
81
  for (const file of files) {
82
+ ensureNoNestedPath(file.path);
40
83
  const full = path.join(dir, file.path);
41
84
  if (file.deleted) {
42
85
  try {
@@ -47,14 +90,25 @@ const writeQuonfigFiles = (dir, changes, source) => {
47
90
  throw error;
48
91
  }
49
92
  livePaths.delete(file.path);
93
+ pathOwners.delete(file.path);
94
+ // qfg-qhk1: clean up any empty parent dirs left over from prior runs
95
+ // that wrote under a nested layout. (New runs never create nested
96
+ // dirs because of normalizeKey + ensureNoNestedPath, but stale dirs
97
+ // from older versions on disk would otherwise persist.)
98
+ rmdirIfEmpty(path.dirname(full));
50
99
  }
51
100
  else {
52
101
  if (file.contents === undefined) {
53
102
  throw new Error(`translate() emitted a write op for ${file.path} without contents`);
54
103
  }
104
+ const existingOwner = pathOwners.get(file.path);
105
+ if (existingOwner !== undefined && existingOwner !== sourceKey) {
106
+ throw new MigratorKeyCollisionError(file.path, existingOwner, sourceKey);
107
+ }
55
108
  fs.mkdirSync(path.dirname(full), { recursive: true });
56
109
  fs.writeFileSync(full, file.contents);
57
110
  livePaths.set(file.path, true);
111
+ pathOwners.set(file.path, sourceKey);
58
112
  }
59
113
  }
60
114
  }
@@ -71,7 +125,47 @@ const writeQuonfigFiles = (dir, changes, source) => {
71
125
  livePaths.delete(toDelete);
72
126
  }
73
127
  }
74
- return resolutions;
128
+ return { livePaths: [...livePaths.keys()], resolutions };
129
+ };
130
+ const countsFromLivePaths = (livePaths) => {
131
+ let flagsMigrated = 0;
132
+ let configsMigrated = 0;
133
+ let segmentsMigrated = 0;
134
+ let schemasMigrated = 0;
135
+ let logLevelsMigrated = 0;
136
+ for (const p of livePaths) {
137
+ if (p.startsWith('feature-flags/'))
138
+ flagsMigrated++;
139
+ else if (p.startsWith('configs/'))
140
+ configsMigrated++;
141
+ else if (p.startsWith('segments/'))
142
+ segmentsMigrated++;
143
+ else if (p.startsWith('schemas/'))
144
+ schemasMigrated++;
145
+ else if (p.startsWith('log-levels/'))
146
+ logLevelsMigrated++;
147
+ }
148
+ return { configsMigrated, flagsMigrated, logLevelsMigrated, schemasMigrated, segmentsMigrated };
149
+ };
150
+ export const buildMigrationCounts = (livePaths, environmentsMapped, itemsSkipped) => ({
151
+ ...countsFromLivePaths(livePaths),
152
+ environmentsMapped,
153
+ itemsSkipped,
154
+ });
155
+ export const buildMigrationCommitMessage = (source, counts) => {
156
+ const parts = [];
157
+ if (counts.flagsMigrated > 0)
158
+ parts.push(`${counts.flagsMigrated} flag(s)`);
159
+ if (counts.configsMigrated > 0)
160
+ parts.push(`${counts.configsMigrated} config(s)`);
161
+ if (counts.segmentsMigrated > 0)
162
+ parts.push(`${counts.segmentsMigrated} segment(s)`);
163
+ if (counts.schemasMigrated > 0)
164
+ parts.push(`${counts.schemasMigrated} schema(s)`);
165
+ if (counts.logLevelsMigrated > 0)
166
+ parts.push(`${counts.logLevelsMigrated} log-level(s)`);
167
+ const summary = parts.length === 0 ? 'no objects' : parts.join(', ');
168
+ return `migrator: imported ${summary} from ${source}`;
75
169
  };
76
170
  export const applyLocalMigration = async (opts) => {
77
171
  const branch = opts.branch ?? 'main';
@@ -80,15 +174,18 @@ export const applyLocalMigration = async (opts) => {
80
174
  await runGit(['-C', opts.localDir, 'config', 'user.name', author.name]);
81
175
  await runGit(['-C', opts.localDir, 'config', 'user.email', author.email]);
82
176
  ensureQuonfigJson(opts.localDir, opts.environments);
83
- const resolutionEntries = writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
177
+ const { livePaths, resolutions: resolutionEntries } = writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
84
178
  removeQfFromGitignore(opts.localDir);
85
179
  writeImportState(opts.localDir, opts.importState);
86
180
  const droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
87
181
  const skippedConfigs = opts.source.getSkippedConfigs?.() ?? null;
88
182
  const coercedSentinels = opts.source.getCoercedSentinels?.() ?? null;
89
183
  const duplicateResolutions = resolutionEntries.length > 0 ? { entries: resolutionEntries, total: resolutionEntries.length } : null;
184
+ const skippedTotal = (skippedConfigs?.total ?? 0) + resolutionEntries.reduce((sum, r) => sum + r.deleted.length, 0);
185
+ const counts = buildMigrationCounts(livePaths, opts.environments.length, skippedTotal);
90
186
  const reportData = {
91
187
  ...opts.reportData,
188
+ counts,
92
189
  ...(coercedSentinels ? { coercedSentinels } : {}),
93
190
  ...(droppedOverrides ? { droppedOverrides } : {}),
94
191
  ...(duplicateResolutions ? { duplicateResolutions } : {}),
@@ -107,8 +204,9 @@ export const applyLocalMigration = async (opts) => {
107
204
  skippedConfigs,
108
205
  };
109
206
  }
207
+ const commitMessage = buildMigrationCommitMessage(opts.source.name, counts);
110
208
  await spawnGit(['-C', opts.localDir, 'commit', '-F', '-'], {
111
- stdin: opts.commitMessage,
209
+ stdin: commitMessage,
112
210
  env: {
113
211
  GIT_AUTHOR_EMAIL: author.email,
114
212
  GIT_AUTHOR_NAME: author.name,
@@ -1,9 +1,12 @@
1
1
  import type { IdentifierMap } from './identifier-map.js';
2
2
  import type { CoercedSentinelSummary, DroppedOverrideSummary, DuplicateResolutionSummary, SkippedConfigSummary } from './source.js';
3
3
  export interface MigrationReportCounts {
4
+ configsMigrated: number;
4
5
  environmentsMapped: number;
5
6
  flagsMigrated: number;
6
7
  itemsSkipped: number;
8
+ logLevelsMigrated: number;
9
+ schemasMigrated: number;
7
10
  segmentsMigrated: number;
8
11
  }
9
12
  export interface CleanMapping {
@@ -5,7 +5,10 @@ const renderCounts = (counts) => [
5
5
  '## Counts',
6
6
  '',
7
7
  `- Flags migrated: ${counts.flagsMigrated}`,
8
+ `- Configs migrated: ${counts.configsMigrated}`,
8
9
  `- Segments migrated: ${counts.segmentsMigrated}`,
10
+ `- Schemas migrated: ${counts.schemasMigrated}`,
11
+ `- Log levels migrated: ${counts.logLevelsMigrated}`,
9
12
  `- Environments mapped: ${counts.environmentsMapped}`,
10
13
  `- Items skipped: ${counts.itemsSkipped}`,
11
14
  ].join('\n');
@@ -17,7 +17,6 @@ export declare class MigratorVerifyError extends Error {
17
17
  export interface PushMigrationToCloudOptions {
18
18
  branch?: string;
19
19
  changes: LegacyChange[];
20
- commitMessage: string;
21
20
  /**
22
21
  * Source-side environments (slugified). Additively merged into the target
23
22
  * workspace's quonfig.json so flag files that reference these envs verify
@@ -27,6 +26,10 @@ export interface PushMigrationToCloudOptions {
27
26
  importState: ImportState;
28
27
  localDir: string;
29
28
  remoteUrl: string;
29
+ /**
30
+ * Base report data. The migrator overrides `counts` after writing files,
31
+ * since counts must reflect what was actually written to disk (qfg-7eig).
32
+ */
30
33
  reportData: MigrationReportData;
31
34
  source: MigrationSource;
32
35
  }
@@ -1,8 +1,8 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { removeQfFromGitignore, writeImportState } from './import-state.js';
4
+ import { buildMigrationCommitMessage, buildMigrationCounts, writeQuonfigFiles } from './local-write.js';
4
5
  import { writeMigrationReport } from './migration-report.js';
5
- import { detectDuplicateKeys } from './sources/launch/translate.js';
6
6
  import { cloneAndStackPush, } from '../util/clone-and-stack-push.js';
7
7
  import { formatResult, validateWorkspace } from '../verify/validate.js';
8
8
  /**
@@ -23,47 +23,6 @@ export class MigratorVerifyError extends Error {
23
23
  this.result = result;
24
24
  }
25
25
  }
26
- const writeQuonfigFiles = (dir, changes, source) => {
27
- const livePaths = new Map();
28
- for (const change of changes) {
29
- const files = source.translate(change);
30
- for (const file of files) {
31
- const full = path.join(dir, file.path);
32
- if (file.deleted) {
33
- try {
34
- fs.unlinkSync(full);
35
- }
36
- catch (error) {
37
- if (error.code !== 'ENOENT')
38
- throw error;
39
- }
40
- livePaths.delete(file.path);
41
- }
42
- else {
43
- if (file.contents === undefined) {
44
- throw new Error(`translate() emitted a write op for ${file.path} without contents`);
45
- }
46
- fs.mkdirSync(path.dirname(full), { recursive: true });
47
- fs.writeFileSync(full, file.contents);
48
- livePaths.set(file.path, true);
49
- }
50
- }
51
- }
52
- const resolutions = detectDuplicateKeys([...livePaths.keys()].map((p) => ({ path: p })));
53
- for (const resolution of resolutions) {
54
- for (const toDelete of resolution.deleted) {
55
- try {
56
- fs.unlinkSync(path.join(dir, toDelete));
57
- }
58
- catch (error) {
59
- if (error.code !== 'ENOENT')
60
- throw error;
61
- }
62
- livePaths.delete(toDelete);
63
- }
64
- }
65
- return resolutions;
66
- };
67
26
  const QUONFIG_JSON_FILENAME = 'quonfig.json';
68
27
  const mergeEnvironmentsIntoQuonfigJson = (dir, sourceEnvs) => {
69
28
  const quonfigPath = path.join(dir, QUONFIG_JSON_FILENAME);
@@ -88,12 +47,13 @@ export const pushMigrationToCloud = async (opts) => {
88
47
  let droppedOverrides = null;
89
48
  let duplicateResolutions = null;
90
49
  let skippedConfigs = null;
50
+ let computedCommitMessage = `migrator: imported 0 objects from ${opts.source.name}`;
91
51
  const cloneOpts = {
92
52
  applyDelta(dir) {
93
53
  if (opts.environments && opts.environments.length > 0) {
94
54
  mergeEnvironmentsIntoQuonfigJson(dir, opts.environments);
95
55
  }
96
- const resolutionEntries = writeQuonfigFiles(dir, opts.changes, opts.source);
56
+ const { livePaths, resolutions: resolutionEntries } = writeQuonfigFiles(dir, opts.changes, opts.source);
97
57
  removeQfFromGitignore(dir);
98
58
  writeImportState(dir, opts.importState);
99
59
  droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
@@ -101,8 +61,13 @@ export const pushMigrationToCloud = async (opts) => {
101
61
  coercedSentinels = opts.source.getCoercedSentinels?.() ?? null;
102
62
  duplicateResolutions =
103
63
  resolutionEntries.length > 0 ? { entries: resolutionEntries, total: resolutionEntries.length } : null;
64
+ const skippedTotal = (skippedConfigs?.total ?? 0) + resolutionEntries.reduce((sum, r) => sum + r.deleted.length, 0);
65
+ const environmentsCount = opts.environments?.length ?? 0;
66
+ const counts = buildMigrationCounts(livePaths, environmentsCount, skippedTotal);
67
+ computedCommitMessage = buildMigrationCommitMessage(opts.source.name, counts);
104
68
  const reportData = {
105
69
  ...opts.reportData,
70
+ counts,
106
71
  ...(coercedSentinels ? { coercedSentinels } : {}),
107
72
  ...(droppedOverrides ? { droppedOverrides } : {}),
108
73
  ...(duplicateResolutions ? { duplicateResolutions } : {}),
@@ -117,7 +82,7 @@ export const pushMigrationToCloud = async (opts) => {
117
82
  if (!verifyResult.valid)
118
83
  throw new MigratorVerifyError(verifyResult);
119
84
  },
120
- commitMessage: opts.commitMessage,
85
+ commitMessage: () => computedCommitMessage,
121
86
  localDir: opts.localDir,
122
87
  remoteUrl: opts.remoteUrl,
123
88
  };
@@ -11,6 +11,22 @@ export declare class InvalidSourceConfigError extends Error {
11
11
  export declare function slugify(value: string): string;
12
12
  export declare function isLegacyLogLevel(type: string): boolean;
13
13
  export declare function normalizeLogLevelKey(key: string): string;
14
+ /**
15
+ * qfg-qhk1: Quonfig keys cannot contain `/` — keys are stored as flat dotted
16
+ * filenames, and a `/` in a source key would create a nested directory tree
17
+ * (data-loss-on-tombstone, cross-key collisions). Normalize `/` to `.` so the
18
+ * destination is always flat.
19
+ */
20
+ export declare function normalizeKey(key: string): string;
21
+ /**
22
+ * qfg-l18w: Operators Quonfig accepts in rule criteria. Mirrors the
23
+ * `OperatorSchema` enum in cli/src/verify/validate.ts (and app-quonfig's
24
+ * config-schemas.ts). If we emit an operator outside this set, qfg verify
25
+ * fails — which is what was happening for `PROP_SEMVER_*` in the FormHealth
26
+ * dry-run before this check existed. Keep this list in lockstep with the
27
+ * verify schema; a CI test in this file enforces parity.
28
+ */
29
+ export declare const QUONFIG_SUPPORTED_OPERATORS: Set<string>;
14
30
  export declare function transformConfig(config: LaunchConfig, envIdMap: Record<string, string>, onDroppedEnv?: (envId: string) => void, onCoercedSentinel?: (envId: string, valueType: string) => void): Record<string, unknown>;
15
31
  export declare function getOutputPath(type: string, key: string): string;
16
32
  /**
@@ -23,6 +23,106 @@ export function isLegacyLogLevel(type) {
23
23
  export function normalizeLogLevelKey(key) {
24
24
  return key.startsWith('log-level.') ? key : `log-level.${key}`;
25
25
  }
26
+ /**
27
+ * qfg-qhk1: Quonfig keys cannot contain `/` — keys are stored as flat dotted
28
+ * filenames, and a `/` in a source key would create a nested directory tree
29
+ * (data-loss-on-tombstone, cross-key collisions). Normalize `/` to `.` so the
30
+ * destination is always flat.
31
+ */
32
+ export function normalizeKey(key) {
33
+ return key.replaceAll('/', '.');
34
+ }
35
+ /**
36
+ * qfg-l18w: Operators Quonfig accepts in rule criteria. Mirrors the
37
+ * `OperatorSchema` enum in cli/src/verify/validate.ts (and app-quonfig's
38
+ * config-schemas.ts). If we emit an operator outside this set, qfg verify
39
+ * fails — which is what was happening for `PROP_SEMVER_*` in the FormHealth
40
+ * dry-run before this check existed. Keep this list in lockstep with the
41
+ * verify schema; a CI test in this file enforces parity.
42
+ */
43
+ export const QUONFIG_SUPPORTED_OPERATORS = new Set([
44
+ 'ALWAYS_TRUE',
45
+ 'PROP_IS_ONE_OF',
46
+ 'PROP_IS_NOT_ONE_OF',
47
+ 'PROP_STARTS_WITH_ONE_OF',
48
+ 'PROP_DOES_NOT_START_WITH_ONE_OF',
49
+ 'PROP_ENDS_WITH_ONE_OF',
50
+ 'PROP_DOES_NOT_END_WITH_ONE_OF',
51
+ 'PROP_CONTAINS_ONE_OF',
52
+ 'PROP_DOES_NOT_CONTAIN_ONE_OF',
53
+ 'PROP_LESS_THAN',
54
+ 'PROP_LESS_THAN_OR_EQUAL',
55
+ 'PROP_GREATER_THAN',
56
+ 'PROP_GREATER_THAN_OR_EQUAL',
57
+ 'PROP_BEFORE',
58
+ 'PROP_AFTER',
59
+ 'PROP_MATCHES',
60
+ 'PROP_DOES_NOT_MATCH',
61
+ 'IN_SEG',
62
+ 'NOT_IN_SEG',
63
+ 'IN_INT_RANGE',
64
+ 'LOOKUP_KEY_IN',
65
+ 'LOOKUP_KEY_NOT_IN',
66
+ ]);
67
+ /**
68
+ * qfg-l18w: Walk every rule criterion in a transformed config and fail-fast at
69
+ * migrate time if it carries an operator Quonfig does not understand. The
70
+ * primary offender today is `PROP_SEMVER_*` (Launch supports semver
71
+ * comparisons; Quonfig does not have a semver operator at all). Without this
72
+ * check the migrator silently writes invalid JSON and the failure surfaces
73
+ * later at qfg-verify — the customer's local file is broken and re-runs
74
+ * re-clobber any manual fix.
75
+ *
76
+ * Routes through `InvalidSourceConfigError` so the failure is per-config (the
77
+ * rest of the run still produces a usable workspace, and the offending key
78
+ * lands in MIGRATION_REPORT.md's "Skipped invalid configs" section with a
79
+ * specific reason — exactly what the customer needs to take to support).
80
+ */
81
+ function collectUnsupportedOperators(rules) {
82
+ if (!Array.isArray(rules))
83
+ return [];
84
+ const found = [];
85
+ for (const rule of rules) {
86
+ if (!rule || typeof rule !== 'object')
87
+ continue;
88
+ const r = rule;
89
+ if (!Array.isArray(r.criteria))
90
+ continue;
91
+ for (const crit of r.criteria) {
92
+ if (!crit || typeof crit !== 'object')
93
+ continue;
94
+ const c = crit;
95
+ if (typeof c.operator !== 'string')
96
+ continue;
97
+ if (!QUONFIG_SUPPORTED_OPERATORS.has(c.operator)) {
98
+ found.push(c.operator);
99
+ }
100
+ }
101
+ }
102
+ return found;
103
+ }
104
+ function validateOperators(out) {
105
+ const offenders = new Set();
106
+ const defaultSection = out.default;
107
+ if (defaultSection) {
108
+ for (const op of collectUnsupportedOperators(defaultSection.rules))
109
+ offenders.add(op);
110
+ }
111
+ if (Array.isArray(out.environments)) {
112
+ for (const env of out.environments) {
113
+ for (const op of collectUnsupportedOperators(env.rules))
114
+ offenders.add(op);
115
+ }
116
+ }
117
+ if (offenders.size === 0)
118
+ return;
119
+ const key = String(out.key ?? 'unknown');
120
+ const sorted = [...offenders].sort();
121
+ throw new InvalidSourceConfigError(`Unsupported operator(s) for "${key}": ${sorted.join(', ')}. ` +
122
+ `Quonfig does not currently translate these from the source schema. ` +
123
+ `Recreate the rule in the source system using a supported operator (see qfg config-schema), ` +
124
+ `or contact support@quonfig.com if you need this operator added to Quonfig.`);
125
+ }
26
126
  function zeroValue(valueType) {
27
127
  switch (valueType) {
28
128
  case 'bool': {
@@ -148,6 +248,11 @@ function normalizeJsonValuesInConfig(out) {
148
248
  }
149
249
  export function transformConfig(config, envIdMap, onDroppedEnv, onCoercedSentinel) {
150
250
  const out = JSON.parse(JSON.stringify(config));
251
+ // qfg-qhk1: normalize `/` to `.` in the key so the JSON `key` field matches
252
+ // the flat dotted filename the migrator will write.
253
+ if (typeof out.key === 'string') {
254
+ out.key = normalizeKey(out.key);
255
+ }
151
256
  delete out.changedBy;
152
257
  delete out.modifiedAt;
153
258
  delete out.createdAt;
@@ -244,25 +349,27 @@ export function transformConfig(config, envIdMap, onDroppedEnv, onCoercedSentine
244
349
  }
245
350
  }
246
351
  normalizeJsonValuesInConfig(out);
352
+ validateOperators(out);
247
353
  return out;
248
354
  }
249
355
  export function getOutputPath(type, key) {
250
356
  const normalizedType = type.toUpperCase();
357
+ const k = normalizeKey(key);
251
358
  switch (normalizedType) {
252
359
  case 'FEATURE_FLAG': {
253
- return `feature-flags/${key}.json`;
360
+ return `feature-flags/${k}.json`;
254
361
  }
255
362
  case 'LOG_LEVEL_V2': {
256
- return `log-levels/${normalizeLogLevelKey(key)}.json`;
363
+ return `log-levels/${normalizeLogLevelKey(k)}.json`;
257
364
  }
258
365
  case 'SCHEMA': {
259
- return `schemas/${key}.json`;
366
+ return `schemas/${k}.json`;
260
367
  }
261
368
  case 'SEGMENT': {
262
- return `segments/${key}.json`;
369
+ return `segments/${k}.json`;
263
370
  }
264
371
  default: {
265
- return `configs/${key}.json`;
372
+ return `configs/${k}.json`;
266
373
  }
267
374
  }
268
375
  }
@@ -13,8 +13,12 @@ export interface CloneAndStackPushOptions {
13
13
  author?: PushIdentity;
14
14
  /** Branch to track and push. Defaults to `main`. */
15
15
  branch?: string;
16
- /** Single commit message for the delta. */
17
- commitMessage: string;
16
+ /**
17
+ * Commit message for the delta. Pass a function when the message depends on
18
+ * counts that are only known after `applyDelta` runs (qfg-7eig). The function
19
+ * is invoked after applyDelta but before commit.
20
+ */
21
+ commitMessage: string | (() => string);
18
22
  /** Where to clone into, or an existing clone to reuse. */
19
23
  localDir: string;
20
24
  /** Gitea repo URL (may include token). */
@@ -62,7 +62,13 @@ const ensureCloneOrReuse = async (remoteUrl, localDir, branch) => {
62
62
  await runGit(localDir, ['merge', '--ff-only', `origin/${branch}`]);
63
63
  return 'reused';
64
64
  }
65
- throw new Error(`Local dir ${localDir} is a git repo but its origin (${origin ? redactToken(origin) : 'unset'}) does not match the target remote. Refuse to clobber — pass an empty/non-existent path or a matching clone.`);
65
+ throw new Error(`Local dir ${localDir} is a git repo but its origin (${origin ? redactToken(origin) : 'unset'}) does not match the target remote.\n` +
66
+ `Refuse to clobber.\n\n` +
67
+ `Most likely cause: you ran \`qfg migrate --dir ${localDir}\` first (which \`git init\`s a fresh repo with no remote) and are now trying to \`--push\` from the same dir.\n\n` +
68
+ `Pick one of:\n` +
69
+ ` - Re-run with \`--push\` against a NEW empty/non-existent path (the canonical flow).\n` +
70
+ ` - Run \`qfg pull --workspace <org/slug> --dir ${localDir}-clone\` to seed a fresh clone, then \`qfg migrate --dir ${localDir}-clone --workspace <org/slug> --push\`.\n` +
71
+ `Docs: https://docs.quonfig.com/docs/migrating/from-launch (Flow B).`);
66
72
  }
67
73
  if (fs.existsSync(localDir)) {
68
74
  const entries = fs.readdirSync(localDir);
@@ -90,8 +96,9 @@ export const cloneAndStackPush = async (opts) => {
90
96
  if (!(await hasStagedChanges(opts.localDir))) {
91
97
  return { action, committed: false, commitSha: null };
92
98
  }
99
+ const commitMessage = typeof opts.commitMessage === 'function' ? opts.commitMessage() : opts.commitMessage;
93
100
  await spawnGit(['-C', opts.localDir, 'commit', '-F', '-'], {
94
- stdin: opts.commitMessage,
101
+ stdin: commitMessage,
95
102
  env: {
96
103
  GIT_AUTHOR_NAME: author.name,
97
104
  GIT_AUTHOR_EMAIL: author.email,
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "0.0.34";
1
+ declare const _default: "0.0.35";
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.34';
2
+ export default '0.0.35';
@@ -2916,5 +2916,5 @@
2916
2916
  ]
2917
2917
  }
2918
2918
  },
2919
- "version": "0.0.34"
2919
+ "version": "0.0.35"
2920
2920
  }
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.34",
4
+ "version": "0.0.35",
5
5
  "bugs": {
6
6
  "url": "https://github.com/quonfig/cli/issues"
7
7
  },