@quonfig/cli 0.0.15 → 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.
Files changed (46) hide show
  1. package/README.md +85 -34
  2. package/dist/commands/init.d.ts +1 -0
  3. package/dist/commands/init.js +18 -0
  4. package/dist/commands/migrate.js +48 -2
  5. package/dist/commands/pull.d.ts +14 -1
  6. package/dist/commands/pull.js +64 -4
  7. package/dist/commands/push.d.ts +53 -0
  8. package/dist/commands/push.js +370 -0
  9. package/dist/commands/workspace/bootstrap.d.ts +7 -0
  10. package/dist/commands/workspace/bootstrap.js +58 -2
  11. package/dist/migrate/local-write.d.ts +8 -2
  12. package/dist/migrate/local-write.js +55 -7
  13. package/dist/migrate/migration-report.d.ts +18 -0
  14. package/dist/migrate/migration-report.js +64 -1
  15. package/dist/migrate/push-to-cloud.d.ts +14 -3
  16. package/dist/migrate/push-to-cloud.js +77 -6
  17. package/dist/migrate/source.d.ts +50 -1
  18. package/dist/migrate/sources/launch/translate.d.ts +27 -1
  19. package/dist/migrate/sources/launch/translate.js +123 -6
  20. package/dist/migrate/sources/launch/types.d.ts +3 -0
  21. package/dist/migrate/sources/launch/zod-to-json-schema.d.ts +9 -0
  22. package/dist/migrate/sources/launch/zod-to-json-schema.js +521 -0
  23. package/dist/migrate/sources/launch.js +52 -7
  24. package/dist/push/bare-path-diff.d.ts +40 -0
  25. package/dist/push/bare-path-diff.js +126 -0
  26. package/dist/push/confirm.d.ts +10 -0
  27. package/dist/push/confirm.js +108 -0
  28. package/dist/push/diff-summary.d.ts +56 -0
  29. package/dist/push/diff-summary.js +115 -0
  30. package/dist/push/identity-check.d.ts +53 -0
  31. package/dist/push/identity-check.js +135 -0
  32. package/dist/push/run-push.d.ts +107 -0
  33. package/dist/push/run-push.js +318 -0
  34. package/dist/util/get-client.js +5 -4
  35. package/dist/util/get-valid-token.d.ts +3 -1
  36. package/dist/util/get-valid-token.js +28 -3
  37. package/dist/util/gitea-api.d.ts +15 -1
  38. package/dist/util/gitea-api.js +1 -0
  39. package/dist/util/gitea-token-storage.d.ts +7 -0
  40. package/dist/util/quonfig-json.d.ts +44 -0
  41. package/dist/util/quonfig-json.js +65 -0
  42. package/dist/verify/validate.js +6 -2
  43. package/oclif.manifest.json +104 -1
  44. package/package.json +3 -3
  45. /package/dist/{migrate/push-strategy.d.ts → util/clone-and-stack-push.d.ts} +0 -0
  46. /package/dist/{migrate/push-strategy.js → util/clone-and-stack-push.js} +0 -0
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.15 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
@@ -46,6 +46,7 @@ USAGE
46
46
  * [`qfg migrate status`](#qfg-migrate-status)
47
47
  * [`qfg override [NAME]`](#qfg-override-name)
48
48
  * [`qfg pull`](#qfg-pull)
49
+ * [`qfg push`](#qfg-push)
49
50
  * [`qfg schema NAME`](#qfg-schema-name)
50
51
  * [`qfg sdk-key`](#qfg-sdk-key)
51
52
  * [`qfg sdk-key create`](#qfg-sdk-key-create)
@@ -88,7 +89,7 @@ EXAMPLES
88
89
  $ qfg config-schema --json-schema # full JSON Schema document (copy into your editor)
89
90
  ```
90
91
 
91
- _See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
92
93
 
93
94
  ## `qfg create NAME`
94
95
 
@@ -160,7 +161,7 @@ EXAMPLES
160
161
  $ qfg set-rollout my.new.flag --environment production --true-percent 20
161
162
  ```
162
163
 
163
- _See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/create.ts)_
164
+ _See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/create.ts)_
164
165
 
165
166
  ## `qfg flag info [NAME]`
166
167
 
@@ -360,7 +361,7 @@ EXAMPLES
360
361
  $ qfg generate --targets node-ts -o ./dist # combine with targets
361
362
  ```
362
363
 
363
- _See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/generate.ts)_
364
+ _See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/generate.ts)_
364
365
 
365
366
  ## `qfg generate-new-hex-key`
366
367
 
@@ -383,7 +384,7 @@ EXAMPLES
383
384
  $ qfg generate-new-hex-key
384
385
  ```
385
386
 
386
- _See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
387
388
 
388
389
  ## `qfg get [NAME]`
389
390
 
@@ -415,7 +416,7 @@ EXAMPLES
415
416
  $ qfg get my.config.name --environment=production
416
417
  ```
417
418
 
418
- _See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/get.ts)_
419
+ _See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/get.ts)_
419
420
 
420
421
  ## `qfg info [NAME]`
421
422
 
@@ -463,7 +464,7 @@ EXAMPLES
463
464
  $ qfg info my.config.name --exclude-evaluations # skip 24h stats
464
465
  ```
465
466
 
466
- _See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/info.ts)_
467
+ _See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/info.ts)_
467
468
 
468
469
  ## `qfg init [DIRECTORY]`
469
470
 
@@ -472,13 +473,15 @@ Initialize or update a Quonfig workspace
472
473
  ```
473
474
  USAGE
474
475
  $ qfg init [DIRECTORY] [--json] [--interactive] [--no-color] [--verbose] [--dry-run] [--samples]
476
+ [--workspace <value>]
475
477
 
476
478
  ARGUMENTS
477
479
  DIRECTORY [default: .] Target directory (default: current directory)
478
480
 
479
481
  FLAGS
480
- --dry-run Show what would be done without writing anything
481
- --[no-]samples Include sample configs (default: yes on first init, no on update)
482
+ --dry-run Show what would be done without writing anything
483
+ --[no-]samples Include sample configs (default: yes on first init, no on update)
484
+ --workspace=<value> Workspace slug to pin in quonfig.json (Guard 1). If omitted, no pin is written.
482
485
 
483
486
  GLOBAL FLAGS
484
487
  --[no-]interactive Force interactive mode
@@ -501,7 +504,7 @@ EXAMPLES
501
504
  $ qfg init --dry-run
502
505
  ```
503
506
 
504
- _See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/init.ts)_
507
+ _See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/init.ts)_
505
508
 
506
509
  ## `qfg interactive`
507
510
 
@@ -531,7 +534,7 @@ EXAMPLES
531
534
  $ qfg
532
535
  ```
533
536
 
534
- _See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/interactive.ts)_
537
+ _See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/interactive.ts)_
535
538
 
536
539
  ## `qfg list`
537
540
 
@@ -571,7 +574,7 @@ EXAMPLES
571
574
  $ qfg list --feature-flags
572
575
  ```
573
576
 
574
- _See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/list.ts)_
577
+ _See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/list.ts)_
575
578
 
