Skip to content

Commit 407ef68

Browse files
committed
fix(org): surface CLI output and fix idempotency in org delete
- Stream sf CLI output to Salesforce Org Management channel - Append failure details when delete fails or throws - Check auth file exists via FsService before removeAuth (fixes Target file doesn't exist) - Pass OUTPUT_CHANNEL to OrgDeleteDefaultExecutor Made-with: Cursor
1 parent e8dc4d7 commit 407ef68

4 files changed

Lines changed: 34 additions & 10 deletions

File tree

packages/salesforcedx-vscode-org/src/commands/orgDelete.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import { AuthRemover } from '@salesforce/core';
8+
import { AuthRemover, Global } from '@salesforce/core';
99
import { ExtensionProviderService, sfProjectPreconditionChecker } from '@salesforce/effect-ext-utils';
1010
import { Command, SfCommandBuilder } from '@salesforce/salesforcedx-utils';
1111
import {
@@ -19,8 +19,9 @@ import {
1919
workspaceUtils
2020
} from '@salesforce/salesforcedx-utils-vscode';
2121
import * as Effect from 'effect/Effect';
22+
import * as path from 'node:path';
2223
import * as vscode from 'vscode';
23-
import { OUTPUT_CHANNEL } from '../channels';
24+
import { channelService, OUTPUT_CHANNEL } from '../channels';
2425
import { getOrgRuntime } from '../extensionProvider';
2526
import { nls } from '../messages';
2627
import { PromptConfirmGatherer } from '../parameterGatherers/promptConfirmGatherer';
@@ -55,6 +56,15 @@ const unsetTargetDevHubIfMatch = Effect.fn('OrgDelete.unsetTargetDevHubIfMatch')
5556
}
5657
);
5758

59+
/** Checks if the auth file exists in .sfdx or .sf; sf org delete may already remove it (idempotent). */
60+
const authFileExists = Effect.fn('OrgDelete.authFileExists')(function* (username: string) {
61+
const api = yield* (yield* ExtensionProviderService).getServicesApi;
62+
const authFileName = `${username}.json`;
63+
const inSfdx = yield* api.services.FsService.fileOrFolderExists(path.join(Global.SFDX_DIR, authFileName));
64+
if (inSfdx) return true;
65+
return yield* api.services.FsService.fileOrFolderExists(path.join(Global.SF_DIR, authFileName));
66+
});
67+
5868
/** Runs sf org:delete:scratch or org:delete:sandbox for a single org and resolves when done. */
5969
const runDeleteCli = (username: string, orgType: 'scratch' | 'sandbox'): Promise<boolean> => {
6070
const deleteArg = orgType === 'sandbox' ? 'org:delete:sandbox' : 'org:delete:scratch';
@@ -73,6 +83,8 @@ const runDeleteCli = (username: string, orgType: 'scratch' | 'sandbox'): Promise
7383
env: { SF_JSON_TO_STDOUT: 'true' }
7484
}).execute(cancellationTokenSource.token);
7585

86+
channelService.streamCommandOutput(execution);
87+
7688
execution.processExitSubject.subscribe(data => {
7789
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
7890
const exitCode = Array.isArray(data) ? data[0] : data;
@@ -96,26 +108,39 @@ class OrgDeleteExecutor extends LibraryCommandletExecutor<{ orgs: OrgToDelete[]
96108
const aliases = await getOrgRuntime().runPromise(getAliasesForUsername(username));
97109
const success = await runDeleteCli(username, orgType);
98110
if (success) {
99-
await authRemover.removeAuth(username);
111+
// sf org delete scratch may already remove the auth file; only call removeAuth if file exists (idempotent)
112+
const authFileExistsResult = await getOrgRuntime().runPromise(authFileExists(username));
113+
if (authFileExistsResult) {
114+
await authRemover.removeAuth(username);
115+
}
100116
await getOrgRuntime().runPromise(removeOrgAliases(username));
101117
await getOrgRuntime().runPromise(unsetTargetOrgIfMatch(username, aliases));
102118
await getOrgRuntime().runPromise(unsetTargetDevHubIfMatch(username, aliases));
103119
} else {
104120
allSucceeded = false;
121+
channelService.appendLine(
122+
nls.localize('org_delete_failed_for_org', username, orgType === 'scratch' ? 'scratch org' : 'sandbox')
123+
);
105124
}
106125
}
107126
await updateConfigAndStateAggregators();
108127
return allSucceeded;
109128
} catch (e) {
110129
const err = e instanceof Error ? e : new Error(String(e));
111130
telemetryService.sendException('org_delete_selected', `Error: name = ${err.name} message = ${err.message}`);
131+
channelService.appendLine(err.message);
132+
channelService.showChannelOutput();
112133
return false;
113134
}
114135
}
115136
}
116137

