@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,143 @@
|
|
|
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 { parseInputData, isUUID, confirmAction } from '../lib/input.js';
|
|
5
|
+
import { SEGMENTS, SEGMENT, SEGMENT_BY_KEY, CREATE_SEGMENT, UPDATE_SEGMENT, DELETE_SEGMENT, PREVIEW_SEGMENT_RULES, TEST_SEGMENT_EVALUATION, } from '../graphql/queries.js';
|
|
6
|
+
export function registerSegmentsCommands(program, globalOpts) {
|
|
7
|
+
const segments = program.command('segments').description('Manage segments');
|
|
8
|
+
// list
|
|
9
|
+
segments
|
|
10
|
+
.command('list')
|
|
11
|
+
.description('List segments')
|
|
12
|
+
.option('--active', 'Only active segments')
|
|
13
|
+
.option('--limit <n>', 'Max results', '50')
|
|
14
|
+
.option('--offset <n>', 'Skip results', '0')
|
|
15
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
16
|
+
const opts = globalOpts();
|
|
17
|
+
const client = await createClient(opts);
|
|
18
|
+
const data = await client.request(SEGMENTS, {
|
|
19
|
+
isActive: cmdOpts.active ? true : undefined,
|
|
20
|
+
limit: parseInt(String(cmdOpts.limit ?? '50'), 10),
|
|
21
|
+
offset: parseInt(String(cmdOpts.offset ?? '0'), 10),
|
|
22
|
+
});
|
|
23
|
+
formatList(data.segments, opts, {
|
|
24
|
+
columns: [
|
|
25
|
+
{ key: 'id', header: 'ID', width: 28 },
|
|
26
|
+
{ key: 'key', header: 'Key', width: 20 },
|
|
27
|
+
{ key: 'name', header: 'Name', width: 24 },
|
|
28
|
+
{ key: 'memberCount', header: 'Members', width: 10 },
|
|
29
|
+
{
|
|
30
|
+
key: 'isActive',
|
|
31
|
+
header: 'Active',
|
|
32
|
+
width: 8,
|
|
33
|
+
format: (v) => (v ? 'yes' : 'no'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'updatedAt',
|
|
37
|
+
header: 'Updated',
|
|
38
|
+
width: 12,
|
|
39
|
+
format: (v) => timeAgo(v),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
}));
|
|
44
|
+
// get
|
|
45
|
+
segments
|
|
46
|
+
.command('get <idOrKey>')
|
|
47
|
+
.description('Get a segment by ID or key')
|
|
48
|
+
.action(withErrorHandler(globalOpts, async (idOrKey) => {
|
|
49
|
+
const opts = globalOpts();
|
|
50
|
+
const client = await createClient(opts);
|
|
51
|
+
let result;
|
|
52
|
+
if (isUUID(idOrKey)) {
|
|
53
|
+
const data = await client.request(SEGMENT, { id: idOrKey });
|
|
54
|
+
result = data.segment;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const data = await client.request(SEGMENT_BY_KEY, { key: idOrKey });
|
|
58
|
+
result = data.segmentByKey;
|
|
59
|
+
}
|
|
60
|
+
if (!result)
|
|
61
|
+
throw new Error(`Segment "${idOrKey}" not found.`);
|
|
62
|
+
formatOutput(result, opts);
|
|
63
|
+
}));
|
|
64
|
+
// create
|
|
65
|
+
segments
|
|
66
|
+
.command('create')
|
|
67
|
+
.description('Create a new segment')
|
|
68
|
+
.option('-d, --data <json>', 'Segment data as JSON')
|
|
69
|
+
.option('-f, --file <path>', 'Read data from file')
|
|
70
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
71
|
+
const opts = globalOpts();
|
|
72
|
+
const client = await createClient(opts);
|
|
73
|
+
const input = await parseInputData(cmdOpts);
|
|
74
|
+
const data = await client.request(CREATE_SEGMENT, { input });
|
|
75
|
+
formatOutput(data.createSegment, opts);
|
|
76
|
+
if (!(opts.json || opts.jsonl || opts.quiet))
|
|
77
|
+
success(`Created segment ${data.createSegment.key}`);
|
|
78
|
+
}));
|
|
79
|
+
// update
|
|
80
|
+
segments
|
|
81
|
+
.command('update <id>')
|
|
82
|
+
.description('Update a segment')
|
|
83
|
+
.option('-d, --data <json>', 'Segment data as JSON')
|
|
84
|
+
.option('-f, --file <path>', 'Read data from file')
|
|
85
|
+
.action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
|
|
86
|
+
const opts = globalOpts();
|
|
87
|
+
const client = await createClient(opts);
|
|
88
|
+
const input = await parseInputData(cmdOpts);
|
|
89
|
+
const data = await client.request(UPDATE_SEGMENT, { id, input });
|
|
90
|
+
formatOutput(data.updateSegment, opts);
|
|
91
|
+
if (!(opts.json || opts.jsonl || opts.quiet))
|
|
92
|
+
success(`Updated segment ${id}`);
|
|
93
|
+
}));
|
|
94
|
+
// delete
|
|
95
|
+
segments
|
|
96
|
+
.command('delete <id>')
|
|
97
|
+
.description('Delete a segment')
|
|
98
|
+
.option('--confirm', 'Skip confirmation prompt')
|
|
99
|
+
.action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
|
|
100
|
+
const opts = globalOpts();
|
|
101
|
+
const confirmed = await confirmAction(`Delete segment ${id}?`, {
|
|
102
|
+
confirm: !!cmdOpts.confirm,
|
|
103
|
+
});
|
|
104
|
+
if (!confirmed) {
|
|
105
|
+
console.log('Aborted.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const client = await createClient(opts);
|
|
109
|
+
await client.request(DELETE_SEGMENT, { id });
|
|
110
|
+
if (opts.json || opts.jsonl)
|
|
111
|
+
formatOutput({ deleted: true, id }, opts);
|
|
112
|
+
else
|
|
113
|
+
success(`Deleted segment ${id}`);
|
|
114
|
+
}));
|
|
115
|
+
// preview
|
|
116
|
+
segments
|
|
117
|
+
.command('preview')
|
|
118
|
+
.description('Preview segment rules against sample customers')
|
|
119
|
+
.option('-f, --file <path>', 'Rules JSON file')
|
|
120
|
+
.option('-d, --data <json>', 'Rules as inline JSON')
|
|
121
|
+
.option('--sample-size <n>', 'Sample size', '100')
|
|
122
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
123
|
+
const opts = globalOpts();
|
|
124
|
+
const client = await createClient(opts);
|
|
125
|
+
const inputData = await parseInputData(cmdOpts);
|
|
126
|
+
const rules = inputData.rules ?? inputData;
|
|
127
|
+
const data = await client.request(PREVIEW_SEGMENT_RULES, {
|
|
128
|
+
rules,
|
|
129
|
+
sampleSize: parseInt(cmdOpts.sampleSize ?? '100', 10),
|
|
130
|
+
});
|
|
131
|
+
formatOutput(data.previewSegmentRules, opts);
|
|
132
|
+
}));
|
|
133
|
+
// test
|
|
134
|
+
segments
|
|
135
|
+
.command('test <segmentId> <customerId>')
|
|
136
|
+
.description('Test whether a customer matches a segment')
|
|
137
|
+
.action(withErrorHandler(globalOpts, async (segmentId, customerId) => {
|
|
138
|
+
const opts = globalOpts();
|
|
139
|
+
const client = await createClient(opts);
|
|
140
|
+
const data = await client.request(TEST_SEGMENT_EVALUATION, { segmentId, customerId });
|
|
141
|
+
formatOutput(data.testSegmentEvaluation, opts);
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select-project.d.ts","sourceRoot":"","sources":["../../src/commands/select-project.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AA+K1B,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAoGN"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { getCredentials, updateCredentials } from '../auth/credentials.js';
|
|
3
|
+
import { getApiUrl, getGraphQLEndpoint, } from '../lib/config.js';
|
|
4
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
5
|
+
const CLI_API_KEY_NAME = 'Foir CLI';
|
|
6
|
+
const CLI_API_KEY_SCOPES = [
|
|
7
|
+
'records:read',
|
|
8
|
+
'records:write',
|
|
9
|
+
'records:delete',
|
|
10
|
+
'records:publish',
|
|
11
|
+
'files:read',
|
|
12
|
+
'files:write',
|
|
13
|
+
'extensions:read',
|
|
14
|
+
'operations:read',
|
|
15
|
+
'operations:execute',
|
|
16
|
+
];
|
|
17
|
+
async function gqlRequest(apiUrl, accessToken, query, variables, extraHeaders) {
|
|
18
|
+
const response = await fetch(getGraphQLEndpoint(apiUrl), {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
Authorization: `Bearer ${accessToken}`,
|
|
23
|
+
...extraHeaders,
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({ query, variables }),
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`GraphQL request failed: ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
const result = (await response.json());
|
|
31
|
+
if (result.errors?.length) {
|
|
32
|
+
throw new Error(`GraphQL error: ${result.errors[0].message}`);
|
|
33
|
+
}
|
|
34
|
+
return result.data;
|
|
35
|
+
}
|
|
36
|
+
async function fetchSessionContext(apiUrl, accessToken) {
|
|
37
|
+
const data = await gqlRequest(apiUrl, accessToken, `query { sessionContext { tenantId projectId availableTenants { id name } availableProjects { id name tenantId } } }`);
|
|
38
|
+
return data.sessionContext;
|
|
39
|
+
}
|
|
40
|
+
async function fetchApiKeys(apiUrl, accessToken, projectId, tenantId) {
|
|
41
|
+
const data = await gqlRequest(apiUrl, accessToken, `query { listApiKeys(includeInactive: false, limit: 100) { apiKeys { id name isActive } } }`, undefined, { 'x-tenant-id': tenantId, 'x-project-id': projectId });
|
|
42
|
+
return data.listApiKeys?.apiKeys ?? [];
|
|
43
|
+
}
|
|
44
|
+
async function createApiKey(apiUrl, accessToken, projectId, tenantId) {
|
|
45
|
+
const data = await gqlRequest(apiUrl, accessToken, `mutation($input: CreateApiKeyInput!) { createApiKey(input: $input) { apiKey { id name isActive } plainKey } }`, {
|
|
46
|
+
input: {
|
|
47
|
+
name: CLI_API_KEY_NAME,
|
|
48
|
+
projectId,
|
|
49
|
+
scopes: CLI_API_KEY_SCOPES,
|
|
50
|
+
},
|
|
51
|
+
}, { 'x-tenant-id': tenantId, 'x-project-id': projectId });
|
|
52
|
+
return data.createApiKey;
|
|
53
|
+
}
|
|
54
|
+
async function rotateApiKey(apiUrl, accessToken, projectId, tenantId, keyId) {
|
|
55
|
+
const data = await gqlRequest(apiUrl, accessToken, `mutation($id: ID!) { rotateApiKey(id: $id) { apiKey { id name isActive } plainKey } }`, { id: keyId }, { 'x-tenant-id': tenantId, 'x-project-id': projectId });
|
|
56
|
+
return data.rotateApiKey;
|
|
57
|
+
}
|
|
58
|
+
async function provisionApiKey(apiUrl, accessToken, projectId, tenantId) {
|
|
59
|
+
const apiKeys = await fetchApiKeys(apiUrl, accessToken, projectId, tenantId);
|
|
60
|
+
const existing = apiKeys.find((k) => k.name === CLI_API_KEY_NAME && k.isActive);
|
|
61
|
+
if (existing) {
|
|
62
|
+
console.log(' Rotating existing CLI API key...');
|
|
63
|
+
const rotated = await rotateApiKey(apiUrl, accessToken, projectId, tenantId, existing.id);
|
|
64
|
+
return { apiKey: rotated.plainKey, apiKeyId: rotated.apiKey.id };
|
|
65
|
+
}
|
|
66
|
+
console.log(' Creating CLI API key...');
|
|
67
|
+
const created = await createApiKey(apiUrl, accessToken, projectId, tenantId);
|
|
68
|
+
return { apiKey: created.plainKey, apiKeyId: created.apiKey.id };
|
|
69
|
+
}
|
|
70
|
+
export function registerSelectProjectCommand(program, globalOpts) {
|
|
71
|
+
program
|
|
72
|
+
.command('select-project')
|
|
73
|
+
.description('Choose which project to work with')
|
|
74
|
+
.option('--project-id <id>', 'Project ID to select directly')
|
|
75
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
76
|
+
const opts = globalOpts();
|
|
77
|
+
const apiUrl = getApiUrl(opts);
|
|
78
|
+
const credentials = await getCredentials();
|
|
79
|
+
if (!credentials) {
|
|
80
|
+
console.log('Not logged in. Run `foir login` first.');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
console.log('Fetching your projects...\n');
|
|
84
|
+
const sessionContext = await fetchSessionContext(apiUrl, credentials.accessToken);
|
|
85
|
+
const { availableTenants: tenants, availableProjects: projects } = sessionContext;
|
|
86
|
+
if (projects.length === 0) {
|
|
87
|
+
console.log('No projects found. Create one in the platform first.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const tenantNameMap = new Map(tenants.map((t) => [t.id, t.name]));
|
|
91
|
+
let selectedProject;
|
|
92
|
+
if (cmdOpts.projectId) {
|
|
93
|
+
const found = projects.find((p) => p.id === cmdOpts.projectId);
|
|
94
|
+
if (!found) {
|
|
95
|
+
console.log(`Project with ID "${cmdOpts.projectId}" not found.`);
|
|
96
|
+
console.log('Available projects:');
|
|
97
|
+
for (const p of projects) {
|
|
98
|
+
console.log(` - ${p.name} (${p.id})`);
|
|
99
|
+
}
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
selectedProject = found;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const byTenant = projects.reduce((acc, p) => {
|
|
106
|
+
const key = tenantNameMap.get(p.tenantId) ?? 'Unknown';
|
|
107
|
+
if (!acc[key])
|
|
108
|
+
acc[key] = [];
|
|
109
|
+
acc[key].push(p);
|
|
110
|
+
return acc;
|
|
111
|
+
}, {});
|
|
112
|
+
const choices = Object.entries(byTenant).flatMap(([tenantName, tenantProjects]) => [
|
|
113
|
+
new inquirer.Separator(`── ${tenantName} ──`),
|
|
114
|
+
...tenantProjects.map((p) => ({
|
|
115
|
+
name: ` ${p.name}`,
|
|
116
|
+
value: p.id,
|
|
117
|
+
short: p.name,
|
|
118
|
+
})),
|
|
119
|
+
]);
|
|
120
|
+
const { projectId } = await inquirer.prompt([
|
|
121
|
+
{
|
|
122
|
+
type: 'list',
|
|
123
|
+
name: 'projectId',
|
|
124
|
+
message: 'Select a project:',
|
|
125
|
+
choices,
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
selectedProject = projects.find((p) => p.id === projectId);
|
|
129
|
+
}
|
|
130
|
+
console.log('\nProvisioning API key for CLI access...');
|
|
131
|
+
const { apiKey, apiKeyId } = await provisionApiKey(apiUrl, credentials.accessToken, selectedProject.id, selectedProject.tenantId);
|
|
132
|
+
await updateCredentials({
|
|
133
|
+
selectedProject: {
|
|
134
|
+
id: selectedProject.id,
|
|
135
|
+
name: selectedProject.name,
|
|
136
|
+
tenantId: selectedProject.tenantId,
|
|
137
|
+
apiKey,
|
|
138
|
+
apiKeyId,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
console.log(`\n✓ Selected project: ${selectedProject.name}`);
|
|
142
|
+
console.log('✓ API key provisioned for CLI access');
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/commands/settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AA0BtD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CA0HN"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
2
|
+
import { createClient } from '../lib/client.js';
|
|
3
|
+
import { formatOutput, formatList, success } from '../lib/output.js';
|
|
4
|
+
import { ALL_SETTINGS, SETTINGS_BY_CATEGORY, SETTING, SET_SETTING, DELETE_SETTING, } from '../graphql/queries.js';
|
|
5
|
+
function inferDataType(value) {
|
|
6
|
+
if (value === 'true' || value === 'false')
|
|
7
|
+
return { dataType: 'BOOLEAN', parsed: value === 'true' };
|
|
8
|
+
const num = Number(value);
|
|
9
|
+
if (!isNaN(num) && value !== '')
|
|
10
|
+
return { dataType: 'NUMBER', parsed: num };
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(value);
|
|
13
|
+
if (typeof parsed === 'object')
|
|
14
|
+
return { dataType: 'JSON', parsed };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* not JSON */
|
|
18
|
+
}
|
|
19
|
+
return { dataType: 'STRING', parsed: value };
|
|
20
|
+
}
|
|
21
|
+
export function registerSettingsCommands(program, globalOpts) {
|
|
22
|
+
const settings = program.command('settings').description('Manage settings');
|
|
23
|
+
// list
|
|
24
|
+
settings
|
|
25
|
+
.command('list')
|
|
26
|
+
.description('List all settings')
|
|
27
|
+
.option('--category <cat>', 'Filter by category')
|
|
28
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
29
|
+
const opts = globalOpts();
|
|
30
|
+
const client = await createClient(opts);
|
|
31
|
+
let items;
|
|
32
|
+
if (cmdOpts.category) {
|
|
33
|
+
const data = await client.request(SETTINGS_BY_CATEGORY, { category: cmdOpts.category });
|
|
34
|
+
items = data.settingsByCategory;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const data = await client.request(ALL_SETTINGS);
|
|
38
|
+
items = data.allSettings;
|
|
39
|
+
}
|
|
40
|
+
formatList(items, opts, {
|
|
41
|
+
columns: [
|
|
42
|
+
{ key: 'key', header: 'Key', width: 28 },
|
|
43
|
+
{
|
|
44
|
+
key: 'value',
|
|
45
|
+
header: 'Value',
|
|
46
|
+
width: 28,
|
|
47
|
+
format: (v) => typeof v === 'object' ? JSON.stringify(v) : String(v ?? ''),
|
|
48
|
+
},
|
|
49
|
+
{ key: 'category', header: 'Category', width: 16 },
|
|
50
|
+
{ key: 'dataType', header: 'Type', width: 10 },
|
|
51
|
+
{
|
|
52
|
+
key: 'isPublic',
|
|
53
|
+
header: 'Public',
|
|
54
|
+
width: 8,
|
|
55
|
+
format: (v) => (v ? 'yes' : 'no'),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
}));
|
|
60
|
+
// get
|
|
61
|
+
settings
|
|
62
|
+
.command('get <key>')
|
|
63
|
+
.description('Get a setting by key')
|
|
64
|
+
.action(withErrorHandler(globalOpts, async (key) => {
|
|
65
|
+
const opts = globalOpts();
|
|
66
|
+
const client = await createClient(opts);
|
|
67
|
+
const data = await client.request(SETTING, { key });
|
|
68
|
+
if (!data.setting)
|
|
69
|
+
throw new Error(`Setting "${key}" not found.`);
|
|
70
|
+
formatOutput(data.setting, opts);
|
|
71
|
+
}));
|
|
72
|
+
// set
|
|
73
|
+
settings
|
|
74
|
+
.command('set <key> <value>')
|
|
75
|
+
.description('Set a setting value')
|
|
76
|
+
.option('--category <cat>', 'Category (required for new settings)')
|
|
77
|
+
.option('--data-type <type>', 'Data type (STRING, NUMBER, BOOLEAN, JSON)')
|
|
78
|
+
.action(withErrorHandler(globalOpts, async (key, value, cmdOpts) => {
|
|
79
|
+
const opts = globalOpts();
|
|
80
|
+
const client = await createClient(opts);
|
|
81
|
+
// Try to fetch existing to get category/dataType
|
|
82
|
+
const existing = await client.request(SETTING, { key });
|
|
83
|
+
const inferred = inferDataType(value);
|
|
84
|
+
const category = cmdOpts.category ?? existing.setting?.category;
|
|
85
|
+
if (!category)
|
|
86
|
+
throw new Error('--category is required for new settings.');
|
|
87
|
+
const dataType = cmdOpts.dataType ?? existing.setting?.dataType ?? inferred.dataType;
|
|
88
|
+
const data = await client.request(SET_SETTING, {
|
|
89
|
+
input: {
|
|
90
|
+
key,
|
|
91
|
+
value: inferred.parsed,
|
|
92
|
+
category,
|
|
93
|
+
dataType,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
formatOutput(data.setSetting, opts);
|
|
97
|
+
if (!(opts.json || opts.jsonl || opts.quiet))
|
|
98
|
+
success(`Set ${key} = ${value}`);
|
|
99
|
+
}));
|
|
100
|
+
// reset (delete)
|
|
101
|
+
settings
|
|
102
|
+
.command('reset <key>')
|
|
103
|
+
.description('Delete a setting (reset to default)')
|
|
104
|
+
.action(withErrorHandler(globalOpts, async (key) => {
|
|
105
|
+
const opts = globalOpts();
|
|
106
|
+
const client = await createClient(opts);
|
|
107
|
+
await client.request(DELETE_SETTING, { key });
|
|
108
|
+
if (opts.json || opts.jsonl)
|
|
109
|
+
formatOutput({ deleted: true, key }, opts);
|
|
110
|
+
else
|
|
111
|
+
success(`Deleted setting ${key}`);
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"variant-catalog.d.ts","sourceRoot":"","sources":["../../src/commands/variant-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AActD,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAgJN"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
2
|
+
import { createClient } from '../lib/client.js';
|
|
3
|
+
import { formatOutput, formatList, success } from '../lib/output.js';
|
|
4
|
+
import { parseInputData, isUUID, confirmAction } from '../lib/input.js';
|
|
5
|
+
import { VARIANT_CATALOG, VARIANT_CATALOG_ENTRY, VARIANT_CATALOG_ENTRY_BY_KEY, CREATE_VARIANT_CATALOG_ENTRY, UPDATE_VARIANT_CATALOG_ENTRY, DELETE_VARIANT_CATALOG_ENTRY, } from '../graphql/queries.js';
|
|
6
|
+
export function registerVariantCatalogCommands(program, globalOpts) {
|
|
7
|
+
const catalog = program
|
|
8
|
+
.command('variant-catalog')
|
|
9
|
+
.description('Manage variant catalog entries (markets, devices, locales)');
|
|
10
|
+
// list
|
|
11
|
+
catalog
|
|
12
|
+
.command('list')
|
|
13
|
+
.description('List variant catalog entries')
|
|
14
|
+
.option('--active', 'Only active entries')
|
|
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(VARIANT_CATALOG, {
|
|
20
|
+
isActive: cmdOpts.active ? true : undefined,
|
|
21
|
+
limit: parseInt(String(cmdOpts.limit ?? '50'), 10),
|
|
22
|
+
});
|
|
23
|
+
formatList(data.variantCatalog, opts, {
|
|
24
|
+
columns: [
|
|
25
|
+
{ key: 'key', header: 'Key', width: 20 },
|
|
26
|
+
{ key: 'name', header: 'Name', width: 24 },
|
|
27
|
+
{
|
|
28
|
+
key: 'isDefault',
|
|
29
|
+
header: 'Default',
|
|
30
|
+
width: 8,
|
|
31
|
+
format: (v) => (v ? 'yes' : ''),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'isActive',
|
|
35
|
+
header: 'Active',
|
|
36
|
+
width: 8,
|
|
37
|
+
format: (v) => (v ? 'yes' : 'no'),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
}));
|
|
42
|
+
// get
|
|
43
|
+
catalog
|
|
44
|
+
.command('get <idOrKey>')
|
|
45
|
+
.description('Get a variant catalog entry by ID or key')
|
|
46
|
+
.action(withErrorHandler(globalOpts, async (idOrKey) => {
|
|
47
|
+
const opts = globalOpts();
|
|
48
|
+
const client = await createClient(opts);
|
|
49
|
+
let result;
|
|
50
|
+
if (isUUID(idOrKey)) {
|
|
51
|
+
const data = await client.request(VARIANT_CATALOG_ENTRY, { id: idOrKey });
|
|
52
|
+
result = data.variantCatalogEntry;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const data = await client.request(VARIANT_CATALOG_ENTRY_BY_KEY, { key: idOrKey });
|
|
56
|
+
result = data.variantCatalogEntryByKey;
|
|
57
|
+
}
|
|
58
|
+
if (!result)
|
|
59
|
+
throw new Error(`Variant catalog entry "${idOrKey}" not found.`);
|
|
60
|
+
formatOutput(result, opts);
|
|
61
|
+
}));
|
|
62
|
+
// create
|
|
63
|
+
catalog
|
|
64
|
+
.command('create')
|
|
65
|
+
.description('Create a variant catalog entry')
|
|
66
|
+
.option('-d, --data <json>', 'Entry data as JSON')
|
|
67
|
+
.option('-f, --file <path>', 'Read data from file')
|
|
68
|
+
.action(withErrorHandler(globalOpts, async (cmdOpts) => {
|
|
69
|
+
const opts = globalOpts();
|
|
70
|
+
const client = await createClient(opts);
|
|
71
|
+
const input = await parseInputData(cmdOpts);
|
|
72
|
+
const data = await client.request(CREATE_VARIANT_CATALOG_ENTRY, { input });
|
|
73
|
+
formatOutput(data.createVariantCatalogEntry, opts);
|
|
74
|
+
if (!(opts.json || opts.jsonl || opts.quiet))
|
|
75
|
+
success(`Created variant catalog entry ${data.createVariantCatalogEntry.key}`);
|
|
76
|
+
}));
|
|
77
|
+
// update
|
|
78
|
+
catalog
|
|
79
|
+
.command('update <id>')
|
|
80
|
+
.description('Update a variant catalog entry')
|
|
81
|
+
.option('-d, --data <json>', 'Entry data as JSON')
|
|
82
|
+
.option('-f, --file <path>', 'Read data from file')
|
|
83
|
+
.action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
|
|
84
|
+
const opts = globalOpts();
|
|
85
|
+
const client = await createClient(opts);
|
|
86
|
+
const input = await parseInputData(cmdOpts);
|
|
87
|
+
const data = await client.request(UPDATE_VARIANT_CATALOG_ENTRY, { id, input });
|
|
88
|
+
formatOutput(data.updateVariantCatalogEntry, opts);
|
|
89
|
+
if (!(opts.json || opts.jsonl || opts.quiet))
|
|
90
|
+
success(`Updated variant catalog entry ${data.updateVariantCatalogEntry.key}`);
|
|
91
|
+
}));
|
|
92
|
+
// delete
|
|
93
|
+
catalog
|
|
94
|
+
.command('delete <id>')
|
|
95
|
+
.description('Delete a variant catalog entry')
|
|
96
|
+
.option('--confirm', 'Skip confirmation prompt')
|
|
97
|
+
.action(withErrorHandler(globalOpts, async (id, cmdOpts) => {
|
|
98
|
+
const opts = globalOpts();
|
|
99
|
+
const confirmed = await confirmAction(`Delete variant catalog entry ${id}?`, { confirm: !!cmdOpts.confirm });
|
|
100
|
+
if (!confirmed) {
|
|
101
|
+
console.log('Aborted.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const client = await createClient(opts);
|
|
105
|
+
await client.request(DELETE_VARIANT_CATALOG_ENTRY, { id });
|
|
106
|
+
if (opts.json || opts.jsonl)
|
|
107
|
+
formatOutput({ deleted: true, id }, opts);
|
|
108
|
+
else
|
|
109
|
+
success(`Deleted variant catalog entry ${id}`);
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAItD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAwDN"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getCredentials, isTokenExpired } from '../auth/credentials.js';
|
|
2
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
3
|
+
import { formatOutput } from '../lib/output.js';
|
|
4
|
+
export function registerWhoamiCommand(program, globalOpts) {
|
|
5
|
+
program
|
|
6
|
+
.command('whoami')
|
|
7
|
+
.description('Show current authentication status')
|
|
8
|
+
.action(withErrorHandler(globalOpts, async () => {
|
|
9
|
+
const opts = globalOpts();
|
|
10
|
+
const credentials = await getCredentials();
|
|
11
|
+
if (!credentials) {
|
|
12
|
+
if (opts.json || opts.jsonl) {
|
|
13
|
+
formatOutput({ authenticated: false }, opts);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log('Not logged in.');
|
|
17
|
+
console.log('\nRun `foir login` to authenticate.');
|
|
18
|
+
}
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const expired = isTokenExpired(credentials);
|
|
22
|
+
if (opts.json || opts.jsonl) {
|
|
23
|
+
formatOutput({
|
|
24
|
+
authenticated: true,
|
|
25
|
+
tokenValid: !expired,
|
|
26
|
+
user: credentials.user,
|
|
27
|
+
selectedProject: credentials.selectedProject ?? null,
|
|
28
|
+
}, opts);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log('Authentication Status');
|
|
32
|
+
console.log('─'.repeat(40));
|
|
33
|
+
console.log(`User: ${credentials.user.name} <${credentials.user.email}>`);
|
|
34
|
+
console.log(`User ID: ${credentials.user.id}`);
|
|
35
|
+
console.log(`Token: ${expired ? '⚠ Expired' : '✓ Valid'}`);
|
|
36
|
+
if (credentials.selectedProject) {
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log('Selected Project');
|
|
39
|
+
console.log('─'.repeat(40));
|
|
40
|
+
console.log(`Name: ${credentials.selectedProject.name}`);
|
|
41
|
+
console.log(`ID: ${credentials.selectedProject.id}`);
|
|
42
|
+
console.log(`Tenant ID: ${credentials.selectedProject.tenantId}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('No project selected.');
|
|
47
|
+
console.log('Run `foir select-project` to choose a project.');
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
}
|