576
579
  ## `qfg login`
577
580
 
@@ -596,7 +599,7 @@ EXAMPLES
596
599
  $ qfg login --profile myprofile
597
600
  ```
598
601
 
599
- _See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/login.ts)_
602
+ _See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/login.ts)_
600
603
 
601
604
  ## `qfg logout`
602
605
 
@@ -619,7 +622,7 @@ EXAMPLES
619
622
  $ qfg logout
620
623
  ```
621
624
 
622
- _See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/logout.ts)_
625
+ _See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/logout.ts)_
623
626
 
624
627
  ## `qfg mcp`
625
628
 
@@ -652,7 +655,7 @@ EXAMPLES
652
655
  $ qfg mcp --url http://local-app.quonfig-staging.com:3003/api/v1/mcp
653
656
  ```
654
657
 
655
- _See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/mcp.ts)_
658
+ _See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/mcp.ts)_
656
659
 
657
660
  ## `qfg migrate`
658
661
 
@@ -706,7 +709,7 @@ EXAMPLES
706
709
  $ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-config --staging
707
710
  ```
708
711
 
709
- _See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/migrate.ts)_
712
+ _See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate.ts)_
710
713
 
711
714
  ## `qfg migrate doctor`
712
715
 
@@ -744,7 +747,7 @@ EXAMPLES
744
747
  $ qfg migrate doctor --json
745
748
  ```
746
749
 
747
- _See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
748
751
 
749
752
  ## `qfg migrate my-code`
750
753
 
@@ -776,7 +779,7 @@ EXAMPLES
776
779
  $ qfg migrate my-code --dry-run
777
780
  ```
