Skip to content

Commit 85df828

Browse files
committed
Audit cleanup: dw.json atomicity, output discipline, timer leaks, ANSI palette
Outcomes from a multi-package code review (CLI, SDK, MCP, mrt-utilities, vs-extension). Fixes correctness and consistency issues; no new features. Concurrency / data integrity (SDK) - dw.json mutations (addInstance/removeInstance/setActiveInstance) now serialize through a per-path async lock, and saveDwJson writes via tmp-file + atomic rename with orphan cleanup. - stateful-store.setStoredSession drops an exists-then-mkdir TOCTOU and cleans up orphan tmp on rename failure. Resource cleanup (SDK) - code deploy/download progress intervals run inside a withProgress helper that always tears down on exception. - jobs/run.ts JobAlreadyRunning detection no longer silently swallows a text() failure; an unreadable body falls through to the normal error path with a debug log. - code/download.ts replaces non-null assertions on Map.get() with explicit guards. Output discipline (CLI) - 9 sandbox commands now route output through this.log; bare console.* in src/commands/** is now banned by ESLint (the Prophet IDE template script is exempt because it serializes JS source for an external runtime). Shared CLI palette (SDK + CLI) - New @salesforce/b2c-tooling-sdk/cli ANSI palette (LEVEL_COLORS, colorLevel, colorDim, colorHighlight); utils/logs/format.ts and utils/mrt-logs/format.ts now consume it instead of redefining. Public-API hygiene (CLI) - Dropped formatApiError re-export aliases in slas/client and scapi/schemas; 7 call sites now import getApiErrorMessage from the SDK directly. Removed redundant alias-passthrough test. Other - mrt-utilities configure-proxying onError guards res.headersSent before writeHead to avoid masking the original error. - vs-extension renderTemplate fixes a latent ordering bug where ${pageName}Data placeholders were left orphaned after the broader ${pageName} replacement ran. - Trimmed a duplicated docstring in mrt-utilities ssr-proxying and re-tagged the diff-with-instance comment in vs-extension as an intentional stub rather than a stale TODO. Verification - pnpm run lint:agent: clean across all packages - pnpm run typecheck:agent: clean - pnpm run test:agent: SDK 1722 / CLI 1215 / MCP 720 / MRT 516 passing
1 parent 3908dbb commit 85df828

33 files changed

Lines changed: 384 additions & 330 deletions

File tree

.changeset/wave-review-cleanup.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@salesforce/b2c-tooling-sdk': patch
3+
'@salesforce/b2c-cli': patch
4+
'@salesforce/mrt-utilities': patch
5+
'b2c-vs-extension': patch
6+
---
7+
8+
Hardens long-running operations and atomic config writes:
9+
10+
- 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.
11+
- The session file written by stateful auth removes an exists-then-mkdir TOCTOU and cleans up orphan tmp files on rename failure.
12+
- Cartridge deploy/download progress intervals are wrapped in a `withProgress` helper that always tears down on exception.
13+
- `b2c jobs run` no longer silently treats a body-read failure as "not the JobAlreadyRunning case" during 400 detection.
14+
- MRT proxy `onError` no longer crashes when upstream begins streaming before erroring (`headersSent` guard).
15+
- VS Code extension page-template renderer fixes a latent ordering bug where `${pageName}Data` placeholders were left orphaned.
16+
- Sandbox CLI commands now route output through `this.log` so `--json` mode and test output silencing work as documented; a lint rule prevents regression.
17+
- Shared ANSI palette consolidated in `@salesforce/b2c-tooling-sdk/cli` so log/MRT-log renderers stay visually consistent.

packages/b2c-cli/eslint.config.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,15 @@ export default [
8282
'import/no-unresolved': 'off',
8383
},
8484
},
85+
{
86+
// Commands must route output through oclif (this.log/this.error/ux.stdout) so that
87+
// --json mode, log redirection, and test stdout silencing work. Bare console.* breaks
88+
// these contracts. The prophet IDE script is exempt because it serializes JS source
89+
// that runs outside the CLI process.
90+
files: ['src/commands/**/*.ts'],
91+
ignores: ['src/commands/setup/ide/prophet.ts'],
92+
rules: {
93+
'no-console': 'error',
94+
},
95+
},
8596
];

