@meltstudio/meltctl 4.33.1 → 4.34.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.
@@ -3,6 +3,7 @@ import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { getStoredAuth, API_BASE } from './auth.js';
5
5
  import { getGitRepository, getGitBranch, getProjectName } from './git.js';
6
+ import { debugLog } from './debug.js';
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
8
9
  function getVersion() {
@@ -16,14 +17,19 @@ function getVersion() {
16
17
  }
17
18
  export async function trackCommand(command, success, errorMessage) {
18
19
  try {
20
+ if (process.env['MELTCTL_NO_ANALYTICS']) {
21
+ debugLog('Analytics disabled via MELTCTL_NO_ANALYTICS');
22
+ return;
23
+ }
19
24
  const auth = await getStoredAuth();
20
25
  if (!auth || new Date(auth.expiresAt) <= new Date()) {
26
+ debugLog('Analytics skipped: not authenticated or token expired');
21
27
  return;
22
28
  }
23
29
  const repo = getGitRepository();
24
30
  const controller = new AbortController();
25
31
  const timeout = setTimeout(() => controller.abort(), 3000);
26
- await fetch(`${API_BASE}/events`, {
32
+ const res = await fetch(`${API_BASE}/events`, {
27
33
  method: 'POST',
28
34
  headers: {
29
35
  Authorization: `Bearer ${auth.token}`,
@@ -39,10 +45,20 @@ export async function trackCommand(command, success, errorMessage) {
39
45
  errorMessage: errorMessage?.slice(0, 500) ?? null,
40
46
  }),
41
47
  signal: controller.signal,
42
- }).catch(() => { });
48
+ }).catch(e => {
49
+ debugLog(`Analytics fetch failed: ${e instanceof Error ? e.message : e}`);
50
+ return null;
51
+ });
43
52
  clearTimeout(timeout);
53
+ if (res && !res.ok) {
54
+ const body = await res.text().catch(() => '');
55
+ debugLog(`Analytics API error ${res.status}: ${body}`);
56
+ }
57
+ else if (res) {
58
+ debugLog(`Analytics event sent for "${command}"`);
59
+ }
44
60
  }
45
- catch {
46
- // Analytics must never break the CLI
61
+ catch (e) {
62
+ debugLog(`Analytics error: ${e instanceof Error ? e.message : e}`);
47
63
  }
48
64
  }
@@ -0,0 +1 @@
1
+ export declare function debugLog(message: string): void;
@@ -0,0 +1,6 @@
1
+ import chalk from 'chalk';
2
+ export function debugLog(message) {
3
+ if (process.env['MELTCTL_DEBUG']) {
4
+ console.error(chalk.dim(`[debug] ${message}`));
5
+ }
6
+ }
@@ -2,4 +2,6 @@ export declare function getCurrentCliVersion(): Promise<string>;
2
2
  export declare function getLatestCliVersion(): Promise<string | null>;
3
3
  export declare function compareVersions(current: string, latest: string): boolean;
4
4
  export declare function isCI(): boolean;
5
+ export type UpdateSeverity = 'none' | 'patch' | 'minor' | 'major';
6
+ export declare function getUpdateSeverity(current: string, latest: string): UpdateSeverity;
5
7
  export declare function checkAndEnforceUpdate(): Promise<void>;
@@ -72,6 +72,19 @@ export function isCI() {
72
72
  process.env.MELTCTL_SKIP_UPDATE_CHECK // Custom override
73
73
  );
74
74
  }
75
+ export function getUpdateSeverity(current, latest) {
76
+ const parseCurrent = current.split('-')[0].split('.').map(Number);
77
+ const parseLatest = latest.split('-')[0].split('.').map(Number);
78
+ const [curMajor = 0, curMinor = 0, curPatch = 0] = parseCurrent;
79
+ const [latMajor = 0, latMinor = 0, latPatch = 0] = parseLatest;
80
+ if (latMajor > curMajor)
81
+ return 'major';
82
+ if (latMajor === curMajor && latMinor > curMinor)
83
+ return 'minor';
84
+ if (latMajor === curMajor && latMinor === curMinor && latPatch > curPatch)
85
+ return 'patch';
86
+ return 'none';
87
+ }
75
88
  export async function checkAndEnforceUpdate() {
76
89
  // Skip update check in CI environments
77
90
  if (isCI()) {
@@ -84,27 +97,37 @@ export async function checkAndEnforceUpdate() {
84
97
  if (!latestVersion) {
85
98
  return;
86
99
  }
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'));
100
+ const severity = getUpdateSeverity(currentVersion, latestVersion);
101
+ if (severity === 'none') {
102
+ return;
103
+ }
104
+ const updateCmd = 'npm install -g @meltstudio/meltctl@latest';
105
+ if (severity === 'patch') {
106
+ // Patch updates: warn but allow continuing
103
107
  console.log();
104
- console.log(chalk.gray('To skip this check (CI/CD), set MELTCTL_SKIP_UPDATE_CHECK=1'));
108
+ console.log(chalk.yellow(` Update available: ${currentVersion} ${latestVersion} (run: ${updateCmd})`));
105
109
  console.log();
106
- process.exit(1);
110
+ return;
107
111
  }
112
+ // Minor/major updates: block until updated
113
+ console.log();
114
+ console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
115
+ console.log(chalk.red.bold(' ⚠️ Update Required'));
116
+ console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
117
+ console.log();
118
+ console.log(chalk.yellow(`Current version: ${currentVersion}`));
119
+ console.log(chalk.green(`Latest version: ${latestVersion}`));
120
+ console.log();
121
+ console.log(chalk.white('Please update meltctl to continue:'));
122
+ console.log();
123
+ console.log(chalk.cyan(` ${updateCmd}`));
124
+ console.log();
125
+ console.log(chalk.gray('Or with yarn:'));
126
+ console.log(chalk.cyan(' yarn global add @meltstudio/meltctl@latest'));
127
+ console.log();
128
+ console.log(chalk.gray('To skip this check (CI/CD), set MELTCTL_SKIP_UPDATE_CHECK=1'));
129
+ console.log();
130
+ process.exit(1);
108
131
  }
109
132
  catch {
110
133
  // If any error occurs during version check, allow continuing
@@ -9,7 +9,7 @@ vi.mock('fs-extra', () => ({
9
9
  }));
10
10
  import { execSync } from 'child_process';
11
11
  import fs from 'fs-extra';
12
- import { getCurrentCliVersion, getLatestCliVersion, compareVersions, isCI, checkAndEnforceUpdate, } from './version-check.js';
12
+ import { getCurrentCliVersion, getLatestCliVersion, compareVersions, getUpdateSeverity, isCI, checkAndEnforceUpdate, } from './version-check.js';
13
13
  beforeEach(() => {
14
14
  vi.clearAllMocks();
15
15
  vi.spyOn(console, 'log').mockImplementation(() => { });
@@ -96,6 +96,26 @@ describe('isCI', () => {
96
96
  expect(isCI()).toBe(true);
97
97
  });
98
98
  });
99
+ describe('getUpdateSeverity', () => {
100
+ it('returns major when major version is higher', () => {
101
+ expect(getUpdateSeverity('1.0.0', '2.0.0')).toBe('major');
102
+ });
103
+ it('returns minor when minor version is higher', () => {
104
+ expect(getUpdateSeverity('1.0.0', '1.1.0')).toBe('minor');
105
+ });
106
+ it('returns patch when only patch is higher', () => {
107
+ expect(getUpdateSeverity('1.0.0', '1.0.1')).toBe('patch');
108
+ });
109
+ it('returns none when versions are equal', () => {
110
+ expect(getUpdateSeverity('1.0.0', '1.0.0')).toBe('none');
111
+ });
112
+ it('returns none when current is newer', () => {
113
+ expect(getUpdateSeverity('2.0.0', '1.0.0')).toBe('none');
114
+ });
115
+ it('returns major over minor', () => {
116
+ expect(getUpdateSeverity('1.5.3', '2.0.0')).toBe('major');
117
+ });
118
+ });
99
119
  describe('checkAndEnforceUpdate', () => {
100
120
  it('skips check in CI environment', async () => {
101
121
  process.env.CI = 'true';
@@ -119,13 +139,29 @@ describe('checkAndEnforceUpdate', () => {
119
139
  await checkAndEnforceUpdate();
120
140
  // Should not throw or exit
121
141
  });
122
- it('calls process.exit when update is required', async () => {
142
+ it('blocks on minor version update', async () => {
123
143
  const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
124
144
  fs.readJson.mockResolvedValue({ version: '4.25.0' });
125
145
  execSync.mockReturnValue('"4.26.0"\n');
126
146
  await checkAndEnforceUpdate();
127
147
  expect(exitSpy).toHaveBeenCalledWith(1);
128
148
  });
149
+ it('blocks on major version update', async () => {
150
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
151
+ fs.readJson.mockResolvedValue({ version: '4.33.0' });
152
+ execSync.mockReturnValue('"5.0.0"\n');
153
+ await checkAndEnforceUpdate();
154
+ expect(exitSpy).toHaveBeenCalledWith(1);
155
+ });
156
+ it('warns but does not block on patch update', async () => {
157
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
158
+ fs.readJson.mockResolvedValue({ version: '4.33.0' });
159
+ execSync.mockReturnValue('"4.33.2"\n');
160
+ await checkAndEnforceUpdate();
161
+ expect(exitSpy).not.toHaveBeenCalled();
162
+ const logCalls = console.log.mock.calls.map((c) => String(c[0]));
163
+ expect(logCalls.some((msg) => msg.includes('Update available'))).toBe(true);
164
+ });
129
165
  it('allows continuing when getCurrentCliVersion throws', async () => {
130
166
  ;
131
167
  fs.readJson.mockRejectedValue(new Error('file not found'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "4.33.1",
3
+ "version": "4.34.0",
4
4
  "description": "AI-first development tools for teams - set up AGENTS.md, Claude Code, Cursor, and OpenCode standards",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",