778
781
 
779
- _See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
780
783
 
781
784
  ## `qfg migrate status`
782
785
 
@@ -807,7 +810,7 @@ EXAMPLES
807
810
  $ qfg migrate status --json
808
811
  ```
809
812
 
810
- _See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
811
814
 
812
815
  ## `qfg override [NAME]`
813
816
 
@@ -851,7 +854,7 @@ EXAMPLES
851
854
  $ qfg override my.double.config --value=3.14159
852
855
  ```
853
856
 
854
- _See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/override.ts)_
857
+ _See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/override.ts)_
855
858
 
856
859
  ## `qfg pull`
857
860
 
@@ -895,7 +898,55 @@ EXAMPLES
895
898
  $ qfg pull # uses QUONFIG_DIR env var
896
899
  ```
897
900
 
898
- _See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.15/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
+
903
+ ## `qfg push`
904
+
905
+ Push local config changes up to your workspace on Quonfig cloud.
906
+
907
+ ```
908
+ USAGE
909
+ $ qfg push [--json] [--interactive] [--no-color] [--verbose] [--dir <value>] [--workspace <value>]
910
+ [--yes] [--skip-validate] [--no-pin-write] [-m <value>]
911
+
912
+ FLAGS
913
+ -m, --message=<value> Commit message for bare-path pushes (ignored on clone-path; push what is already committed)
914
+ --dir=<value> Local directory to push (defaults to QUONFIG_DIR env var)
915
+ --no-pin-write Do not offer to write workspace slug into quonfig.json on success
916
+ --skip-validate Skip `qfg validate` preflight
917
+ --workspace=<value> Workspace slug or UUID (defaults to active profile)
918
+ --yes Skip the standard Y/N confirm. Never skips typed-slug prompts.
919
+
920
+ GLOBAL FLAGS
921
+ --[no-]interactive Force interactive mode
922
+ --json Format output as json.
923
+ --no-color Do not colorize output
924
+ --verbose Verbose output
925
+
926
+ DESCRIPTION
927
+ Push local config changes up to your workspace on Quonfig cloud.
928
+
929
+ Enforces three guards before touching the remote:
930
+ 1. The dir's quonfig.json "workspace" pin must match the backend (if set).
931
+ 2. The dir's git origin must match the backend repo URL (if set).
932
+ 3. A diff summary is shown; destructive changes (10+ deletes, >=25% of
933
+ files, or an unpinned dir) require typing the workspace slug to confirm.
934
+
935
+ qfg push # uses QUONFIG_DIR + profile workspace
936
+ qfg push --dir ./our-config
937
+ qfg push --dir ./our-config --workspace acme-prod
938
+ qfg push --dir ./our-config --yes # skip normal Y/N (never skips typed-slug)
939
+ qfg push --dir ./our-config --skip-validate
940
+
941
+ EXAMPLES
942
+ $ qfg push --dir ./our-config
943
+
944
+ $ qfg push --workspace acme-prod --dir ./our-config
945
+
946
+ $ qfg push --dir ./our-config --yes
947
+ ```
948
+
949
+ _See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/push.ts)_
899
950
 
900
951
  ## `qfg schema NAME`
901
952
 
@@ -933,7 +984,7 @@ EXAMPLES
933
984
  $ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
934
985
  ```
935
986
 
936
- _See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/schema.ts)_
987
+ _See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/schema.ts)_
937
988
 
938
989
  ## `qfg sdk-key`
939
990
 
@@ -960,7 +1011,7 @@ EXAMPLES
960
1011
  $ qfg sdk-key revoke <key-id>
961
1012
  ```
962
1013
 
963
- _See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
964
1015
 
965
1016
  ## `qfg sdk-key create`
966
1017
 
@@ -992,7 +1043,7 @@ EXAMPLES
992
1043
  $ qfg sdk-key create --environment staging --type browser
993
1044
  ```
994
1045
 
995
- _See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
996
1047
 
997
1048
  ## `qfg sdk-key list`
998
1049
 
@@ -1021,7 +1072,7 @@ EXAMPLES
1021
1072
  $ qfg sdk-key list --environment production
