@meltstudio/meltctl 2.1.0 โ†’ 2.3.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,40 @@ 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
+
85
+ ### Version Check
86
+
87
+ ```bash
88
+ meltctl version --check
89
+ ```
90
+
91
+ Checks for available updates to the meltctl CLI. This command:
92
+ - Compares your current version with the latest published version
93
+ - Provides update instructions based on your package manager (npm/yarn)
94
+ - Handles network errors gracefully
95
+
96
+ ### CI/CD Usage
97
+
98
+ For automated environments (CI/CD pipelines), you can skip the update check that runs before every command:
99
+
100
+ ```bash
101
+ export MELTCTL_SKIP_UPDATE_CHECK=1
102
+ meltctl project init
103
+ ```
104
+
105
+ This environment variable bypasses the automatic update enforcement that normally prevents running commands with outdated versions.
106
+
73
107
  ## ๐Ÿ› ๏ธ Requirements
74
108
 
75
109
  - 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
+ }
@@ -4,66 +4,15 @@ import { execSync } from 'child_process';
4
4
  import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
+ import { getCurrentCliVersion, getLatestCliVersion, compareVersions, } from '../../utils/version-check.js';
8
+ import { detectPackageManager } from '../../utils/package-manager.js';
7
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- async function getCurrentCliVersion() {
9
- const packagePath = path.join(__dirname, '../../../package.json');
10
- const packageJson = await fs.readJson(packagePath);
11
- return packageJson.version;
12
- }
13
- async function getLatestCliVersion() {
14
- try {
15
- const result = execSync('npm view @meltstudio/meltctl version --json', {
16
- encoding: 'utf-8',
17
- stdio: 'pipe',
18
- });
19
- return JSON.parse(result.trim());
20
- }
21
- catch (error) {
22
- console.error(chalk.red('Failed to check latest version from npm registry'));
23
- throw error;
24
- }
25
- }
26
- function compareVersions(current, latest) {
27
- // Parse semver strings, handling pre-release tags
28
- const parseVersion = (version) => {
29
- const [base, prerelease] = version.split('-');
30
- const parts = (base || '').split('.').map(Number);
31
- return {
32
- major: parts[0] || 0,
33
- minor: parts[1] || 0,
34
- patch: parts[2] || 0,
35
- prerelease: prerelease || null,
36
- };
37
- };
38
- const currentVer = parseVersion(current);
39
- const latestVer = parseVersion(latest);
40
- // Compare major.minor.patch first
41
- if (latestVer.major !== currentVer.major) {
42
- return latestVer.major > currentVer.major;
43
- }
44
- if (latestVer.minor !== currentVer.minor) {
45
- return latestVer.minor > currentVer.minor;
46
- }
47
- if (latestVer.patch !== currentVer.patch) {
48
- return latestVer.patch > currentVer.patch;
49
- }
50
- // If base versions are equal, handle pre-release comparison
51
- // No pre-release (stable) > pre-release
52
- if (!latestVer.prerelease && currentVer.prerelease)
53
- return true;
54
- if (latestVer.prerelease && !currentVer.prerelease)
55
- return false;
56
- // Both have pre-release or both are stable
57
- if (latestVer.prerelease && currentVer.prerelease) {
58
- return latestVer.prerelease > currentVer.prerelease;
59
- }
60
- return false; // Versions are equal
61
- }
62
10
  async function updateCliPackage() {
63
11
  const s = spinner();
64
12
  s.start('Updating @meltstudio/meltctl package...');
65
13
  try {
66
- execSync('npm install -g @meltstudio/meltctl@latest', {
14
+ const { updateCommand } = detectPackageManager();
15
+ execSync(updateCommand, {
67
16
  stdio: 'pipe',
68
17
  });
69
18
  s.stop('CLI package updated successfully!');
@@ -115,22 +64,29 @@ export async function updateCommand() {
115
64
  try {
116
65
  const currentVersion = await getCurrentCliVersion();
117
66
  const latestVersion = await getLatestCliVersion();
118
- console.log(chalk.gray(`Current CLI version: ${currentVersion}`));
119
- console.log(chalk.gray(`Latest CLI version: ${latestVersion}`));
120
- console.log();
121
- if (compareVersions(currentVersion, latestVersion)) {
122
- console.log(chalk.yellow(`๐Ÿ“ฆ New version available: ${latestVersion}`));
123
- const shouldUpdate = await confirm({
124
- message: 'Would you like to update the CLI package?',
125
- });
126
- if (shouldUpdate) {
127
- await updateCliPackage();
128
- console.log();
129
- }
67
+ if (!latestVersion) {
68
+ console.log(chalk.yellow('โš ๏ธ Unable to check for updates (network error)'));
69
+ console.log(chalk.gray(`Current CLI version: ${currentVersion}`));
70
+ console.log();
130
71
  }
131
72
  else {
132
- console.log(chalk.green('โœ… CLI package is up to date'));
73
+ console.log(chalk.gray(`Current CLI version: ${currentVersion}`));
74
+ console.log(chalk.gray(`Latest CLI version: ${latestVersion}`));
133
75
  console.log();
76
+ if (compareVersions(currentVersion, latestVersion)) {
77
+ console.log(chalk.yellow(`๐Ÿ“ฆ New version available: ${latestVersion}`));
78
+ const shouldUpdate = await confirm({
79
+ message: 'Would you like to update the CLI package?',
80
+ });
81
+ if (shouldUpdate) {
82
+ await updateCliPackage();
83
+ console.log();
84
+ }
85
+ }
86
+ else {
87
+ console.log(chalk.green('โœ… CLI package is up to date'));
88
+ console.log();
89
+ }
134
90
  }
135
91
  const shouldUpdateCommands = await confirm({
136
92
  message: 'Would you like to update .cursor/commands/ with latest prompts?',
@@ -0,0 +1 @@
1
+ export declare function versionCheckCommand(): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+ import { getCurrentCliVersion, getLatestCliVersion, compareVersions, } from '../utils/version-check.js';
3
+ import { getUpdateInstructions } from '../utils/package-manager.js';
4
+ export async function versionCheckCommand() {
5
+ try {
6
+ const currentVersion = await getCurrentCliVersion();
7
+ const latestVersion = await getLatestCliVersion();
8
+ if (!latestVersion) {
9
+ console.log(chalk.yellow('โš ๏ธ Unable to check for updates (network error)'));
10
+ console.log(chalk.gray(`Current version: ${currentVersion}`));
11
+ return;
12
+ }
13
+ if (compareVersions(currentVersion, latestVersion)) {
14
+ // Update available
15
+ console.log(chalk.yellow(`โš  Update available: ${currentVersion} โ†’ ${latestVersion}`));
16
+ console.log();
17
+ console.log(chalk.white('To update, run:'));
18
+ const instructions = getUpdateInstructions();
19
+ instructions.forEach((instruction, index) => {
20
+ if (index === 0) {
21
+ console.log(chalk.cyan(` ${instruction}`));
22
+ }
23
+ else if (instruction === '') {
24
+ console.log();
25
+ }
26
+ else if (instruction.startsWith('Or with')) {
27
+ console.log(chalk.gray(instruction));
28
+ }
29
+ else {
30
+ console.log(chalk.cyan(instruction));
31
+ }
32
+ });
33
+ }
34
+ else {
35
+ // Up to date
36
+ console.log(chalk.green(`โœ“ meltctl ${currentVersion} is up to date`));
37
+ }
38
+ }
39
+ catch (error) {
40
+ console.error(chalk.red('Failed to check for updates:'), error instanceof Error ? error.message : String(error));
41
+ process.exit(1);
42
+ }
43
+ }
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ 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';
9
+ import { checkAndEnforceUpdate } from './utils/version-check.js';
10
+ import { versionCheckCommand } from './commands/version.js';
8
11
  // Read version from package.json
9
12
  const __filename = fileURLToPath(import.meta.url);
10
13
  const __dirname = dirname(__filename);
@@ -13,7 +16,10 @@ const program = new Command();
13
16
  program
14
17
  .name('meltctl')
15
18
  .description('CLI tool for Melt development process automation')
16
- .version(packageJson.version);
19
+ .version(packageJson.version)
20
+ .hook('preAction', async () => {
21
+ await checkAndEnforceUpdate();
22
+ });
17
23
  // Project management commands
18
24
  const projectCommand = program
19
25
  .command('project')
@@ -34,4 +40,23 @@ projectCommand
34
40
  .command('update')
35
41
  .description('Update project configurations to latest version')
36
42
  .action(updateCommand);
37
- program.parse(process.argv);
43
+ projectCommand
44
+ .command('clean')
45
+ .description('Remove all Melt-generated files from the project')
46
+ .action(() => {
47
+ return cleanCommand();
48
+ });
49
+ // Version check command
50
+ program
51
+ .command('version')
52
+ .description('Display version information')
53
+ .option('--check', 'check for updates')
54
+ .action(async (options) => {
55
+ if (options.check) {
56
+ await versionCheckCommand();
57
+ }
58
+ else {
59
+ console.log(packageJson.version);
60
+ }
61
+ });
62
+ program.parseAsync(process.argv);
@@ -0,0 +1,7 @@
1
+ export type PackageManager = 'npm' | 'yarn' | 'unknown';
2
+ export interface PackageManagerInfo {
3
+ type: PackageManager;
4
+ updateCommand: string;
5
+ }
6
+ export declare function detectPackageManager(): PackageManagerInfo;
7
+ export declare function getUpdateInstructions(): string[];
@@ -0,0 +1,55 @@
1
+ import { execSync } from 'child_process';
2
+ export function detectPackageManager() {
3
+ // Try to detect if meltctl is installed via npm or yarn
4
+ try {
5
+ // Check npm global installation
6
+ execSync('npm list -g @meltstudio/meltctl', {
7
+ encoding: 'utf-8',
8
+ stdio: 'pipe',
9
+ });
10
+ return {
11
+ type: 'npm',
12
+ updateCommand: 'npm install -g @meltstudio/meltctl@latest',
13
+ };
14
+ }
15
+ catch {
16
+ // npm check failed, try yarn
17
+ }
18
+ try {
19
+ // Check yarn global installation
20
+ execSync('yarn global list @meltstudio/meltctl', {
21
+ encoding: 'utf-8',
22
+ stdio: 'pipe',
23
+ });
24
+ return {
25
+ type: 'yarn',
26
+ updateCommand: 'yarn global add @meltstudio/meltctl@latest',
27
+ };
28
+ }
29
+ catch {
30
+ // yarn check failed
31
+ }
32
+ // Default to npm if detection fails
33
+ return {
34
+ type: 'unknown',
35
+ updateCommand: 'npm install -g @meltstudio/meltctl@latest',
36
+ };
37
+ }
38
+ export function getUpdateInstructions() {
39
+ const { type, updateCommand } = detectPackageManager();
40
+ if (type === 'npm') {
41
+ return [updateCommand, '', 'Or with yarn:', ' yarn global add @meltstudio/meltctl@latest'];
42
+ }
43
+ else if (type === 'yarn') {
44
+ return [updateCommand, '', 'Or with npm:', ' npm install -g @meltstudio/meltctl@latest'];
45
+ }
46
+ else {
47
+ // Unknown, show both options
48
+ return [
49
+ 'npm install -g @meltstudio/meltctl@latest',
50
+ '',
51
+ 'Or with yarn:',
52
+ ' yarn global add @meltstudio/meltctl@latest',
53
+ ];
54
+ }
55
+ }
@@ -0,0 +1,5 @@
1
+ export declare function getCurrentCliVersion(): Promise<string>;
2
+ export declare function getLatestCliVersion(): Promise<string | null>;
3
+ export declare function compareVersions(current: string, latest: string): boolean;
4
+ export declare function isCI(): boolean;
5
+ export declare function checkAndEnforceUpdate(): Promise<void>;
@@ -0,0 +1,114 @@
1
+ import chalk from 'chalk';
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ export async function getCurrentCliVersion() {
8
+ const packagePath = path.join(__dirname, '../../package.json');
9
+ const packageJson = await fs.readJson(packagePath);
10
+ return packageJson.version;
11
+ }
12
+ export async function getLatestCliVersion() {
13
+ try {
14
+ const result = execSync('npm view @meltstudio/meltctl version --json', {
15
+ encoding: 'utf-8',
16
+ stdio: 'pipe',
17
+ timeout: 5000, // 5 second timeout
18
+ });
19
+ return JSON.parse(result.trim());
20
+ }
21
+ catch {
22
+ // Network error or timeout - return null to indicate check failed
23
+ return null;
24
+ }
25
+ }
26
+ export function compareVersions(current, latest) {
27
+ // Parse semver strings, handling pre-release tags
28
+ const parseVersion = (version) => {
29
+ const [base, prerelease] = version.split('-');
30
+ const parts = (base || '').split('.').map(Number);
31
+ return {
32
+ major: parts[0] || 0,
33
+ minor: parts[1] || 0,
34
+ patch: parts[2] || 0,
35
+ prerelease: prerelease || null,
36
+ };
37
+ };
38
+ const currentVer = parseVersion(current);
39
+ const latestVer = parseVersion(latest);
40
+ // Compare major.minor.patch first
41
+ if (latestVer.major !== currentVer.major) {
42
+ return latestVer.major > currentVer.major;
43
+ }
44
+ if (latestVer.minor !== currentVer.minor) {
45
+ return latestVer.minor > currentVer.minor;
46
+ }
47
+ if (latestVer.patch !== currentVer.patch) {
48
+ return latestVer.patch > currentVer.patch;
49
+ }
50
+ // If base versions are equal, handle pre-release comparison
51
+ // No pre-release (stable) > pre-release
52
+ if (!latestVer.prerelease && currentVer.prerelease)
53
+ return true;
54
+ if (latestVer.prerelease && !currentVer.prerelease)
55
+ return false;
56
+ // Both have pre-release or both are stable
57
+ if (latestVer.prerelease && currentVer.prerelease) {
58
+ return latestVer.prerelease > currentVer.prerelease;
59
+ }
60
+ return false; // Versions are equal
61
+ }
62
+ export function isCI() {
63
+ // Check common CI environment variables
64
+ return !!(process.env.CI || // Generic CI flag
65
+ process.env.GITHUB_ACTIONS || // GitHub Actions
66
+ process.env.GITLAB_CI || // GitLab CI
67
+ process.env.CIRCLECI || // CircleCI
68
+ process.env.TRAVIS || // Travis CI
69
+ process.env.JENKINS_URL || // Jenkins
70
+ process.env.BUILDKITE || // Buildkite
71
+ process.env.DRONE || // Drone
72
+ process.env.MELTCTL_SKIP_UPDATE_CHECK // Custom override
73
+ );
74
+ }
75
+ export async function checkAndEnforceUpdate() {
76
+ // Skip update check in CI environments
77
+ if (isCI()) {
78
+ return;
79
+ }
80
+ try {
81
+ const currentVersion = await getCurrentCliVersion();
82
+ const latestVersion = await getLatestCliVersion();
83
+ // If we can't fetch latest version (network error), allow continuing
84
+ if (!latestVersion) {
85
+ return;
86
+ }
87
+ // Check if update is needed
88
+ if (compareVersions(currentVersion, latestVersion)) {
89
+ console.log();
90
+ console.log(chalk.red('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'));
91
+ console.log(chalk.red.bold(' โš ๏ธ Update Required'));
92
+ console.log(chalk.red('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'));
93
+ console.log();
94
+ console.log(chalk.yellow(`Current version: ${currentVersion}`));
95
+ console.log(chalk.green(`Latest version: ${latestVersion}`));
96
+ console.log();
97
+ console.log(chalk.white('Please update meltctl to continue:'));
98
+ console.log();
99
+ console.log(chalk.cyan(' npm install -g @meltstudio/meltctl@latest'));
100
+ console.log();
101
+ console.log(chalk.gray('Or with yarn:'));
102
+ console.log(chalk.cyan(' yarn global add @meltstudio/meltctl@latest'));
103
+ console.log();
104
+ console.log(chalk.gray('To skip this check (CI/CD), set MELTCTL_SKIP_UPDATE_CHECK=1'));
105
+ console.log();
106
+ process.exit(1);
107
+ }
108
+ }
109
+ catch {
110
+ // If any error occurs during version check, allow continuing
111
+ // This ensures the CLI remains usable even if version check fails
112
+ return;
113
+ }
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "2.1.0",
3
+ "version": "2.3.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",