@meltstudio/meltctl 2.1.0 โ†’ 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,6 +70,18 @@ Updates your project templates to the latest version. This command:
70
70
  - Verifies .melt/ workspace migration status
71
71
  - Handles version compatibility automatically
72
72
 
73
+ ### Clean Project
74
+
75
+ ```bash
76
+ meltctl project clean
77
+ ```
78
+
79
+ Safely removes all Melt-generated files from your project while preserving user-created content. This command:
80
+ - Removes the entire `.melt/` directory (all Melt-generated content)
81
+ - Selectively removes only Melt commands from `.cursor/commands/`
82
+ - Preserves user-created files in `.cursor/commands/`
83
+ - Provides interactive confirmation before deletion
84
+
73
85
  ## ๐Ÿ› ๏ธ Requirements
74
86
 
75
87
  - Node.js 22+ (works with Node.js 18+ but 22+ recommended)
@@ -0,0 +1,5 @@
1
+ interface CleanOptions {
2
+ placeholder?: never;
3
+ }
4
+ export declare function cleanCommand(_options?: CleanOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,239 @@
1
+ import { intro, outro, confirm, spinner } from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ // List of Melt-generated command files to remove
6
+ const MELT_COMMAND_FILES = [
7
+ 'melt-plan.md',
8
+ 'melt-test-plan.md',
9
+ 'melt-docs.md',
10
+ 'melt-implement.md',
11
+ 'melt-pr.md',
12
+ 'melt-review.md',
13
+ 'melt-complete.md',
14
+ 'melt-debug.md',
15
+ ];
16
+ export async function cleanCommand(_options = {}) {
17
+ intro(chalk.blue('๐Ÿงน Melt Project - Clean'));
18
+ const currentDir = process.cwd();
19
+ const meltDir = path.join(currentDir, '.melt');
20
+ const cursorCommandsDir = path.join(currentDir, '.cursor', 'commands');
21
+ // Check if this is a Melt workspace
22
+ const hasMeltWorkspace = await fs.pathExists(meltDir);
23
+ const hasCursorCommands = await fs.pathExists(cursorCommandsDir);
24
+ if (!hasMeltWorkspace && !hasCursorCommands) {
25
+ console.log(chalk.yellow('โš ๏ธ No Melt workspace found in this directory.'));
26
+ console.log("This project doesn't appear to have Melt tools installed.");
27
+ outro(chalk.gray('Nothing to clean.'));
28
+ return;
29
+ }
30
+ // Analyze what will be cleaned
31
+ const analysisResult = await analyzeCleanupTarget(currentDir);
32
+ if (analysisResult.removedFiles.length === 0 && analysisResult.removedDirs.length === 0) {
33
+ console.log(chalk.yellow('โš ๏ธ No Melt files found to clean.'));
34
+ outro(chalk.gray('Nothing to clean.'));
35
+ return;
36
+ }
37
+ // Show what will be cleaned
38
+ displayCleanupPlan(analysisResult);
39
+ // Get confirmation
40
+ const shouldProceed = await confirm({
41
+ message: 'Do you want to proceed with cleaning? This action cannot be undone.',
42
+ });
43
+ if (!shouldProceed) {
44
+ outro(chalk.gray('Clean operation cancelled.'));
45
+ return;
46
+ }
47
+ // Perform the cleanup
48
+ const s = spinner();
49
+ s.start('Cleaning Melt workspace...');
50
+ try {
51
+ const cleanResult = await performCleanup(currentDir);
52
+ s.stop('โœ… Cleanup completed!');
53
+ displayCleanupResults(cleanResult);
54
+ outro(chalk.green('๐ŸŽ‰ Melt workspace cleaned successfully!'));
55
+ }
56
+ catch (error) {
57
+ s.stop('โŒ Cleanup failed');
58
+ console.error(chalk.red('Error during cleanup:'), error);
59
+ process.exit(1);
60
+ }
61
+ }
62
+ async function analyzeCleanupTarget(baseDir) {
63
+ const result = {
64
+ removedFiles: [],
65
+ removedDirs: [],
66
+ preservedFiles: [],
67
+ errors: [],
68
+ };
69
+ const meltDir = path.join(baseDir, '.melt');
70
+ const cursorCommandsDir = path.join(baseDir, '.cursor', 'commands');
71
+ try {
72
+ // Check .melt directory (remove entirely)
73
+ if (await fs.pathExists(meltDir)) {
74
+ result.removedDirs.push('.melt/');
75
+ }
76
+ // Check .cursor/commands directory (selective removal)
77
+ if (await fs.pathExists(cursorCommandsDir)) {
78
+ const commandFiles = await fs.readdir(cursorCommandsDir);
79
+ for (const file of commandFiles) {
80
+ const filePath = path.join(cursorCommandsDir, file);
81
+ const stat = await fs.stat(filePath);
82
+ if (stat.isFile()) {
83
+ if (MELT_COMMAND_FILES.includes(file)) {
84
+ result.removedFiles.push(path.join('.cursor/commands', file));
85
+ }
86
+ else {
87
+ result.preservedFiles.push(path.join('.cursor/commands', file));
88
+ }
89
+ }
90
+ }
91
+ // Check if .cursor/commands will be empty after cleaning
92
+ const meltFiles = commandFiles.filter(file => MELT_COMMAND_FILES.includes(file));
93
+ const nonMeltFiles = commandFiles.filter(file => !MELT_COMMAND_FILES.includes(file));
94
+ if (meltFiles.length > 0 && nonMeltFiles.length === 0) {
95
+ result.removedDirs.push('.cursor/commands/');
96
+ // Also check if .cursor itself would be empty
97
+ const cursorDir = path.join(baseDir, '.cursor');
98
+ const cursorContents = await fs.readdir(cursorDir);
99
+ if (cursorContents.length === 1 && cursorContents[0] === 'commands') {
100
+ result.removedDirs.push('.cursor/');
101
+ }
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ result.errors.push(`Failed to analyze cleanup target: ${error instanceof Error ? error.message : String(error)}`);
107
+ }
108
+ return result;
109
+ }
110
+ async function performCleanup(baseDir) {
111
+ const result = {
112
+ removedFiles: [],
113
+ removedDirs: [],
114
+ preservedFiles: [],
115
+ errors: [],
116
+ };
117
+ const meltDir = path.join(baseDir, '.melt');
118
+ const cursorCommandsDir = path.join(baseDir, '.cursor', 'commands');
119
+ try {
120
+ // Remove .melt directory entirely
121
+ if (await fs.pathExists(meltDir)) {
122
+ await fs.remove(meltDir);
123
+ result.removedDirs.push('.melt/');
124
+ }
125
+ // Remove Melt command files from .cursor/commands
126
+ if (await fs.pathExists(cursorCommandsDir)) {
127
+ const commandFiles = await fs.readdir(cursorCommandsDir);
128
+ let hasNonMeltFiles = false;
129
+ for (const file of commandFiles) {
130
+ const filePath = path.join(cursorCommandsDir, file);
131
+ const stat = await fs.stat(filePath);
132
+ if (stat.isFile()) {
133
+ if (MELT_COMMAND_FILES.includes(file)) {
134
+ try {
135
+ await fs.remove(filePath);
136
+ result.removedFiles.push(path.join('.cursor/commands', file));
137
+ }
138
+ catch (error) {
139
+ result.errors.push(`Failed to remove ${file}: ${error instanceof Error ? error.message : String(error)}`);
140
+ }
141
+ }
142
+ else {
143
+ hasNonMeltFiles = true;
144
+ result.preservedFiles.push(path.join('.cursor/commands', file));
145
+ }
146
+ }
147
+ }
148
+ // Remove empty directories if no user files remain
149
+ if (!hasNonMeltFiles) {
150
+ try {
151
+ await fs.remove(cursorCommandsDir);
152
+ result.removedDirs.push('.cursor/commands/');
153
+ // Check if .cursor directory is now empty
154
+ const cursorDir = path.join(baseDir, '.cursor');
155
+ if (await fs.pathExists(cursorDir)) {
156
+ const cursorContents = await fs.readdir(cursorDir);
157
+ if (cursorContents.length === 0) {
158
+ await fs.remove(cursorDir);
159
+ result.removedDirs.push('.cursor/');
160
+ }
161
+ }
162
+ }
163
+ catch (error) {
164
+ result.errors.push(`Failed to remove empty directories: ${error instanceof Error ? error.message : String(error)}`);
165
+ }
166
+ }
167
+ }
168
+ }
169
+ catch (error) {
170
+ result.errors.push(`Cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
171
+ }
172
+ return result;
173
+ }
174
+ function displayCleanupPlan(result) {
175
+ console.log();
176
+ console.log(chalk.cyan('๐Ÿ“‹ Cleanup Plan:'));
177
+ console.log();
178
+ if (result.removedDirs.length > 0) {
179
+ console.log(chalk.yellow('Directories to be removed:'));
180
+ result.removedDirs.forEach(dir => {
181
+ console.log(` โ€ข ${chalk.red(dir)}`);
182
+ });
183
+ console.log();
184
+ }
185
+ if (result.removedFiles.length > 0) {
186
+ console.log(chalk.yellow('Files to be removed:'));
187
+ result.removedFiles.forEach(file => {
188
+ console.log(` โ€ข ${chalk.red(file)}`);
189
+ });
190
+ console.log();
191
+ }
192
+ if (result.preservedFiles.length > 0) {
193
+ console.log(chalk.green('Files to be preserved:'));
194
+ result.preservedFiles.forEach(file => {
195
+ console.log(` โ€ข ${chalk.cyan(file)}`);
196
+ });
197
+ console.log();
198
+ }
199
+ if (result.errors.length > 0) {
200
+ console.log(chalk.red('Analysis errors:'));
201
+ result.errors.forEach(error => {
202
+ console.log(` โš ๏ธ ${error}`);
203
+ });
204
+ console.log();
205
+ }
206
+ }
207
+ function displayCleanupResults(result) {
208
+ console.log();
209
+ console.log(chalk.green('๐ŸŽฏ Cleanup Summary:'));
210
+ console.log();
211
+ if (result.removedDirs.length > 0) {
212
+ console.log(chalk.green(`โœ… Removed ${result.removedDirs.length} directories:`));
213
+ result.removedDirs.forEach(dir => {
214
+ console.log(` โ€ข ${dir}`);
215
+ });
216
+ console.log();
217
+ }
218
+ if (result.removedFiles.length > 0) {
219
+ console.log(chalk.green(`โœ… Removed ${result.removedFiles.length} files:`));
220
+ result.removedFiles.forEach(file => {
221
+ console.log(` โ€ข ${file}`);
222
+ });
223
+ console.log();
224
+ }
225
+ if (result.preservedFiles.length > 0) {
226
+ console.log(chalk.cyan(`๐Ÿ”’ Preserved ${result.preservedFiles.length} user files:`));
227
+ result.preservedFiles.forEach(file => {
228
+ console.log(` โ€ข ${file}`);
229
+ });
230
+ console.log();
231
+ }
232
+ if (result.errors.length > 0) {
233
+ console.log(chalk.red(`โŒ Errors encountered (${result.errors.length}):`));
234
+ result.errors.forEach(error => {
235
+ console.log(` โ€ข ${error}`);
236
+ });
237
+ console.log();
238
+ }
239
+ }
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { join, dirname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { initCommand } from './commands/project/init.js';
7
7
  import { updateCommand } from './commands/project/update.js';
8
+ import { cleanCommand } from './commands/project/clean.js';
8
9
  // Read version from package.json
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
@@ -34,4 +35,10 @@ projectCommand
34
35
  .command('update')
35
36
  .description('Update project configurations to latest version')
36
37
  .action(updateCommand);
38
+ projectCommand
39
+ .command('clean')
40
+ .description('Remove all Melt-generated files from the project')
41
+ .action(() => {
42
+ return cleanCommand();
43
+ });
37
44
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "CLI tool for Melt development process automation - initialize and update project configurations",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",