Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/b2c-cli/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ const gitignorePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
headerPlugin.rules.header.meta.schema = false;

export default [
includeIgnoreFile(gitignorePath),
// Global ignores must come first - these patterns apply to all subsequent configs
// node_modules must be explicitly ignored because the .gitignore pattern only covers
// packages/b2c-cli/node_modules, not the monorepo root node_modules
{
ignores: ['test/functional/fixtures/**/*.js'],
ignores: ['**/node_modules/**', 'test/functional/fixtures/**/*.js'],
},
includeIgnoreFile(gitignorePath),
...oclif,
prettierPlugin,
{
Expand Down
162 changes: 91 additions & 71 deletions packages/b2c-cli/src/commands/job/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
import {Args, Flags} from '@oclif/core';
import {JobCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {JobCommand, type B2COperationContext} from '@salesforce/b2c-tooling-sdk/cli';
import {
executeJob,
waitForJob,
Expand Down Expand Up @@ -112,19 +112,7 @@ export default class JobRun extends JobCommand<typeof JobRun> {
waitForRunning: !noWaitRunning,
});
} catch (error) {
// Run afterOperation hooks with failure
await this.runAfterHooks(context, {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
duration: Date.now() - context.startTime,
});

if (error instanceof Error) {
this.error(
t('commands.job.run.executionFailed', 'Failed to execute job: {{message}}', {message: error.message}),
);
}
throw error;
this.handleExecutionError(error, context);
}

this.log(
Expand All @@ -136,59 +124,13 @@ export default class JobRun extends JobCommand<typeof JobRun> {

// Wait for completion if requested
if (wait) {
this.log(t('commands.job.run.waiting', 'Waiting for job to complete...'));

try {
execution = await waitForJob(this.instance, jobId, execution.id!, {
timeout: timeout ? timeout * 1000 : undefined,
onProgress: (exec, elapsed) => {
if (!this.jsonEnabled()) {
const elapsedSec = Math.floor(elapsed / 1000);
this.log(
t('commands.job.run.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
status: exec.execution_status,
elapsed: elapsedSec.toString(),
}),
);
}
},
});

const durationSec = execution.duration ? (execution.duration / 1000).toFixed(1) : 'N/A';
this.log(
t('commands.job.run.completed', 'Job completed: {{status}} (duration: {{duration}}s)', {
status: execution.exit_status?.code || execution.execution_status,
duration: durationSec,
}),
);

// Run afterOperation hooks with success
await this.runAfterHooks(context, {
success: true,
duration: Date.now() - context.startTime,
data: execution,
});
} catch (error) {
// Run afterOperation hooks with failure
await this.runAfterHooks(context, {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
duration: Date.now() - context.startTime,
data: error instanceof JobExecutionError ? error.execution : undefined,
});

if (error instanceof JobExecutionError) {
if (showLog) {
await this.showJobLog(error.execution);
}
this.error(
t('commands.job.run.jobFailed', 'Job failed: {{status}}', {
status: error.execution.exit_status?.code || 'ERROR',
}),
);
}
throw error;
}
execution = await this.waitForJobCompletion({
jobId,
executionId: execution.id!,
timeout,
showLog,
context,
});
} else {
// Not waiting - run afterOperation hooks with current state
await this.runAfterHooks(context, {
Expand All @@ -198,12 +140,43 @@ export default class JobRun extends JobCommand<typeof JobRun> {
});
}

// JSON output handled by oclif
if (this.jsonEnabled()) {
return execution;
return execution;
}

private handleExecutionError(error: unknown, context: B2COperationContext): never {
// Run afterOperation hooks with failure (fire-and-forget, errors ignored)
this.runAfterHooks(context, {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
duration: Date.now() - context.startTime,
}).catch(() => {});

if (error instanceof Error) {
this.error(t('commands.job.run.executionFailed', 'Failed to execute job: {{message}}', {message: error.message}));
}
throw error;
}

return execution;
private async handleWaitError(error: unknown, showLog: boolean, context: B2COperationContext): Promise<never> {
// Run afterOperation hooks with failure
await this.runAfterHooks(context, {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
duration: Date.now() - context.startTime,
data: error instanceof JobExecutionError ? error.execution : undefined,
});

if (error instanceof JobExecutionError) {
if (showLog) {
await this.showJobLog(error.execution);
}
this.error(
t('commands.job.run.jobFailed', 'Job failed: {{status}}', {
status: error.execution.exit_status?.code || 'ERROR',
}),
);
}
throw error;
}

private parseBody(body: string): Record<string, unknown> {
Expand All @@ -228,4 +201,51 @@ export default class JobRun extends JobCommand<typeof JobRun> {
};
});
}

private async waitForJobCompletion(options: {
jobId: string;
executionId: string;
timeout: number | undefined;
showLog: boolean;
context: B2COperationContext;
}): Promise<JobExecution> {
const {jobId, executionId, timeout, showLog, context} = options;
this.log(t('commands.job.run.waiting', 'Waiting for job to complete...'));

try {
const execution = await waitForJob(this.instance, jobId, executionId, {
timeout: timeout ? timeout * 1000 : undefined,
onProgress: (exec, elapsed) => {
if (!this.jsonEnabled()) {
const elapsedSec = Math.floor(elapsed / 1000);
this.log(
t('commands.job.run.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
status: exec.execution_status,
elapsed: elapsedSec.toString(),
}),
);
}
},
});

const durationSec = execution.duration ? (execution.duration / 1000).toFixed(1) : 'N/A';
this.log(
t('commands.job.run.completed', 'Job completed: {{status}} (duration: {{duration}}s)', {
status: execution.exit_status?.code || execution.execution_status,
duration: durationSec,
}),
);

// Run afterOperation hooks with success
await this.runAfterHooks(context, {
success: true,
duration: Date.now() - context.startTime,
data: execution,
});

return execution;
} catch (error) {
return this.handleWaitError(error, showLog, context);
}
}
}
106 changes: 44 additions & 62 deletions packages/b2c-cli/src/commands/ods/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,83 +84,65 @@ export default class OdsInfo extends OdsCommand<typeof OdsInfo> {
// User Info Section
ui.div({text: 'User Information', padding: [1, 0, 0, 0]});
ui.div({text: '─'.repeat(40), padding: [0, 0, 0, 0]});
this.renderUserInfo(ui, info.user);

if (info.user?.user) {
ui.div(
{text: 'Name:', width: 20, padding: [0, 2, 0, 0]},
{text: info.user.user.name || '-', padding: [0, 0, 0, 0]},
);
ui.div(
{text: 'Email:', width: 20, padding: [0, 2, 0, 0]},
{text: info.user.user.email || '-', padding: [0, 0, 0, 0]},
);
ui.div(
{text: 'User ID:', width: 20, padding: [0, 2, 0, 0]},
{text: info.user.user.id || '-', padding: [0, 0, 0, 0]},
);
}
// System Info Section
ui.div({text: '', padding: [0, 0, 0, 0]});
ui.div({text: 'System Information', padding: [1, 0, 0, 0]});
ui.div({text: '─'.repeat(40), padding: [0, 0, 0, 0]});
this.renderSystemInfo(ui, info.system);

if (info.user?.client) {
ui.div(
{text: 'Client ID:', width: 20, padding: [0, 2, 0, 0]},
{text: info.user.client.id || '-', padding: [0, 0, 0, 0]},
);
}
ux.stdout(ui.toString());
}

