@eide/foir-cli 0.1.0

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 (91) hide show
  1. package/dist/auth/credentials.d.ts +29 -0
  2. package/dist/auth/credentials.d.ts.map +1 -0
  3. package/dist/auth/credentials.js +79 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +74 -0
  7. package/dist/commands/api-keys.d.ts +4 -0
  8. package/dist/commands/api-keys.d.ts.map +1 -0
  9. package/dist/commands/api-keys.js +127 -0
  10. package/dist/commands/context.d.ts +4 -0
  11. package/dist/commands/context.d.ts.map +1 -0
  12. package/dist/commands/context.js +92 -0
  13. package/dist/commands/customers.d.ts +4 -0
  14. package/dist/commands/customers.d.ts.map +1 -0
  15. package/dist/commands/customers.js +120 -0
  16. package/dist/commands/experiments.d.ts +4 -0
  17. package/dist/commands/experiments.d.ts.map +1 -0
  18. package/dist/commands/experiments.js +177 -0
  19. package/dist/commands/extensions.d.ts +4 -0
  20. package/dist/commands/extensions.d.ts.map +1 -0
  21. package/dist/commands/extensions.js +94 -0
  22. package/dist/commands/files.d.ts +4 -0
  23. package/dist/commands/files.d.ts.map +1 -0
  24. package/dist/commands/files.js +143 -0
  25. package/dist/commands/locales.d.ts +4 -0
  26. package/dist/commands/locales.d.ts.map +1 -0
  27. package/dist/commands/locales.js +126 -0
  28. package/dist/commands/login.d.ts +4 -0
  29. package/dist/commands/login.d.ts.map +1 -0
  30. package/dist/commands/login.js +125 -0
  31. package/dist/commands/logout.d.ts +4 -0
  32. package/dist/commands/logout.d.ts.map +1 -0
  33. package/dist/commands/logout.js +16 -0
  34. package/dist/commands/media.d.ts +4 -0
  35. package/dist/commands/media.d.ts.map +1 -0
  36. package/dist/commands/media.js +44 -0
  37. package/dist/commands/models.d.ts +4 -0
  38. package/dist/commands/models.d.ts.map +1 -0
  39. package/dist/commands/models.js +155 -0
  40. package/dist/commands/notes.d.ts +4 -0
  41. package/dist/commands/notes.d.ts.map +1 -0
  42. package/dist/commands/notes.js +120 -0
  43. package/dist/commands/notifications.d.ts +4 -0
  44. package/dist/commands/notifications.d.ts.map +1 -0
  45. package/dist/commands/notifications.js +73 -0
  46. package/dist/commands/operations.d.ts +4 -0
  47. package/dist/commands/operations.d.ts.map +1 -0
  48. package/dist/commands/operations.js +161 -0
  49. package/dist/commands/records.d.ts +4 -0
  50. package/dist/commands/records.d.ts.map +1 -0
  51. package/dist/commands/records.js +216 -0
  52. package/dist/commands/schedules.d.ts +4 -0
  53. package/dist/commands/schedules.d.ts.map +1 -0
  54. package/dist/commands/schedules.js +150 -0
  55. package/dist/commands/search.d.ts +4 -0
  56. package/dist/commands/search.d.ts.map +1 -0
  57. package/dist/commands/search.js +60 -0
  58. package/dist/commands/segments.d.ts +4 -0
  59. package/dist/commands/segments.d.ts.map +1 -0
  60. package/dist/commands/segments.js +143 -0
  61. package/dist/commands/select-project.d.ts +4 -0
  62. package/dist/commands/select-project.d.ts.map +1 -0
  63. package/dist/commands/select-project.js +144 -0
  64. package/dist/commands/settings.d.ts +4 -0
  65. package/dist/commands/settings.d.ts.map +1 -0
  66. package/dist/commands/settings.js +113 -0
  67. package/dist/commands/variant-catalog.d.ts +4 -0
  68. package/dist/commands/variant-catalog.d.ts.map +1 -0
  69. package/dist/commands/variant-catalog.js +111 -0
  70. package/dist/commands/whoami.d.ts +4 -0
  71. package/dist/commands/whoami.d.ts.map +1 -0
  72. package/dist/commands/whoami.js +50 -0
  73. package/dist/graphql/queries.d.ts +101 -0
  74. package/dist/graphql/queries.d.ts.map +1 -0
  75. package/dist/graphql/queries.js +373 -0
  76. package/dist/lib/client.d.ts +17 -0
  77. package/dist/lib/client.d.ts.map +1 -0
  78. package/dist/lib/client.js +61 -0
  79. package/dist/lib/config.d.ts +12 -0
  80. package/dist/lib/config.d.ts.map +1 -0
  81. package/dist/lib/config.js +8 -0
  82. package/dist/lib/errors.d.ts +6 -0
  83. package/dist/lib/errors.d.ts.map +1 -0
  84. package/dist/lib/errors.js +62 -0
  85. package/dist/lib/input.d.ts +36 -0
  86. package/dist/lib/input.d.ts.map +1 -0
  87. package/dist/lib/input.js +106 -0
  88. package/dist/lib/output.d.ts +31 -0
  89. package/dist/lib/output.d.ts.map +1 -0
  90. package/dist/lib/output.js +107 -0
  91. package/package.json +59 -0
