@j-256/ccam 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/LICENSE +21 -0
- package/README.md +209 -0
- package/dist/auth/browser-login.d.ts +14 -0
- package/dist/auth/browser-login.js +72 -0
- package/dist/auth/manual-login.d.ts +10 -0
- package/dist/auth/manual-login.js +33 -0
- package/dist/auth/paths.d.ts +4 -0
- package/dist/auth/paths.js +15 -0
- package/dist/auth/profile-resolver.d.ts +26 -0
- package/dist/auth/profile-resolver.js +42 -0
- package/dist/auth/profile-store.d.ts +38 -0
- package/dist/auth/profile-store.js +125 -0
- package/dist/auth/prompt.d.ts +9 -0
- package/dist/auth/prompt.js +70 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.js +4 -0
- package/dist/client-factory.d.ts +6 -0
- package/dist/client-factory.js +40 -0
- package/dist/commands/auth.d.ts +77 -0
- package/dist/commands/auth.js +387 -0
- package/dist/commands/client.d.ts +3 -0
- package/dist/commands/client.js +365 -0
- package/dist/commands/instance.d.ts +11 -0
- package/dist/commands/instance.js +128 -0
- package/dist/commands/org-config.d.ts +3 -0
- package/dist/commands/org-config.js +31 -0
- package/dist/commands/org.d.ts +11 -0
- package/dist/commands/org.js +234 -0
- package/dist/commands/permission.d.ts +3 -0
- package/dist/commands/permission.js +60 -0
- package/dist/commands/realm.d.ts +3 -0
- package/dist/commands/realm.js +58 -0
- package/dist/commands/role.d.ts +3 -0
- package/dist/commands/role.js +77 -0
- package/dist/commands/service-type.d.ts +3 -0
- package/dist/commands/service-type.js +57 -0
- package/dist/commands/user.d.ts +14 -0
- package/dist/commands/user.js +573 -0
- package/dist/error-handler.d.ts +2 -0
- package/dist/error-handler.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/output/csv.d.ts +3 -0
- package/dist/output/csv.js +57 -0
- package/dist/output/default-columns.d.ts +2 -0
- package/dist/output/default-columns.js +16 -0
- package/dist/output/detect.d.ts +4 -0
- package/dist/output/detect.js +6 -0
- package/dist/output/index.d.ts +15 -0
- package/dist/output/index.js +34 -0
- package/dist/output/json.d.ts +2 -0
- package/dist/output/json.js +10 -0
- package/dist/output/shared.d.ts +13 -0
- package/dist/output/shared.js +41 -0
- package/dist/output/table.d.ts +2 -0
- package/dist/output/table.js +72 -0
- package/dist/output/types.d.ts +2 -0
- package/dist/output/types.js +2 -0
- package/dist/output/yaml-fmt.d.ts +2 -0
- package/dist/output/yaml-fmt.js +11 -0
- package/dist/program.d.ts +3 -0
- package/dist/program.js +37 -0
- package/dist/shared.d.ts +46 -0
- package/dist/shared.js +96 -0
- package/dist/tui/App.d.ts +7 -0
- package/dist/tui/App.js +30 -0
- package/dist/tui/components/AuditTab.d.ts +8 -0
- package/dist/tui/components/AuditTab.js +80 -0
- package/dist/tui/components/FooterBar.d.ts +17 -0
- package/dist/tui/components/FooterBar.js +23 -0
- package/dist/tui/components/FullScreenLayout.d.ts +6 -0
- package/dist/tui/components/FullScreenLayout.js +11 -0
- package/dist/tui/components/HeaderBar.d.ts +5 -0
- package/dist/tui/components/HeaderBar.js +10 -0
- package/dist/tui/components/InfoTab.d.ts +8 -0
- package/dist/tui/components/InfoTab.js +70 -0
- package/dist/tui/components/ResourcePicker.d.ts +10 -0
- package/dist/tui/components/ResourcePicker.js +36 -0
- package/dist/tui/components/SubResourceTab.d.ts +7 -0
- package/dist/tui/components/SubResourceTab.js +193 -0
- package/dist/tui/components/TabBar.d.ts +11 -0
- package/dist/tui/components/TabBar.js +13 -0
- package/dist/tui/components/Table.d.ts +32 -0
- package/dist/tui/components/Table.js +175 -0
- package/dist/tui/context/client.d.ts +7 -0
- package/dist/tui/context/client.js +14 -0
- package/dist/tui/context/navigation.d.ts +14 -0
- package/dist/tui/context/navigation.js +25 -0
- package/dist/tui/context/terminal-size.d.ts +9 -0
- package/dist/tui/context/terminal-size.js +26 -0
- package/dist/tui/format.d.ts +20 -0
- package/dist/tui/format.js +57 -0
- package/dist/tui/hooks/use-audit-log.d.ts +12 -0
- package/dist/tui/hooks/use-audit-log.js +71 -0
- package/dist/tui/hooks/use-local-collection.d.ts +8 -0
- package/dist/tui/hooks/use-local-collection.js +30 -0
- package/dist/tui/hooks/use-paginated-resource.d.ts +23 -0
- package/dist/tui/hooks/use-paginated-resource.js +115 -0
- package/dist/tui/hooks/use-resource-detail.d.ts +7 -0
- package/dist/tui/hooks/use-resource-detail.js +30 -0
- package/dist/tui/hooks/use-scroll-window.d.ts +7 -0
- package/dist/tui/hooks/use-scroll-window.js +29 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.js +22 -0
- package/dist/tui/navigation.d.ts +11 -0
- package/dist/tui/navigation.js +29 -0
- package/dist/tui/resource-configs/api-clients.d.ts +3 -0
- package/dist/tui/resource-configs/api-clients.js +118 -0
- package/dist/tui/resource-configs/index.d.ts +14 -0
- package/dist/tui/resource-configs/index.js +28 -0
- package/dist/tui/resource-configs/instances.d.ts +3 -0
- package/dist/tui/resource-configs/instances.js +24 -0
- package/dist/tui/resource-configs/org-configuration.d.ts +3 -0
- package/dist/tui/resource-configs/org-configuration.js +28 -0
- package/dist/tui/resource-configs/organizations.d.ts +3 -0
- package/dist/tui/resource-configs/organizations.js +104 -0
- package/dist/tui/resource-configs/permissions.d.ts +3 -0
- package/dist/tui/resource-configs/permissions.js +25 -0
- package/dist/tui/resource-configs/realms.d.ts +3 -0
- package/dist/tui/resource-configs/realms.js +36 -0
- package/dist/tui/resource-configs/roles.d.ts +3 -0
- package/dist/tui/resource-configs/roles.js +56 -0
- package/dist/tui/resource-configs/service-types.d.ts +3 -0
- package/dist/tui/resource-configs/service-types.js +24 -0
- package/dist/tui/resource-configs/users.d.ts +3 -0
- package/dist/tui/resource-configs/users.js +126 -0
- package/dist/tui/types.d.ts +99 -0
- package/dist/tui/types.js +23 -0
- package/dist/tui/views/ResourceDetailView.d.ts +7 -0
- package/dist/tui/views/ResourceDetailView.js +123 -0
- package/dist/tui/views/ResourceListView.d.ts +6 -0
- package/dist/tui/views/ResourceListView.js +140 -0
- package/dist/tui/views/ViewRouter.d.ts +2 -0
- package/dist/tui/views/ViewRouter.js +60 -0
- package/package.json +62 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const DEFAULT_COLUMNS = {
|
|
2
|
+
user: ['mail', 'firstName', 'lastName', 'userState', 'primaryOrganization', 'lastLoginDate', 'roles'],
|
|
3
|
+
userDetail: ['id', 'mail', 'firstName', 'lastName', 'displayName', 'userState', 'primaryOrganization', 'organizations', 'roles', 'lastLoginDate', 'createdAt', 'lastModified'],
|
|
4
|
+
role: ['description', 'id', 'serviceType', 'scope', 'targetType', 'privileged', 'twoFAEnabled'],
|
|
5
|
+
roleDetail: ['description', 'id', 'serviceType', 'scope', 'targetType', 'privileged', 'twoFAEnabled', 'permissions'],
|
|
6
|
+
org: ['name', 'id', 'type', 'twoFAEnabled'],
|
|
7
|
+
orgDetail: ['name', 'id', 'type', 'twoFAEnabled', 'emailDomains', 'sfAccountIds', 'realms'],
|
|
8
|
+
client: ['id', 'name', 'active', 'organizationCount', 'tokenEndpointAuthMethod', 'lastAuthenticatedDate'],
|
|
9
|
+
clientDetail: ['id', 'name', 'description', 'active', 'organizations', 'roles', 'scopes', 'tokenEndpointAuthMethod', 'lastAuthenticatedDate', 'createdAt'],
|
|
10
|
+
instance: ['id', 'description', 'podId', 'tenantType'],
|
|
11
|
+
realm: ['id', 'description', 'customerName', 'organizationId'],
|
|
12
|
+
permission: ['name', 'adminPermission'],
|
|
13
|
+
serviceType: ['id', 'description'],
|
|
14
|
+
auditLog: ['timestamp', 'eventType', 'eventMessage', 'authorDisplayName', 'authorEmail'],
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=default-columns.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type { OutputFormat } from './types.js';
|
|
2
|
+
export { resolveFormat } from './detect.js';
|
|
3
|
+
export { formatJson } from './json.js';
|
|
4
|
+
export { formatCsv, formatTsv } from './csv.js';
|
|
5
|
+
export { formatYaml } from './yaml-fmt.js';
|
|
6
|
+
export { formatTable } from './table.js';
|
|
7
|
+
export { DEFAULT_COLUMNS } from './default-columns.js';
|
|
8
|
+
import type { OutputFormat } from './types.js';
|
|
9
|
+
export interface RenderOptions {
|
|
10
|
+
format: OutputFormat;
|
|
11
|
+
fields?: string[];
|
|
12
|
+
defaultFields?: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function renderOutput(data: unknown, options: RenderOptions): void;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { resolveFormat } from './detect.js';
|
|
2
|
+
export { formatJson } from './json.js';
|
|
3
|
+
export { formatCsv, formatTsv } from './csv.js';
|
|
4
|
+
export { formatYaml } from './yaml-fmt.js';
|
|
5
|
+
export { formatTable } from './table.js';
|
|
6
|
+
export { DEFAULT_COLUMNS } from './default-columns.js';
|
|
7
|
+
import { formatJson } from './json.js';
|
|
8
|
+
import { formatCsv, formatTsv } from './csv.js';
|
|
9
|
+
import { formatYaml } from './yaml-fmt.js';
|
|
10
|
+
import { formatTable } from './table.js';
|
|
11
|
+
export function renderOutput(data, options) {
|
|
12
|
+
let output;
|
|
13
|
+
switch (options.format) {
|
|
14
|
+
case 'json':
|
|
15
|
+
output = formatJson(data, options.fields);
|
|
16
|
+
break;
|
|
17
|
+
case 'csv':
|
|
18
|
+
output = formatCsv(data, options.fields);
|
|
19
|
+
break;
|
|
20
|
+
case 'tsv':
|
|
21
|
+
output = formatTsv(data, options.fields);
|
|
22
|
+
break;
|
|
23
|
+
case 'yaml':
|
|
24
|
+
output = formatYaml(data, options.fields);
|
|
25
|
+
break;
|
|
26
|
+
case 'table':
|
|
27
|
+
output = formatTable(data, options.fields ?? options.defaultFields);
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(`Unsupported format: ${options.format}`);
|
|
31
|
+
}
|
|
32
|
+
process.stdout.write(output + '\n');
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { applyFieldSelection } from './shared.js';
|
|
2
|
+
export function formatJson(data, fields) {
|
|
3
|
+
if (!fields) {
|
|
4
|
+
return JSON.stringify(data, null, 2);
|
|
5
|
+
}
|
|
6
|
+
// Apply field selection
|
|
7
|
+
const filtered = applyFieldSelection(data, fields);
|
|
8
|
+
return JSON.stringify(filtered, null, 2);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for output formatters.
|
|
3
|
+
*/
|
|
4
|
+
/** Collect the union of top-level keys across a list of objects. */
|
|
5
|
+
export declare function extractColumns(data: unknown[]): string[];
|
|
6
|
+
/** Return the top-level property of an object by key. */
|
|
7
|
+
export declare function getNestedValue(obj: unknown, path: string): unknown;
|
|
8
|
+
/**
|
|
9
|
+
* Apply a field allowlist to a single object or an array of objects.
|
|
10
|
+
* Used by JSON/YAML formatters where the goal is to drop un-requested fields.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyFieldSelection(data: unknown, fields: string[]): unknown;
|
|
13
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for output formatters.
|
|
3
|
+
*/
|
|
4
|
+
/** Collect the union of top-level keys across a list of objects. */
|
|
5
|
+
export function extractColumns(data) {
|
|
6
|
+
const columns = new Set();
|
|
7
|
+
for (const item of data) {
|
|
8
|
+
if (typeof item === 'object' && item !== null) {
|
|
9
|
+
Object.keys(item).forEach((key) => columns.add(key));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return Array.from(columns);
|
|
13
|
+
}
|
|
14
|
+
/** Return the top-level property of an object by key. */
|
|
15
|
+
export function getNestedValue(obj, path) {
|
|
16
|
+
if (typeof obj !== 'object' || obj === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
return obj[path];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Apply a field allowlist to a single object or an array of objects.
|
|
22
|
+
* Used by JSON/YAML formatters where the goal is to drop un-requested fields.
|
|
23
|
+
*/
|
|
24
|
+
export function applyFieldSelection(data, fields) {
|
|
25
|
+
if (Array.isArray(data)) {
|
|
26
|
+
return data.map((item) => pickFields(item, fields));
|
|
27
|
+
}
|
|
28
|
+
return pickFields(data, fields);
|
|
29
|
+
}
|
|
30
|
+
function pickFields(obj, fields) {
|
|
31
|
+
if (typeof obj !== 'object' || obj === null)
|
|
32
|
+
return obj;
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const field of fields) {
|
|
35
|
+
if (field in obj) {
|
|
36
|
+
result[field] = obj[field];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import Table from 'cli-table3';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { extractColumns, getNestedValue } from './shared.js';
|
|
4
|
+
const MAX_CELL_LENGTH = 60;
|
|
5
|
+
export function formatTable(data, fields) {
|
|
6
|
+
let items;
|
|
7
|
+
if (!Array.isArray(data)) {
|
|
8
|
+
items = [data];
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
items = data;
|
|
12
|
+
}
|
|
13
|
+
if (items.length === 0) {
|
|
14
|
+
return 'No results.';
|
|
15
|
+
}
|
|
16
|
+
// Determine columns
|
|
17
|
+
const columns = fields || extractColumns(items);
|
|
18
|
+
if (columns.length === 0) {
|
|
19
|
+
return 'No results.';
|
|
20
|
+
}
|
|
21
|
+
// Create table with styled headers
|
|
22
|
+
const table = new Table({
|
|
23
|
+
head: columns.map((col) => chalk.gray(col)),
|
|
24
|
+
style: {
|
|
25
|
+
head: [],
|
|
26
|
+
border: [],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
// Add data rows
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
const row = columns.map((col) => {
|
|
32
|
+
const value = getNestedValue(item, col);
|
|
33
|
+
return formatCell(value);
|
|
34
|
+
});
|
|
35
|
+
table.push(row);
|
|
36
|
+
}
|
|
37
|
+
return table.toString();
|
|
38
|
+
}
|
|
39
|
+
function formatCell(value) {
|
|
40
|
+
if (value === null || value === undefined) {
|
|
41
|
+
return chalk.dim('-');
|
|
42
|
+
}
|
|
43
|
+
let stringValue;
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
stringValue = value.join(', ');
|
|
46
|
+
}
|
|
47
|
+
else if (typeof value === 'object') {
|
|
48
|
+
stringValue = JSON.stringify(value);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
stringValue = String(value);
|
|
52
|
+
}
|
|
53
|
+
// Truncate long values before colorizing so ANSI escapes aren't split.
|
|
54
|
+
if (stringValue.length > MAX_CELL_LENGTH) {
|
|
55
|
+
stringValue = stringValue.slice(0, MAX_CELL_LENGTH - 3) + '...';
|
|
56
|
+
}
|
|
57
|
+
// Apply color coding for known status values after truncation.
|
|
58
|
+
return colorizeStatus(stringValue);
|
|
59
|
+
}
|
|
60
|
+
function colorizeStatus(value) {
|
|
61
|
+
if (value === 'ENABLED' || value === 'true') {
|
|
62
|
+
return chalk.green(value);
|
|
63
|
+
}
|
|
64
|
+
if (value === 'DELETED') {
|
|
65
|
+
return chalk.red(value);
|
|
66
|
+
}
|
|
67
|
+
if (value === 'false') {
|
|
68
|
+
return chalk.dim(value);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=table.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { stringify } from 'yaml';
|
|
2
|
+
import { applyFieldSelection } from './shared.js';
|
|
3
|
+
export function formatYaml(data, fields) {
|
|
4
|
+
if (!fields) {
|
|
5
|
+
return stringify(data);
|
|
6
|
+
}
|
|
7
|
+
// Apply field selection
|
|
8
|
+
const filtered = applyFieldSelection(data, fields);
|
|
9
|
+
return stringify(filtered);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=yaml-fmt.js.map
|
package/dist/program.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { registerAuthCommands } from './commands/auth.js';
|
|
3
|
+
import { registerUserCommands } from './commands/user.js';
|
|
4
|
+
import { registerOrgCommands } from './commands/org.js';
|
|
5
|
+
import { registerClientCommands } from './commands/client.js';
|
|
6
|
+
import { registerRoleCommands } from './commands/role.js';
|
|
7
|
+
import { registerRealmCommands } from './commands/realm.js';
|
|
8
|
+
import { registerPermissionCommands } from './commands/permission.js';
|
|
9
|
+
import { registerServiceTypeCommands } from './commands/service-type.js';
|
|
10
|
+
import { registerInstanceCommands } from './commands/instance.js';
|
|
11
|
+
import { registerOrgConfigCommands } from './commands/org-config.js';
|
|
12
|
+
export const program = new Command()
|
|
13
|
+
.name('ccam')
|
|
14
|
+
.description('CLI for the Salesforce Commerce Cloud Account Manager API')
|
|
15
|
+
.version('0.1.0')
|
|
16
|
+
.option('-i, --interactive', 'Launch interactive TUI')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
if (options.interactive || process.stdout.isTTY) {
|
|
19
|
+
const { startTui } = await import('./tui/index.js');
|
|
20
|
+
await startTui();
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
program.help();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// Register all command groups
|
|
27
|
+
registerAuthCommands(program);
|
|
28
|
+
registerUserCommands(program);
|
|
29
|
+
registerOrgCommands(program);
|
|
30
|
+
registerClientCommands(program);
|
|
31
|
+
registerRoleCommands(program);
|
|
32
|
+
registerRealmCommands(program);
|
|
33
|
+
registerPermissionCommands(program);
|
|
34
|
+
registerServiceTypeCommands(program);
|
|
35
|
+
registerInstanceCommands(program);
|
|
36
|
+
registerOrgConfigCommands(program);
|
|
37
|
+
//# sourceMappingURL=program.js.map
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { OutputFormat } from './output/index.js';
|
|
3
|
+
export interface GlobalOptions {
|
|
4
|
+
format?: string;
|
|
5
|
+
fields?: string;
|
|
6
|
+
page?: string;
|
|
7
|
+
size?: string;
|
|
8
|
+
sort?: string;
|
|
9
|
+
profile?: string;
|
|
10
|
+
host?: string;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedSort {
|
|
14
|
+
field: string;
|
|
15
|
+
direction: 'asc' | 'desc';
|
|
16
|
+
}
|
|
17
|
+
export interface ResolvedGlobalOptions {
|
|
18
|
+
format?: OutputFormat;
|
|
19
|
+
fields?: string[];
|
|
20
|
+
page?: number;
|
|
21
|
+
size?: number;
|
|
22
|
+
sort?: ParsedSort;
|
|
23
|
+
profile?: string;
|
|
24
|
+
host?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function parseFields(value: string | undefined): string[] | undefined;
|
|
27
|
+
export declare function parseSort(value: string | undefined): ParsedSort | undefined;
|
|
28
|
+
export declare function parsePageSize(value: string | undefined, defaultValue?: number): number | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Parse and validate a user-provided --expand flag against an allowed set.
|
|
31
|
+
* Throws with a clear message on unknown values, so the user gets a CLI error
|
|
32
|
+
* instead of the AM server silently ignoring the parameter.
|
|
33
|
+
*
|
|
34
|
+
* @param input - raw --expand value (e.g. 'organizations')
|
|
35
|
+
* @param allowed - array of allowed expand tokens for this endpoint
|
|
36
|
+
* @returns the validated expand value, or undefined if input was undefined
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseExpand<T extends string>(input: string | undefined, allowed: readonly T[]): T | undefined;
|
|
39
|
+
export declare function addGlobalOptions(cmd: Command): Command;
|
|
40
|
+
/**
|
|
41
|
+
* Write "Page X of Y (N total)" to stderr if the format is 'table' and the
|
|
42
|
+
* result has page metadata. No-op otherwise.
|
|
43
|
+
*/
|
|
44
|
+
export declare function writePageInfoIfTable(format: string, result: unknown): void;
|
|
45
|
+
export declare function resolveGlobalOptions(opts: GlobalOptions): ResolvedGlobalOptions;
|
|
46
|
+
//# sourceMappingURL=shared.d.ts.map
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function parseFields(value) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return undefined;
|
|
4
|
+
return value.split(',').map((field) => field.trim()).filter((field) => field.length > 0);
|
|
5
|
+
}
|
|
6
|
+
export function parseSort(value) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return undefined;
|
|
9
|
+
const [field, direction = 'asc'] = value.split(':');
|
|
10
|
+
if (!field) {
|
|
11
|
+
throw new Error(`Invalid sort field: empty. Use "field:asc" or "field:desc"`);
|
|
12
|
+
}
|
|
13
|
+
if (direction !== 'asc' && direction !== 'desc') {
|
|
14
|
+
throw new Error(`Invalid sort direction: "${direction}". Use "asc" or "desc"`);
|
|
15
|
+
}
|
|
16
|
+
return { field, direction };
|
|
17
|
+
}
|
|
18
|
+
export function parsePageSize(value, defaultValue) {
|
|
19
|
+
if (!value)
|
|
20
|
+
return defaultValue;
|
|
21
|
+
const parsed = parseInt(value, 10);
|
|
22
|
+
if (isNaN(parsed) || parsed < 0) {
|
|
23
|
+
throw new Error(`Invalid number: ${value}`);
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse and validate a user-provided --expand flag against an allowed set.
|
|
29
|
+
* Throws with a clear message on unknown values, so the user gets a CLI error
|
|
30
|
+
* instead of the AM server silently ignoring the parameter.
|
|
31
|
+
*
|
|
32
|
+
* @param input - raw --expand value (e.g. 'organizations')
|
|
33
|
+
* @param allowed - array of allowed expand tokens for this endpoint
|
|
34
|
+
* @returns the validated expand value, or undefined if input was undefined
|
|
35
|
+
*/
|
|
36
|
+
export function parseExpand(input, allowed) {
|
|
37
|
+
if (input === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (!allowed.includes(input)) {
|
|
40
|
+
throw new Error(`Invalid --expand value: "${input}". Allowed: ${allowed.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
return input;
|
|
43
|
+
}
|
|
44
|
+
export function addGlobalOptions(cmd) {
|
|
45
|
+
return cmd
|
|
46
|
+
.option('--format <format>', 'Output format: table, json, csv, tsv, yaml')
|
|
47
|
+
.option('--fields <fields>', 'Comma-separated list of fields to include')
|
|
48
|
+
.option('--page <page>', 'Page number for pagination', '0')
|
|
49
|
+
.option('--size <size>', 'Page size for pagination', '25')
|
|
50
|
+
.option('--sort <sort>', 'Sort field and direction (e.g., name:asc)')
|
|
51
|
+
.option('--profile <profile>', 'Profile name from config')
|
|
52
|
+
.option('--host <host>', 'API host URL (default: https://account.demandware.com)')
|
|
53
|
+
.option('-j, --json', 'Shorthand for --format json');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Write "Page X of Y (N total)" to stderr if the format is 'table' and the
|
|
57
|
+
* result has page metadata. No-op otherwise.
|
|
58
|
+
*/
|
|
59
|
+
export function writePageInfoIfTable(format, result) {
|
|
60
|
+
if (format !== 'table')
|
|
61
|
+
return;
|
|
62
|
+
if (!result || typeof result !== 'object')
|
|
63
|
+
return;
|
|
64
|
+
if (!('page' in result))
|
|
65
|
+
return;
|
|
66
|
+
const page = result.page;
|
|
67
|
+
process.stderr.write(`Page ${page.number + 1} of ${page.totalPages} (${page.totalElements} total)\n`);
|
|
68
|
+
}
|
|
69
|
+
export function resolveGlobalOptions(opts) {
|
|
70
|
+
const resolved = {
|
|
71
|
+
profile: opts.profile,
|
|
72
|
+
};
|
|
73
|
+
// -j flag overrides format
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
resolved.format = 'json';
|
|
76
|
+
}
|
|
77
|
+
else if (opts.format) {
|
|
78
|
+
resolved.format = opts.format;
|
|
79
|
+
}
|
|
80
|
+
// Parse fields
|
|
81
|
+
if (opts.fields) {
|
|
82
|
+
resolved.fields = parseFields(opts.fields);
|
|
83
|
+
}
|
|
84
|
+
// Parse page and size
|
|
85
|
+
resolved.page = parsePageSize(opts.page, 0);
|
|
86
|
+
resolved.size = parsePageSize(opts.size, 25);
|
|
87
|
+
// Parse sort
|
|
88
|
+
if (opts.sort) {
|
|
89
|
+
resolved.sort = parseSort(opts.sort);
|
|
90
|
+
}
|
|
91
|
+
if (opts.host) {
|
|
92
|
+
resolved.host = opts.host;
|
|
93
|
+
}
|
|
94
|
+
return resolved;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=shared.js.map
|
package/dist/tui/App.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useApp, useInput } from 'ink';
|
|
3
|
+
import { ClientProvider } from './context/client.js';
|
|
4
|
+
import { TerminalSizeProvider } from './context/terminal-size.js';
|
|
5
|
+
import { NavigationProvider, useNav } from './context/navigation.js';
|
|
6
|
+
import { FullScreenLayout } from './components/FullScreenLayout.js';
|
|
7
|
+
import { ViewRouter } from './views/ViewRouter.js';
|
|
8
|
+
function AppInner({ host }) {
|
|
9
|
+
const nav = useNav();
|
|
10
|
+
const { exit } = useApp();
|
|
11
|
+
useInput((input, key) => {
|
|
12
|
+
if (input === 'q' && nav.current.view === 'resource-picker') {
|
|
13
|
+
exit();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (key.escape || input === 'q') {
|
|
17
|
+
if (nav.canGoBack) {
|
|
18
|
+
nav.pop();
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
exit();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return (_jsx(FullScreenLayout, { host: host, children: _jsx(ViewRouter, {}) }));
|
|
26
|
+
}
|
|
27
|
+
export function App({ client, host }) {
|
|
28
|
+
return (_jsx(ClientProvider, { client: client, children: _jsx(TerminalSizeProvider, { children: _jsx(NavigationProvider, { initial: { view: 'resource-picker', label: 'Home' }, children: _jsx(AppInner, { host: host }) }) }) }));
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=App.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ColumnDef } from '../types.js';
|
|
2
|
+
import type { AuditLogRecord, ContentResponse } from 'ccam-sdk';
|
|
3
|
+
export interface AuditTabProps {
|
|
4
|
+
fetchFn: (querySize?: number) => Promise<ContentResponse<AuditLogRecord>>;
|
|
5
|
+
columns?: ColumnDef[];
|
|
6
|
+
}
|
|
7
|
+
export declare function AuditTab({ fetchFn, columns }: AuditTabProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=AuditTab.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { useAuditLog } from '../hooks/use-audit-log.js';
|
|
6
|
+
import { useScrollWindow } from '../hooks/use-scroll-window.js';
|
|
7
|
+
import { useTerminalSize } from '../context/terminal-size.js';
|
|
8
|
+
import { Table } from './Table.js';
|
|
9
|
+
import { HintText } from './FooterBar.js';
|
|
10
|
+
const BORDER_INSET = 2;
|
|
11
|
+
const DEFAULT_COLUMNS = [
|
|
12
|
+
{ key: 'timestamp', label: 'Time', width: 2 },
|
|
13
|
+
{ key: 'authorDisplayName', label: 'Author', width: 2 },
|
|
14
|
+
{ key: 'eventType', label: 'Event', width: 2 },
|
|
15
|
+
{ key: 'eventMessage', label: 'Message', width: 4 },
|
|
16
|
+
];
|
|
17
|
+
export function AuditTab({ fetchFn, columns }) {
|
|
18
|
+
const { cols } = useTerminalSize();
|
|
19
|
+
const [highlight, setHighlight] = useState(0);
|
|
20
|
+
const { data, loading, error, canLoadMore, needsConfirmation, loadMore, confirmLoadAll, retry, } = useAuditLog(fetchFn);
|
|
21
|
+
const scroll = useScrollWindow(highlight, data.length);
|
|
22
|
+
const handleLoadMore = useCallback(() => {
|
|
23
|
+
if (needsConfirmation) {
|
|
24
|
+
confirmLoadAll();
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
loadMore();
|
|
28
|
+
}
|
|
29
|
+
}, [needsConfirmation, confirmLoadAll, loadMore]);
|
|
30
|
+
useInput((input, key) => {
|
|
31
|
+
if (loading)
|
|
32
|
+
return;
|
|
33
|
+
if (error) {
|
|
34
|
+
if (input === 'r')
|
|
35
|
+
retry();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Vertical movement
|
|
39
|
+
if (input === 'j' || key.downArrow) {
|
|
40
|
+
setHighlight((i) => Math.min(i + 1, data.length - 1));
|
|
41
|
+
}
|
|
42
|
+
if (input === 'k' || key.upArrow) {
|
|
43
|
+
setHighlight((i) => Math.max(i - 1, 0));
|
|
44
|
+
}
|
|
45
|
+
// Jump to first/last
|
|
46
|
+
if (input === 'g')
|
|
47
|
+
setHighlight(0);
|
|
48
|
+
if (input === 'G')
|
|
49
|
+
setHighlight(Math.max(0, data.length - 1));
|
|
50
|
+
// Load more
|
|
51
|
+
if (input === 'm' && canLoadMore) {
|
|
52
|
+
handleLoadMore();
|
|
53
|
+
}
|
|
54
|
+
// Scroll
|
|
55
|
+
if (input === 'n') {
|
|
56
|
+
setHighlight((i) => Math.min(i + scroll.visibleRows, data.length - 1));
|
|
57
|
+
}
|
|
58
|
+
if (input === 'p') {
|
|
59
|
+
setHighlight((i) => Math.max(i - scroll.visibleRows, 0));
|
|
60
|
+
}
|
|
61
|
+
// Refresh (resets to initial querySize)
|
|
62
|
+
if (input === 'r')
|
|
63
|
+
retry();
|
|
64
|
+
});
|
|
65
|
+
if (error) {
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error.message] }), _jsx(Text, { dimColor: true, children: "r:retry" })] }));
|
|
67
|
+
}
|
|
68
|
+
if (loading && data.length === 0) {
|
|
69
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading audit log..." })] }));
|
|
70
|
+
}
|
|
71
|
+
// Footer hints
|
|
72
|
+
const hintParts = ['[j/k:nav]'];
|
|
73
|
+
if (canLoadMore) {
|
|
74
|
+
hintParts.push(needsConfirmation ? '[m:load all]' : '[m:more]');
|
|
75
|
+
}
|
|
76
|
+
hintParts.push('[r:refresh]');
|
|
77
|
+
const hints = hintParts.join(' ');
|
|
78
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Table, { columns: columns ?? DEFAULT_COLUMNS, data: data, highlightIndex: highlight, visibleRows: scroll.visibleRows, scrollOffset: scroll.scrollOffset, contentWidth: cols - BORDER_INSET }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(HintText, { hints: hints }), _jsxs(Text, { dimColor: true, children: [data.length, " records", loading ? ' (loading...)' : ''] })] })] }));
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=AuditTab.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface FooterBarProps {
|
|
2
|
+
hints?: string;
|
|
3
|
+
pageInfo?: string;
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
sortLabel?: string;
|
|
6
|
+
statsLabel?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Format hint strings in bracket notation.
|
|
10
|
+
* Input format: "[j/k:nav] [Enter:open] [Esc:back]"
|
|
11
|
+
* Each segment: "[key:description]"
|
|
12
|
+
*/
|
|
13
|
+
export declare function HintText({ hints }: {
|
|
14
|
+
hints: string;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export declare function FooterBar({ hints, pageInfo, loading, sortLabel, statsLabel }: FooterBarProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=FooterBar.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
/**
|
|
5
|
+
* Format hint strings in bracket notation.
|
|
6
|
+
* Input format: "[j/k:nav] [Enter:open] [Esc:back]"
|
|
7
|
+
* Each segment: "[key:description]"
|
|
8
|
+
*/
|
|
9
|
+
export function HintText({ hints }) {
|
|
10
|
+
const segments = hints.match(/\[[^\]]+\]/g) || [];
|
|
11
|
+
return (_jsx(Text, { children: segments.map((seg, i) => {
|
|
12
|
+
// Strip brackets, split on first colon
|
|
13
|
+
const inner = seg.slice(1, -1);
|
|
14
|
+
const colonIdx = inner.indexOf(':');
|
|
15
|
+
const key = colonIdx > 0 ? inner.slice(0, colonIdx) : inner;
|
|
16
|
+
const desc = colonIdx > 0 ? inner.slice(colonIdx) : '';
|
|
17
|
+
return (_jsxs(Text, { children: [i > 0 && _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "[" }), _jsx(Text, { bold: true, color: "cyan", children: key }), _jsxs(Text, { dimColor: true, children: [desc, "]"] })] }, i));
|
|
18
|
+
}) }));
|
|
19
|
+
}
|
|
20
|
+
export function FooterBar({ hints, pageInfo, loading, sortLabel, statsLabel }) {
|
|
21
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", children: [(loading || sortLabel || statsLabel) && (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [loading && (_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), ' '] })), sortLabel && _jsx(Text, { dimColor: true, children: sortLabel })] }), statsLabel && _jsx(Text, { dimColor: true, children: statsLabel })] })), _jsxs(Box, { justifyContent: "space-between", children: [hints && _jsx(HintText, { hints: hints }), pageInfo && _jsx(Text, { dimColor: true, children: pageInfo })] })] }));
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=FooterBar.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HeaderBarProps } from './HeaderBar.js';
|
|
2
|
+
export interface FullScreenLayoutProps extends HeaderBarProps {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
}
|
|
5
|
+
export declare function FullScreenLayout({ children, host, }: FullScreenLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=FullScreenLayout.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import { useTerminalSize } from '../context/terminal-size.js';
|
|
4
|
+
import { HeaderBar } from './HeaderBar.js';
|
|
5
|
+
export function FullScreenLayout({ children, host, }) {
|
|
6
|
+
const { rows } = useTerminalSize();
|
|
7
|
+
// Header: 1 line top border + 1 line breadcrumb + 1 line bottom border = 3
|
|
8
|
+
const contentHeight = rows - 3;
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(HeaderBar, { host: host }), _jsx(Box, { height: contentHeight, children: children })] }));
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=FullScreenLayout.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useNav } from '../context/navigation.js';
|
|
4
|
+
export function HeaderBar({ host }) {
|
|
5
|
+
const { breadcrumbs } = useNav();
|
|
6
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "gray", justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "ccam" }), breadcrumbs.map((crumb, i) => (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [" ", '>', " "] }), i === breadcrumbs.length - 1
|
|
7
|
+
? _jsx(Text, { bold: true, color: "white", children: crumb })
|
|
8
|
+
: _jsx(Text, { children: crumb })] }, i)))] }), host && _jsx(Text, { dimColor: true, children: host })] }));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=HeaderBar.js.map
|