if (info.user?.roles && info.user.roles.length > 0) {
ui.div(
{text: 'Roles:', width: 20, padding: [0, 2, 0, 0]},
{text: info.user.roles.join(', '), padding: [0, 0, 0, 0]},
);
private renderArrayField(ui: ReturnType<typeof cliui>, label: string, values: string[] | undefined): void {
if (values && values.length > 0) {
this.renderField(ui, label, values.join(', '));
}
}

if (info.user?.realms && info.user.realms.length > 0) {
ui.div(
{text: 'Realms:', width: 20, padding: [0, 2, 0, 0]},
{text: info.user.realms.join(', '), padding: [0, 0, 0, 0]},
);
}
private renderField(ui: ReturnType<typeof cliui>, label: string, value: string): void {
ui.div({text: label, width: 20, padding: [0, 2, 0, 0]}, {text: value, padding: [0, 0, 0, 0]});
}

if (info.user?.sandboxes && info.user.sandboxes.length > 0) {
ui.div(
{text: 'Sandboxes:', width: 20, padding: [0, 2, 0, 0]},
{text: info.user.sandboxes.length.toString(), padding: [0, 0, 0, 0]},
);
private renderSystemInfo(ui: ReturnType<typeof cliui>, system: SystemInfoSpec | undefined): void {
if (!system) return;

if (system.region) {
this.renderField(ui, 'Region:', system.region);
}

// System Info Section
ui.div({text: '', padding: [0, 0, 0, 0]});
ui.div({text: 'System Information', padding: [1, 0, 0, 0]});
ui.div({text: '─'.repeat(40), padding: [0, 0, 0, 0]});
this.renderArrayField(ui, 'Inbound IPs:', system.inboundIps);
this.renderArrayField(ui, 'Outbound IPs:', system.outboundIps);

if (info.system?.region) {
ui.div({text: 'Region:', width: 20, padding: [0, 2, 0, 0]}, {text: info.system.region, padding: [0, 0, 0, 0]});
// Sandbox IPs with truncation
if (system.sandboxIps && system.sandboxIps.length > 0) {
const truncated = system.sandboxIps.length > 5;
const displayValue = system.sandboxIps.slice(0, 5).join(', ') + (truncated ? '...' : '');
this.renderField(ui, 'Sandbox IPs:', displayValue);
}
}

if (info.system?.inboundIps && info.system.inboundIps.length > 0) {
ui.div(
{text: 'Inbound IPs:', width: 20, padding: [0, 2, 0, 0]},
{text: info.system.inboundIps.join(', '), padding: [0, 0, 0, 0]},
);
}
private renderUserInfo(ui: ReturnType<typeof cliui>, user: undefined | UserInfoSpec): void {
if (!user) return;

if (info.system?.outboundIps && info.system.outboundIps.length > 0) {
ui.div(
{text: 'Outbound IPs:', width: 20, padding: [0, 2, 0, 0]},
{text: info.system.outboundIps.join(', '), padding: [0, 0, 0, 0]},
);
// User details
if (user.user) {
this.renderField(ui, 'Name:', user.user.name || '-');
this.renderField(ui, 'Email:', user.user.email || '-');
this.renderField(ui, 'User ID:', user.user.id || '-');
}

if (info.system?.sandboxIps && info.system.sandboxIps.length > 0) {
ui.div(
{text: 'Sandbox IPs:', width: 20, padding: [0, 2, 0, 0]},
{
text: info.system.sandboxIps.slice(0, 5).join(', ') + (info.system.sandboxIps.length > 5 ? '...' : ''),
padding: [0, 0, 0, 0],
},
);
// Client info
if (user.client) {
this.renderField(ui, 'Client ID:', user.client.id || '-');
}

ux.stdout(ui.toString());
// Arrays with length checks
this.renderArrayField(ui, 'Roles:', user.roles);
this.renderArrayField(ui, 'Realms:', user.realms);
if (user.sandboxes && user.sandboxes.length > 0) {
this.renderField(ui, 'Sandboxes:', user.sandboxes.length.toString());
}
}
}
Loading