* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as childProcess from 'child_process';
import * as vscode from 'vscode';
import { getCjmpProjectWorkspaceFolder } from './cjmpProjectContext';
import { resolveKeelsCommand } from './cjmpSdk';
import { getCjmpOutputChannel } from './cjmpOutputChannel';
import { getProjectType } from './utils';
export type CleanPlatform = 'all' | 'android' | 'hos' | 'ios';
type SpecificCleanPlatform = Exclude<CleanPlatform, 'all'>;
function isSpecificCleanPlatform(value: CleanPlatform): value is SpecificCleanPlatform {
return value !== 'all';
}
type PlatformSelection =
| { kind: 'all' }
| { kind: 'specific'; values: readonly SpecificCleanPlatform[] };
export type CleanMode = 'normal' | 'deep' | 'test-results' | 'deep-test-results';
interface CleanPlatformOption extends vscode.QuickPickItem {
value: CleanPlatform;
}
interface SpecificCleanPlatformOption extends vscode.QuickPickItem {
value: SpecificCleanPlatform;
}
interface CleanModeOption extends vscode.QuickPickItem {
value: CleanMode;
}
interface CleanCommandContext {
executablePath: string;
environment: NodeJS.ProcessEnv;
}
export interface CleanPrompt {
pickPlatforms(
items: readonly CleanPlatformOption[],
options: vscode.QuickPickOptions & { canPickMany: true },
): Promise<readonly CleanPlatformOption[] | undefined>;
pickMode(
items: readonly CleanModeOption[],
options: vscode.QuickPickOptions,
): Promise<CleanModeOption | undefined>;
confirm(message: string): Promise<boolean>;
}
export interface CleanProcessRunner {
run(
executablePath: string,
args: readonly string[],
options: childProcess.SpawnOptionsWithoutStdio,
onOutput: (output: string) => void,
): Promise<number | null>;
}
export interface CleanDependencies {
prompt: CleanPrompt;
processRunner: CleanProcessRunner;
resolveCommand: () => CleanCommandContext | undefined;
createOutputChannel: () => vscode.OutputChannel;
}
const SUPPORTED_CLEAN_PLATFORMS: readonly SpecificCleanPlatform[] = ['android', 'hos', 'ios'];
const PLATFORM_OPTIONS: readonly CleanPlatformOption[] = [
{ label: 'All Platforms', value: 'all' },
{ label: 'Android', value: 'android' },
{ label: 'HarmonyOS', value: 'hos' },
{ label: 'iOS', value: 'ios' },
];
const MODE_OPTIONS: readonly CleanModeOption[] = [
{ label: 'Normal Clean', value: 'normal' },
{ label: 'Deep Clean', value: 'deep' },
{ label: 'Clean Including Test Results', value: 'test-results' },
{ label: 'Deep Clean Including Test Results', value: 'deep-test-results' },
];
function normalizePlatformSelection(
selection: readonly CleanPlatformOption[],
): PlatformSelection {
const specificPlatforms = [...new Set(
selection
.map((item) => item.value)
.filter(isSpecificCleanPlatform),
)];
return specificPlatforms.length > 0
? { kind: 'specific', values: specificPlatforms }
: { kind: 'all' };
}
function getSelectedPlatformOptions(
selection: PlatformSelection,
allOption: CleanPlatformOption,
specificOptions: readonly SpecificCleanPlatformOption[],
): readonly CleanPlatformOption[] {
if (selection.kind === 'all') {
return [allOption];
}
return specificOptions.filter((item) => selection.values.includes(item.value));
}
function hasSamePlatformOptions(
current: readonly CleanPlatformOption[],
expected: readonly CleanPlatformOption[],
): boolean {
return current.length === expected.length
&& current.every((item) => expected.some((expectedItem) => expectedItem.value === item.value));
}
function showPlatformQuickPick(
items: readonly CleanPlatformOption[],
options: vscode.QuickPickOptions & { canPickMany: true },
): Promise<readonly CleanPlatformOption[] | undefined> {
return new Promise((resolve) => {
const quickPick = vscode.window.createQuickPick<CleanPlatformOption>();
const allOption = items.find((item) => item.value === 'all');
const specificOptions: readonly SpecificCleanPlatformOption[] = items.filter(
(item): item is SpecificCleanPlatformOption => item.value !== 'all',
);
if (!allOption) {
quickPick.dispose();
resolve(undefined);
return;
}
let platformSelection: PlatformSelection = { kind: 'all' };
let accepted = false;
let hidden = false;
const getExpectedSelection = () => getSelectedPlatformOptions(
platformSelection,
allOption,
specificOptions,
);
quickPick.placeholder = options.placeHolder;
quickPick.canSelectMany = true;
quickPick.items = [allOption, ...specificOptions];
quickPick.selectedItems = getExpectedSelection();
quickPick.onDidChangeSelection((selection) => {
if (hidden) {
return;
}
platformSelection = normalizePlatformSelection(selection);
const expectedSelection = getExpectedSelection();
if (!hasSamePlatformOptions(selection, expectedSelection)) {
quickPick.selectedItems = expectedSelection;
}
});
quickPick.onDidAccept(() => {
const result = getExpectedSelection();
accepted = true;
resolve(result);
quickPick.hide();
});
quickPick.onDidHide(() => {
hidden = true;
if (!accepted) {
resolve(undefined);
}
quickPick.dispose();
});
quickPick.show();
});
}
const defaultCleanDependencies: CleanDependencies = {
prompt: {
pickPlatforms: showPlatformQuickPick,
pickMode: async (items, options) => vscode.window.showQuickPick(items, options),
confirm: async (message) => {
const selection = await vscode.window.showWarningMessage(
message,
{ modal: true },
'Clean',
);
return selection === 'Clean';
},
},
processRunner: {
run: (executablePath, args, options, onOutput) => new Promise((resolve, reject) => {
const spawnExecutable = process.platform === 'win32'
? `"${executablePath}"`
: executablePath;
const cleanProcess = childProcess.spawn(spawnExecutable, [...args], {
...options,
shell: process.platform === 'win32',
});
cleanProcess.stdout?.on('data', (data) => onOutput(data.toString()));
cleanProcess.stderr?.on('data', (data) => onOutput(data.toString()));
cleanProcess.on('error', reject);
cleanProcess.on('close', resolve);
}),
},
resolveCommand: () => resolveKeelsCommand(),
createOutputChannel: () => getCjmpOutputChannel(),
};
export function getCleanCommandArgs(
platforms: readonly CleanPlatform[],
mode: CleanMode,
): string[] {
const args = ['clean'];
const specificPlatforms = [...new Set(
platforms
.filter(isSpecificCleanPlatform)
.filter((platform) => SUPPORTED_CLEAN_PLATFORMS.includes(platform)),
)];
const cleansAllPlatforms = platforms.includes('all')
|| SUPPORTED_CLEAN_PLATFORMS.every((platform) => specificPlatforms.includes(platform));
if (!cleansAllPlatforms) {
args.push('--platform', specificPlatforms.join(','));
}
if (mode === 'deep' || mode === 'deep-test-results') {
args.push('--deep');
}
if (mode === 'test-results' || mode === 'deep-test-results') {
args.push('--test-results');
}
args.push('-v');
return args;
}
export async function cleanCjmpProject(
dependencies: CleanDependencies = defaultCleanDependencies,
): Promise<void> {
const workspaceFolder = getCjmpProjectWorkspaceFolder(vscode.workspace.workspaceFolders);
if (!workspaceFolder) {
vscode.window.showErrorMessage('The opened folder is not a valid CJMP project.');
return;
}
if (getProjectType() === 'logic-module') {
vscode.window.showErrorMessage('CJMP clean does not support logic-module projects.');
return;
}
const workspaceRoot = workspaceFolder.uri.fsPath;
const platforms = await dependencies.prompt.pickPlatforms(PLATFORM_OPTIONS, {
placeHolder: 'Select platforms to clean',
canPickMany: true,
});
if (!platforms || platforms.length === 0) {
return;
}
const mode = await dependencies.prompt.pickMode(MODE_OPTIONS, {
placeHolder: 'Select the clean mode',
});
if (!mode) {
return;
}
const platformLabels = platforms.map((platform) => platform.label).join(', ');
const confirmed = await dependencies.prompt.confirm(
`Clean generated files for ${platformLabels} using ${mode.label}?`,
);
if (!confirmed) {
return;
}
const keelsCommand = dependencies.resolveCommand();
if (!keelsCommand) {
return;
}
const args = getCleanCommandArgs(
platforms.map((platform) => platform.value),
mode.value,
);
const outputChannel = dependencies.createOutputChannel();
outputChannel.show(true);
outputChannel.appendLine('');
outputChannel.appendLine('=== CJMP Clean ===');
outputChannel.appendLine(`> ${keelsCommand.executablePath} ${args.join(' ')}`);
try {
const exitCode = await dependencies.processRunner.run(
keelsCommand.executablePath,
args,
{
cwd: workspaceRoot,
env: keelsCommand.environment,
},
(output) => outputChannel.append(output),
);
if (exitCode === 0) {
vscode.window.showInformationMessage('CJMP clean completed successfully.');
} else {
vscode.window.showErrorMessage(
`CJMP clean failed. Exit code: ${exitCode ?? 'unknown'}. See the CJMP Clean output for details.`,
);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
outputChannel.appendLine(`Error: ${message}`);
vscode.window.showErrorMessage(`Failed to execute CJMP clean: ${message}`);
}
}