Skip to content

Commit a821e4a

Browse files
authored
MRT commands: push, env-var management (#8)
* mrt wip * fixing site import * env vars * various table cleanups; env-var cleanup * update typedoc entries * linting
1 parent 5816fc3 commit a821e4a

File tree

30 files changed

+10653
-346
lines changed

30 files changed

+10653
-346
lines changed

AGENTS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,26 @@
2323
- when logging use the logger instance from `@salesforce/b2c-tooling/logger` package
2424
- CLI commands have access to this logger via `this.log` method from oclif Command class
2525
- CLI commands can write directly to stdout/stderr if their primary purpose is to output or stream data
26+
27+
## Table Output
28+
29+
When rendering tabular data in CLI commands, use the shared `TableRenderer` utility from `@salesforce/b2c-tooling/cli`:
30+
31+
```typescript
32+
import { createTable, type ColumnDef } from '@salesforce/b2c-tooling/cli';
33+
34+
// Define columns with header and getter function
35+
const COLUMNS: Record<string, ColumnDef<MyDataType>> = {
36+
id: { header: 'ID', get: (item) => item.id },
37+
name: { header: 'Name', get: (item) => item.name },
38+
status: { header: 'Status', get: (item) => item.status },
39+
};
40+
41+
// Render the table
42+
createTable(COLUMNS).render(data, ['id', 'name', 'status']);
43+
```
44+
45+
Features:
46+
- Dynamic column widths based on content
47+
- Supports `extended` flag on columns for optional fields
48+
- Use `TableRenderer` class directly for column validation helpers (e.g., `--columns` flag support)
Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
import {ux} from '@oclif/core';
2-
import cliui from 'cliui';
3-
import {InstanceCommand} from '@salesforce/b2c-tooling/cli';
2+
import {InstanceCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling/cli';
43
import {listCodeVersions, type CodeVersion, type CodeVersionResult} from '@salesforce/b2c-tooling/operations/code';
54
import {t} from '../../i18n/index.js';
65

6+
const COLUMNS: Record<string, ColumnDef<CodeVersion>> = {
7+
id: {
8+
header: 'ID',
9+
get: (v) => v.id || '-',
10+
},
11+
active: {
12+
header: 'Active',
13+
get: (v) => (v.active ? 'Yes' : 'No'),
14+
},
15+
rollback: {
16+
header: 'Rollback',
17+
get: (v) => (v.rollback ? 'Yes' : 'No'),
18+
},
19+
lastModified: {
20+
header: 'Last Modified',
21+
get: (v) => (v.last_modification_time ? new Date(v.last_modification_time).toLocaleString() : '-'),
22+
},
23+
cartridges: {
24+
header: 'Cartridges',
25+
get: (v) => String(v.cartridges?.length ?? 0),
26+
},
27+
};
28+
29+
const DEFAULT_COLUMNS = ['id', 'active', 'rollback', 'lastModified', 'cartridges'];
30+
731
export default class CodeList extends InstanceCommand<typeof CodeList> {
832
static description = t('commands.code.list.description', 'List code versions on a B2C Commerce instance');
933

@@ -41,42 +65,8 @@ export default class CodeList extends InstanceCommand<typeof CodeList> {
4165
return result;
4266
}
4367

44-
this.printVersionsTable(versions);
68+
createTable(COLUMNS).render(versions, DEFAULT_COLUMNS);
4569

4670
return result;
4771
}
48-
49-
private printVersionsTable(versions: CodeVersion[]): void {
50-
const ui = cliui({width: process.stdout.columns || 80});
51-
52-
// Header
53-
ui.div(
54-
{text: 'ID', width: 25, padding: [0, 2, 0, 0]},
55-
{text: 'Active', width: 10, padding: [0, 2, 0, 0]},
56-
{text: 'Rollback', width: 10, padding: [0, 2, 0, 0]},
57-
{text: 'Last Modified', width: 25, padding: [0, 2, 0, 0]},
58-
{text: 'Cartridges', padding: [0, 0, 0, 0]},
59-
);
60-
61-
// Separator
62-
ui.div({text: '─'.repeat(80), padding: [0, 0, 0, 0]});
63-
64-
// Rows
65-
for (const version of versions) {
66-
const lastModified = version.last_modification_time
67-
? new Date(version.last_modification_time).toLocaleString()
68-
: '-';
69-
const cartridgeCount = version.cartridges?.length ?? 0;
70-
71-
ui.div(
72-
{text: version.id || '', width: 25, padding: [0, 2, 0, 0]},
73-
{text: version.active ? 'Yes' : 'No', width: 10, padding: [0, 2, 0, 0]},
74-
{text: version.rollback ? 'Yes' : 'No', width: 10, padding: [0, 2, 0, 0]},
75-
{text: lastModified, width: 25, padding: [0, 2, 0, 0]},
76-
{text: String(cartridgeCount), padding: [0, 0, 0, 0]},
77-
);
78-
}
79-
80-
ux.stdout(ui.toString());
81-
}
8272
}

packages/b2c-cli/src/commands/job/search.ts

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,13 @@
11
import {Flags, ux} from '@oclif/core';
2-
import cliui from 'cliui';
3-
import {InstanceCommand} from '@salesforce/b2c-tooling/cli';
2+
import {InstanceCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling/cli';
43
import {
54
searchJobExecutions,
65
type JobExecutionSearchResult,
76
type JobExecution,
87
} from '@salesforce/b2c-tooling/operations/jobs';
98
import {t} from '../../i18n/index.js';
109

11-
/**
12-
* Column definition for table output.
13-
*/
14-
interface ColumnDef {
15-
header: string;
16-
get: (e: JobExecution) => string;
17-
}
18-
19-
/**
20-
* Available columns for job execution list output.
21-
*/
22-
const COLUMNS: Record<string, ColumnDef> = {
10+
const COLUMNS: Record<string, ColumnDef<JobExecution>> = {
2311
id: {
2412
header: 'Execution ID',
2513
get: (e) => e.id ?? '-',
@@ -124,62 +112,8 @@ export default class JobSearch extends InstanceCommand<typeof JobSearch> {
124112
}),
125113
);
126114

127-
this.printExecutionsTable(results.hits);
115+
createTable(COLUMNS).render(results.hits, DEFAULT_COLUMNS);
128116

129117
return results;
130118
}
131-
132-
/**
133-
* Calculate dynamic column widths based on content.
134-
*/
135-
private calculateColumnWidths(executions: JobExecution[], columnKeys: string[]): Map<string, number> {
136-
const widths = new Map<string, number>();
137-
const padding = 2;
138-
139-
for (const key of columnKeys) {
140-
const col = COLUMNS[key];
141-
let maxWidth = col.header.length;
142-
143-
for (const exec of executions) {
144-
const value = col.get(exec);
145-
maxWidth = Math.max(maxWidth, value.length);
146-
}
147-
148-
widths.set(key, maxWidth + padding);
149-
}
150-
151-
return widths;
152-
}
153-
154-
private printExecutionsTable(executions: JobExecution[]): void {
155-
const termWidth = process.stdout.columns || 120;
156-
const ui = cliui({width: termWidth});
157-
const columnKeys = DEFAULT_COLUMNS;
158-
159-
const widths = this.calculateColumnWidths(executions, columnKeys);
160-
161-
// Header
162-
const headerCols = columnKeys.map((key) => ({
163-
text: COLUMNS[key].header,
164-
width: widths.get(key),
165-
padding: [0, 1, 0, 0] as [number, number, number, number],
166-
}));
167-
ui.div(...headerCols);
168-
169-
// Separator
170-
const totalWidth = [...widths.values()].reduce((sum, w) => sum + w, 0);
171-
ui.div({text: '─'.repeat(Math.min(totalWidth, termWidth)), padding: [0, 0, 0, 0]});
172-
173-
// Rows
174-
for (const exec of executions) {
175-
const rowCols = columnKeys.map((key) => ({
176-
text: COLUMNS[key].get(exec),
177-
width: widths.get(key),
178-
padding: [0, 1, 0, 0] as [number, number, number, number],
179-
}));
180-
ui.div(...rowCols);
181-
}
182-
183-
ux.stdout(ui.toString());
184-
}
185119
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {Args, Flags} from '@oclif/core';
2+
import {MrtCommand} from '@salesforce/b2c-tooling/cli';
3+
import {deleteEnvVar} from '@salesforce/b2c-tooling/operations/mrt';
4+
import {t} from '../../../i18n/index.js';
5+
6+
/**
7+
* Delete an environment variable from an MRT project environment.
8+
*/
9+
export default class MrtEnvVarDelete extends MrtCommand<typeof MrtEnvVarDelete> {
10+
static args = {
11+
key: Args.string({
12+
description: 'Environment variable name',
13+
required: true,
14+
}),
15+
};
16+
17+
static description = t(
18+
'commands.mrt.envVar.delete.description',
19+
'Delete an environment variable from a Managed Runtime environment',
20+
);
21+
22+
static enableJsonFlag = true;
23+
24+
static examples = [
25+
'<%= config.bin %> <%= command.id %> MY_VAR --project acme-storefront --environment production',
26+
'<%= config.bin %> <%= command.id %> OLD_API_KEY -p my-project -e staging',
27+
];
28+
29+
static flags = {
30+
...MrtCommand.baseFlags,
31+
project: Flags.string({
32+
char: 'p',
33+
description: 'MRT project slug',
34+
required: true,
35+
}),
36+
environment: Flags.string({
37+
char: 'e',
38+
description: 'Target environment (e.g., staging, production)',
39+
required: true,
40+
}),
41+
};
42+
43+
async run(): Promise<{key: string; project: string; environment: string}> {
44+
this.requireMrtCredentials();
45+
46+
const {key} = this.args;
47+
const {project, environment} = this.flags;
48+
49+
await deleteEnvVar(
50+
{
51+
projectSlug: project,
52+
environment,
53+
key,
54+
},
55+
this.getMrtAuth(),
56+
);
57+
58+
this.log(
59+
t('commands.mrt.envVar.delete.success', 'Deleted {{key}} from {{project}}/{{environment}}', {
60+
key,
61+
project,
62+
environment,
63+
}),
64+
);
65+
66+
return {key, project, environment};
67+
}
68+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {Flags} from '@oclif/core';
2+
import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling/cli';
3+
import {listEnvVars, type ListEnvVarsResult, type EnvironmentVariable} from '@salesforce/b2c-tooling/operations/mrt';
4+
import {t} from '../../../i18n/index.js';
5+
6+
const COLUMNS: Record<string, ColumnDef<EnvironmentVariable>> = {
7+
name: {
8+
header: 'Name',
9+
get: (v) => v.name,
10+
},
11+
value: {
12+
header: 'Value',
13+
get: (v) => v.value,
14+
},
15+
status: {
16+
header: 'Status',
17+
get: (v) => v.publishingStatusDescription,
18+
},
19+
updated: {
20+
header: 'Updated',
21+
get: (v) => (v.updatedAt ? new Date(v.updatedAt).toLocaleString() : '-'),
22+
},
23+
};
24+
25+
const DEFAULT_COLUMNS = ['name', 'value', 'status', 'updated'];
26+
27+
/**
28+
* List environment variables on an MRT project environment.
29+
*/
30+
export default class MrtEnvVarList extends MrtCommand<typeof MrtEnvVarList> {
31+
static description = t(
32+
'commands.mrt.envVar.list.description',
33+
'List environment variables on a Managed Runtime environment',
34+
);
35+
36+
static enableJsonFlag = true;
37+
38+
static examples = [
39+
'<%= config.bin %> <%= command.id %> --project acme-storefront --environment production',
40+
'<%= config.bin %> <%= command.id %> -p my-project -e staging',
41+
'<%= config.bin %> <%= command.id %> -p my-project -e production --json',
42+
];
43+
44+
static flags = {
45+
...MrtCommand.baseFlags,
46+
project: Flags.string({
47+
char: 'p',
48+
description: 'MRT project slug',
49+
required: true,
50+
}),
51+
environment: Flags.string({
52+
char: 'e',
53+
description: 'Target environment (e.g., staging, production)',
54+
required: true,
55+
}),
56+
};
57+
58+
async run(): Promise<ListEnvVarsResult> {
59+
this.requireMrtCredentials();
60+
61+
const {project, environment} = this.flags;
62+
63+
this.log(
64+
t('commands.mrt.envVar.list.fetching', 'Listing env vars for {{project}}/{{environment}}...', {
65+
project,
66+
environment,
67+
}),
68+
);
69+
70+
const result = await listEnvVars(
71+
{
72+
projectSlug: project,
73+
environment,
74+
},
75+
this.getMrtAuth(),
76+
);
77+
78+
if (!this.jsonEnabled()) {
79+
if (result.variables.length === 0) {
80+
this.log(t('commands.mrt.envVar.list.empty', 'No environment variables found.'));
81+
} else {
82+
createTable(COLUMNS).render(result.variables, DEFAULT_COLUMNS);
83+
}
84+
}
85+
86+
return result;
87+
}
88+
}

0 commit comments

Comments
 (0)