@quonfig/cli 0.0.29 → 0.0.30

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 (37) hide show
  1. package/README.md +35 -35
  2. package/dist/codegen/fetch-workspace-snapshot.js +3 -3
  3. package/dist/commands/delete.js +2 -1
  4. package/dist/commands/login.js +0 -1
  5. package/dist/commands/migrate/doctor.js +3 -1
  6. package/dist/commands/migrate.js +10 -13
  7. package/dist/commands/override.js +1 -1
  8. package/dist/commands/pull.js +12 -12
  9. package/dist/commands/push.d.ts +1 -1
  10. package/dist/commands/push.js +5 -5
  11. package/dist/commands/sync.d.ts +1 -0
  12. package/dist/commands/sync.js +6 -13
  13. package/dist/commands/whoami.js +15 -3
  14. package/dist/commands/workspace/bootstrap.js +8 -12
  15. package/dist/commands/workspace/create.d.ts +5 -1
  16. package/dist/commands/workspace/create.js +18 -7
  17. package/dist/commands/workspace/switch.d.ts +7 -1
  18. package/dist/commands/workspace/switch.js +45 -27
  19. package/dist/commands/workspace.d.ts +5 -0
  20. package/dist/commands/workspace.js +28 -9
  21. package/dist/migrate/sources/launch/translate.js +6 -3
  22. package/dist/migrate/sources/launch/zod-to-json-schema.js +4 -0
  23. package/dist/push/config-push-client.d.ts +1 -1
  24. package/dist/push/config-push-client.js +3 -5
  25. package/dist/util/get-client.js +12 -12
  26. package/dist/util/get-valid-token.d.ts +7 -7
  27. package/dist/util/get-valid-token.js +19 -13
  28. package/dist/util/gitea-api.d.ts +2 -2
  29. package/dist/util/gitea-api.js +5 -7
  30. package/dist/util/resolve-workspace.d.ts +14 -4
  31. package/dist/util/resolve-workspace.js +32 -20
  32. package/dist/util/token-storage.d.ts +10 -5
  33. package/dist/util/token-storage.js +14 -7
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/oclif.manifest.json +1 -1
  37. package/package.json +1 -1
@@ -1,14 +1,14 @@
1
1
  import { getApiUrl } from './domain-urls.js';
2
- import { getValidAccessToken, resolveDefaultOrgId } from './get-valid-token.js';
2
+ import { getValidAccessToken } from './get-valid-token.js';
3
3
  import { tryParseWorkspacePin } from './quonfig-json.js';
4
4
  import { findOrgIdBySlug, getActiveProfile, loadAuthConfig, loadTokens, saveAuthConfig, } from './token-storage.js';
5
5
  const UUID_PATTERN = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i;
6
6
  const BARE_SLUG_ENV_MIGRATION_MESSAGE = 'QUONFIG_WORKSPACE must be in org/workspace form (e.g. acme/foo). ' +
7
7
  'Bare workspace slugs are no longer accepted. Update your .env and run `qfg login` if you have not yet migrated.';