1022
1073
  ```
1023
1074
 
1024
- _See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
1025
1076
 
1026
1077
  ## `qfg sdk-key revoke KEYID`
1027
1078
 
@@ -1048,7 +1099,7 @@ EXAMPLES
1048
1099
  $ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
1049
1100
  ```
1050
1101
 
1051
- _See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
1052
1103
 
1053
1104
  ## `qfg set-default [NAME]`
1054
1105
 
@@ -1119,7 +1170,7 @@ EXAMPLES
1119
1170
  $ qfg pull && qfg config-schema
1120
1171
  ```
1121
1172
 
1122
- _See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
1123
1174
 
1124
1175
  ## `qfg set-rollout [NAME]`
1125
1176
 
@@ -1196,7 +1247,7 @@ EXAMPLES
1196
1247
  $ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
1197
1248
  ```
1198
1249
 
1199
- _See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
1200
1251
 
1201
1252
  ## `qfg sync`
1202
1253
 
@@ -1228,7 +1279,7 @@ EXAMPLES
1228
1279
  $ qfg sync --watch --dir ./our-config --interval 30
1229
1280
  ```
1230
1281
 
1231
- _See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/sync.ts)_
1282
+ _See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sync.ts)_
1232
1283
 
1233
1284
  ## `qfg toggle [NAME]`
1234
1285
 
@@ -1330,7 +1381,7 @@ EXAMPLES
1330
1381
  $ qfg verify --json
1331
1382
  ```
1332
1383
 
1333
- _See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/verify.ts)_
1384
+ _See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/verify.ts)_
1334
1385
 
1335
1386
  ## `qfg whoami`
1336
1387
 
@@ -1353,7 +1404,7 @@ EXAMPLES
1353
1404
  $ qfg whoami
1354
1405
  ```
1355
1406
 
1356
- _See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/whoami.ts)_
1407
+ _See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/whoami.ts)_
1357
1408
 
1358
1409
  ## `qfg workspace`
1359
1410
 
@@ -1376,7 +1427,7 @@ EXAMPLES
1376
1427
  $ qfg workspace
1377
1428
  ```
1378
1429
 
1379
- _See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.15/src/commands/workspace.ts)_
1430
+ _See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/workspace.ts)_
1380
1431
 
1381
1432
  ## `qfg workspace bootstrap`
1382
1433
 
@@ -1409,7 +1460,7 @@ EXAMPLES
1409
1460
  $ qfg workspace bootstrap --dir ./our-config --skip-validate
1410
1461
  ```
1411
1462
 
1412
- _See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
1413
1464
 
1414
1465
  ## `qfg workspace switch [SLUG]`
1415
1466
 
@@ -1437,7 +1488,7 @@ EXAMPLES
1437
1488
  $ qfg workspace switch my-workspace-slug
1438
1489
  ```
1439
1490
 
1440
- _See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.15/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)_
1441
1492
  <!-- commandsstop -->
1442
1493
 
1443
1494
  ## Local Development
@@ -8,6 +8,7 @@ export default class Init extends BaseCommand {
8
8
  static flags: {
9
9
  'dry-run': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
10
  samples: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ workspace: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
12
  };
12
13
  run(): Promise<object>;
13
14
  }
@@ -1,6 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../index.js';
3
3
  import { executeInit, planInit } from '../init/init-workspace.js';
