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
11 changes: 11 additions & 0 deletions .changeset/audit-cleanup-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@salesforce/b2c-cli': patch
---

CLI cleanup and correctness fixes:

- `b2c cip query`, `cip describe`, `cip tables`, and `cip report *` now stream output through oclif's `ux.stdout` instead of writing directly to `process.stdout`. This restores the `--json` flag and makes output capturable by tests and CI.
- Long-running commands (`code:watch`, `logs:tail`, `mrt:tail-logs`) now deregister their SIGINT/SIGTERM handlers when finished, so re-invocations no longer stack handlers on the same process.
- Hook and signal-handler errors that were previously swallowed (`job:run` afterOperation hooks, `logs:tail` stop, `setup:ide:prophet` console fallbacks) now log at debug instead of disappearing.
- AM list commands (`am clients|roles|users list`) share a single `amPageSizeFlag` definition.
- Removed deprecated `LocalSourceResult` re-export.
6 changes: 6 additions & 0 deletions .changeset/audit-cleanup-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@salesforce/b2c-dx-mcp': patch
---

- Telemetry send failures are no longer silently swallowed; they now log at debug level so deployment-monitoring drift is visible behind the `--debug` flag.
- `registerToolsets()` throws a clear error if invoked more than once for the same server instance (instead of producing a cryptic duplicate-tool error from the SDK).
6 changes: 6 additions & 0 deletions .changeset/audit-cleanup-mrt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@salesforce/mrt-utilities': patch
---

- The Lambda response adapter's `pipeToDestination` now destroys the destination stream when the underlying pipeline rejects, so consumers fail fast instead of hanging.
- `pipedDestinations` cleanup is unified between the success and error paths.
13 changes: 13 additions & 0 deletions .changeset/audit-cleanup-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@salesforce/b2c-tooling-sdk': patch
---

Hardened auth and long-running operation paths:

- Token store now writes atomically (temp file + rename) so concurrent CLI invocations cannot corrupt `auth-session.json`.
- `OAuthStrategy.getAccessToken()` coalesces concurrent refreshes onto a single in-flight request, preventing token-endpoint stampedes.
- Debug session cleans up its keepalive/poll timers if `connect()` fails after starting them.
- `downloadCartridges` and `deployCartridges` use try/finally around progress timers so an aborted or failing request can no longer leak intervals.
- New `@salesforce/b2c-tooling-sdk/ux` export surfaces the canonical `confirm()` prompt; CLI re-exports from here.
- New `auth/jwt-utils` consolidates JWT `exp`/`scope` decoding previously duplicated across three auth strategies.
- Better error message when the implicit-OAuth port is already in use (suggests `SFCC_OAUTH_LOCAL_PORT`).
9 changes: 9 additions & 0 deletions .changeset/audit-cleanup-vsx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'b2c-vs-extension': patch
---

VS Code extension reliability fixes:

