@quonfig/cli 0.0.5 → 0.0.7

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 +209 -161
  2. package/dist/codegen/local-config-reader.d.ts +6 -0
  3. package/dist/codegen/local-config-reader.js +113 -0
  4. package/dist/commands/create.js +3 -0
  5. package/dist/commands/generate.d.ts +3 -2
  6. package/dist/commands/generate.js +18 -10
  7. package/dist/commands/login.js +22 -15
  8. package/dist/commands/profile.d.ts +1 -0
  9. package/dist/commands/profile.js +6 -42
  10. package/dist/commands/sdk-key/create.d.ts +11 -0
  11. package/dist/commands/sdk-key/create.js +100 -0
  12. package/dist/commands/sdk-key/list.d.ts +10 -0
  13. package/dist/commands/sdk-key/list.js +48 -0
  14. package/dist/commands/sdk-key/revoke.d.ts +10 -0
  15. package/dist/commands/sdk-key/revoke.js +30 -0
  16. package/dist/commands/sdk-key.d.ts +6 -0
  17. package/dist/commands/sdk-key.js +15 -0
  18. package/dist/commands/whoami.js +8 -8
  19. package/dist/commands/workspace/switch.d.ts +7 -0
  20. package/dist/commands/workspace/switch.js +90 -0
  21. package/dist/commands/workspace.js +14 -23
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +9 -2
  24. package/dist/interactive-prompt.js +0 -4
  25. package/dist/ui/get-value.js +4 -1
  26. package/dist/util/get-client.js +34 -11
  27. package/dist/util/token-storage.d.ts +6 -0
  28. package/dist/util/token-storage.js +17 -0
  29. package/dist/verify/validate.js +28 -8
  30. package/oclif.manifest.json +433 -118
  31. package/package.json +1 -1
  32. package/dist/codegen/config-downloader.d.ts +0 -7
  33. package/dist/codegen/config-downloader.js +0 -26
  34. package/dist/commands/download.d.ts +0 -13
  35. package/dist/commands/download.js +0 -63
  36. package/dist/commands/serve.d.ts +0 -13
  37. package/dist/commands/serve.js +0 -101
@@ -0,0 +1,90 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { BaseCommand } from '../../index.js';
3
+ import { getApiUrl } from '../../util/domain-urls.js';
4
+ import { getValidAccessToken } from '../../util/get-valid-token.js';
5
+ import { loadAuthConfig, saveAuthConfig } from '../../util/token-storage.js';
6
+ export default class WorkspaceSwitch extends BaseCommand {
7
+ static description = 'Switch to a different workspace';
8
+ static examples = ['<%= config.bin %> <%= command.id %>'];
9
+ async run() {
10
+ // Get a valid token — refresh silently if needed, error if not logged in
11
+ let accessToken;
12
+ try {
13
+ accessToken = await getValidAccessToken();
14
+ }
15
+ catch {
16
+ return this.err('Not logged in. Run `qfg login` first.');
17
+ }
18
+ const authConfig = await loadAuthConfig();
19
+ if (!authConfig) {
20
+ return this.err('Not logged in. Run `qfg login` first.');
21
+ }
22
+ // Fetch all workspaces the user has access to
23
+ const apiUrl = getApiUrl();
24
+ let candidates = [];
25
+ try {
26
+ const res = await fetch(`${apiUrl}/api/v1/userWorkspaces/list`, {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Authorization': `Bearer ${accessToken}`,
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ body: JSON.stringify({ json: {} }),
33
+ });
34
+ if (!res.ok) {
35
+ return this.err(`Failed to fetch workspaces (${res.status}). Try \`qfg login\` to re-authenticate.`);
36
+ }
37
+ const body = await res.json();
38
+ const all = (body.json ?? body);
39
+ candidates = Array.isArray(all) ? all : [];
40
+ }
41
+ catch (error) {
42
+ return this.err(`Failed to fetch workspaces: ${String(error)}`);
43
+ }
44
+ if (candidates.length === 0) {
45
+ return this.err('No workspaces found for your account.');
46
+ }
47
+ // Pick workspace
48
+ let match;
49
+ if (candidates.length === 1) {
50
+ match = candidates[0];
51
+ this.log(`Only one workspace available: ${match.organizationName} / ${match.workspaceSlug}`);
52
+ }
53
+ else {
54
+ const chosen = await select({
55
+ choices: candidates.map((w) => ({
56
+ name: `${w.organizationName} / ${w.workspaceSlug}`,
57
+ value: w.workspaceId,
58
+ })),
59
+ message: 'Select workspace:',
60
+ });
61
+ const found = candidates.find((w) => w.workspaceId === chosen);
62
+ if (!found)
63
+ return this.err('No workspace selected.');
64
+ match = found;
65
+ }
66
+ // Save as the default profile
67
+ const defaultProfile = authConfig.defaultProfile || 'default';
68
+ await saveAuthConfig({
69
+ ...authConfig,
70
+ profiles: {
71
+ ...authConfig.profiles,
72
+ [defaultProfile]: {
73
+ workspace: match.workspaceId,
74
+ workspaceName: match.workspaceSlug,
75
+ workspaceSlug: match.workspaceSlug,
76
+ organizationName: match.organizationName,
77
+ },
78
+ },
79
+ });
80
+ this.log(`\nSwitched to: ${match.organizationName} / ${match.workspaceSlug}`);
81
+ this.log(`\nTo use this workspace in a project, add to your .env:`);
82
+ this.log(` QUONFIG_WORKSPACE=${match.workspaceSlug}`);
83
+ return {
84
+ organizationName: match.organizationName,
85
+ success: true,
86
+ workspaceId: match.workspaceId,
87
+ workspaceSlug: match.workspaceSlug,
88
+ };
89
+ }
90
+ }
@@ -1,38 +1,29 @@
1
1
  import { BaseCommand } from '../index.js';