8
8
  /**
9
- * Resolve the target workspace UUID for commands that don't extend APICommand
10
- * (e.g. pull, push — which talk to Gitea, not the oRPC API, but still need to
11
- * know which workspace to mint a Gitea token for).
9
+ * Resolve the target workspace UUID + owning org slug for commands that
10
+ * don't extend APICommand (e.g. pull, push — which talk to Gitea, not the
11
+ * oRPC API, but still need to mint a Gitea token for the right org).
12
12
  *
13
13
  * Handles all three auth paths in the same order as util/get-client.ts:
14
14
  * 1. QUONFIG_API_KEY + QUONFIG_WORKSPACE (or --workspace flag) — CI/headless.
@@ -26,7 +26,7 @@ export async function resolveWorkspaceUuid(command, flagOverride) {
26
26
  }
27
27
  if (UUID_PATTERN.test(override)) {
28
28
  command.verboseLog('ApiKey auth', { source: 'uuid', workspaceId: override });
29
- return override;
29
+ return { workspaceId: override, orgSlug: '' };
30
30
  }
31
31
  // Slug — resolve via the server since there's no local auth config in CI mode.
32
32
  // API-key path short-circuits before consulting the orgId.
@@ -55,12 +55,9 @@ export async function resolveWorkspaceUuid(command, flagOverride) {
55
55
  });
56
56
  }
57
57
  command.verboseLog('ApiKey auth', { source: 'slug', slug: override, workspaceId: match.workspaceId });
58
- return match.workspaceId;
58
+ return { workspaceId: match.workspaceId, orgSlug: '' };
59
59
  }
60
- // OAuth path with an explicit override → must be in org/ws form. Parse,
61
- // look up the workosOrgId for that org slug in the local token store, and
62
- // resolve the workspaceSlug *within that org*. Bare slug is rejected with
63
- // a migration message (the API-key branch above is the one exception).
60
+ // OAuth path with an explicit override → must be in org/ws form.
64
61
  if (override) {
65
62
  return resolveOrgScopedWorkspace(command, override);
66
63
  }
@@ -68,11 +65,6 @@ export async function resolveWorkspaceUuid(command, flagOverride) {
68
65
  // auth config from tokens if the config file is absent.
69
66
  let authConfig = await loadAuthConfig();
70
67
  if (!authConfig) {
71
- // Auth config can be missing while tokens.json still holds a valid
72
- // refresh_token — e.g. after the domain-scoped config split (commit
73
- // 0f8bee6) or a partial login that wrote tokens but never the config
74
- // (qfg-2qj). getValidAccessToken already knows how to refresh, so try
75
- // that path before declaring "Not logged in".
76
68
  const store = await loadTokens();
77
69
  const tokens = store ? Object.values(store.tokensByOrg)[0] : undefined;
78
70
  if (!tokens?.refresh_token) {
@@ -85,7 +77,10 @@ export async function resolveWorkspaceUuid(command, flagOverride) {
85
77
  if (!profileData?.workspace) {
86
78
  command.error('No active workspace. Run `qfg login` / `qfg workspace switch`, or set QUONFIG_API_KEY + QUONFIG_WORKSPACE.', { exit: 1 });
87
79
  }
88
- return profileData.workspace;
80
+ if (!profileData.organizationSlug) {
81
+ command.error('Saved profile is missing organization_slug. Run `qfg login` to refresh, or `qfg workspace switch <org>/<ws>`.', { exit: 1 });
82
+ }
83
+ return { workspaceId: profileData.workspace, orgSlug: profileData.organizationSlug };
89
84
  }
90
85
  /**
91
86
  * OAuth-mode resolver for an explicit `<org-slug>/<workspace-slug>` override.
@@ -142,7 +137,7 @@ async function resolveOrgScopedWorkspace(command, override) {
142
137
  command.error(`Workspace "${workspaceSlug}" not found in org "${orgSlug}". Available: ${available}.`, { exit: 1 });
143
138
  }
144
139
  command.verboseLog('OAuth workspace lookup', { orgSlug, workspaceSlug, workosOrgId, workspaceId: match.workspaceId });
145
- return match.workspaceId;
140
+ return { workspaceId: match.workspaceId, orgSlug };
146
141
  }
147
142
  /**
148
143
  * Best-effort rebuild of the on-disk auth config from a working refresh_token.
@@ -153,11 +148,21 @@ async function resolveOrgScopedWorkspace(command, override) {
153
148
  * a message that points the user at `qfg login`.
154
149
  */