4
+ import { writeWorkspaceSlug } from '../util/quonfig-json.js';
4
5
  export default class Init extends BaseCommand {
5
6
  static args = {
6
7
  directory: Args.string({
@@ -25,6 +26,10 @@ export default class Init extends BaseCommand {
25
26
  allowNo: true,
26
27
  description: 'Include sample configs (default: yes on first init, no on update)',
27
28
  }),
29
+ workspace: Flags.string({
30
+ description: 'Workspace slug to pin in quonfig.json (Guard 1). If omitted, no pin is written.',
31
+ required: false,
32
+ }),
28
33
  };
29
34
  async run() {
30
35
  const { args, flags } = await this.parse(Init);
@@ -45,6 +50,9 @@ export default class Init extends BaseCommand {
45
50
  this.log(` + ${action.description}`);
46
51
  }
47
52
  }
53
+ if (flags.workspace) {
54
+ this.log(` + Pin quonfig.json to workspace "${flags.workspace}"`);
55
+ }
48
56
  if (skipActions.length > 0) {
49
57
  this.log('\nSkipped:');
50
58
  for (const action of skipActions) {
@@ -66,10 +74,20 @@ export default class Init extends BaseCommand {
66
74
  }
67
75
  // Execute for real
68
76
  executeInit(plan, dir);
77
+ // If the user passed --workspace, pin the slug in quonfig.json
78
+ // (Guard 1 in project/plans/cli-git-sync.md). We deliberately keep this
79
+ // non-interactive: flag → write, no flag → no write. Users who need the
80
+ // pin later can run `qfg pull` (backfills) or edit the file directly.
81
+ if (flags.workspace) {
82
+ await writeWorkspaceSlug(dir, flags.workspace);
83
+ }
69
84
  this.log(`${mode} workspace: ${dir === '.' ? process.cwd() : dir}\n`);
70
85
  for (const action of writeActions) {
71
86
  this.log(` + ${action.description}`);
72
87
  }
88
+ if (flags.workspace) {
89
+ this.log(` + Pin quonfig.json to workspace "${flags.workspace}"`);
90
+ }
73
91
  for (const action of skipActions) {
74
92
  this.log(` - ${action.description}`);
75
93
  }
@@ -4,10 +4,10 @@ import path from 'node:path';
4
4
  import { BaseCommand } from '../index.js';
5
5
  import { CrossSourceError, assertSourceMatches, readImportState } from '../migrate/import-state.js';
6
6
  import { applyLocalMigration } from '../migrate/local-write.js';
7
- import { PushConflictError } from '../migrate/push-strategy.js';
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);
@@ -8,8 +8,21 @@ export default class Pull extends BaseCommand {
8
8
  workspace: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
9
  };
10
10
  run(): Promise<JsonObj | void>;
11
+ /**
12
+ * Local-only backfill of the `workspace` pin in `quonfig.json`.
13
+ *
14
+ * - Missing pin → write it from the token response's `workspaceSlug`.
15
+ * - Pin already set and matches → no-op.
16
+ * - Pin already set and disagrees → log a warning showing both values
17
+ * and LEAVE IT ALONE. Overwriting someone's explicit pin is a Guard 2
18
+ * concern, not a pull concern — the next `qfg push` is where the
19
+ * identity-check dispatches a hard abort.
20
+ *
21
+ * This never throws — a broken `quonfig.json` shouldn't fail `qfg pull`.
22
+ */
23
+ private backfillWorkspacePin;
11
24
  private looksLike401;
12
25
  private maybeWriteQuonfigDir;
13
26
  private refreshAndGetUrl;
14
- private resolveRepoUrl;
27
+ private resolveTokenEntry;
15
28
  }
@@ -5,6 +5,7 @@ import { BaseCommand } from '../index.js';
5
5
  import { getActiveProfile, loadAuthConfig } from '../util/token-storage.js';
6
6
  import { loadGiteaToken, isGiteaTokenExpired, saveGiteaToken } from '../util/gitea-token-storage.js';
7
7
  import { mintAndStoreGiteaReadToken, mintGiteaToken } from '../util/gitea-api.js';
8
+ import { readWorkspaceSlug, writeWorkspaceSlug } from '../util/quonfig-json.js';
8
9
  import { isGitRepo, getRemoteUrl, gitFetch, gitSetRemote, canFastForward, hasDivergedFromRemote, gitMergeFfOnly, gitClone, displayUrl, } from '../util/git-ops.js';
9
10
  export default class Pull extends BaseCommand {
10
11
  static description = `Clone or update a local copy of your workspace config files.
@@ -56,7 +57,8 @@ CLI shortcuts (no JSON editing needed for simple cases):
56
57
  }
57
58
  this.verboseLog('Pull', { workspaceId, dir: resolvedDir });
58
59
  // Get or mint Gitea read token
59
- const repoUrl = await this.resolveRepoUrl(workspaceId);
60
+ const tokenEntry = await this.resolveTokenEntry(workspaceId);
61
+ const repoUrl = tokenEntry.repoUrl;
60
62
  // Perform git operation
61
63
  const isRepo = await isGitRepo(resolvedDir);
62
64
  if (isRepo) {
@@ -132,8 +134,61 @@ CLI shortcuts (no JSON editing needed for simple cases):
132
134
  }
133
135
  // Write QUONFIG_DIR to ~/.quonfig/config if not set
134
136
  await this.maybeWriteQuonfigDir(resolvedDir);
137
+ // Backfill the `workspace` pin in `quonfig.json` if missing
138
+ // (Guard 1 in project/plans/cli-git-sync.md). Local-only write — we
139
+ // intentionally do NOT commit or push here; the user's next `qfg push`
140
+ // will sweep it up naturally.
141
+ await this.backfillWorkspacePin(resolvedDir, tokenEntry, workspaceId);
135
142
  return { dir: resolvedDir, workspaceId };
136
143
  }
144
+ /**
145
+ * Local-only backfill of the `workspace` pin in `quonfig.json`.
146
+ *
147
+ * - Missing pin → write it from the token response's `workspaceSlug`.
148
+ * - Pin already set and matches → no-op.
149
+ * - Pin already set and disagrees → log a warning showing both values
150
+ * and LEAVE IT ALONE. Overwriting someone's explicit pin is a Guard 2
151
+ * concern, not a pull concern — the next `qfg push` is where the
152
+ * identity-check dispatches a hard abort.
153
+ *
154
+ * This never throws — a broken `quonfig.json` shouldn't fail `qfg pull`.
155
+ */
156
+ async backfillWorkspacePin(dir, tokenEntry, workspaceId) {
157
+ let backendSlug = tokenEntry.workspaceSlug;
158
+ if (!backendSlug) {
159
+ // Older cached entries lack the slug. Mint a fresh one solely to learn the
160
+ // slug. Cheap (one API call), and the refresh updates the cache for next time.
161
+ try {
162
+ const fresh = await mintAndStoreGiteaReadToken(workspaceId);
163
+ backendSlug = fresh.workspaceSlug;
164
+ }
165
+ catch (error) {
166
+ this.verboseLog('Pull', `Could not mint token to learn workspace slug: ${String(error)}`);
167
+ return;
168
+ }
169
+ }
170
+ if (!backendSlug) {
171
+ this.verboseLog('Pull', 'Backend did not return a workspaceSlug; skipping pin backfill.');
172
+ return;
173
+ }
174
+ try {
175
+ const existingPin = await readWorkspaceSlug(dir);
176
+ if (!existingPin) {
177
+ await writeWorkspaceSlug(dir, backendSlug);
178
+ this.verboseLog('Pull', `Backfilled workspace pin "${backendSlug}" into quonfig.json.`);
179
+ return;
180
+ }
181
+ if (existingPin !== backendSlug) {
182
+ this.log('');
183
+ this.log(`Warning: quonfig.json pins workspace "${existingPin}", but the backend says this workspace is "${backendSlug}".`);
184
+ this.log('Leaving the existing pin in place. Run `qfg push` to see the full identity check.');
185
+ }
186
+ }
187
+ catch (error) {
188
+ // Non-fatal — pull itself already succeeded.
189
+ this.verboseLog('Pull', `Could not backfill workspace pin: ${String(error)}`);
190
+ }
191
+ }
137
192
  looksLike401(err) {
138
193
  const msg = err instanceof Error ? err.message : String(err);
139
194
  return msg.includes('401') || msg.toLowerCase().includes('authentication failed');
@@ -158,17 +213,22 @@ CLI shortcuts (no JSON editing needed for simple cases):
158
213
  }
159
214
  async refreshAndGetUrl(workspaceId) {
160
215
  const data = await mintGiteaToken(workspaceId, 'read', 'pull');
161
- const entry = { token: data.token, repoUrl: data.repoUrl, expiresAt: data.expiresAt };
216
+ const entry = {
217
+ token: data.token,
218
+ repoUrl: data.repoUrl,
219
+ expiresAt: data.expiresAt,
220
+ workspaceSlug: data.workspaceSlug,
221
+ };
162
222
  await saveGiteaToken(workspaceId, entry);
163
223
  return entry.repoUrl;
164
224
  }
165
- async resolveRepoUrl(workspaceId) {
225
+ async resolveTokenEntry(workspaceId) {
166
226
  let entry = await loadGiteaToken(workspaceId);
167
227
  if (!entry || isGiteaTokenExpired(entry)) {
168
228
  this.verboseLog('Pull', 'Minting new Gitea read token...');
169
229
  entry = await mintAndStoreGiteaReadToken(workspaceId);
170
230
  }
171
- return entry.repoUrl;
231
+ return entry;
172
232
  }
173
233
  }
174
234
  /**