2
2
  import { getActiveProfile, loadAuthConfig } from '../util/token-storage.js';
3
3
  export default class Workspace extends BaseCommand {
4
- static description = 'Display current workspace. To switch workspaces, run `qfg login --profile <name>`.';
4
+ static description = 'Show the current workspace';
5
5
  static examples = ['<%= config.bin %> <%= command.id %>'];
6
6
  async run() {
7
7
  const authConfig = await loadAuthConfig();
8
8
  if (!authConfig || Object.keys(authConfig.profiles).length === 0) {
9
- return this.err('Not logged in. Please run `qfg login` first.');
9
+ return this.err('Not logged in. Run `qfg login` first.');
10
10
  }
11
- // Get the active profile
12
- const activeProfile = getActiveProfile();
13
- const profile = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
11
+ const activeProfileName = getActiveProfile();
12
+ const profile = authConfig.profiles[activeProfileName] || authConfig.profiles[authConfig.defaultProfile || 'default'];
14
13
  if (!profile) {
15
- return this.err('No active profile found. Please run `qfg login` first.');
16
- }
17
- this.log(`Active profile: ${activeProfile}`);
18
- this.log(`Workspace: ${profile.workspaceName || profile.workspace}`);
19
- // Show all profiles if more than one
20
- const profileNames = Object.keys(authConfig.profiles);
21
- if (profileNames.length > 1) {
22
- this.log('\nAll profiles:');
23
- for (const name of profileNames) {
24
- const p = authConfig.profiles[name];
25
- const marker = name === activeProfile ? ' (active)' : '';
26
- this.log(` ${name}: ${p.workspaceName || p.workspace}${marker}`);
27
- }
28
- this.log('\nTo switch, use: qfg login --profile <name>');
29
- this.log('To change default: qfg profile');
14
+ return this.err('No workspace configured. Run `qfg login` first.');
30
15
  }
16
+ const workspaceSlug = profile.workspaceSlug || profile.workspaceName || profile.workspace;
17
+ const orgLine = profile.organizationName ? `${profile.organizationName} / ` : '';
18
+ this.log(`Workspace: ${orgLine}${workspaceSlug}`);
19
+ this.log(`ID: ${profile.workspace}`);
20
+ this.log(`\nTo switch: qfg workspace switch`);
21
+ this.log(`To pin in a project, add to .env:`);
22
+ this.log(` QUONFIG_WORKSPACE=${workspaceSlug}`);
31
23
  return {
32
- activeProfile,
33
- profiles: Object.fromEntries(profileNames.map((name) => [name, authConfig.profiles[name]])),
24
+ organizationName: profile.organizationName,
34
25
  workspace: profile.workspace,
35
- workspaceName: profile.workspaceName,
26
+ workspaceSlug,
36
27
  };
37
28
  }
38
29
  }
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare abstract class BaseCommand extends Command {
23
23
  }