155
150
  async function recoverAuthConfigFromTokens(command) {
151
+ const store = await loadTokens();
152
+ if (!store) {
153
+ command.error('Not logged in. Run `qfg login` first.', { exit: 401 });
154
+ }
155
+ // Pick any logged-in org's tokens to call /userWorkspaces/list. The list is
156
+ // org-scoped, so a single first-org JWT only sees that org's workspaces —
157
+ // but we only need one workspace to seed the default profile, and the user
158
+ // can always `qfg workspace switch` to a different one afterwards.
159
+ const [seedOrgId, seedTokens] = Object.entries(store.tokensByOrg)[0] ?? [];
160
+ if (!seedOrgId || !seedTokens) {
161
+ command.error('Not logged in. Run `qfg login` first.', { exit: 401 });
162
+ }
156
163
  let jwt;
157
164
  try {
158
- // TODO(qfg-kr7.5): pick the orgId resolved from the workspace address.
159
- const orgId = await resolveDefaultOrgId();
160
- jwt = await getValidAccessToken(orgId, command.verboseLog);
165
+ jwt = await getValidAccessToken(seedOrgId, command.verboseLog);
161
166
  }
162
167
  catch (error) {
163
168
  const detail = error instanceof Error ? error.message : String(error);
@@ -186,6 +191,12 @@ async function recoverAuthConfigFromTokens(command) {
186
191
  command.error('Tokens found but no workspace profile on disk. Run `qfg login` to repopulate.', { exit: 401 });
187
192
  }
188
193
  const match = candidates[0];
194
+ // Prefer the slug the server returned; fall back to the slug we have
195
+ // cached on the seed token (qfg-kr7 stores this on every login).
196
+ const orgSlug = match.organizationSlug ?? seedTokens.org_slug;
197
+ if (!orgSlug) {
198
+ command.error('Tokens found but org slug is missing. Run `qfg login` to repopulate.', { exit: 401 });
199
+ }
189
200
  const recovered = {
190
201
  defaultProfile: 'default',
191
202
  profiles: {
@@ -194,6 +205,7 @@ async function recoverAuthConfigFromTokens(command) {
194
205
  workspaceName: match.workspaceSlug,
195
206
  workspaceSlug: match.workspaceSlug,
196
207
  organizationName: match.organizationName,
208
+ organizationSlug: orgSlug,
197
209
  },
198
210
  },
199
211
  };
@@ -17,7 +17,14 @@ export interface TokenSet {
17
17
  /** Org display name for `qfg whoami` / `qfg workspace`. */
18
18
  org_name?: string;
19
19
  }
20
+ /**
21
+ * Schema version of tokens.json. Stamped on every saveTokens write so a
22
+ * future CLI can refuse files written by an even-newer CLI with a clear
23
+ * "upgrade your CLI" error.
24
+ */
25
+ export declare const TOKEN_STORE_VERSION = 2;
20
26
  export interface TokenStore {
27
+ version?: number;
21
28
  defaultOrgId?: string;
22
29
  tokensByOrg: {
23
30
  [workosOrgId: string]: TokenSet;
@@ -40,12 +47,10 @@ export interface AuthConfig {
40
47
  workspaceSlug?: string;
41
48
  organizationName?: string;
42
49
  /**
43
- * WorkOS organization UUID for the saved workspace. Persisted by
44
- * `qfg workspace switch` (qfg-kr7.9) so other commands can pick the
45
- * matching per-org token without round-tripping `me.organizations`.
50
+ * Org URL slug. Used both for rendering the org/ws pin form and as
51
+ * the lookup key into the token store (slug → workosOrgId via
52
+ * findOrgIdBySlug) when a command needs an org-scoped JWT.
46
53
  */
47
- workosOrgId?: string;
48
- /** Org URL slug — used when rendering the org/ws pin form. */
49
54
  organizationSlug?: string;
50
55
  };
51
56
  };
@@ -13,6 +13,12 @@ const getConfigFile = (options) => {
13
13
  const filename = domain === 'quonfig.com' ? 'config' : `config-${domain.replaceAll('.', '-')}`;
14
14
  return path.join(getQuonfigDir(options), filename);
15
15
  };
16
+ /**
17
+ * Schema version of tokens.json. Stamped on every saveTokens write so a
18
+ * future CLI can refuse files written by an even-newer CLI with a clear
19
+ * "upgrade your CLI" error.
20
+ */
21
+ export const TOKEN_STORE_VERSION = 2;
16
22
  export const getTokenForOrg = (store, workosOrgId) => store.tokensByOrg[workosOrgId];
17
23
  /**
18
24
  * Find the workosOrgId in the per-org token store whose TokenSet has the
@@ -67,7 +73,8 @@ export const saveTokens = async (store, options) => {
67
73
  await ensureQuonfigDir(options);
68
74
  const targetFile = getTokenFile(options);
69
75
  const tmpFile = `${targetFile}.tmp`;
70
- await fs.promises.writeFile(tmpFile, JSON.stringify(store, null, 2), { encoding: 'utf8', mode: 0o600 });
76
+ const stamped = { ...store, version: TOKEN_STORE_VERSION };
77
+ await fs.promises.writeFile(tmpFile, JSON.stringify(stamped, null, 2), { encoding: 'utf8', mode: 0o600 });
71
78
  await fs.promises.rename(tmpFile, targetFile);
72
79
  await verifyWritten(targetFile, 'tokens');
73
80
  // Round-trip parse to confirm the file on disk is the JSON we just wrote.
@@ -91,7 +98,12 @@ export const loadTokens = async (options) => {
91
98
  }
92
99
  const parsed = JSON.parse(data);
93
100
  if (!isTokenStore(parsed)) {
94
- throw new Error('Token store format has changed. Run `qfg login` to re-authenticate.');
101
+ throw new Error('Token store malformed. Run `qfg login` to re-authenticate.');
102
+ }
103
+ const fileVersion = parsed.version ?? TOKEN_STORE_VERSION;
104
+ if (fileVersion > TOKEN_STORE_VERSION) {
105
+ throw new Error(`Token store version ${fileVersion} is from a newer CLI than this one supports (max ${TOKEN_STORE_VERSION}). ` +
106
+ 'Upgrade with `npm i -g @quonfig/cli@latest`.');
95
107
  }
96
108
  return parsed;
97
109
  };
@@ -119,9 +131,6 @@ export const saveAuthConfig = async (config, options) => {
119
131
  if (profileData.organizationSlug) {
120
132
  configContent += `organization_slug = ${profileData.organizationSlug}\n`;
121
133
  }
122
- if (profileData.workosOrgId) {
123
- configContent += `workos_org_id = ${profileData.workosOrgId}\n`;
124
- }
125
134
  configContent += '\n';
126
135
  }
127
136
  const configTarget = getConfigFile(options);
@@ -152,7 +161,6 @@ export const loadAuthConfig = async (options) => {
152
161
  const slugMatch = block.match(/workspace_slug\s*=\s*(\S+)/);
153
162
  const orgMatch = block.match(/organization_name\s*=\s*(.+)/);
154
163
  const orgSlugMatch = block.match(/organization_slug\s*=\s*(\S+)/);
155
- const workosOrgIdMatch = block.match(/workos_org_id\s*=\s*(\S+)/);
156
164
  if (!workspaceMatch)
157
165
  continue;
158
166
  config.profiles[profileName] = {
@@ -161,7 +169,6 @@ export const loadAuthConfig = async (options) => {
161
169
  workspaceSlug: slugMatch?.[1]?.trim(),
162
170
  organizationName: orgMatch?.[1]?.trim(),
163
171
  organizationSlug: orgSlugMatch?.[1]?.trim(),
164
- workosOrgId: workosOrgIdMatch?.[1]?.trim(),
165
172
  };
166
173
  }
167
174
  return Object.keys(config.profiles).length > 0 ? config : null;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "0.0.29";
1
+ declare const _default: "0.0.30";
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.29';
2
+ export default '0.0.30';
@@ -2910,5 +2910,5 @@
2910
2910
  ]
2911
2911
  }
2912
2912
  },
2913
- "version": "0.0.29"
2913
+ "version": "0.0.30"
2914
2914
  }
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.29",
4
+ "version": "0.0.30",
5
5
  "bugs": {
6
6
  "url": "https://github.com/quonfig/cli/issues"
7
7
  },