@quonfig/cli 0.0.2 → 0.0.4
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 +119 -22
- package/bin/dev.js +5 -9
- package/bin/dev.mjs +6 -0
- package/dist/codegen/zod-introspection.js +13 -4
- package/dist/commands/login.js +27 -6
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/pull.d.ts +15 -0
- package/dist/commands/pull.js +176 -0
- package/dist/commands/serve.js +3 -1
- package/dist/commands/sync.d.ts +18 -0
- package/dist/commands/sync.js +142 -0
- package/dist/commands/whoami.js +4 -1
- package/dist/commands/workspace/bootstrap.d.ts +13 -0
- package/dist/commands/workspace/bootstrap.js +155 -0
- package/dist/init/init-workspace.js +4 -4
- package/dist/init/templates.js +1 -1
- package/dist/util/coerce.js +2 -1
- package/dist/util/get-client.js +6 -44
- package/dist/util/get-valid-token.d.ts +5 -0
- package/dist/util/get-valid-token.js +44 -0
- package/dist/util/git-ops.d.ts +32 -0
- package/dist/util/git-ops.js +164 -0
- package/dist/util/gitea-api.d.ts +11 -0
- package/dist/util/gitea-api.js +45 -0
- package/dist/util/gitea-token-storage.d.ts +8 -0
- package/dist/util/gitea-token-storage.js +47 -0
- package/dist/util/token-storage.d.ts +1 -0
- package/dist/util/token-storage.js +24 -10
- package/dist/verify/validate.d.ts +1 -0
- package/dist/verify/validate.js +136 -6
- package/oclif.manifest.json +223 -2
- package/package.json +2 -2
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.
|
|
17
|
+
@quonfig/cli/0.0.4 linux-x64 node-v24.4.1
|
|
18
18
|
$ qfg --help [COMMAND]
|
|
19
19
|
USAGE
|
|
20
20
|
$ qfg COMMAND
|
|
@@ -39,12 +39,15 @@ USAGE
|
|
|
39
39
|
* [`qfg mcp`](#qfg-mcp)
|
|
40
40
|
* [`qfg override [NAME]`](#qfg-override-name)
|
|
41
41
|
* [`qfg profile`](#qfg-profile)
|
|
42
|
+
* [`qfg pull`](#qfg-pull)
|
|
42
43
|
* [`qfg schema NAME`](#qfg-schema-name)
|
|
43
44
|
* [`qfg serve DATA-FILE`](#qfg-serve-data-file)
|
|
44
45
|
* [`qfg set-default [NAME]`](#qfg-set-default-name)
|
|
46
|
+
* [`qfg sync`](#qfg-sync)
|
|
45
47
|
* [`qfg verify [PATH]`](#qfg-verify-path)
|
|
46
48
|
* [`qfg whoami`](#qfg-whoami)
|
|
47
49
|
* [`qfg workspace`](#qfg-workspace)
|
|
50
|
+
* [`qfg workspace bootstrap`](#qfg-workspace-bootstrap)
|
|
48
51
|
|
|
49
52
|
## `qfg create NAME`
|
|
50
53
|
|
|
@@ -93,7 +96,7 @@ EXAMPLES
|
|
|
93
96
|
$ qfg create my.new.string --type json --value="{\"key\": \"value\"}"
|
|
94
97
|
```
|
|
95
98
|
|
|
96
|
-
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
99
|
+
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/create.ts)_
|
|
97
100
|
|
|
98
101
|
## `qfg download`
|
|
99
102
|
|
|
@@ -126,7 +129,7 @@ EXAMPLES
|
|
|
126
129
|
$ qfg download --environment=test --sdk-key=YOUR_SDK_KEY
|
|
127
130
|
```
|
|
128
131
|
|
|
129
|
-
_See code: [src/commands/download.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
132
|
+
_See code: [src/commands/download.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/download.ts)_
|
|
130
133
|
|
|
131
134
|
## `qfg generate`
|
|
132
135
|
|
|
@@ -196,7 +199,7 @@ EXAMPLES
|
|
|
196
199
|
$ qfg generate --targets node-ts -o ./dist # combine with targets
|
|
197
200
|
```
|
|
198
201
|
|
|
199
|
-
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
202
|
+
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/generate.ts)_
|
|
200
203
|
|
|
201
204
|
## `qfg generate-new-hex-key`
|
|
202
205
|
|
|
@@ -219,7 +222,7 @@ EXAMPLES
|
|
|
219
222
|
$ qfg generate-new-hex-key
|
|
220
223
|
```
|
|
221
224
|
|
|
222
|
-
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
225
|
+
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/generate-new-hex-key.ts)_
|
|
223
226
|
|
|
224
227
|
## `qfg get [NAME]`
|
|
225
228
|
|
|
@@ -251,7 +254,7 @@ EXAMPLES
|
|
|
251
254
|
$ qfg get my.config.name --environment=production
|
|
252
255
|
```
|
|
253
256
|
|
|
254
|
-
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
257
|
+
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/get.ts)_
|
|
255
258
|
|
|
256
259
|
## `qfg info [NAME]`
|
|
257
260
|
|
|
@@ -281,7 +284,7 @@ EXAMPLES
|
|
|
281
284
|
$ qfg info my.config.name
|
|
282
285
|
```
|
|
283
286
|
|
|
284
|
-
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
287
|
+
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/info.ts)_
|
|
285
288
|
|
|
286
289
|
## `qfg init [DIRECTORY]`
|
|
287
290
|
|
|
@@ -319,7 +322,7 @@ EXAMPLES
|
|
|
319
322
|
$ qfg init --dry-run
|
|
320
323
|
```
|
|
321
324
|
|
|
322
|
-
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
325
|
+
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/init.ts)_
|
|
323
326
|
|
|
324
327
|
## `qfg interactive`
|
|
325
328
|
|
|
@@ -337,7 +340,7 @@ EXAMPLES
|
|
|
337
340
|
$ qfg
|
|
338
341
|
```
|
|
339
342
|
|
|
340
|
-
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
343
|
+
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/interactive.ts)_
|
|
341
344
|
|
|
342
345
|
## `qfg list`
|
|
343
346
|
|
|
@@ -374,7 +377,7 @@ EXAMPLES
|
|
|
374
377
|
$ qfg list --feature-flags
|
|
375
378
|
```
|
|
376
379
|
|
|
377
|
-
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
380
|
+
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/list.ts)_
|
|
378
381
|
|
|
379
382
|
## `qfg login`
|
|
380
383
|
|
|
@@ -402,7 +405,7 @@ EXAMPLES
|
|
|
402
405
|
$ qfg login --profile myprofile
|
|
403
406
|
```
|
|
404
407
|
|
|
405
|
-
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
408
|
+
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/login.ts)_
|
|
406
409
|
|
|
407
410
|
## `qfg logout`
|
|
408
411
|
|
|
@@ -425,7 +428,7 @@ EXAMPLES
|
|
|
425
428
|
$ qfg logout
|
|
426
429
|
```
|
|
427
430
|
|
|
428
|
-
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
431
|
+
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/logout.ts)_
|
|
429
432
|
|
|
430
433
|
## `qfg mcp`
|
|
431
434
|
|
|
@@ -455,10 +458,10 @@ EXAMPLES
|
|
|
455
458
|
|
|
456
459
|
$ qfg mcp --editor cursor
|
|
457
460
|
|
|
458
|
-
$ qfg mcp --url http://local-app.
|
|
461
|
+
$ qfg mcp --url http://local-app.quonfig-staging.com:3003/api/v1/mcp
|
|
459
462
|
```
|
|
460
463
|
|
|
461
|
-
_See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
464
|
+
_See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/mcp.ts)_
|
|
462
465
|
|
|
463
466
|
## `qfg override [NAME]`
|
|
464
467
|
|
|
@@ -497,7 +500,7 @@ EXAMPLES
|
|
|
497
500
|
$ qfg override my.double.config --value=3.14159
|
|
498
501
|
```
|
|
499
502
|
|
|
500
|
-
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
503
|
+
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/override.ts)_
|
|
501
504
|
|
|
502
505
|
## `qfg profile`
|
|
503
506
|
|
|
@@ -520,7 +523,36 @@ EXAMPLES
|
|
|
520
523
|
$ qfg profile
|
|
521
524
|
```
|
|
522
525
|
|
|
523
|
-
_See code: [src/commands/profile.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
526
|
+
_See code: [src/commands/profile.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/profile.ts)_
|
|
527
|
+
|
|
528
|
+
## `qfg pull`
|
|
529
|
+
|
|
530
|
+
Clone or update a local directory from the workspace Gitea repo
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
USAGE
|
|
534
|
+
$ qfg pull [--json] [--interactive] [--no-color] [--verbose] [--dir <value>] [--workspace <value>]
|
|
535
|
+
|
|
536
|
+
FLAGS
|
|
537
|
+
--dir=<value> Local directory to clone/update (defaults to QUONFIG_DIR env var)
|
|
538
|
+
--workspace=<value> Workspace ID (defaults to active profile)
|
|
539
|
+
|
|
540
|
+
GLOBAL FLAGS
|
|
541
|
+
--[no-]interactive Force interactive mode
|
|
542
|
+
--json Format output as json.
|
|
543
|
+
--no-color Do not colorize output
|
|
544
|
+
--verbose Verbose output
|
|
545
|
+
|
|
546
|
+
DESCRIPTION
|
|
547
|
+
Clone or update a local directory from the workspace Gitea repo
|
|
548
|
+
|
|
549
|
+
EXAMPLES
|
|
550
|
+
$ qfg pull --dir ./our-config
|
|
551
|
+
|
|
552
|
+
$ qfg pull # uses QUONFIG_DIR env var
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/pull.ts)_
|
|
524
556
|
|
|
525
557
|
## `qfg schema NAME`
|
|
526
558
|
|
|
@@ -558,7 +590,7 @@ EXAMPLES
|
|
|
558
590
|
$ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
|
|
559
591
|
```
|
|
560
592
|
|
|
561
|
-
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
593
|
+
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/schema.ts)_
|
|
562
594
|
|
|
563
595
|
## `qfg serve DATA-FILE`
|
|
564
596
|
|
|
@@ -594,7 +626,7 @@ EXAMPLES
|
|
|
594
626
|
$ qfg serve ./quonfig.test.588.config.json --port=3099
|
|
595
627
|
```
|
|
596
628
|
|
|
597
|
-
_See code: [src/commands/serve.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
629
|
+
_See code: [src/commands/serve.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/serve.ts)_
|
|
598
630
|
|
|
599
631
|
## `qfg set-default [NAME]`
|
|
600
632
|
|
|
@@ -638,7 +670,39 @@ EXAMPLES
|
|
|
638
670
|
$ qfg set-default my.config.name --env-var=MY_ENV_VAR_NAME --environment=production
|
|
639
671
|
```
|
|
640
672
|
|
|
641
|
-
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
673
|
+
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/set-default.ts)_
|
|
674
|
+
|
|
675
|
+
## `qfg sync`
|
|
676
|
+
|
|
677
|
+
Continuously poll for workspace config updates and apply them locally
|
|
678
|
+
|
|
679
|
+
```
|
|
680
|
+
USAGE
|
|
681
|
+
$ qfg sync --watch [--json] [--interactive] [--no-color] [--verbose] [--dir <value>] [--interval <value>]
|
|
682
|
+
|
|
683
|
+
FLAGS
|
|
684
|
+
--dir=<value> Local directory to sync (defaults to QUONFIG_DIR env var)
|
|
685
|
+
--interval=<value> [default: 60] Poll interval in seconds
|
|
686
|
+
--watch (required) Run as a continuous polling daemon (required)
|
|
687
|
+
|
|
688
|
+
GLOBAL FLAGS
|
|
689
|
+
--[no-]interactive Force interactive mode
|
|
690
|
+
--json Format output as json.
|
|
691
|
+
--no-color Do not colorize output
|
|
692
|
+
--verbose Verbose output
|
|
693
|
+
|
|
694
|
+
DESCRIPTION
|
|
695
|
+
Continuously poll for workspace config updates and apply them locally
|
|
696
|
+
|
|
697
|
+
EXAMPLES
|
|
698
|
+
$ qfg sync --watch
|
|
699
|
+
|
|
700
|
+
$ qfg sync --watch --interval 10
|
|
701
|
+
|
|
702
|
+
$ qfg sync --watch --dir ./our-config --interval 30
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/sync.ts)_
|
|
642
706
|
|
|
643
707
|
## `qfg verify [PATH]`
|
|
644
708
|
|
|
@@ -671,7 +735,7 @@ EXAMPLES
|
|
|
671
735
|
$ qfg verify --json
|
|
672
736
|
```
|
|
673
737
|
|
|
674
|
-
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
738
|
+
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/verify.ts)_
|
|
675
739
|
|
|
676
740
|
## `qfg whoami`
|
|
677
741
|
|
|
@@ -694,7 +758,7 @@ EXAMPLES
|
|
|
694
758
|
$ qfg whoami
|
|
695
759
|
```
|
|
696
760
|
|
|
697
|
-
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
761
|
+
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/whoami.ts)_
|
|
698
762
|
|
|
699
763
|
## `qfg workspace`
|
|
700
764
|
|
|
@@ -717,7 +781,40 @@ EXAMPLES
|
|
|
717
781
|
$ qfg workspace
|
|
718
782
|
```
|
|
719
783
|
|
|
720
|
-
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
784
|
+
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/workspace.ts)_
|
|
785
|
+
|
|
786
|
+
## `qfg workspace bootstrap`
|
|
787
|
+
|
|
788
|
+
Push a local git repo to Gitea as this workspace's config repository
|
|
789
|
+
|
|
790
|
+
```
|
|
791
|
+
USAGE
|
|
792
|
+
$ qfg workspace bootstrap [--json] [--interactive] [--no-color] [--verbose] [--dir <value>] [--force]
|
|
793
|
+
[--skip-validate]
|
|
794
|
+
|
|
795
|
+
FLAGS
|
|
796
|
+
--dir=<value> Local directory to push (defaults to current directory)
|
|
797
|
+
--force Force push even if remote already has commits
|
|
798
|
+
--skip-validate Skip config validation before pushing
|
|
799
|
+
|
|
800
|
+
GLOBAL FLAGS
|
|
801
|
+
--[no-]interactive Force interactive mode
|
|
802
|
+
--json Format output as json.
|
|
803
|
+
--no-color Do not colorize output
|
|
804
|
+
--verbose Verbose output
|
|
805
|
+
|
|
806
|
+
DESCRIPTION
|
|
807
|
+
Push a local git repo to Gitea as this workspace's config repository
|
|
808
|
+
|
|
809
|
+
EXAMPLES
|
|
810
|
+
$ qfg workspace bootstrap --dir ./our-config
|
|
811
|
+
|
|
812
|
+
$ qfg workspace bootstrap --dir ./launch-migrator/output
|
|
813
|
+
|
|
814
|
+
$ qfg workspace bootstrap --dir ./our-config --skip-validate
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.4/src/commands/workspace/bootstrap.ts)_
|
|
721
818
|
<!-- commandsstop -->
|
|
722
819
|
|
|
723
820
|
## Local Development
|
package/bin/dev.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
await execute({development: true, dir: import.meta.url})
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
await main()
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run dev CLI from any directory — ensures ts-node resolves from cli's node_modules
|
|
3
|
+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
export TS_NODE_PROJECT="$DIR/tsconfig.json"
|
|
5
|
+
exec node --loader "$DIR/node_modules/ts-node/esm.mjs" --no-warnings=ExperimentalWarning "$DIR/bin/dev.mjs" "$@"
|
package/bin/dev.mjs
ADDED
|
@@ -183,18 +183,27 @@ export function getMetaDescription(schema) {
|
|
|
183
183
|
if (!meta || Object.keys(meta).length === 0) {
|
|
184
184
|
return undefined;
|
|
185
185
|
}
|
|
186
|
-
// If meta only has a description key, return just the description
|
|
187
186
|
const keys = Object.keys(meta);
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
const standardKeys = new Set(['title', 'description']);
|
|
188
|
+
const hasExtraKeys = keys.some((k) => !standardKeys.has(k));
|
|
189
|
+
// If meta has non-standard keys (e.g. id, required, deprecated),
|
|
190
|
+
// return JSON representation of the entire meta object so all metadata is preserved
|
|
191
|
+
if (hasExtraKeys) {
|
|
192
|
+
try {
|
|
193
|
+
return JSON.stringify(meta, null, 2);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// If JSON serialization fails, fall through
|
|
197
|
+
}
|
|
190
198
|
}
|
|
199
|
+
// For standard keys only (title, description), prefer description then title
|
|
191
200
|
if (typeof meta.description === 'string') {
|
|
192
201
|
return meta.description;
|
|
193
202
|
}
|
|
194
203
|
if (typeof meta.title === 'string') {
|
|
195
204
|
return meta.title;
|
|
196
205
|
}
|
|
197
|
-
//
|
|
206
|
+
// Fallback: return JSON representation of the entire meta object
|
|
198
207
|
try {
|
|
199
208
|
return JSON.stringify(meta, null, 2);
|
|
200
209
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { select } from '@inquirer/prompts';
|
|
2
3
|
import { BaseCommand } from '../index.js';
|
|
3
4
|
import { decodeJWT, pollForToken, requestDeviceCode } from '../util/oauth-client.js';
|
|
4
5
|
import { getApiUrl } from '../util/domain-urls.js';
|
|
@@ -53,6 +54,8 @@ export default class Login extends BaseCommand {
|
|
|
53
54
|
// Resolve org_id → workspace UUID via the API
|
|
54
55
|
let workspaceId;
|
|
55
56
|
let workspaceName;
|
|
57
|
+
let workspaceSlug;
|
|
58
|
+
let organizationName;
|
|
56
59
|
try {
|
|
57
60
|
const apiUrl = getApiUrl();
|
|
58
61
|
this.verboseLog('Resolving workspace...', { apiUrl, orgId });
|
|
@@ -66,14 +69,28 @@ export default class Login extends BaseCommand {
|
|
|
66
69
|
});
|
|
67
70
|
if (res.ok) {
|
|
68
71
|
const body = await res.json();
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
?
|
|
72
|
-
:
|
|
72
|
+
const allWorkspaces = (body.json ?? body);
|
|
73
|
+
const orgWorkspaces = Array.isArray(allWorkspaces)
|
|
74
|
+
? allWorkspaces.filter((w) => w.workosOrgId === orgId)
|
|
75
|
+
: [];
|
|
76
|
+
const candidates = orgWorkspaces.length > 0 ? orgWorkspaces : (Array.isArray(allWorkspaces) ? allWorkspaces : []);
|
|
77
|
+
let match;
|
|
78
|
+
if (candidates.length === 1) {
|
|
79
|
+
match = candidates[0];
|
|
80
|
+
}
|
|
81
|
+
else if (candidates.length > 1) {
|
|
82
|
+
const chosen = await select({
|
|
83
|
+
choices: candidates.map((w) => ({ name: w.workspaceSlug, value: w.workspaceId })),
|
|
84
|
+
message: 'Select workspace:',
|
|
85
|
+
});
|
|
86
|
+
match = candidates.find((w) => w.workspaceId === chosen);
|
|
87
|
+
}
|
|
73
88
|
if (match) {
|
|
74
89
|
workspaceId = match.workspaceId;
|
|
75
|
-
workspaceName = match.
|
|
76
|
-
|
|
90
|
+
workspaceName = match.workspaceSlug;
|
|
91
|
+
workspaceSlug = match.workspaceSlug;
|
|
92
|
+
organizationName = match.organizationName;
|
|
93
|
+
this.verboseLog('Resolved workspace', { workspaceId, workspaceName, workspaceSlug, organizationName });
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
else {
|
|
@@ -95,6 +112,7 @@ export default class Login extends BaseCommand {
|
|
|
95
112
|
[profileName]: {
|
|
96
113
|
workspace: workspaceId || orgId || 'unknown',
|
|
97
114
|
workspaceName: workspaceName || (orgId ? `org:${orgId}` : undefined),
|
|
115
|
+
workspaceSlug,
|
|
98
116
|
},
|
|
99
117
|
},
|
|
100
118
|
});
|
|
@@ -108,6 +126,9 @@ export default class Login extends BaseCommand {
|
|
|
108
126
|
}
|
|
109
127
|
if (workspaceName) {
|
|
110
128
|
this.log(`Workspace: ${workspaceName} (${workspaceId})`);
|
|
129
|
+
if (organizationName) {
|
|
130
|
+
this.log(`Organization: ${organizationName}`);
|
|
131
|
+
}
|
|
111
132
|
}
|
|
112
133
|
else if (orgId) {
|
|
113
134
|
this.log(`Organization: ${orgId}`);
|
package/dist/commands/mcp.js
CHANGED
|
@@ -12,7 +12,7 @@ export default class Mcp extends BaseCommand {
|
|
|
12
12
|
static examples = [
|
|
13
13
|
'<%= config.bin %> <%= command.id %>',
|
|
14
14
|
'<%= config.bin %> <%= command.id %> --editor cursor',
|
|
15
|
-
'<%= config.bin %> <%= command.id %> --url http://local-app.
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --url http://local-app.quonfig-staging.com:3003/api/v1/mcp',
|
|
16
16
|
];
|
|
17
17
|
static flags = {
|
|
18
18
|
...BaseCommand.baseFlags,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { JsonObj } from '../result.js';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
export default class Pull extends BaseCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
dir: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
workspace: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<JsonObj | void>;
|
|
11
|
+
private resolveRepoUrl;
|
|
12
|
+
private refreshAndGetUrl;
|
|
13
|
+
private looksLike401;
|
|
14
|
+
private maybeWriteQuonfigDir;
|
|
15
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { Flags } from '@oclif/core';
|
|
4
|
+
import { BaseCommand } from '../index.js';
|
|
5
|
+
import { getActiveProfile, loadAuthConfig } from '../util/token-storage.js';
|
|
6
|
+
import { loadGiteaToken, isGiteaTokenExpired, saveGiteaToken } from '../util/gitea-token-storage.js';
|
|
7
|
+
import { mintAndStoreGiteaReadToken, mintGiteaToken } from '../util/gitea-api.js';
|
|
8
|
+
import { isGitRepo, getRemoteUrl, isWorkingTreeClean, gitFetch, gitSetRemote, canFastForward, hasDivergedFromRemote, gitMergeFfOnly, gitClone, displayUrl, } from '../util/git-ops.js';
|
|
9
|
+
export default class Pull extends BaseCommand {
|
|
10
|
+
static description = 'Clone or update a local directory from the workspace Gitea repo';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> pull --dir ./our-config',
|
|
13
|
+
'<%= config.bin %> pull # uses QUONFIG_DIR env var',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
dir: Flags.string({
|
|
17
|
+
description: 'Local directory to clone/update (defaults to QUONFIG_DIR env var)',
|
|
18
|
+
}),
|
|
19
|
+
workspace: Flags.string({
|
|
20
|
+
description: 'Workspace ID (defaults to active profile)',
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { flags } = await this.parse(Pull);
|
|
25
|
+
// Resolve target directory
|
|
26
|
+
const dir = flags.dir || process.env.QUONFIG_DIR;
|
|
27
|
+
if (!dir) {
|
|
28
|
+
return this.err('No directory specified. Use --dir <path> or set QUONFIG_DIR.');
|
|
29
|
+
}
|
|
30
|
+
const resolvedDir = path.resolve(dir);
|
|
31
|
+
// Resolve workspace ID
|
|
32
|
+
let workspaceId = flags.workspace;
|
|
33
|
+
if (!workspaceId) {
|
|
34
|
+
const authConfig = await loadAuthConfig();
|
|
35
|
+
if (!authConfig) {
|
|
36
|
+
return this.err('Not logged in. Please run `qfg login` first.');
|
|
37
|
+
}
|
|
38
|
+
const activeProfile = getActiveProfile();
|
|
39
|
+
const profile = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
40
|
+
if (!profile) {
|
|
41
|
+
return this.err('No active profile found. Please run `qfg login` first.');
|
|
42
|
+
}
|
|
43
|
+
workspaceId = profile.workspace;
|
|
44
|
+
}
|
|
45
|
+
this.verboseLog('Pull', { workspaceId, dir: resolvedDir });
|
|
46
|
+
// Get or mint Gitea read token
|
|
47
|
+
const repoUrl = await this.resolveRepoUrl(workspaceId);
|
|
48
|
+
// Perform git operation
|
|
49
|
+
const isRepo = await isGitRepo(resolvedDir);
|
|
50
|
+
if (!isRepo) {
|
|
51
|
+
// Clone fresh
|
|
52
|
+
this.log(`Cloning workspace into ${resolvedDir}...`);
|
|
53
|
+
try {
|
|
54
|
+
await gitClone(repoUrl, resolvedDir);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// On 401, refresh token and retry once
|
|
58
|
+
if (this.looksLike401(err)) {
|
|
59
|
+
this.verboseLog('Pull', 'Got 401, refreshing Gitea token...');
|
|
60
|
+
const freshUrl = await this.refreshAndGetUrl(workspaceId);
|
|
61
|
+
await gitClone(freshUrl, resolvedDir);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.log(`Cloned successfully into ${resolvedDir}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Existing repo — check remote
|
|
71
|
+
const existingRemote = await getRemoteUrl(resolvedDir);
|
|
72
|
+
const expectedUrlBase = stripAuth(repoUrl);
|
|
73
|
+
if (existingRemote && stripAuth(existingRemote) !== expectedUrlBase) {
|
|
74
|
+
return this.err(`Directory has a different git remote:\n existing: ${displayUrl(existingRemote)}\n expected: ${expectedUrlBase}\n\nResolve manually or use a different --dir.`);
|
|
75
|
+
}
|
|
76
|
+
// Update the remote URL to use current (possibly refreshed) token
|
|
77
|
+
await gitSetRemote(resolvedDir, repoUrl);
|
|
78
|
+
// Check working tree
|
|
79
|
+
const clean = await isWorkingTreeClean(resolvedDir);
|
|
80
|
+
if (!clean) {
|
|
81
|
+
return this.err('Local changes present — commit, stash, or run `git reset --hard origin/main` to discard.');
|
|
82
|
+
}
|
|
83
|
+
// Fetch
|
|
84
|
+
this.log('Fetching from remote...');
|
|
85
|
+
try {
|
|
86
|
+
await gitFetch(resolvedDir);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (this.looksLike401(err)) {
|
|
90
|
+
this.verboseLog('Pull', 'Got 401 on fetch, refreshing Gitea token...');
|
|
91
|
+
const freshUrl = await this.refreshAndGetUrl(workspaceId);
|
|
92
|
+
await gitSetRemote(resolvedDir, freshUrl);
|
|
93
|
+
await gitFetch(resolvedDir);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const ff = await canFastForward(resolvedDir);
|
|
100
|
+
const diverged = !ff && (await hasDivergedFromRemote(resolvedDir));
|
|
101
|
+
if (diverged) {
|
|
102
|
+
return this.err('Local commits diverge from remote — resolve manually.');
|
|
103
|
+
}
|
|
104
|
+
if (!ff) {
|
|
105
|
+
this.log('Already up to date.');
|
|
106
|
+
return { dir: resolvedDir, newCommits: [], workspaceId };
|
|
107
|
+
}
|
|
108
|
+
// Merge
|
|
109
|
+
const newCommits = await gitMergeFfOnly(resolvedDir);
|
|
110
|
+
if (newCommits.length > 0) {
|
|
111
|
+
this.log(`Pulled ${newCommits.length} new commit(s):`);
|
|
112
|
+
for (const msg of newCommits) {
|
|
113
|
+
this.log(` - ${msg}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.log('Updated successfully.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Write QUONFIG_DIR to ~/.quonfig/config if not set
|
|
121
|
+
await this.maybeWriteQuonfigDir(resolvedDir);
|
|
122
|
+
return { dir: resolvedDir, workspaceId };
|
|
123
|
+
}
|
|
124
|
+
async resolveRepoUrl(workspaceId) {
|
|
125
|
+
let entry = await loadGiteaToken(workspaceId);
|
|
126
|
+
if (!entry || isGiteaTokenExpired(entry)) {
|
|
127
|
+
this.verboseLog('Pull', 'Minting new Gitea read token...');
|
|
128
|
+
entry = await mintAndStoreGiteaReadToken(workspaceId);
|
|
129
|
+
}
|
|
130
|
+
return entry.repoUrl;
|
|
131
|
+
}
|
|
132
|
+
async refreshAndGetUrl(workspaceId) {
|
|
133
|
+
const data = await mintGiteaToken(workspaceId, 'read', 'pull');
|
|
134
|
+
const entry = { token: data.token, repoUrl: data.repoUrl, expiresAt: data.expiresAt };
|
|
135
|
+
await saveGiteaToken(workspaceId, entry);
|
|
136
|
+
return entry.repoUrl;
|
|
137
|
+
}
|
|
138
|
+
looksLike401(err) {
|
|
139
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
+
return msg.includes('401') || msg.toLowerCase().includes('authentication failed');
|
|
141
|
+
}
|
|
142
|
+
async maybeWriteQuonfigDir(dir) {
|
|
143
|
+
if (process.env.QUONFIG_DIR)
|
|
144
|
+
return; // already set via env
|
|
145
|
+
const os = await import('node:os');
|
|
146
|
+
const pathMod = await import('node:path');
|
|
147
|
+
const configFilePath = pathMod.join(os.homedir(), '.quonfig', 'config');
|
|
148
|
+
try {
|
|
149
|
+
const existing = await fs.promises.readFile(configFilePath, 'utf8').catch(() => '');
|
|
150
|
+
if (existing.includes('QUONFIG_DIR') || existing.includes('quonfig_dir'))
|
|
151
|
+
return;
|
|
152
|
+
const line = `\nQUONFIG_DIR = ${dir}\n`;
|
|
153
|
+
await fs.promises.appendFile(configFilePath, line, 'utf8');
|
|
154
|
+
this.verboseLog('Pull', `Wrote QUONFIG_DIR to ${configFilePath}`);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Non-fatal
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Strip auth credentials from a git URL for comparison purposes.
|
|
163
|
+
* Converts `https://user:token@host/path` to `https://host/path`.
|
|
164
|
+
*/
|
|
165
|
+
function stripAuth(url) {
|
|
166
|
+
try {
|
|
167
|
+
const u = new URL(url);
|
|
168
|
+
u.username = '';
|
|
169
|
+
u.password = '';
|
|
170
|
+
return u.toString();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Not a valid URL (e.g. SSH) — return as-is
|
|
174
|
+
return url;
|
|
175
|
+
}
|
|
176
|
+
}
|
package/dist/commands/serve.js
CHANGED
|
@@ -47,7 +47,9 @@ export default class Serve extends BaseCommand {
|
|
|
47
47
|
}
|
|
48
48
|
catch (error_) {
|
|
49
49
|
const error = error_;
|
|
50
|
-
if (/No projectEnvId found in config/.test(error.message) ||
|
|
50
|
+
if (/No projectEnvId found in config/.test(error.message) ||
|
|
51
|
+
/is not iterable/.test(error.message) ||
|
|
52
|
+
/Unexpected end of JSON input/.test(error.message)) {
|
|
51
53
|
return this.error(`${error.message}\nYour download file seems invalid or corrupt. Please redownload your datafile.`);
|
|
52
54
|
}
|
|
53
55
|
return this.error(error);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JsonObj } from '../result.js';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
export default class Sync extends BaseCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
dir: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
interval: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
|
+
watch: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
private workspaceId;
|
|
12
|
+
private resolvedDir;
|
|
13
|
+
run(): Promise<JsonObj | void>;
|
|
14
|
+
private runSync;
|
|
15
|
+
private ensureRemoteUrl;
|
|
16
|
+
private refreshToken;
|
|
17
|
+
private looksLike401;
|
|
18
|
+
}
|