117138
/** Executor for deleting the default org (no picker). */
118139
class OrgDeleteDefaultExecutor extends SfCommandletExecutor<{}> {
140+
constructor() {
141+
super(OUTPUT_CHANNEL);
142+
}
143+
119144
public build(_data: {}): Command {
120145
return new SfCommandBuilder()
121146
.withDescription(nls.localize('org_delete_default_text'))

packages/salesforcedx-vscode-org/src/extensionProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const buildAllServicesLayer = (context: ExtensionContext) =>
4747
ExtensionProviderServiceLive,
4848
api.services.ExtensionContextServiceLayer(context),
4949
api.services.ChannelServiceLayer(displayName),
50+
api.services.FsService.Default,
5051
api.services.AliasService.Default,
5152
api.services.ConfigService.Default,
5253
api.services.ConnectionService.Default,

packages/salesforcedx-vscode-org/src/messages/i18n.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const messages = {
5555
org_logout_all_text: 'SFDX: Log Out from Authorized Orgs',
5656
org_logout_default_text: 'SFDX: Log Out from Default Org',
5757
org_logout_no_default_org: 'No default org to logout from',
58+
org_delete_failed_for_org: 'Failed to delete %s (%s). Check the output above for details.',
5859
org_delete_select_orgs_placeholder: 'Select scratch orgs and sandboxes to delete',
5960
org_delete_confirm_prompt: 'Permanently delete %d org(s)? This cannot be undone.',
6061
org_delete_confirm_label: 'Delete',

packages/salesforcedx-vscode-services/src/index.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,12 @@ import { ConfigService } from './core/configService';
2020
import { ConnectionService } from './core/connectionService';
2121
import { getDefaultOrgRef } from './core/defaultOrgRef';
2222
import { ExecuteAnonymousService } from './core/executeAnonymousService';
23-
import { subscribeLifecycleWarnings } from './core/lifecycleWarningListener';
2423
import { MetadataDeleteService } from './core/metadataDeleteService';
2524
import { MetadataDeployService } from './core/metadataDeployService';
2625
import { MetadataDescribeService } from './core/metadataDescribeService';
2726
import { MetadataRegistryService } from './core/metadataRegistryService';
2827
import { MetadataRetrieveService } from './core/metadataRetrieveService';
2928
import { ProjectService } from './core/projectService';
30-
import { retrieveOnLoadEffect } from './core/retrieveOnLoad';
3129
import { SourceTrackingService } from './core/sourceTrackingService';
3230
import { TemplateService, TemplateType } from './core/templateService';
3331
import { TraceFlagItemStruct, TraceFlagService } from './core/traceFlagService';
@@ -38,7 +36,6 @@ import { isItReadOnlyLayer } from './virtualFsProvider/fileSystemProvider';
3836
import { fileSystemSetup } from './virtualFsProvider/fileSystemSetup';
3937
import { IndexedDBStorageServiceShared } from './virtualFsProvider/indexedDbStorage';
4038
import { ChannelServiceLayer, ChannelService } from './vscode/channelService';
41-
import { watchSettingsService } from './vscode/configWatcher';
4239
import { watchDefaultOrgContext } from './vscode/context';
4340
import { watchApexTestContext, watchPackageDirectoriesContext } from './vscode/editorContext';
4441
import { EditorService } from './vscode/editorService';
@@ -210,9 +207,7 @@ const activationEffect = Effect.fn('activationEffect')(function* (context: vscod
210207
// watch active editor changes to update package directories context
211208
Effect.forkIn(watchPackageDirectoriesContext(), scope),
212209
// watch active editor changes to update apex test context
213-
Effect.forkIn(watchApexTestContext(), scope),
214-
// watch alias.json for changes and refresh defaultOrgRef.aliases accordingly
215-
Effect.forkIn(watchDefaultOrgAliases(), scope)
210+
Effect.forkIn(watchApexTestContext(), scope)
216211
],
217212
{ concurrency: 'unbounded' }
218213
);
@@ -227,7 +222,9 @@ const activationEffect = Effect.fn('activationEffect')(function* (context: vscod
227222
// watch active editor changes to update package directories context
228223
Effect.forkIn(watchPackageDirectoriesContext(), scope),
229224
// watch active editor changes to update apex test context
230-
Effect.forkIn(watchApexTestContext(), scope)
225+
Effect.forkIn(watchApexTestContext(), scope),
226+
// watch alias.json for changes and refresh defaultOrgRef.aliases accordingly
227+
Effect.forkIn(watchDefaultOrgAliases(), scope)
231228
],
232229
{
233230
concurrency: 'unbounded'

0 commit comments

Comments
 (0)