@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 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.2 darwin-arm64 node-v24.4.1
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.2/src/commands/create.ts)_
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.2/src/commands/download.ts)_
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.2/src/commands/generate.ts)_
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.2/src/commands/generate-new-hex-key.ts)_
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.2/src/commands/get.ts)_
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.2/src/commands/info.ts)_
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.2/src/commands/init.ts)_
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.2/src/commands/interactive.ts)_
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.2/src/commands/list.ts)_
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.2/src/commands/login.ts)_
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.2/src/commands/logout.ts)_
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.goatsofquonfig.com:3003/api/v1/mcp
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.2/src/commands/mcp.ts)_
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.2/src/commands/override.ts)_
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.2/src/commands/profile.ts)_
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.2/src/commands/schema.ts)_
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.2/src/commands/serve.ts)_
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.2/src/commands/set-default.ts)_
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.2/src/commands/verify.ts)_
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.2/src/commands/whoami.ts)_
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.2/src/commands/workspace.ts)_
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 -S node --loader ts-node/esm --no-warnings=ExperimentalWarning
2
-
3
- // eslint-disable-next-line node/shebang
4
- async function main() {
5
- const {execute} = await import('@oclif/core')
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
@@ -0,0 +1,6 @@
1
+ async function main() {
2
+ const {execute} = await import('@oclif/core')
3
+ await execute({development: true, dir: import.meta.url})
4
+ }
5
+
6
+ await main()
@@ -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
- if (keys.length === 1 && keys[0] === 'description' && typeof meta.description === 'string') {
189
- return meta.description;
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
- // Otherwise, return JSON representation of the entire meta object
206
+ // Fallback: return JSON representation of the entire meta object
198
207
  try {
199
208
  return JSON.stringify(meta, null, 2);
200
209
  }
@@ -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 workspaces = body.json ?? body;
70
- const match = Array.isArray(workspaces)
71
- ? workspaces.find((w) => w.workosOrgId === orgId) ?? workspaces[0]
72
- : undefined;
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.organizationName;
76
- this.verboseLog('Resolved workspace', { workspaceId, workspaceName });
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}`);
@@ -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.goatsofquonfig.com:3003/api/v1/mcp',
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
+ }
@@ -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) || /Unexpected end of JSON input/.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
+ }