@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.
- package/dist/auth/credentials.d.ts +29 -0
- package/dist/auth/credentials.d.ts.map +1 -0
- package/dist/auth/credentials.js +79 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +74 -0
- package/dist/commands/api-keys.d.ts +4 -0
- package/dist/commands/api-keys.d.ts.map +1 -0
- package/dist/commands/api-keys.js +127 -0
- package/dist/commands/context.d.ts +4 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +92 -0
- package/dist/commands/customers.d.ts +4 -0
- package/dist/commands/customers.d.ts.map +1 -0
- package/dist/commands/customers.js +120 -0
- package/dist/commands/experiments.d.ts +4 -0
- package/dist/commands/experiments.d.ts.map +1 -0
- package/dist/commands/experiments.js +177 -0
- package/dist/commands/extensions.d.ts +4 -0
- package/dist/commands/extensions.d.ts.map +1 -0
- package/dist/commands/extensions.js +94 -0
- package/dist/commands/files.d.ts +4 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/files.js +143 -0
- package/dist/commands/locales.d.ts +4 -0
- package/dist/commands/locales.d.ts.map +1 -0
- package/dist/commands/locales.js +126 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +125 -0
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +16 -0
- package/dist/commands/media.d.ts +4 -0
- package/dist/commands/media.d.ts.map +1 -0
- package/dist/commands/media.js +44 -0
- package/dist/commands/models.d.ts +4 -0
- package/dist/commands/models.d.ts.map +1 -0
- package/dist/commands/models.js +155 -0
- package/dist/commands/notes.d.ts +4 -0
- package/dist/commands/notes.d.ts.map +1 -0
- package/dist/commands/notes.js +120 -0
- package/dist/commands/notifications.d.ts +4 -0
- package/dist/commands/notifications.d.ts.map +1 -0
- package/dist/commands/notifications.js +73 -0
- package/dist/commands/operations.d.ts +4 -0
- package/dist/commands/operations.d.ts.map +1 -0
- package/dist/commands/operations.js +161 -0
- package/dist/commands/records.d.ts +4 -0
- package/dist/commands/records.d.ts.map +1 -0
- package/dist/commands/records.js +216 -0
- package/dist/commands/schedules.d.ts +4 -0
- package/dist/commands/schedules.d.ts.map +1 -0
- package/dist/commands/schedules.js +150 -0
- package/dist/commands/search.d.ts +4 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +60 -0
- package/dist/commands/segments.d.ts +4 -0
- package/dist/commands/segments.d.ts.map +1 -0
- package/dist/commands/segments.js +143 -0
- package/dist/commands/select-project.d.ts +4 -0
- package/dist/commands/select-project.d.ts.map +1 -0
- package/dist/commands/select-project.js +144 -0
- package/dist/commands/settings.d.ts +4 -0
- package/dist/commands/settings.d.ts.map +1 -0
- package/dist/commands/settings.js +113 -0
- package/dist/commands/variant-catalog.d.ts +4 -0
- package/dist/commands/variant-catalog.d.ts.map +1 -0
- package/dist/commands/variant-catalog.js +111 -0
- package/dist/commands/whoami.d.ts +4 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +50 -0
- package/dist/graphql/queries.d.ts +101 -0
- package/dist/graphql/queries.d.ts.map +1 -0
- package/dist/graphql/queries.js +373 -0
- package/dist/lib/client.d.ts +17 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +61 -0
- package/dist/lib/config.d.ts +12 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +8 -0
- package/dist/lib/errors.d.ts +6 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +62 -0
- package/dist/lib/input.d.ts +36 -0
- package/dist/lib/input.d.ts.map +1 -0
- package/dist/lib/input.js +106 -0
- package/dist/lib/output.d.ts +31 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +107 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|