packages/b2c-cli/src/commands/sandbox/ips.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,24 @@ export default class SandboxIps extends OdsCommand<typeof SandboxIps> {
8484
private printIps(system: SystemInfoSpec, realm?: string): void {
8585
const context = realm ? t('commands.sandbox.ips.realmLabel', ' (for realm {{realm}})', {realm}) : '';
8686

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

92-
console.log();
92+
this.log('');
9393

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

9999
if (system.sandboxIps && system.sandboxIps.length > 0) {
100-
console.log();
100+
this.log('');
101101

102-
console.log(t('commands.sandbox.ips.sandboxHeader', 'Sandbox IP addresses{{context}}:', {context}));
102+
this.log(t('commands.sandbox.ips.sandboxHeader', 'Sandbox IP addresses{{context}}:', {context}));
103103
for (const ip of system.sandboxIps) {
104-
console.log(` - ${ip}`);
104+
this.log(` - ${ip}`);
105105
}
106106
}
107107
}

packages/b2c-cli/src/commands/sandbox/realm/configuration.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ export default class SandboxRealmConfiguration extends OdsCommand<typeof Sandbox
7171
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7272
const cfg = configuration as any;
7373

74-
console.log('Realm Configuration');
75-
console.log('───────────────────');
74+
this.log('Realm Configuration');
75+
this.log('───────────────────');
7676

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

9999
for (const [label, value] of rows) {
100100
if (value !== undefined) {
101-
console.log(`${label}: ${value}`);
101+
this.log(`${label}: ${value}`);
102102
}
103103
}
104104

105105
if (cfg.sandbox?.startScheduler) {
106-
console.log(`Start Scheduler: ${JSON.stringify(cfg.sandbox.startScheduler)}`);
106+
this.log(`Start Scheduler: ${JSON.stringify(cfg.sandbox.startScheduler)}`);
107107
}
108108

109109
if (cfg.sandbox?.stopScheduler) {
110-
console.log(`Stop Scheduler: ${JSON.stringify(cfg.sandbox.stopScheduler)}`);
110+
this.log(`Stop Scheduler: ${JSON.stringify(cfg.sandbox.stopScheduler)}`);
111111
}
112112
}
113113
}

packages/b2c-cli/src/commands/sandbox/realm/list.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ export default class SandboxRealmList extends OdsCommand<typeof SandboxRealmList
8282

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

85-
console.log('Realm');
85+
this.log('Realm');
8686

87-
console.log('─────');
87+
this.log('─────');
8888

8989
for (const realm of realms) {
90-
console.log(realm.realmId);
90+
this.log(realm.realmId);
9191
}
9292

9393
return response;

packages/b2c-cli/src/commands/sandbox/realm/usage.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ export default class SandboxRealmUsage extends OdsCommand<typeof SandboxRealmUsa
104104
}
105105

106106
private printRealmUsageSummary(usage: RealmUsageModel): void {
107-
console.log('Realm Usage Summary');
107+
this.log('Realm Usage Summary');
108108

109-
console.log('───────────────────');
109+
this.log('───────────────────');
110110

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

129-
console.log(`${label}: ${value}`);
129+
this.log(`${label}: ${value}`);
130130
}
131131
}
132132