@@ -0,0 +1,29 @@
1
+ export interface StoredCredentials {
2
+ accessToken: string;
3
+ refreshToken: string;
4
+ expiresAt: string;
5
+ user: {
6
+ id: string;
7
+ email: string;
8
+ name: string;
9
+ };
10
+ selectedProject?: {
11
+ id: string;
12
+ name: string;
13
+ tenantId: string;
14
+ apiKey?: string;
15
+ apiKeyId?: string;
16
+ };
17
+ }
18
+ export declare function getCredentialsDir(): string;
19
+ export declare function getCredentialsPath(): string;
20
+ export declare function getCredentials(): Promise<StoredCredentials | null>;
21
+ export declare function writeCredentials(credentials: StoredCredentials): Promise<void>;
22
+ export declare function updateCredentials(updates: Partial<StoredCredentials>): Promise<void>;
23
+ export declare function deleteCredentials(): Promise<void>;
24
+ export declare function isTokenExpired(credentials: StoredCredentials): boolean;
25
+ export declare function getValidCredentials(refreshFn?: (refreshToken: string) => Promise<{
26
+ accessToken: string;
27
+ expiresAt: string;
28
+ }>): Promise<StoredCredentials | null>;
29
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/auth/credentials.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,eAAe,CAAC,EAAE;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAaD,wBAAsB,cAAc,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAUxE;AAED,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQvD;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAItE;AAED,wBAAsB,mBAAmB,CACvC,SAAS,CAAC,EAAE,CACV,YAAY,EAAE,MAAM,KACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,GACvD,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAoBnC"}
@@ -0,0 +1,79 @@
1
+ import { promises as fs } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ export function getCredentialsDir() {
5
+ return join(homedir(), '.foir');
6
+ }
7
+ export function getCredentialsPath() {
8
+ return join(getCredentialsDir(), 'credentials.json');
9
+ }
10
+ async function ensureCredentialsDir() {
11
+ const dir = getCredentialsDir();
12
+ try {
13
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
14
+ }
15
+ catch (error) {
16
+ if (error.code !== 'EEXIST') {
17
+ throw error;
18
+ }
19
+ }
20
+ }
21
+ export async function getCredentials() {
22
+ try {
23
+ const content = await fs.readFile(getCredentialsPath(), 'utf-8');
24
+ return JSON.parse(content);
25
+ }
26
+ catch (error) {
27
+ if (error.code === 'ENOENT') {
28
+ return null;
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ export async function writeCredentials(credentials) {
34
+ await ensureCredentialsDir();
35
+ const path = getCredentialsPath();
36
+ await fs.writeFile(path, JSON.stringify(credentials, null, 2), {
37
+ mode: 0o600,
38
+ });
39
+ }
40
+ export async function updateCredentials(updates) {
41
+ const existing = await getCredentials();
42
+ if (!existing) {
43
+ throw new Error('No credentials found. Run `foir login` first.');
44
+ }
45
+ await writeCredentials({ ...existing, ...updates });
46
+ }
47
+ export async function deleteCredentials() {
48
+ try {
49
+ await fs.unlink(getCredentialsPath());
50
+ }
51
+ catch (error) {
52
+ if (error.code !== 'ENOENT') {
53
+ throw error;
54
+ }
55
+ }
56
+ }
57
+ export function isTokenExpired(credentials) {
58
+ const expiresAt = new Date(credentials.expiresAt);
59
+ const bufferMs = 5 * 60 * 1000;
60
+ return Date.now() > expiresAt.getTime() - bufferMs;
61
+ }
62
+ export async function getValidCredentials(refreshFn) {
63
+ const credentials = await getCredentials();
64
+ if (!credentials) {
65
+ return null;
66
+ }
67
+ if (isTokenExpired(credentials) && refreshFn) {
68
+ try {
69
+ const { accessToken, expiresAt } = await refreshFn(credentials.refreshToken);
70
+ const updated = { ...credentials, accessToken, expiresAt };
71
+ await writeCredentials(updated);
72
+ return updated;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ return credentials;
79
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ import { config } from 'dotenv';
3
+ import { resolve, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ config({ path: resolve(__dirname, '../.env.local') });
8
+ import { Command } from 'commander';
9
+ // Auth commands
10
+ import { registerLoginCommand } from './commands/login.js';
11
+ import { registerLogoutCommand } from './commands/logout.js';
12
+ import { registerSelectProjectCommand } from './commands/select-project.js';
13
+ import { registerWhoamiCommand } from './commands/whoami.js';
14
+ // Resource commands
15
+ import { registerRecordsCommands } from './commands/records.js';
16
+ import { registerModelsCommands } from './commands/models.js';
17
+ import { registerSearchCommands } from './commands/search.js';
18
+ import { registerCustomersCommands } from './commands/customers.js';
19
+ import { registerSegmentsCommands } from './commands/segments.js';
20
+ import { registerExperimentsCommands } from './commands/experiments.js';
21
+ import { registerSettingsCommands } from './commands/settings.js';
22
+ import { registerApiKeysCommands } from './commands/api-keys.js';
23
+ import { registerExtensionsCommands } from './commands/extensions.js';
24
+ import { registerOperationsCommands } from './commands/operations.js';
25
+ import { registerSchedulesCommands } from './commands/schedules.js';
26
+ import { registerMediaCommands } from './commands/media.js';
27
+ import { registerContextCommands } from './commands/context.js';
28
+ import { registerNotificationsCommands } from './commands/notifications.js';
29
+ import { registerLocalesCommands } from './commands/locales.js';
30
+ import { registerFilesCommands } from './commands/files.js';
31
+ import { registerNotesCommands } from './commands/notes.js';
32
+ import { registerVariantCatalogCommands } from './commands/variant-catalog.js';
33
+ const program = new Command();
34
+ program
35
+ .name('foir')
36
+ .description('Universal platform CLI for EIDE')
37
+ .version('0.1.0')
38
+ .option('--api-url <url>', 'API endpoint URL')
39
+ .option('--json', 'Output as JSON')
40
+ .option('--jsonl', 'Output as JSON Lines')
41
+ .option('--quiet', 'Minimal output (IDs only)');
42
+ function getGlobalOpts() {
43
+ const opts = program.opts();
44
+ return {
45
+ apiUrl: opts.apiUrl,
46
+ json: !!opts.json,
47
+ jsonl: !!opts.jsonl,
48
+ quiet: !!opts.quiet,
49
+ };
50
+ }
51
+ // Register all command groups
52
+ registerLoginCommand(program, getGlobalOpts);
53
+ registerLogoutCommand(program, getGlobalOpts);
54
+ registerSelectProjectCommand(program, getGlobalOpts);
55
+ registerWhoamiCommand(program, getGlobalOpts);
56
+ registerRecordsCommands(program, getGlobalOpts);
57
+ registerModelsCommands(program, getGlobalOpts);
58
+ registerSearchCommands(program, getGlobalOpts);
59
+ registerCustomersCommands(program, getGlobalOpts);
60
+ registerSegmentsCommands(program, getGlobalOpts);
61
+ registerExperimentsCommands(program, getGlobalOpts);
62
+ registerSettingsCommands(program, getGlobalOpts);
63
+ registerApiKeysCommands(program, getGlobalOpts);
64
+ registerExtensionsCommands(program, getGlobalOpts);
65
+ registerOperationsCommands(program, getGlobalOpts);
66
+ registerSchedulesCommands(program, getGlobalOpts);
67
+ registerMediaCommands(program, getGlobalOpts);
68
+ registerContextCommands(program, getGlobalOpts);
69
+ registerNotificationsCommands(program, getGlobalOpts);
70
+ registerLocalesCommands(program, getGlobalOpts);
71
+ registerFilesCommands(program, getGlobalOpts);
72
+ registerNotesCommands(program, getGlobalOpts);
73
+ registerVariantCatalogCommands(program, getGlobalOpts);
74
+ program.parse();
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerApiKeysCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=api-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-keys.d.ts","sourceRoot":"","sources":["../../src/commands/api-keys.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAYtD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CA6KN"}
@@ -0,0 +1,127 @@
1
+ import chalk from 'chalk';
2
+ import { withErrorHandler } from '../lib/errors.js';
3
+ import { createClient } from '../lib/client.js';
4
+ import { formatOutput, formatList, timeAgo, success } from '../lib/output.js';
5
+ import { confirmAction } from '../lib/input.js';
6
+ import { LIST_API_KEYS, CREATE_API_KEY, ROTATE_API_KEY, REVOKE_API_KEY, } from '../graphql/queries.js';
7
+ export function registerApiKeysCommands(program, globalOpts) {
8
+ const apiKeys = program.command('api-keys').description('Manage API keys');
9
+ // list
10
+ apiKeys
11
+ .command('list')
12
+ .description('List API keys')
13
+ .option('--include-inactive', 'Include revoked/inactive keys')
14
+ .option('--search <term>', 'Search by name')
15
+ .option('--limit <n>', 'Max results', '50')
16
+ .action(withErrorHandler(globalOpts, async (cmdOpts) => {
17
+ const opts = globalOpts();
18
+ const client = await createClient(opts);
19
+ const data = await client.request(LIST_API_KEYS, {
20
+ includeInactive: !!cmdOpts.includeInactive,
21
+ search: cmdOpts.search,
22
+ limit: parseInt(String(cmdOpts.limit ?? '50'), 10),
23
+ });
24
+ formatList(data.listApiKeys.apiKeys, opts, {
25
+ columns: [
26
+ { key: 'id', header: 'ID', width: 28 },
27
+ { key: 'name', header: 'Name', width: 24 },
28
+ { key: 'keyPrefix', header: 'Prefix', width: 12 },
29
+ {
30
+ key: 'isActive',
31
+ header: 'Active',
32
+ width: 8,
33
+ format: (v) => (v ? 'yes' : 'no'),
34
+ },
35
+ {
36
+ key: 'scopes',
37
+ header: 'Scopes',
38
+ width: 30,
39
+ format: (v) => (Array.isArray(v) ? v.join(', ') : ''),
40
+ },
41
+ {
42
+ key: 'lastUsedAt',
43
+ header: 'Last Used',
44
+ width: 12,
45
+ format: (v) => timeAgo(v),
46
+ },
47
+ ],
48
+ total: data.listApiKeys.total,
49
+ });
50
+ }));
51
+ // create
52
+ apiKeys
53
+ .command('create')
54
+ .description('Create a new API key')
55
+ .requiredOption('--name <name>', 'API key name')
56
+ .requiredOption('--scopes <scopes>', 'Comma-separated scopes (e.g. records:read,records:write)')
57
+ .option('--project-id <id>', 'Project ID (defaults to selected project)')
58
+ .action(withErrorHandler(globalOpts, async (cmdOpts) => {
59
+ const opts = globalOpts();
60
+ const client = await createClient(opts);
61
+ const scopes = cmdOpts.scopes.split(',').map((s) => s.trim());
62
+ const input = {
63
+ name: cmdOpts.name,
64
+ scopes,
65
+ };
66
+ if (cmdOpts.projectId)
67
+ input.projectId = cmdOpts.projectId;
68
+ const data = await client.request(CREATE_API_KEY, { input });
69
+ if (opts.json || opts.jsonl) {
70
+ formatOutput(data.createApiKey, opts);
71
+ }
72
+ else {
73
+ success(`Created API key: ${data.createApiKey.apiKey.name}`);
74
+ console.log('');
75
+ console.log(chalk.yellow(' Save this key now — it cannot be retrieved later:'));
76
+ console.log(chalk.bold(` ${data.createApiKey.plainKey}`));
77
+ if (data.createApiKey.warning) {
78
+ console.log(chalk.gray(` ${data.createApiKey.warning}`));
79
+ }
80
+ }
81
+ }));
82
+ // rotate
83
+ apiKeys
84
+ .command('rotate <id>')
85
+ .description('Rotate an API key (generates new secret)')
86
+ .option('--confirm', 'Skip confirmation prompt')
87
+ .action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
88
+ const opts = globalOpts();
89
+ const confirmed = await confirmAction(`Rotate API key ${id}? The old key will stop working.`, { confirm: !!cmdOpts.confirm });
90
+ if (!confirmed) {
91
+ console.log('Aborted.');
92
+ return;
93
+ }
94
+ const client = await createClient(opts);
95
+ const data = await client.request(ROTATE_API_KEY, { id });
96
+ if (opts.json || opts.jsonl) {
97
+ formatOutput(data.rotateApiKey, opts);
98
+ }
99
+ else {
100
+ success(`Rotated API key: ${data.rotateApiKey.apiKey.name}`);
101
+ console.log('');
102
+ console.log(chalk.yellow(' Save this key now — it cannot be retrieved later:'));
103
+ console.log(chalk.bold(` ${data.rotateApiKey.plainKey}`));
104
+ }
105
+ }));
106
+ // revoke
107
+ apiKeys
108
+ .command('revoke <id>')
109
+ .description('Revoke an API key')
110
+ .option('--confirm', 'Skip confirmation prompt')
111
+ .action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
112
+ const opts = globalOpts();
113
+ const confirmed = await confirmAction(`Revoke API key ${id}? This cannot be undone.`, { confirm: !!cmdOpts.confirm });
114
+ if (!confirmed) {
115
+ console.log('Aborted.');
116
+ return;
117
+ }
118
+ const client = await createClient(opts);
119
+ const data = await client.request(REVOKE_API_KEY, { id });
120
+ if (opts.json || opts.jsonl) {
121
+ formatOutput(data.revokeApiKey, opts);
122
+ }
123
+ else {
124
+ success(`Revoked API key ${id}`);
125
+ }
126
+ }));
127
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import { type GlobalOptions } from '../lib/config.js';
3
+ export declare function registerContextCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/commands/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AA8B1B,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAmIN"}
@@ -0,0 +1,92 @@
1
+ import { getCredentials, updateCredentials } from '../auth/credentials.js';
2
+ import { getApiUrl, getGraphQLEndpoint, } from '../lib/config.js';
3
+ import { withErrorHandler } from '../lib/errors.js';
4
+ import { formatList, success } from '../lib/output.js';
5
+ async function gqlRequest(apiUrl, accessToken, query, extraHeaders) {
6
+ const response = await fetch(getGraphQLEndpoint(apiUrl), {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ Authorization: `Bearer ${accessToken}`,
11
+ ...extraHeaders,
12
+ },
13
+ body: JSON.stringify({ query }),
14
+ });
15
+ if (!response.ok)
16
+ throw new Error(`Request failed: ${response.statusText}`);
17
+ const result = (await response.json());
18
+ if (result.errors?.length)
19
+ throw new Error(result.errors[0].message);
20
+ return result.data;
21
+ }
22
+ export function registerContextCommands(program, globalOpts) {
23
+ const context = program
24
+ .command('context')
25
+ .description('Manage project and tenant context');
26
+ // projects list
27
+ context
28
+ .command('projects')
29
+ .description('List available projects')
30
+ .action(withErrorHandler(globalOpts, async () => {
31
+ const opts = globalOpts();
32
+ const credentials = await getCredentials();
33
+ if (!credentials)
34
+ throw new Error('Not logged in. Run `foir login` first.');
35
+ const apiUrl = getApiUrl(opts);
36
+ const data = await gqlRequest(apiUrl, credentials.accessToken, `query { sessionContext { projectId availableProjects { id name tenantId } } }`);
37
+ const projects = data.sessionContext.availableProjects.map((p) => ({
38
+ ...p,
39
+ current: p.id === credentials.selectedProject?.id ? '*' : '',
40
+ }));
41
+ formatList(projects, opts, {
42
+ columns: [
43
+ { key: 'current', header: '', width: 2 },
44
+ { key: 'id', header: 'ID', width: 28 },
45
+ { key: 'name', header: 'Name', width: 30 },
46
+ { key: 'tenantId', header: 'Tenant', width: 28 },
47
+ ],
48
+ });
49
+ }));
50
+ // switch project
51
+ context
52
+ .command('switch <projectId>')
53
+ .description('Switch to a different project')
54
+ .action(withErrorHandler(globalOpts, async (projectId) => {
55
+ const opts = globalOpts();
56
+ const credentials = await getCredentials();
57
+ if (!credentials)
58
+ throw new Error('Not logged in. Run `foir login` first.');
59
+ const apiUrl = getApiUrl(opts);
60
+ const data = await gqlRequest(apiUrl, credentials.accessToken, `query { sessionContext { availableProjects { id name tenantId } } }`);
61
+ const project = data.sessionContext.availableProjects.find((p) => p.id === projectId);
62
+ if (!project)
63
+ throw new Error(`Project "${projectId}" not found.`);
64
+ await updateCredentials({
65
+ selectedProject: {
66
+ id: project.id,
67
+ name: project.name,
68
+ tenantId: project.tenantId,
69
+ },
70
+ });
71
+ success(`Switched to project: ${project.name}`);
72
+ console.log('Run `foir select-project` to provision a new API key for this project.');
73
+ }));
74
+ // tenants
75
+ context
76
+ .command('tenants')
77
+ .description('List available tenants')
78
+ .action(withErrorHandler(globalOpts, async () => {
79
+ const opts = globalOpts();
80
+ const credentials = await getCredentials();
81
+ if (!credentials)
82
+ throw new Error('Not logged in. Run `foir login` first.');
83
+ const apiUrl = getApiUrl(opts);
84
+ const data = await gqlRequest(apiUrl, credentials.accessToken, `query { sessionContext { availableTenants { id name } } }`);
85
+ formatList(data.sessionContext.availableTenants, opts, {
86
+ columns: [
87
+ { key: 'id', header: 'ID', width: 28 },
88
+ { key: 'name', header: 'Name', width: 30 },
89
+ ],
90
+ });
91
+ }));
92
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerCustomersCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=customers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customers.d.ts","sourceRoot":"","sources":["../../src/commands/customers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAatD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAqJN"}
@@ -0,0 +1,120 @@
1
+ import { withErrorHandler } from '../lib/errors.js';
2
+ import { createClient } from '../lib/client.js';
3
+ import { formatOutput, formatList, timeAgo, success } from '../lib/output.js';
4
+ import { confirmAction } from '../lib/input.js';
5
+ import { CUSTOMERS, CUSTOMER, CUSTOMER_BY_EMAIL, CREATE_CUSTOMER, DELETE_CUSTOMER, } from '../graphql/queries.js';
6
+ export function registerCustomersCommands(program, globalOpts) {
7
+ const customers = program
8
+ .command('customers')
9
+ .description('Manage customers');
10
+ // list
11
+ customers
12
+ .command('list')
13
+ .description('List customers')
14
+ .option('--status <status>', 'Filter by status (ACTIVE, PENDING, SUSPENDED)')
15
+ .option('--search <term>', 'Search by email')
16
+ .option('--limit <n>', 'Max results', '20')
17
+ .option('--offset <n>', 'Skip results', '0')
18
+ .action(withErrorHandler(globalOpts, async (cmdOpts) => {
19
+ const opts = globalOpts();
20
+ const client = await createClient(opts);
21
+ const data = await client.request(CUSTOMERS, {
22
+ status: cmdOpts.status,
23
+ search: cmdOpts.search,
24
+ limit: parseInt(cmdOpts.limit ?? '20', 10),
25
+ offset: parseInt(cmdOpts.offset ?? '0', 10),
26
+ });
27
+ formatList(data.customers.customers, opts, {
28
+ columns: [
29
+ { key: 'id', header: 'ID', width: 28 },
30
+ { key: 'email', header: 'Email', width: 30 },
31
+ { key: 'status', header: 'Status', width: 12 },
32
+ {
33
+ key: 'lastLoginAt',
34
+ header: 'Last Login',
35
+ width: 12,
36
+ format: (v) => timeAgo(v),
37
+ },
38
+ {
39
+ key: 'createdAt',
40
+ header: 'Created',
41
+ width: 12,
42
+ format: (v) => timeAgo(v),
43
+ },
44
+ ],
45
+ total: data.customers.total,
46
+ });
47
+ }));
48
+ // get
49
+ customers
50
+ .command('get <idOrEmail>')
51
+ .description('Get a customer by ID or email')
52
+ .action(withErrorHandler(globalOpts, async (idOrEmail) => {
53
+ const opts = globalOpts();
54
+ const client = await createClient(opts);
55
+ let result;
56
+ if (idOrEmail.includes('@')) {
57
+ const data = await client.request(CUSTOMER_BY_EMAIL, { email: idOrEmail });
58
+ result = data.customerByEmail;
59
+ }
60
+ else {
61
+ const data = await client.request(CUSTOMER, { id: idOrEmail });
62
+ result = data.customer;
63
+ }
64
+ if (!result) {
65
+ throw new Error(`Customer "${idOrEmail}" not found.`);
66
+ }
67
+ formatOutput(result, opts);
68
+ }));
69
+ // create
70
+ customers
71
+ .command('create')
72
+ .description('Create a new customer')
73
+ .option('--email <email>', 'Customer email (required)')
74
+ .option('-d, --data <json>', 'Additional data as JSON')
75
+ .action(withErrorHandler(globalOpts, async (cmdOpts) => {
76
+ const opts = globalOpts();
77
+ const client = await createClient(opts);
78
+ let input = {};
79
+ if (cmdOpts.data) {
80
+ try {
81
+ input = JSON.parse(cmdOpts.data);
82
+ }
83
+ catch {
84
+ throw new Error('Invalid JSON in --data. Provide valid JSON, e.g. --data \'{"name":"Jo"}\'');
85
+ }
86
+ }
87
+ if (cmdOpts.email) {
88
+ input.email = cmdOpts.email;
89
+ }
90
+ if (!input.email) {
91
+ throw new Error('--email is required.');
92
+ }
93
+ const data = await client.request(CREATE_CUSTOMER, { input });
94
+ formatOutput(data.createCustomer, opts);
95
+ if (!(opts.json || opts.jsonl || opts.quiet)) {
96
+ success(`Created customer ${data.createCustomer.email}`);
97
+ }
98
+ }));
99
+ // delete
100
+ customers
101
+ .command('delete <id>')
102
+ .description('Delete a customer (hard delete for GDPR)')
103
+ .option('--confirm', 'Skip confirmation prompt')
104
+ .action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
105
+ const opts = globalOpts();
106
+ const confirmed = await confirmAction(`Delete customer ${id}? This cannot be undone.`, { confirm: !!cmdOpts.confirm });
107
+ if (!confirmed) {
108
+ console.log('Aborted.');
109
+ return;
110
+ }
111
+ const client = await createClient(opts);
112
+ const data = await client.request(DELETE_CUSTOMER, { id });
113
+ if (opts.json || opts.jsonl) {
114
+ formatOutput(data.deleteCustomer, opts);
115
+ }
116
+ else {
117
+ success(`Deleted customer ${id}`);
118
+ }
119
+ }));
120
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerExperimentsCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=experiments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiments.d.ts","sourceRoot":"","sources":["../../src/commands/experiments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAmBtD,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAmON"}