- Swagger API Browser webview no longer attempts `postMessage` after the panel has been disposed (previously could throw on token refresh or proxy responses arriving after close).
- Sandbox tree polling no longer stacks "stop-check" timers when the configured polling interval is shorter than the 3-second stabilization window.
- Code Sync now drains pending uploads/deletes before tearing down its file watchers, so saves immediately preceding a stop are no longer dropped.
6 changes: 2 additions & 4 deletions packages/b2c-cli/src/commands/am/clients/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Flags, Errors} from '@oclif/core';
import {AmCommand, TableRenderer, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli';
import type {AccountManagerApiClient, APIClientCollection} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../../i18n/index.js';
import {amPageSizeFlag} from '../../../utils/am/flags.js';

/** Format date as MM/DD/YYYY HH:MM:SS with zero-padding for equal column width. */
function formatDateEqualLength(value: Date | number | string): string {
Expand Down Expand Up @@ -75,10 +76,7 @@ export default class ClientList extends AmCommand<typeof ClientList> {
];

static flags = {
size: Flags.integer({
char: 's',
description: 'Page size (default: 20, min: 1, max: 4000)',
}),
size: amPageSizeFlag,
page: Flags.integer({
description: 'Page number (zero-based index, default: 0, min: 0)',
}),
Expand Down
6 changes: 2 additions & 4 deletions packages/b2c-cli/src/commands/am/roles/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Flags, Errors} from '@oclif/core';
import {AmCommand, TableRenderer, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli';
import type {AccountManagerRole, RoleCollection} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../../i18n/index.js';
import {amPageSizeFlag} from '../../../utils/am/flags.js';

const COLUMNS: Record<string, ColumnDef<AccountManagerRole>> = {
id: {
Expand Down Expand Up @@ -64,10 +65,7 @@ export default class RoleList extends AmCommand<typeof RoleList> {
];

static flags = {
size: Flags.integer({
char: 's',
description: 'Page size (default: 20, min: 1, max: 4000)',
}),
size: amPageSizeFlag,
page: Flags.integer({
description: 'Page number (zero-based index, default: 0, min: 0)',
}),
Expand Down
6 changes: 2 additions & 4 deletions packages/b2c-cli/src/commands/am/users/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Flags, Errors} from '@oclif/core';
import {AmCommand, TableRenderer, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli';
import type {AccountManagerUser, UserCollection} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../../i18n/index.js';
import {amPageSizeFlag} from '../../../utils/am/flags.js';

const COLUMNS: Record<string, ColumnDef<AccountManagerUser>> = {
mail: {
Expand Down Expand Up @@ -89,10 +90,7 @@ export default class UserList extends AmCommand<typeof UserList> {
];

static flags = {
size: Flags.integer({
char: 's',
description: 'Page size (default: 20, min: 1, max: 4000)',
}),
size: amPageSizeFlag,
page: Flags.integer({
description: 'Page number (zero-based index, default: 0, min: 0)',
}),
Expand Down
6 changes: 3 additions & 3 deletions packages/b2c-cli/src/commands/cip/describe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Args, Flags} from '@oclif/core';
import {describeCipTable} from '@salesforce/b2c-tooling-sdk';
import {withDocs} from '../../i18n/index.js';
import {CipCommand} from '../../utils/cip/command.js';
import {renderTable, toCsv, type CipOutputFormat} from '../../utils/cip/format.js';
import {renderTable, writeCsv, writeJson, type CipOutputFormat} from '../../utils/cip/format.js';

const {from: _unusedFrom, to: _unusedTo, ...cipMetadataFlags} = CipCommand.baseFlags;

Expand Down Expand Up @@ -77,7 +77,7 @@ export default class CipDescribe extends CipCommand<typeof CipDescribe> {
}

if (this.flags.format === 'json') {
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
writeJson(output);
return output;
}

Expand All @@ -88,7 +88,7 @@ export default class CipDescribe extends CipCommand<typeof CipDescribe> {
private renderRows(rows: CipDescribeCommandResult['columns'], format: CipOutputFormat): void {
const columns = ['column', 'dataType', 'isNullable'];
if (format === 'csv') {
process.stdout.write(`${toCsv(columns, rows)}\n`);
writeCsv(columns, rows);
return;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/b2c-cli/src/commands/cip/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import fs from 'node:fs';
import {Args, Flags} from '@oclif/core';
import {withDocs} from '../../i18n/index.js';
import {CipCommand} from '../../utils/cip/command.js';
import {renderTable, toCsv, type CipOutputFormat} from '../../utils/cip/format.js';
import {renderTable, writeCsv, writeJson, type CipOutputFormat} from '../../utils/cip/format.js';

interface CipQueryCommandResult {
columns: string[];
Expand Down Expand Up @@ -56,7 +56,7 @@ export default class CipQuery extends CipCommand<typeof CipQuery> {
}

if (this.flags.format === 'json') {
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
writeJson(output);
return output;
}

Expand All @@ -66,7 +66,7 @@ export default class CipQuery extends CipCommand<typeof CipQuery> {

private renderRows(columns: string[], rows: Array<Record<string, unknown>>, format: CipOutputFormat): void {
if (format === 'csv') {
process.stdout.write(`${toCsv(columns, rows)}\n`);
writeCsv(columns, rows);
return;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/b2c-cli/src/commands/cip/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Flags} from '@oclif/core';
import {listCipTables} from '@salesforce/b2c-tooling-sdk';
import {withDocs} from '../../i18n/index.js';
import {CipCommand} from '../../utils/cip/command.js';
import {renderTable, toCsv, type CipOutputFormat} from '../../utils/cip/format.js';
import {renderTable, writeCsv, writeJson, type CipOutputFormat} from '../../utils/cip/format.js';

const {from: _unusedFrom, to: _unusedTo, ...cipMetadataFlags} = CipCommand.baseFlags;

Expand Down Expand Up @@ -73,7 +73,7 @@ export default class CipTables extends CipCommand<typeof CipTables> {
}

if (this.flags.format === 'json') {
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
writeJson(output);
return output;
}

Expand All @@ -84,7 +84,7 @@ export default class CipTables extends CipCommand<typeof CipTables> {
private renderRows(rows: CipTablesCommandResult['tables'], format: CipOutputFormat): void {
const columns = ['table', 'type'];
if (format === 'csv') {
process.stdout.write(`${toCsv(columns, rows)}\n`);
writeCsv(columns, rows);
return;
}

Expand Down
24 changes: 19 additions & 5 deletions packages/b2c-cli/src/commands/code/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,32 @@ export default class CodeWatch extends CartridgeCommand<typeof CodeWatch> {
);
this.log(t('commands.code.watch.pressCtrlC', 'Press Ctrl+C to stop'));

// Keep the process running until interrupted
// Keep the process running until interrupted. Capture handler ref so we
// can deregister on resolve — otherwise repeated invocations stack handlers.
let cleanupRef: (() => void) | undefined;
await new Promise<void>((resolve) => {
const cleanup = () => {
let stopping = false;
const cleanup = (): void => {
if (stopping) return;
stopping = true;
this.log(t('commands.code.watch.stopping', '\nStopping watcher...'));
result.stop().then(() => {
resolve();
});
result.stop().then(
() => resolve(),
(error: unknown) => {
this.logger.debug({err: error}, '[code:watch] stop() failed during signal handling');
resolve();
},
);
};
cleanupRef = cleanup;

process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
});
if (cleanupRef) {
process.removeListener('SIGINT', cleanupRef);
process.removeListener('SIGTERM', cleanupRef);
}
} catch (error) {
if (error instanceof Error) {
this.error(t('commands.code.watch.failed', 'Watch failed: {{message}}', {message: error.message}));
Expand Down
8 changes: 6 additions & 2 deletions packages/b2c-cli/src/commands/job/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,16 @@ export default class JobRun extends JobCommand<typeof JobRun> {
}

private handleExecutionError(error: unknown, context: B2COperationContext): never {
// Run afterOperation hooks with failure (fire-and-forget, errors ignored)
// Fire-and-forget: we're already on the error path and rethrow below; surface
// hook failures in the debug log so they aren't completely invisible, but
// don't shadow the original error.
this.runAfterHooks(context, {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
duration: Date.now() - context.startTime,
}).catch(() => {});
}).catch((error_: unknown) => {
this.logger.debug({err: error_}, '[job:run] afterOperation hook failed');
});

if (error instanceof Error) {
this.error(t('commands.job.run.executionFailed', 'Failed to execute job: {{message}}', {message: error.message}));
Expand Down
29 changes: 18 additions & 11 deletions packages/b2c-cli/src/commands/logs/tail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,28 @@ export default class LogsTail extends InstanceCommand<typeof LogsTail> {
},
});

// Handle SIGINT (Ctrl+C) for graceful shutdown
const handleSignal = async (): Promise<void> => {
// Handle SIGINT (Ctrl+C) for graceful shutdown. Capture handler refs so we
// can deregister on exit — otherwise repeated invocations of this command
// (e.g. in tests) would stack handlers on the global process.
let stopping = false;
const handleSignal = (): void => {
if (stopping) return;
stopping = true;
this.log(t('commands.logs.tail.stopping', '\nStopping log tail...'));
await stop();
stop().catch((error: unknown) => {
this.logger.debug({err: error}, '[logs:tail] stop() failed during signal handling');
});
};

process.on('SIGINT', () => {
handleSignal().catch(() => {});
});
process.on('SIGTERM', () => {
handleSignal().catch(() => {});
});
process.on('SIGINT', handleSignal);
process.on('SIGTERM', handleSignal);

// Wait for tailing to complete
await done;
try {
await done;
} finally {
process.removeListener('SIGINT', handleSignal);
process.removeListener('SIGTERM', handleSignal);
}

this.log(t('commands.logs.tail.stopped', 'Log tailing stopped.'));
}
Expand Down
9 changes: 6 additions & 3 deletions packages/b2c-cli/src/commands/mrt/save-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {DEFAULT_MRT_ORIGIN} from '@salesforce/b2c-tooling-sdk/clients';
import {t, withDocs} from '../../i18n/index.js';
import {confirm} from '../../prompts.js';

/** Basename of the Mobify-format credentials file in the user's home directory. */
const MOBIFY_FILE_BASENAME = '.mobify';

interface MobifyConfigFile {
username?: string;
api_key?: string;
Expand Down Expand Up @@ -107,11 +110,11 @@ export default class MrtSaveCredentials extends BaseCommand<typeof MrtSaveCreden
if (cloudOrigin) {
try {
const url = new URL(cloudOrigin);
return path.join(os.homedir(), `.mobify--${url.hostname}`);
return path.join(os.homedir(), `${MOBIFY_FILE_BASENAME}--${url.hostname}`);
} catch {
return path.join(os.homedir(), `.mobify--${cloudOrigin}`);
return path.join(os.homedir(), `${MOBIFY_FILE_BASENAME}--${cloudOrigin}`);
}
}
return path.join(os.homedir(), '.mobify');
return path.join(os.homedir(), MOBIFY_FILE_BASENAME);
}
}
31 changes: 20 additions & 11 deletions packages/b2c-cli/src/commands/mrt/tail-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,15 @@ export default class MrtTailLogs extends MrtCommand<typeof MrtTailLogs> {
const levelFilter = this.flags.level;
const searchFilter = this.flags.search;

// Compile search regex (case-insensitive, global for highlighting)
// Compile search regexes:
// - `searchTestRegex` (no `g` flag) is used for `.test()` so we don't have
// to manage `lastIndex` on a shared global regex.
// - `searchRegex` (global) is passed to the highlighter to mark every match.
let searchTestRegex: RegExp | undefined;
let searchRegex: RegExp | undefined;
if (searchFilter) {
try {
searchTestRegex = new RegExp(searchFilter, 'i');
searchRegex = new RegExp(searchFilter, 'gi');
} catch {
this.error(`Invalid search pattern: "${searchFilter}". Must be a valid regular expression.`);
Expand Down Expand Up @@ -112,15 +117,10 @@ export default class MrtTailLogs extends MrtCommand<typeof MrtTailLogs> {
if (upperLevels && (!entry.level || !upperLevels.has(entry.level.toUpperCase()))) return;

// Apply search filter (regex match against message and raw)
if (searchRegex) {
// Reset lastIndex since we reuse the global regex
searchRegex.lastIndex = 0;
const matchesMessage = searchRegex.test(entry.message);
searchRegex.lastIndex = 0;
const matchesRaw = searchRegex.test(entry.raw);
if (searchTestRegex) {
const matchesMessage = searchTestRegex.test(entry.message);
const matchesRaw = searchTestRegex.test(entry.raw);
if (!matchesMessage && !matchesRaw) return;
// Reset for highlighting pass
searchRegex.lastIndex = 0;
}

if (this.jsonEnabled()) {
Expand All @@ -147,14 +147,23 @@ export default class MrtTailLogs extends MrtCommand<typeof MrtTailLogs> {
auth,
);

// Graceful shutdown on signals
// Graceful shutdown on signals. Capture ref so we can deregister; otherwise
// repeated invocations of this command stack handlers on the global process.
let stopping = false;
const handleSignal = (): void => {
if (stopping) return;
stopping = true;
stop();
};

process.on('SIGINT', handleSignal);
process.on('SIGTERM', handleSignal);

await done;
try {
await done;
} finally {
process.removeListener('SIGINT', handleSignal);
process.removeListener('SIGTERM', handleSignal);
}
}
}
11 changes: 9 additions & 2 deletions packages/b2c-cli/src/commands/setup/ide/prophet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,20 @@ function logProphetDw(message, error) {
var suffix = error && error.message ? ': ' + String(error.message) : '';
var line = '[b2c setup ide prophet] ' + message + suffix;

// Best-effort logging: this script is loaded by external tools (Prophet) where
// console may be redirected or unavailable. A failure to log a log message is
// not actionable and must not interfere with returning the resolved config.
try {
console.error(line);
} catch (logError) {}
} catch (logError) {
/* ignore */
}

try {
console.log(line);
} catch (logError) {}
} catch (logError) {
/* ignore */
}
}

function loadDotEnv() {
Expand Down
Loading
Loading