24
24
  export declare abstract class APICommand extends BaseCommand {
25
25
  static baseFlags: {
26
+ workspace: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
26
27
  profile: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
27
28
  interactive: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
28
29
  'no-color': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
package/dist/index.js CHANGED
@@ -84,10 +84,17 @@ export class BaseCommand extends Command {
84
84
  export class APICommand extends BaseCommand {
85
85
  static baseFlags = {
86
86
  ...globalFlags,
87
+ workspace: Flags.string({
88
+ char: 'w',
89
+ description: 'Workspace slug to use (overrides QUONFIG_WORKSPACE env var and saved default)',
90
+ helpGroup: 'GLOBAL',
91
+ required: false,
92
+ }),
87
93
  profile: Flags.string({
88
94
  char: 'p',
89
- description: 'Profile to use (defaults to ENV var QUONFIG_PROFILE or "default")',
95
+ description: 'Deprecated: use --workspace instead',
90
96
  helpGroup: 'GLOBAL',
97
+ hidden: true,
91
98
  required: false,
92
99
  }),
93
100
  };
@@ -104,7 +111,7 @@ export class APICommand extends BaseCommand {
104
111
  async init() {
105
112
  await super.init();
106
113
  const { flags } = await this.parse();
107
- this.rawApiClient = await rawGetClient(this, undefined, flags.profile);
114
+ this.rawApiClient = await rawGetClient(this, undefined, flags.workspace ?? flags.profile);
108
115
  // For JWT-based auth, we'll need to get environment info from the token
109
116
  // For now, set a placeholder - this should be enhanced later
110
117
  this.currentEnvironment = { id: 'unknown', projectId: 0 };
@@ -1,12 +1,10 @@
1
1
  import Create from './commands/create.js';
2
- import Download from './commands/download.js';
3
2
  import GenerateNewHexKey from './commands/generate-new-hex-key.js';
4
3
  import Get from './commands/get.js';
5
4
  import Info from './commands/info.js';
6
5
  import List from './commands/list.js';
7
6
  import Mcp from './commands/mcp.js';
8
7
  import Override from './commands/override.js';
9
- import Serve from './commands/serve.js';
10
8
  import SetDefault from './commands/set-default.js';
11
9
  import { APICommand } from './index.js';
12
10
  import getString from './ui/get-string.js';
@@ -23,11 +21,9 @@ const commands = [
23
21
  Create,
24
22
  Get,
25
23
  Info,
26
- Download,
27
24
  Mcp,
28
25
  Override,
29
26
  List,
30
- Serve,
31
27
  ].map((command) => {
32
28
  const id = convertToKebabCase(command.name);
33
29
  return {
@@ -7,7 +7,10 @@ const getValue = async ({ allowBlank = true, desiredValue, environment, flags, k
7
7
  return failure(`No value provided for ${key}`);
8
8
  }
9
9
  if (!key) {
10
- const value = desiredValue ?? (await promptForValue({ allowBlank, message }));
10
+ if (desiredValue !== undefined) {
11
+ return success(desiredValue);
12
+ }
13
+ const value = await promptForValue({ allowBlank, message });
11
14
  if (value === undefined || value === null) {
12
15
  return noop();
13
16
  }
@@ -1,7 +1,7 @@
1
1
  import { Client } from '@quonfig/node';
2
2
  import { getApiUrl } from '../util/domain-urls.js';
3
3
  import jsonMaybe from '../util/json-maybe.js';
4
- import { getActiveProfile, loadAuthConfig, loadTokens } from '../util/token-storage.js';
4
+ import { getActiveProfile, loadAuthConfig, loadTokens, resolveWorkspaceId } from '../util/token-storage.js';
5
5
  import { getValidAccessToken } from '../util/get-valid-token.js';
6
6
  import version from '../version.js';
7
7
  let clientInstance;
@@ -39,16 +39,39 @@ const getClient = async (command, sdkKey, profile) => {
39
39
  catch {
40
40
  command.error('Session expired. Please run `qfg login` to re-authenticate.', { exit: 401 });
41
41
  }
42
- // Get the active profile to find the workspace
43
- const activeProfile = getActiveProfile(profile);
44
- const profileData = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
45
- command.verboseLog('Profile lookup', {
46
- activeProfile,
47
- hasProfileData: Boolean(profileData),
48
- workspaceId: profileData?.workspace,
49
- });
50
- if (profileData) {
51
- workspaceId = profileData.workspace;
42
+ // Resolve workspace: --workspace/QUONFIG_WORKSPACE (slug) → QUONFIG_PROFILE (profile name) → default
43
+ const workspaceSlugOverride = profile ?? process.env.QUONFIG_WORKSPACE;
44
+ const profileNameOverride = process.env.QUONFIG_PROFILE;
45
+ if (workspaceSlugOverride) {
46
+ // Try slug resolution first, then fall back to treating it as a profile name
47
+ const resolved = resolveWorkspaceId(authConfig, workspaceSlugOverride);
48
+ if (resolved) {
49
+ workspaceId = resolved;
50
+ command.verboseLog('Workspace lookup', { source: 'slug', slug: workspaceSlugOverride, workspaceId });
51
+ }
52
+ else {
53
+ const profileData = authConfig.profiles[workspaceSlugOverride];
54
+ if (profileData) {
55
+ workspaceId = profileData.workspace;
56
+ command.verboseLog('Workspace lookup', { source: 'profile-name', profile: workspaceSlugOverride, workspaceId });
57
+ }
58
+ else {
59
+ command.error(`Workspace '${workspaceSlugOverride}' not found. Run \`qfg workspace switch\` to select and save a workspace.`, { exit: 1 });
60
+ }
61
+ }
62
+ }
63
+ else {
64
+ const activeProfile = getActiveProfile(profileNameOverride);
65
+ const profileData = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
66
+ command.verboseLog('Workspace lookup', {
67
+ source: 'default-profile',
68
+ activeProfile,
69
+ hasProfileData: Boolean(profileData),
70
+ workspaceId: profileData?.workspace,
71
+ });
72
+ if (profileData) {
73
+ workspaceId = profileData.workspace;
74
+ }
52
75
  }
53
76
  }
54
77
  // Store workspaceId on the command for use in building URLs and cache it
@@ -15,9 +15,15 @@ export interface AuthConfig {
15
15
  workspace: string;
16
16
  workspaceName?: string;
17
17
  workspaceSlug?: string;
18
+ organizationName?: string;
18
19
  };
19
20
  };
20
21
  }
22
+ /**
23
+ * Find a workspace ID by slug across all saved profiles.
24
+ * Used to resolve QUONFIG_WORKSPACE and --workspace flag values.
25
+ */
26
+ export declare const resolveWorkspaceId: (config: AuthConfig, slug: string) => string | undefined;
21
27
  export declare const saveTokens: (tokens: TokenData, options?: TokenStorageOptions) => Promise<void>;
22
28
  export declare const loadTokens: (options?: TokenStorageOptions) => Promise<TokenData | null>;
23
29
  export declare const saveAuthConfig: (config: AuthConfig, options?: TokenStorageOptions) => Promise<void>;
@@ -9,6 +9,18 @@ const getTokenFile = (options) => {
9
9
  return path.join(getQuonfigDir(options), filename);
10
10
  };
11
11
  const getConfigFile = (options) => path.join(getQuonfigDir(options), 'config');
12
+ /**
13
+ * Find a workspace ID by slug across all saved profiles.
14
+ * Used to resolve QUONFIG_WORKSPACE and --workspace flag values.
15
+ */
16
+ export const resolveWorkspaceId = (config, slug) => {
17
+ for (const profile of Object.values(config.profiles)) {
18
+ if (profile.workspaceSlug === slug || profile.workspaceName === slug) {
19
+ return profile.workspace;
20
+ }
21
+ }
22
+ return undefined;
23
+ };
12
24
  const ensureQuonfigDir = async (options) => {
13
25
  const quonfigDir = getQuonfigDir(options);
14
26
  try {
@@ -52,6 +64,9 @@ export const saveAuthConfig = async (config, options) => {
52
64
  if (profileData.workspaceSlug) {
53
65
  configContent += `workspace_slug = ${profileData.workspaceSlug}\n`;
54
66
  }
67
+ if (profileData.organizationName) {
68
+ configContent += `organization_name = ${profileData.organizationName}\n`;
69
+ }
55
70
  configContent += '\n';
56
71
  }
57
72
  const configTarget = getConfigFile(options);
@@ -78,12 +93,14 @@ export const loadAuthConfig = async (options) => {
78
93
  const block = match[2];
79
94
  const workspaceMatch = block.match(/workspace\s*=\s*([^\s#]+)(?:\s*#\s*(.+))?/);
80
95
  const slugMatch = block.match(/workspace_slug\s*=\s*(\S+)/);
96
+ const orgMatch = block.match(/organization_name\s*=\s*(.+)/);
81
97
  if (!workspaceMatch)
82
98
  continue;
83
99
  config.profiles[profileName] = {
84
100
  workspace: workspaceMatch[1],
85
101
  workspaceName: workspaceMatch[2]?.trim(),
86
102
  workspaceSlug: slugMatch?.[1]?.trim(),
103
+ organizationName: orgMatch?.[1]?.trim(),
87
104
  };
88
105
  }
89
106
  return Object.keys(config.profiles).length > 0 ? config : null;
@@ -387,12 +387,22 @@ export function validateWorkspace(workspaceDir) {
387
387
  }
388
388
  segmentKeys.add(config.key);
389
389
  }
390
- if (config.type === "log_level" && config.valueType !== "log_level") {
391
- issues.push({
392
- file: relPath,
393
- message: `Log level must have valueType "log_level", got "${config.valueType}"`,
394
- severity: "error",
395
- });
390
+ if (config.type === "log_level") {
391
+ if (config.valueType !== "log_level") {
392
+ issues.push({
393
+ file: relPath,
394
+ message: `Log level must have valueType "log_level", got "${config.valueType}"`,
395
+ severity: "error",
396
+ });
397
+ }
398
+ if (!config.key.startsWith("log-level.")) {
399
+ issues.push({
400
+ file: relPath,
401
+ message: `Log level key "${config.key}" must start with "log-level."`,
402
+ severity: "error",
403
+ suggestion: `Rename to "log-level.${config.key}" (and rename the file to "log-level.${config.key}.json")`,
404
+ });
405
+ }
396
406
  }
397
407
  // Validate environment IDs are slugs, not UUIDs
398
408
  validateEnvironmentIds(config.environments, relPath, issues);
@@ -661,8 +671,18 @@ export function validateFileMap(files) {
661
671
  }
662
672
  segmentKeys.add(config.key);
663
673
  }
664
- if (config.type === "log_level" && config.valueType !== "log_level") {
665
- issues.push({ file: relPath, message: `Log level must have valueType "log_level", got "${config.valueType}"`, severity: "error" });
674
+ if (config.type === "log_level") {
675
+ if (config.valueType !== "log_level") {
676
+ issues.push({ file: relPath, message: `Log level must have valueType "log_level", got "${config.valueType}"`, severity: "error" });
677
+ }
678
+ if (!config.key.startsWith("log-level.")) {
679
+ issues.push({
680
+ file: relPath,
681
+ message: `Log level key "${config.key}" must start with "log-level."`,
682
+ severity: "error",
683
+ suggestion: `Rename to "log-level.${config.key}" (and rename the file to "log-level.${config.key}.json")`,
684
+ });
685
+ }
666
686
  }
667
687
  validateEnvironmentIds(config.environments, relPath, issues);
668
688
  validateRules(config.default.rules, relPath, "default", config.valueType, issues, config.variants);