Skip to content
Draft
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
17 changes: 17 additions & 0 deletions .changeset/wave-review-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@salesforce/b2c-tooling-sdk': patch
'@salesforce/b2c-cli': patch
'@salesforce/mrt-utilities': patch
'b2c-vs-extension': patch
---

Hardens long-running operations and atomic config writes:

- dw.json mutations (`addInstance`, `removeInstance`, `setActiveInstance`) now go through a per-path async serializer and write via temp-file + rename, so concurrent CLI invocations within the same process can no longer interleave reads and writes.
- The session file written by stateful auth removes an exists-then-mkdir TOCTOU and cleans up orphan tmp files on rename failure.
- Cartridge deploy/download progress intervals are wrapped in a `withProgress` helper that always tears down on exception.
- `b2c jobs run` no longer silently treats a body-read failure as "not the JobAlreadyRunning case" during 400 detection.
- MRT proxy `onError` no longer crashes when upstream begins streaming before erroring (`headersSent` guard).
- VS Code extension page-template renderer fixes a latent ordering bug where `${pageName}Data` placeholders were left orphaned.
- Sandbox CLI commands now route output through `this.log` so `--json` mode and test output silencing work as documented; a lint rule prevents regression.
- Shared ANSI palette consolidated in `@salesforce/b2c-tooling-sdk/cli` so log/MRT-log renderers stay visually consistent.
11 changes: 11 additions & 0 deletions packages/b2c-cli/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,15 @@ export default [
'import/no-unresolved': 'off',
},
},
{
// Commands must route output through oclif (this.log/this.error/ux.stdout) so that
// --json mode, log redirection, and test stdout silencing work. Bare console.* breaks
// these contracts. The prophet IDE script is exempt because it serializes JS source
// that runs outside the CLI process.
files: ['src/commands/**/*.ts'],
ignores: ['src/commands/setup/ide/prophet.ts'],
rules: {
'no-console': 'error',
},
},
];
16 changes: 8 additions & 8 deletions packages/b2c-cli/src/commands/sandbox/ips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,24 +84,24 @@ export default class SandboxIps extends OdsCommand<typeof SandboxIps> {
private printIps(system: SystemInfoSpec, realm?: string): void {
const context = realm ? t('commands.sandbox.ips.realmLabel', ' (for realm {{realm}})', {realm}) : '';

console.log(t('commands.sandbox.ips.inboundHeader', 'Inbound IP addresses{{context}}:', {context}));
this.log(t('commands.sandbox.ips.inboundHeader', 'Inbound IP addresses{{context}}:', {context}));
for (const ip of system.inboundIps ?? []) {
console.log(` - ${ip}`);
this.log(` - ${ip}`);
}

console.log();
this.log('');

console.log(t('commands.sandbox.ips.outboundHeader', 'Outbound IP addresses{{context}}:', {context}));
this.log(t('commands.sandbox.ips.outboundHeader', 'Outbound IP addresses{{context}}:', {context}));
for (const ip of system.outboundIps ?? []) {
console.log(` - ${ip}`);
this.log(` - ${ip}`);
}

if (system.sandboxIps && system.sandboxIps.length > 0) {
console.log();
this.log('');

console.log(t('commands.sandbox.ips.sandboxHeader', 'Sandbox IP addresses{{context}}:', {context}));
this.log(t('commands.sandbox.ips.sandboxHeader', 'Sandbox IP addresses{{context}}:', {context}));
for (const ip of system.sandboxIps) {
console.log(` - ${ip}`);
this.log(` - ${ip}`);
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/b2c-cli/src/commands/sandbox/realm/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export default class SandboxRealmConfiguration extends OdsCommand<typeof Sandbox
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cfg = configuration as any;

console.log('Realm Configuration');
console.log('───────────────────');
this.log('Realm Configuration');
this.log('───────────────────');

const maxTtlRaw = cfg.sandbox?.sandboxTTL?.maximum as number | undefined;
const maxTtlDisplay = maxTtlRaw === undefined ? undefined : maxTtlRaw >= 2_147_483_647 ? '0' : String(maxTtlRaw);
Expand All @@ -98,16 +98,16 @@ export default class SandboxRealmConfiguration extends OdsCommand<typeof Sandbox

for (const [label, value] of rows) {
if (value !== undefined) {
console.log(`${label}: ${value}`);
this.log(`${label}: ${value}`);
}
}

if (cfg.sandbox?.startScheduler) {
console.log(`Start Scheduler: ${JSON.stringify(cfg.sandbox.startScheduler)}`);
this.log(`Start Scheduler: ${JSON.stringify(cfg.sandbox.startScheduler)}`);
}

if (cfg.sandbox?.stopScheduler) {
console.log(`Stop Scheduler: ${JSON.stringify(cfg.sandbox.stopScheduler)}`);
this.log(`Stop Scheduler: ${JSON.stringify(cfg.sandbox.stopScheduler)}`);
}
}
}
6 changes: 3 additions & 3 deletions packages/b2c-cli/src/commands/sandbox/realm/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ export default class SandboxRealmList extends OdsCommand<typeof SandboxRealmList

// Human-readable output: simple table-like listing

console.log('Realm');
this.log('Realm');

console.log('─────');
this.log('─────');

for (const realm of realms) {
console.log(realm.realmId);
this.log(realm.realmId);
}

return response;
Expand Down
18 changes: 9 additions & 9 deletions packages/b2c-cli/src/commands/sandbox/realm/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ export default class SandboxRealmUsage extends OdsCommand<typeof SandboxRealmUsa
}

private printRealmUsageSummary(usage: RealmUsageModel): void {
console.log('Realm Usage Summary');
this.log('Realm Usage Summary');

console.log('───────────────────');
this.log('───────────────────');

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyUsage = usage as any;
Expand All @@ -126,17 +126,17 @@ export default class SandboxRealmUsage extends OdsCommand<typeof SandboxRealmUsa
if (value !== undefined) {
hasSummaryMetric = true;

console.log(`${label}: ${value}`);
this.log(`${label}: ${value}`);
}
}

if (anyUsage.minutesUpByProfile && anyUsage.minutesUpByProfile.length > 0) {
console.log();
this.log('');

console.log('Minutes up by profile:');
this.log('Minutes up by profile:');
for (const item of anyUsage.minutesUpByProfile) {
if (item.profile && item.minutes !== undefined) {
console.log(` ${item.profile}: ${item.minutes} minutes`);
this.log(` ${item.profile}: ${item.minutes} minutes`);
}
}
}
Expand All @@ -150,13 +150,13 @@ export default class SandboxRealmUsage extends OdsCommand<typeof SandboxRealmUsa
!hasDetailedData &&
!(anyUsage.minutesUpByProfile && anyUsage.minutesUpByProfile.length > 0)
) {
console.log(
this.log(
t('commands.realm.usage.emptyPeriod', 'No usage data was returned for this realm in the requested period.'),
);
} else if (hasDetailedData) {
console.log();
this.log('');

console.log(
this.log(
t(
'commands.realm.usage.detailedHint',
'Detailed usage data is available; re-run with --json to see full details.',
Expand Down
8 changes: 4 additions & 4 deletions packages/b2c-cli/src/commands/sandbox/realm/usages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export default class SandboxRealmUsages extends OdsCommand<typeof SandboxRealmUs
}

private printUsage(items: MultiRealmUsageModel[]): void {
console.log('Realm Active Created Deleted Minutes Up Minutes Down Sandbox Seconds');
console.log('───── ────── ─────── ─────── ────────── ──────────── ───────────────');
this.log('Realm Active Created Deleted Minutes Up Minutes Down Sandbox Seconds');
this.log('───── ────── ─────── ─────── ────────── ──────────── ───────────────');

for (const item of items) {
const usage = item.realmUsage;
Expand All @@ -107,10 +107,10 @@ export default class SandboxRealmUsages extends OdsCommand<typeof SandboxRealmUs
String(usage?.sandboxSeconds ?? '-').padStart(15),
];

console.log(row.join(' '));
this.log(row.join(' '));

if (item.error) {
console.log(` ! ${item.error}`);
this.log(` ! ${item.error}`);
}
}
}
Expand Down
20 changes: 10 additions & 10 deletions packages/b2c-cli/src/commands/sandbox/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,26 +73,26 @@ export default class SandboxSettingsCommand extends OdsCommand<typeof SandboxSet
const ocapi = settings.ocapi ?? [];
const webdav = settings.webdav ?? [];

console.log('Sandbox Settings');
console.log('────────────────');
console.log(`OCAPI client entries: ${ocapi.length}`);
console.log(`WebDAV client entries: ${webdav.length}`);
this.log('Sandbox Settings');
this.log('────────────────');
this.log(`OCAPI client entries: ${ocapi.length}`);
this.log(`WebDAV client entries: ${webdav.length}`);

if (ocapi.length > 0) {
console.log();
console.log('OCAPI');
this.log('');
this.log('OCAPI');
for (const entry of ocapi) {
const resources = entry.resources?.length ?? 0;
console.log(` - ${entry.client_id ?? 'unknown-client'} (${resources} resource rules)`);
this.log(` - ${entry.client_id ?? 'unknown-client'} (${resources} resource rules)`);
}
}

if (webdav.length > 0) {
console.log();
console.log('WebDAV');
this.log('');
this.log('WebDAV');
for (const entry of webdav) {
const permissions = entry.permissions?.length ?? 0;
console.log(` - ${entry.client_id ?? 'unknown-client'} (${permissions} permission rules)`);
this.log(` - ${entry.client_id ?? 'unknown-client'} (${permissions} permission rules)`);
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/b2c-cli/src/commands/sandbox/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,17 @@ export default class SandboxStorage extends OdsCommand<typeof SandboxStorage> {
}

private printStorage(storage: SandboxStorageModel): void {
console.log('Sandbox Storage');
console.log('───────────────');
console.log('Filesystem Total (MB) Used (MB) Used (%)');
console.log('──────────────────────── ────────── ───────── ────────');
this.log('Sandbox Storage');
this.log('───────────────');
this.log('Filesystem Total (MB) Used (MB) Used (%)');
this.log('──────────────────────── ────────── ───────── ────────');

for (const [name, usage] of Object.entries(storage)) {
const total = usage?.spaceTotal ?? '-';
const used = usage?.spaceUsed ?? '-';
const percentage = usage?.percentageUsed ?? '-';

console.log(
this.log(
`${name.padEnd(24)} ${String(total).padStart(10)} ${String(used).padStart(9)} ${String(percentage).padStart(8)}`,
);
}
Expand Down
18 changes: 9 additions & 9 deletions packages/b2c-cli/src/commands/sandbox/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ export default class SandboxUsage extends OdsCommand<typeof SandboxUsage> {
}

private printSandboxUsageSummary(usage: SandboxUsageModel): void {
console.log('Sandbox Usage Summary');
this.log('Sandbox Usage Summary');

console.log('─────────────────────');
this.log('─────────────────────');

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyUsage = usage as any;
Expand All @@ -113,17 +113,17 @@ export default class SandboxUsage extends OdsCommand<typeof SandboxUsage> {
if (value !== undefined) {
hasSummaryMetric = true;

console.log(`${label}: ${value}`);
this.log(`${label}: ${value}`);
}
}

if (anyUsage.minutesUpByProfile && anyUsage.minutesUpByProfile.length > 0) {
console.log();
this.log('');

console.log('Minutes up by profile:');
this.log('Minutes up by profile:');
for (const item of anyUsage.minutesUpByProfile) {
if (item.profile && item.minutes !== undefined) {
console.log(` ${item.profile}: ${item.minutes} minutes`);
this.log(` ${item.profile}: ${item.minutes} minutes`);
}
}
}
Expand All @@ -137,13 +137,13 @@ export default class SandboxUsage extends OdsCommand<typeof SandboxUsage> {
!hasDetailedData &&
!(anyUsage.minutesUpByProfile && anyUsage.minutesUpByProfile.length > 0)
) {
console.log(
this.log(
t('commands.sandbox.usage.emptyPeriod', 'No usage data was returned for this sandbox in the requested period.'),
);
} else if (hasDetailedData) {
console.log();
this.log('');

console.log(
this.log(
t(
'commands.sandbox.usage.detailedHint',
'Detailed usage data is available; re-run with --json to see full details.',
Expand Down
5 changes: 3 additions & 2 deletions packages/b2c-cli/src/commands/scapi/schemas/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
getExampleNames,
type OpenApiSchemaInput,
} from '@salesforce/b2c-tooling-sdk/schemas';
import {ScapiSchemasCommand, formatApiError} from '../../../utils/scapi/schemas.js';
import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
import {ScapiSchemasCommand} from '../../../utils/scapi/schemas.js';
import {t, withDocs} from '../../../i18n/index.js';

/**
Expand Down Expand Up @@ -186,7 +187,7 @@ export default class ScapiSchemasGet extends ScapiSchemasCommand<typeof ScapiSch
if (error) {
this.error(
t('commands.scapi.schemas.get.error', 'Failed to fetch schema: {{message}}', {
message: formatApiError(error, response),
message: getApiErrorMessage(error, response),
}),
);
}
Expand Down
5 changes: 3 additions & 2 deletions packages/b2c-cli/src/commands/scapi/schemas/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
import {Flags} from '@oclif/core';
import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
import {TableRenderer, columnFlagsFor, selectColumns, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli';
import type {SchemaListItem} from '@salesforce/b2c-tooling-sdk/clients';
import {ScapiSchemasCommand, formatApiError} from '../../../utils/scapi/schemas.js';
import {ScapiSchemasCommand} from '../../../utils/scapi/schemas.js';
import {t, withDocs} from '../../../i18n/index.js';

/**
Expand Down Expand Up @@ -116,7 +117,7 @@ export default class ScapiSchemasList extends ScapiSchemasCommand<typeof ScapiSc
if (error) {
this.error(
t('commands.scapi.schemas.list.error', 'Failed to fetch SCAPI schemas: {{message}}', {
message: formatApiError(error, response),
message: getApiErrorMessage(error, response),
}),
);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/b2c-cli/src/commands/slas/client/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
* 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 {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
import {randomUUID} from 'node:crypto';
import {
SlasClientCommand,
type Client,
type ClientOutput,
normalizeClientResponse,
printClientDetails,
formatApiError,
} from '../../../utils/slas/client.js';
import {t, withDocs} from '../../../i18n/index.js';

Expand Down Expand Up @@ -182,7 +182,7 @@ export default class SlasClientCreate extends SlasClientCommand<typeof SlasClien
if (error) {
this.error(
t('commands.slas.client.create.error', 'Failed to create/update SLAS client: {{message}}', {
message: formatApiError(error, response),
message: getApiErrorMessage(error, response),
}),
);
}
Expand Down
5 changes: 3 additions & 2 deletions packages/b2c-cli/src/commands/slas/client/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
import {Args} from '@oclif/core';
import {SlasClientCommand, formatApiError} from '../../../utils/slas/client.js';
import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
import {SlasClientCommand} from '../../../utils/slas/client.js';
import {t, withDocs} from '../../../i18n/index.js';

interface DeleteOutput {
Expand Down Expand Up @@ -60,7 +61,7 @@ export default class SlasClientDelete extends SlasClientCommand<typeof SlasClien
if (error) {
this.error(
t('commands.slas.client.delete.error', 'Failed to delete SLAS client: {{message}}', {
message: formatApiError(error, response),
message: getApiErrorMessage(error, response),
}),
);
}
Expand Down
Loading
Loading