已合并
[feature][ide-plugins] #80 新增 clean 子命令 #78
[feature][ide-plugins] #80 新增 clean 子命令 #78
已合并
Qunka创建于 11 天前
7 个文件变更+510-9
@@ -46,6 +46,11 @@
46 "light": "images/hammer_light.svg"46 "light": "images/hammer_light.svg"
47 }47 }
48 },48 },
49+ {
50+ "command": "cjmp.clean",
51+ "title": "CJMP Clean",
52+ "category": "CJMP"
53+ },
49 {54 {
50 "command": "cjmp.selectDevice",55 "command": "cjmp.selectDevice",
51 "category": "CJMP",56 "category": "CJMP",
@@ -67,6 +72,10 @@
67 "command": "cjmp.build",72 "command": "cjmp.build",
68 "when": "cjmp.isCjmpProject"73 "when": "cjmp.isCjmpProject"
69 },74 },
75+ {
76+ "command": "cjmp.clean",
77+ "when": "cjmp.isCjmpProject"
78+ },
70 {79 {
71 "command": "cjmp.selectDevice",80 "command": "cjmp.selectDevice",
72 "when": "cjmp.isCjmpProject"81 "when": "cjmp.isCjmpProject"
@@ -0,0 +1,30 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import * as vscode from 'vscode';
17+ 
18+let cjmpOutputChannel: vscode.OutputChannel | undefined;
19+ 
20+export function getCjmpOutputChannel(): vscode.OutputChannel {
21+ if (!cjmpOutputChannel) {
22+ cjmpOutputChannel = vscode.window.createOutputChannel('CJMP');
23+ }
24+ return cjmpOutputChannel;
25+}
26+ 
27+export function disposeCjmpOutputChannel(): void {
28+ cjmpOutputChannel?.dispose();
29+ cjmpOutputChannel = undefined;
30+}
@@ -15,6 +15,8 @@
15 15 
16import * as vscode from 'vscode';16import * as vscode from 'vscode';
17import { buildCjmpProject } from './keelsBuild';17import { buildCjmpProject } from './keelsBuild';
18+import { cleanCjmpProject } from './keelsClean';
19+import { disposeCjmpOutputChannel } from './cjmpOutputChannel';
18import { createCjmpProject, executeSDK } from './keelsCreate';20import { createCjmpProject, executeSDK } from './keelsCreate';
19import { LaunchEmulator } from './keelsEmulators';21import { LaunchEmulator } from './keelsEmulators';
20import { KeelsDebugAdapterDescriptorFactory, KeelsConfigurationProvider } from './keelsRun';22import { KeelsDebugAdapterDescriptorFactory, KeelsConfigurationProvider } from './keelsRun';
@@ -65,6 +67,14 @@ export async function activate(context: vscode.ExtensionContext) {
65 await buildCjmpProject();67 await buildCjmpProject();
66 });68 });
67 69 
70+ const cleanCommand = vscode.commands.registerCommand('cjmp.clean', async () => {
71+ const isSdkReady = await executeSDK(context);
G
Gguomengwei10 天前

clean不支持logic-module,所以插件里也要加一下限制。可以在这个之前判断一下,如果是logic-module给出不支持提示,并退出。

likedislike
Qunka
10 天前 评论:
72+ if (!isSdkReady) {
73+ return;
74+ }
75+ await cleanCjmpProject();
76+ });
77+ 
68 context.subscriptions.push(78 context.subscriptions.push(
69 vscode.commands.registerCommand(79 vscode.commands.registerCommand(
70 'cjmp.selectDevice',80 'cjmp.selectDevice',
@@ -81,8 +91,10 @@ export async function activate(context: vscode.ExtensionContext) {
81 91 
82 // Dispose commands with the extension.92 // Dispose commands with the extension.
83 context.subscriptions.push(buildCommand);93 context.subscriptions.push(buildCommand);
94+ context.subscriptions.push(cleanCommand);
84 context.subscriptions.push(createCommand);95 context.subscriptions.push(createCommand);
85 context.subscriptions.push(launchCommand);96 context.subscriptions.push(launchCommand);
97+ context.subscriptions.push({ dispose: disposeCjmpOutputChannel });
86 context.subscriptions.push(deviceManager);98 context.subscriptions.push(deviceManager);
87 context.subscriptions.push(vscode.debug.onDidTerminateDebugSession((session) => sessionTerminated(session)));99 context.subscriptions.push(vscode.debug.onDidTerminateDebugSession((session) => sessionTerminated(session)));
88 context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => {100 context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => {
@@ -0,0 +1,326 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import * as childProcess from 'child_process';
17+import * as vscode from 'vscode';
18+import { getCjmpProjectWorkspaceFolder } from './cjmpProjectContext';
19+import { resolveKeelsCommand } from './cjmpSdk';
20+import { getCjmpOutputChannel } from './cjmpOutputChannel';
21+import { getProjectType } from './utils';
22+ 
23+export type CleanPlatform = 'all' | 'android' | 'hos' | 'ios';
24+type SpecificCleanPlatform = Exclude<CleanPlatform, 'all'>;
25+ 
26+function isSpecificCleanPlatform(value: CleanPlatform): value is SpecificCleanPlatform {
27+ return value !== 'all';
28+}
29+ 
30+type PlatformSelection =
31+ | { kind: 'all' }
32+ | { kind: 'specific'; values: readonly SpecificCleanPlatform[] };
33+export type CleanMode = 'normal' | 'deep' | 'test-results' | 'deep-test-results';
34+ 
35+interface CleanPlatformOption extends vscode.QuickPickItem {
36+ value: CleanPlatform;
37+}
38+ 
39+interface SpecificCleanPlatformOption extends vscode.QuickPickItem {
40+ value: SpecificCleanPlatform;
41+}
42+ 
43+interface CleanModeOption extends vscode.QuickPickItem {
44+ value: CleanMode;
45+}
46+ 
47+interface CleanCommandContext {
48+ executablePath: string;
49+ environment: NodeJS.ProcessEnv;
50+}
51+ 
52+export interface CleanPrompt {
53+ pickPlatforms(
54+ items: readonly CleanPlatformOption[],
55+ options: vscode.QuickPickOptions & { canPickMany: true },
56+ ): Promise<readonly CleanPlatformOption[] | undefined>;
57+ pickMode(
58+ items: readonly CleanModeOption[],
59+ options: vscode.QuickPickOptions,
60+ ): Promise<CleanModeOption | undefined>;
61+ confirm(message: string): Promise<boolean>;
62+}
63+ 
64+export interface CleanProcessRunner {
65+ run(
66+ executablePath: string,
67+ args: readonly string[],
68+ options: childProcess.SpawnOptionsWithoutStdio,
69+ onOutput: (output: string) => void,
70+ ): Promise<number | null>;
71+}
72+ 
73+export interface CleanDependencies {
74+ prompt: CleanPrompt;
75+ processRunner: CleanProcessRunner;
76+ resolveCommand: () => CleanCommandContext | undefined;
77+ createOutputChannel: () => vscode.OutputChannel;
78+}
79+ 
80+const SUPPORTED_CLEAN_PLATFORMS: readonly SpecificCleanPlatform[] = ['android', 'hos', 'ios'];
81+ 
82+const PLATFORM_OPTIONS: readonly CleanPlatformOption[] = [
83+ { label: 'All Platforms', value: 'all' },
84+ { label: 'Android', value: 'android' },
85+ { label: 'HarmonyOS', value: 'hos' },
86+ { label: 'iOS', value: 'ios' },
87+];
88+ 
89+const MODE_OPTIONS: readonly CleanModeOption[] = [
90+ { label: 'Normal Clean', value: 'normal' },
91+ { label: 'Deep Clean', value: 'deep' },
92+ { label: 'Clean Including Test Results', value: 'test-results' },
93+ { label: 'Deep Clean Including Test Results', value: 'deep-test-results' },
94+];
95+ 
96+function normalizePlatformSelection(
97+ selection: readonly CleanPlatformOption[],
98+): PlatformSelection {
99+ const specificPlatforms = [...new Set(
100+ selection
101+ .map((item) => item.value)
102+ .filter(isSpecificCleanPlatform),
103+ )];
104+ 
105+ return specificPlatforms.length > 0
106+ ? { kind: 'specific', values: specificPlatforms }
107+ : { kind: 'all' };
108+}
109+ 
110+function getSelectedPlatformOptions(
111+ selection: PlatformSelection,
112+ allOption: CleanPlatformOption,
113+ specificOptions: readonly SpecificCleanPlatformOption[],
114+): readonly CleanPlatformOption[] {
115+ if (selection.kind === 'all') {
116+ return [allOption];
117+ }
118+ return specificOptions.filter((item) => selection.values.includes(item.value));
119+}
120+ 
121+function hasSamePlatformOptions(
122+ current: readonly CleanPlatformOption[],
123+ expected: readonly CleanPlatformOption[],
124+): boolean {
125+ return current.length === expected.length
126+ && current.every((item) => expected.some((expectedItem) => expectedItem.value === item.value));
127+}
128+ 
129+function showPlatformQuickPick(
130+ items: readonly CleanPlatformOption[],
131+ options: vscode.QuickPickOptions & { canPickMany: true },
132+): Promise<readonly CleanPlatformOption[] | undefined> {
133+ return new Promise((resolve) => {
134+ const quickPick = vscode.window.createQuickPick<CleanPlatformOption>();
135+ const allOption = items.find((item) => item.value === 'all');
136+ const specificOptions: readonly SpecificCleanPlatformOption[] = items.filter(
137+ (item): item is SpecificCleanPlatformOption => item.value !== 'all',
138+ );
139+ if (!allOption) {
140+ quickPick.dispose();
141+ resolve(undefined);
142+ return;
143+ }
144+ 
145+ let platformSelection: PlatformSelection = { kind: 'all' };
146+ let accepted = false;
147+ let hidden = false;
148+ 
149+ const getExpectedSelection = () => getSelectedPlatformOptions(
150+ platformSelection,
151+ allOption,
152+ specificOptions,
153+ );
154+ 
155+ quickPick.placeholder = options.placeHolder;
156+ quickPick.canSelectMany = true;
157+ quickPick.items = [allOption, ...specificOptions];
158+ quickPick.selectedItems = getExpectedSelection();
159+ 
160+ quickPick.onDidChangeSelection((selection) => {
161+ if (hidden) {
162+ return;
163+ }
164+ 
165+ platformSelection = normalizePlatformSelection(selection);
166+ const expectedSelection = getExpectedSelection();
167+ if (!hasSamePlatformOptions(selection, expectedSelection)) {
168+ // Keep the item list stable. Updating selectedItems alone avoids the
169+ // Windows QuickPick flicker caused by replacing items in this event.
170+ quickPick.selectedItems = expectedSelection;
171+ }
172+ });
173+ 
174+ quickPick.onDidAccept(() => {
175+ const result = getExpectedSelection();
176+ accepted = true;
177+ resolve(result);
178+ quickPick.hide();
179+ });
180+ quickPick.onDidHide(() => {
181+ hidden = true;
182+ if (!accepted) {
183+ resolve(undefined);
184+ }
185+ quickPick.dispose();
186+ });
187+ quickPick.show();
188+ });
189+}
190+ 
191+const defaultCleanDependencies: CleanDependencies = {
192+ prompt: {
193+ pickPlatforms: showPlatformQuickPick,
194+ pickMode: async (items, options) => vscode.window.showQuickPick(items, options),
195+ confirm: async (message) => {
196+ const selection = await vscode.window.showWarningMessage(
197+ message,
198+ { modal: true },
199+ 'Clean',
200+ );
201+ return selection === 'Clean';
202+ },
203+ },
204+ processRunner: {
205+ run: (executablePath, args, options, onOutput) => new Promise((resolve, reject) => {
206+ const spawnExecutable = process.platform === 'win32'
207+ ? `"${executablePath}"`
208+ : executablePath;
209+ const cleanProcess = childProcess.spawn(spawnExecutable, [...args], {
210+ ...options,
211+ shell: process.platform === 'win32',
212+ });
213+ 
214+ cleanProcess.stdout?.on('data', (data) => onOutput(data.toString()));
215+ cleanProcess.stderr?.on('data', (data) => onOutput(data.toString()));
216+ cleanProcess.on('error', reject);
217+ cleanProcess.on('close', resolve);
218+ }),
219+ },
220+ resolveCommand: () => resolveKeelsCommand(),
221+ createOutputChannel: () => getCjmpOutputChannel(),
222+};
223+ 
224+export function getCleanCommandArgs(
225+ platforms: readonly CleanPlatform[],
226+ mode: CleanMode,
227+): string[] {
228+ const args = ['clean'];
229+ const specificPlatforms = [...new Set(
230+ platforms
231+ .filter(isSpecificCleanPlatform)
232+ .filter((platform) => SUPPORTED_CLEAN_PLATFORMS.includes(platform)),
233+ )];
234+ const cleansAllPlatforms = platforms.includes('all')
235+ || SUPPORTED_CLEAN_PLATFORMS.every((platform) => specificPlatforms.includes(platform));
236+ 
237+ if (!cleansAllPlatforms) {
238+ args.push('--platform', specificPlatforms.join(','));
239+ }
240+ if (mode === 'deep' || mode === 'deep-test-results') {
241+ args.push('--deep');
242+ }
243+ if (mode === 'test-results' || mode === 'deep-test-results') {
244+ args.push('--test-results');
245+ }
246+ 
247+ args.push('-v');
248+ return args;
249+}
250+ 
251+export async function cleanCjmpProject(
252+ dependencies: CleanDependencies = defaultCleanDependencies,
253+): Promise<void> {
254+ const workspaceFolder = getCjmpProjectWorkspaceFolder(vscode.workspace.workspaceFolders);
255+ if (!workspaceFolder) {
256+ vscode.window.showErrorMessage('The opened folder is not a valid CJMP project.');
257+ return;
258+ }
259+ if (getProjectType() === 'logic-module') {
260+ vscode.window.showErrorMessage('CJMP clean does not support logic-module projects.');
261+ return;
262+ }
263+ const workspaceRoot = workspaceFolder.uri.fsPath;
264+ 
265+ const platforms = await dependencies.prompt.pickPlatforms(PLATFORM_OPTIONS, {
266+ placeHolder: 'Select platforms to clean',
267+ canPickMany: true,
268+ });
269+ if (!platforms || platforms.length === 0) {
270+ return;
271+ }
272+ 
273+ const mode = await dependencies.prompt.pickMode(MODE_OPTIONS, {
274+ placeHolder: 'Select the clean mode',
275+ });
276+ if (!mode) {
277+ return;
278+ }
279+ 
280+ const platformLabels = platforms.map((platform) => platform.label).join(', ');
281+ const confirmed = await dependencies.prompt.confirm(
282+ `Clean generated files for ${platformLabels} using ${mode.label}?`,
283+ );
284+ if (!confirmed) {
285+ return;
286+ }
287+ 
288+ const keelsCommand = dependencies.resolveCommand();
289+ if (!keelsCommand) {
290+ return;
291+ }
292+ 
293+ const args = getCleanCommandArgs(
294+ platforms.map((platform) => platform.value),
295+ mode.value,
296+ );
297+ const outputChannel = dependencies.createOutputChannel();
G
Gguomengwei11 天前

每次执行 Clean 都会创建一个新的 CJMP Clean 输出通道,但通道既未复用、也未注册到 context.subscriptions 或显式释放。建议复用已有的CJMP通道。

likedislike
Qunka
11 天前 评论:
298+ outputChannel.show(true);
299+ outputChannel.appendLine('');
300+ outputChannel.appendLine('=== CJMP Clean ===');
301+ outputChannel.appendLine(`> ${keelsCommand.executablePath} ${args.join(' ')}`);
302+ 
303+ try {
304+ const exitCode = await dependencies.processRunner.run(
305+ keelsCommand.executablePath,
306+ args,
307+ {
308+ cwd: workspaceRoot,
309+ env: keelsCommand.environment,
310+ },
311+ (output) => outputChannel.append(output),
312+ );
313+ 
314+ if (exitCode === 0) {
315+ vscode.window.showInformationMessage('CJMP clean completed successfully.');
316+ } else {
317+ vscode.window.showErrorMessage(
318+ `CJMP clean failed. Exit code: ${exitCode ?? 'unknown'}. See the CJMP Clean output for details.`,
319+ );
320+ }
321+ } catch (error) {
322+ const message = error instanceof Error ? error.message : String(error);
323+ outputChannel.appendLine(`Error: ${message}`);
324+ vscode.window.showErrorMessage(`Failed to execute CJMP clean: ${message}`);
325+ }
326+}
@@ -25,6 +25,7 @@ import { KeelsDebugAdapterConfigProvider } from './keelsDebugConfigProvider';
25import { RemotePlatform } from './types';25import { RemotePlatform } from './types';
26import { PythonEnvUtils } from './apple/pythonEnvUtils';26import { PythonEnvUtils } from './apple/pythonEnvUtils';
27import { resolveKeelsCommand } from './cjmpSdk';27import { resolveKeelsCommand } from './cjmpSdk';
28+import { getCjmpOutputChannel } from './cjmpOutputChannel';
28 29 
29export class KeelsDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory {30export class KeelsDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory {
30 constructor(private deviceManager: KeelsDeviceManager, private context: vscode.ExtensionContext) { }31 constructor(private deviceManager: KeelsDeviceManager, private context: vscode.ExtensionContext) { }
@@ -69,15 +70,6 @@ interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
69 remotePlatform?: string;70 remotePlatform?: string;
70}71}
71 72 
72-let cjmpOutputChannel: vscode.OutputChannel | undefined;
73- 
74-function getCjmpOutputChannel(): vscode.OutputChannel {
75- if (!cjmpOutputChannel) {
76- cjmpOutputChannel = vscode.window.createOutputChannel('CJMP');
77- }
78- return cjmpOutputChannel;
79-}
80- 
81export class KeelsDebugAdapter implements vscode.DebugAdapter {73export class KeelsDebugAdapter implements vscode.DebugAdapter {
82 private _onDidSendMessage = new vscode.EventEmitter<vscode.DebugProtocolMessage>();74 private _onDidSendMessage = new vscode.EventEmitter<vscode.DebugProtocolMessage>();
83 75 
@@ -39,6 +39,7 @@ interface ExtensionManifest {
39 contributes: {39 contributes: {
40 commands: CommandContribution[];40 commands: CommandContribution[];
41 menus: {41 menus: {
42+ commandPalette: MenuContribution[];
42 'editor/title': MenuContribution[];43 'editor/title': MenuContribution[];
43 };44 };
44 };45 };
@@ -70,6 +71,22 @@ suite('Extension manifest', () => {
70 assert.strictEqual(buildMenu?.when, cjmpProjectContextKey);71 assert.strictEqual(buildMenu?.when, cjmpProjectContextKey);
71 });72 });
72 73 
74+ test('shows CJMP Clean only in the command palette for CJMP projects', () => {
75+ const cleanCommand = manifest.contributes.commands.find(
76+ ({ command }) => command === 'cjmp.clean',
77+ );
78+ const cleanPaletteMenu = manifest.contributes.menus.commandPalette.find(
79+ ({ command }) => command === 'cjmp.clean',
80+ );
81+ const cleanEditorMenu = manifest.contributes.menus['editor/title'].find(
82+ ({ command }) => command === 'cjmp.clean',
83+ );
84+ 
85+ assert.strictEqual(cleanCommand?.command, 'cjmp.clean');
86+ assert.strictEqual(cleanPaletteMenu?.when, cjmpProjectContextKey);
87+ assert.strictEqual(cleanEditorMenu, undefined);
88+ });
89+ 
73 test('activates early enough to publish the mutual-exclusion context', () => {90 test('activates early enough to publish the mutual-exclusion context', () => {
74 assert.ok(manifest.activationEvents.includes('workspaceContains:project.conf'));91 assert.ok(manifest.activationEvents.includes('workspaceContains:project.conf'));
75 });92 });
@@ -0,0 +1,115 @@
1+/*
2+ * Copyright (c) 2026 Huawei Device Co., Ltd.
3+ * Licensed under the Apache License, Version 2.0 (the "License");
4+ * you may not use this file except in compliance with the License.
5+ * You may obtain a copy of the License at
6+ *
7+ * http://www.apache.org/licenses/LICENSE-2.0
8+ *
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ */
15+ 
16+import * as assert from 'assert';
17+import * as vscode from 'vscode';
18+ 
19+import {
20+ cleanCjmpProject,
21+ CleanMode,
22+ CleanPlatform,
23+ CleanPrompt,
24+ getCleanCommandArgs,
25+} from '../../src/keelsClean';
26+ 
27+function createPrompt(
28+ platforms: readonly CleanPlatform[],
29+ mode: CleanMode,
30+ confirmed: boolean = true,
31+): CleanPrompt {
32+ return {
33+ pickPlatforms: async (items) => items.filter((item) => platforms.includes(item.value)),
34+ pickMode: async (items) => items.find((item) => item.value === mode),
35+ confirm: async () => confirmed,
36+ };
37+}
38+ 
39+suite('CJMP clean command flows', () => {
40+ test('maps any selected platform combination to one CLI platform argument', () => {
41+ assert.deepStrictEqual(
42+ getCleanCommandArgs(['android', 'ios'], 'deep-test-results'),
43+ ['clean', '--platform', 'android,ios', '--deep', '--test-results', '-v'],
44+ );
45+ assert.deepStrictEqual(
46+ getCleanCommandArgs(['all'], 'normal'),
47+ ['clean', '-v'],
48+ );
49+ });
50+ 
51+ test('executes clean from the workspace root with the resolved SDK environment', async () => {
52+ const runs: Array<{
53+ executablePath: string;
54+ args: readonly string[];
55+ options: import('child_process').SpawnOptionsWithoutStdio;
56+ }> = [];
57+ let output = '';
58+ 
59+ await cleanCjmpProject({
60+ prompt: createPrompt(['android', 'ios'], 'deep'),
61+ resolveCommand: () => ({
62+ executablePath: 'C:\\CJMP SDK\\cjmp-tools\\bin\\keels.bat',
63+ environment: { CJMP_SDK_HOME: 'C:\\CJMP SDK' },
64+ }),
65+ processRunner: {
66+ run: async (executablePath, args, options, onOutput) => {
67+ runs.push({ executablePath, args, options });
68+ onOutput('clean output');
69+ return 0;
70+ },
71+ },
72+ createOutputChannel: () => ({
73+ name: 'CJMP Clean',
74+ append: (value: string) => { output += value; },
75+ appendLine: (value: string) => { output += `${value}\n`; },
76+ clear: () => undefined,
77+ replace: () => undefined,
78+ show: () => undefined,
79+ hide: () => undefined,
80+ dispose: () => undefined,
81+ } as vscode.OutputChannel),
82+ });
83+ 
84+ assert.strictEqual(runs.length, 1);
85+ assert.strictEqual(runs[0].executablePath, 'C:\\CJMP SDK\\cjmp-tools\\bin\\keels.bat');
86+ assert.deepStrictEqual(
87+ runs[0].args,
88+ ['clean', '--platform', 'android,ios', '--deep', '-v'],
89+ );
90+ assert.strictEqual(
91+ runs[0].options.cwd,
92+ vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
93+ );
94+ assert.strictEqual(runs[0].options.env?.CJMP_SDK_HOME, 'C:\\CJMP SDK');
95+ assert.ok(output.includes('clean output'));
96+ });
97+ 
98+ test('does not start clean when confirmation is cancelled', async () => {
99+ let runCount = 0;
100+ 
101+ await cleanCjmpProject({
102+ prompt: createPrompt(['all'], 'normal', false),
103+ resolveCommand: () => ({ executablePath: 'keels', environment: {} }),
104+ processRunner: {
105+ run: async () => {
106+ runCount += 1;
107+ return 0;
108+ },
109+ },
110+ createOutputChannel: () => vscode.window.createOutputChannel('Unused'),
111+ });
112+ 
113+ assert.strictEqual(runCount, 0);
114+ });
115+});