133133
if (anyUsage.minutesUpByProfile && anyUsage.minutesUpByProfile.length > 0) {
134-
console.log();
134+
this.log('');
135135

136-
console.log('Minutes up by profile:');
136+
this.log('Minutes up by profile:');
137137
for (const item of anyUsage.minutesUpByProfile) {
138138
if (item.profile && item.minutes !== undefined) {
139-
console.log(` ${item.profile}: ${item.minutes} minutes`);
139+
this.log(` ${item.profile}: ${item.minutes} minutes`);
140140
}
141141
}
142142
}
@@ -150,13 +150,13 @@ export default class SandboxRealmUsage extends OdsCommand<typeof SandboxRealmUsa
150150
!hasDetailedData &&
151151
!(anyUsage.minutesUpByProfile && anyUsage.minutesUpByProfile.length > 0)
152152
) {
153-
console.log(
153+
this.log(
154154
t('commands.realm.usage.emptyPeriod', 'No usage data was returned for this realm in the requested period.'),
155155
);
156156
} else if (hasDetailedData) {
157-
console.log();
157+
this.log('');
158158

159-
console.log(
159+
this.log(
160160
t(
161161
'commands.realm.usage.detailedHint',
162162
'Detailed usage data is available; re-run with --json to see full details.',

packages/b2c-cli/src/commands/sandbox/realm/usages.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ export default class SandboxRealmUsages extends OdsCommand<typeof SandboxRealmUs
9292
}
9393

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

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

110-
console.log(row.join(' '));
110+
this.log(row.join(' '));
111111

112112
if (item.error) {
113-
console.log(` ! ${item.error}`);
113+
this.log(` ! ${item.error}`);
114114
}
115115
}
116116
}

packages/b2c-cli/src/commands/sandbox/settings.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,26 +73,26 @@ export default class SandboxSettingsCommand extends OdsCommand<typeof SandboxSet
7373
const ocapi = settings.ocapi ?? [];
7474
const webdav = settings.webdav ?? [];
7575

76-
console.log('Sandbox Settings');
77-
console.log('────────────────');
78-
console.log(`OCAPI client entries: ${ocapi.length}`);
79-
console.log(`WebDAV client entries: ${webdav.length}`);
76+
this.log('Sandbox Settings');
77+
this.log('────────────────');
78+
this.log(`OCAPI client entries: ${ocapi.length}`);
79+
this.log(`WebDAV client entries: ${webdav.length}`);
8080

8181
if (ocapi.length > 0) {
82-
console.log();
83-
console.log('OCAPI');
82+
this.log('');
83+
this.log('OCAPI');
8484
for (const entry of ocapi) {
8585
const resources = entry.resources?.length ?? 0;
86-
console.log(` - ${entry.client_id ?? 'unknown-client'} (${resources} resource rules)`);
86+
this.log(` - ${entry.client_id ?? 'unknown-client'} (${resources} resource rules)`);
8787
}
8888
}
8989

9090
if (webdav.length > 0) {
91-
console.log();
92-
console.log('WebDAV');
91+
this.log('');
92+
this.log('WebDAV');
9393
for (const entry of webdav) {
9494
const permissions = entry.permissions?.length ?? 0;
95-
console.log(` - ${entry.client_id ?? 'unknown-client'} (${permissions} permission rules)`);
95+
this.log(` - ${entry.client_id ?? 'unknown-client'} (${permissions} permission rules)`);
9696
}
9797
}
9898
}

packages/b2c-cli/src/commands/sandbox/storage.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,17 @@ export default class SandboxStorage extends OdsCommand<typeof SandboxStorage> {
7070
}
7171

7272
private printStorage(storage: SandboxStorageModel): void {
73-
console.log('Sandbox Storage');
74-
console.log('───────────────');
75-
console.log('Filesystem Total (MB) Used (MB) Used (%)');
76-
console.log('──────────────────────── ────────── ───────── ────────');
73+
this.log('Sandbox Storage');
74+
this.log('───────────────');
75+
this.log('Filesystem Total (MB) Used (MB) Used (%)');
76+
this.log('──────────────────────── ────────── ───────── ────────');
7777

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

83-
console.log(
83+
this.log(
8484
`${name.padEnd(24)} ${String(total).padStart(10)} ${String(used).padStart(9)} ${String(percentage).padStart(8)}`,
8585
);
8686
}

packages/b2c-cli/src/commands/sandbox/usage.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ export default class SandboxUsage extends OdsCommand<typeof SandboxUsage> {
9494
}
9595

9696
private printSandboxUsageSummary(usage: SandboxUsageModel): void {
97-
console.log('Sandbox Usage Summary');
97+
this.log('Sandbox Usage Summary');
9898

99-
console.log('─────────────────────');
99+
this.log('─────────────────────');
100100

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

116-
console.log(`${label}: ${value}`);
116+
this.log(`${label}: ${value}`);
117117
}
118118
}
119119

120120
if (anyUsage.minutesUpByProfile && anyUsage.minutesUpByProfile.length > 0) {
121-
console.log();
121+
this.log('');
122122

123-
console.log('Minutes up by profile:');
123+
this.log('Minutes up by profile:');
124124
for (const item of anyUsage.minutesUpByProfile) {
125125
if (item.profile && item.minutes !== undefined) {
126-
console.log(` ${item.profile}: ${item.minutes} minutes`);
126+
this.log(` ${item.profile}: ${item.minutes} minutes`);
127127
}
128128
}
129129
}
@@ -137,13 +137,13 @@ export default class SandboxUsage extends OdsCommand<typeof SandboxUsage> {
137137
!hasDetailedData &&
138138
!(anyUsage.minutesUpByProfile && anyUsage.minutesUpByProfile.length > 0)
139139
) {
140-
console.log(
140+
this.log(
141141
t('commands.sandbox.usage.emptyPeriod', 'No usage data was returned for this sandbox in the requested period.'),
142142
);
143143
} else if (hasDetailedData) {
144-
console.log();
144+
this.log('');
145145

146-
console.log(
146+
this.log(
147147
t(
148148
'commands.sandbox.usage.detailedHint',
149149
'Detailed usage data is available; re-run with --json to see full details.',

0 commit comments

Comments
 (0)