@meltstudio/meltctl 4.33.2 → 4.35.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/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +74 -0
- package/dist/commands/update.test.d.ts +1 -0
- package/dist/commands/update.test.js +93 -0
- package/dist/index.js +7 -0
- package/dist/utils/version-check.d.ts +2 -0
- package/dist/utils/version-check.js +41 -16
- package/dist/utils/version-check.test.js +56 -2
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { getCurrentCliVersion, getLatestCliVersion, getUpdateSeverity, } from '../utils/version-check.js';
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export function detectPackageManager() {
|
|
8
|
+
const installPath = path.resolve(__dirname, '../..');
|
|
9
|
+
try {
|
|
10
|
+
const npmGlobalPrefix = execSync('npm prefix -g', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
11
|
+
if (installPath.startsWith(npmGlobalPrefix)) {
|
|
12
|
+
return 'npm';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// npm not available
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const yarnGlobalDir = execSync('yarn global dir', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
20
|
+
if (installPath.startsWith(yarnGlobalDir)) {
|
|
21
|
+
return 'yarn';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// yarn not available
|
|
26
|
+
}
|
|
27
|
+
// Check path heuristics as fallback
|
|
28
|
+
if (installPath.includes('yarn'))
|
|
29
|
+
return 'yarn';
|
|
30
|
+
if (installPath.includes('npm'))
|
|
31
|
+
return 'npm';
|
|
32
|
+
return 'unknown';
|
|
33
|
+
}
|
|
34
|
+
export async function updateCommand() {
|
|
35
|
+
const currentVersion = await getCurrentCliVersion();
|
|
36
|
+
const latestVersion = await getLatestCliVersion();
|
|
37
|
+
if (!latestVersion) {
|
|
38
|
+
console.error(chalk.red('Could not check for updates. Check your network connection.'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const severity = getUpdateSeverity(currentVersion, latestVersion);
|
|
42
|
+
if (severity === 'none') {
|
|
43
|
+
console.log(chalk.green(` ✓ Already on the latest version (${currentVersion})`));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log(chalk.dim(` ${currentVersion} → ${latestVersion}`));
|
|
47
|
+
console.log();
|
|
48
|
+
const pm = detectPackageManager();
|
|
49
|
+
let cmd;
|
|
50
|
+
if (pm === 'yarn') {
|
|
51
|
+
cmd = 'yarn global add @meltstudio/meltctl@latest';
|
|
52
|
+
}
|
|
53
|
+
else if (pm === 'npm') {
|
|
54
|
+
cmd = 'npm install -g @meltstudio/meltctl@latest';
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Unknown install method — try npm as default
|
|
58
|
+
console.log(chalk.dim(' Could not detect package manager, trying npm...'));
|
|
59
|
+
cmd = 'npm install -g @meltstudio/meltctl@latest';
|
|
60
|
+
}
|
|
61
|
+
console.log(chalk.dim(` Running: ${cmd}`));
|
|
62
|
+
console.log();
|
|
63
|
+
try {
|
|
64
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(chalk.green(` ✓ Updated to ${latestVersion}`));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
console.error();
|
|
70
|
+
console.error(chalk.red(' Update failed. Try running manually:'));
|
|
71
|
+
console.error(chalk.cyan(` ${cmd}`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('child_process', () => ({
|
|
3
|
+
execSync: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('fs-extra', () => ({
|
|
6
|
+
default: {
|
|
7
|
+
readJson: vi.fn(),
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('../utils/version-check.js', () => ({
|
|
11
|
+
getCurrentCliVersion: vi.fn(),
|
|
12
|
+
getLatestCliVersion: vi.fn(),
|
|
13
|
+
getUpdateSeverity: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { getCurrentCliVersion, getLatestCliVersion, getUpdateSeverity, } from '../utils/version-check.js';
|
|
17
|
+
import { updateCommand, detectPackageManager } from './update.js';
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
21
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
22
|
+
vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
23
|
+
});
|
|
24
|
+
describe('detectPackageManager', () => {
|
|
25
|
+
it('returns npm when install path matches npm prefix', () => {
|
|
26
|
+
;
|
|
27
|
+
execSync.mockImplementation((cmd) => {
|
|
28
|
+
if (cmd === 'npm prefix -g')
|
|
29
|
+
return '/usr/local/lib\n';
|
|
30
|
+
throw new Error('not found');
|
|
31
|
+
});
|
|
32
|
+
// This test depends on the actual install path, so just verify it returns a valid value
|
|
33
|
+
const result = detectPackageManager();
|
|
34
|
+
expect(['npm', 'yarn', 'unknown']).toContain(result);
|
|
35
|
+
});
|
|
36
|
+
it('returns unknown when neither npm nor yarn detected', () => {
|
|
37
|
+
;
|
|
38
|
+
execSync.mockImplementation(() => {
|
|
39
|
+
throw new Error('not found');
|
|
40
|
+
});
|
|
41
|
+
const result = detectPackageManager();
|
|
42
|
+
expect(['npm', 'yarn', 'unknown']).toContain(result);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('updateCommand', () => {
|
|
46
|
+
it('prints already up to date when on latest', async () => {
|
|
47
|
+
vi.mocked(getCurrentCliVersion).mockResolvedValue('4.34.0');
|
|
48
|
+
vi.mocked(getLatestCliVersion).mockResolvedValue('4.34.0');
|
|
49
|
+
vi.mocked(getUpdateSeverity).mockReturnValue('none');
|
|
50
|
+
await updateCommand();
|
|
51
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
52
|
+
expect(logCalls.some((msg) => msg.includes('Already on the latest'))).toBe(true);
|
|
53
|
+
expect(process.exit).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
it('exits when latest version cannot be fetched', async () => {
|
|
56
|
+
vi.mocked(getCurrentCliVersion).mockResolvedValue('4.34.0');
|
|
57
|
+
vi.mocked(getLatestCliVersion).mockResolvedValue(null);
|
|
58
|
+
await updateCommand();
|
|
59
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
60
|
+
});
|
|
61
|
+
it('runs npm install when update available and npm detected', async () => {
|
|
62
|
+
vi.mocked(getCurrentCliVersion).mockResolvedValue('4.33.0');
|
|
63
|
+
vi.mocked(getLatestCliVersion).mockResolvedValue('4.34.0');
|
|
64
|
+
vi.mocked(getUpdateSeverity).mockReturnValue('minor');
|
|
65
|
+
execSync.mockImplementation((cmd) => {
|
|
66
|
+
if (cmd === 'npm prefix -g')
|
|
67
|
+
return '/usr/local/lib\n';
|
|
68
|
+
if (cmd.startsWith('npm install -g'))
|
|
69
|
+
return '';
|
|
70
|
+
throw new Error('not found');
|
|
71
|
+
});
|
|
72
|
+
await updateCommand();
|
|
73
|
+
expect(execSync).toHaveBeenCalledWith('npm install -g @meltstudio/meltctl@latest', {
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
it('exits with error when install command fails', async () => {
|
|
78
|
+
vi.mocked(getCurrentCliVersion).mockResolvedValue('4.33.0');
|
|
79
|
+
vi.mocked(getLatestCliVersion).mockResolvedValue('4.34.0');
|
|
80
|
+
vi.mocked(getUpdateSeverity).mockReturnValue('minor');
|
|
81
|
+
execSync.mockImplementation((cmd) => {
|
|
82
|
+
if (cmd === 'npm prefix -g')
|
|
83
|
+
return '/usr/local/lib\n';
|
|
84
|
+
if (cmd.startsWith('npm install -g'))
|
|
85
|
+
throw new Error('permission denied');
|
|
86
|
+
throw new Error('not found');
|
|
87
|
+
});
|
|
88
|
+
await updateCommand();
|
|
89
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
90
|
+
const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
|
|
91
|
+
expect(errorCalls.some((msg) => msg.includes('Update failed'))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { feedbackCommand } from './commands/feedback.js';
|
|
|
16
16
|
import { coinsCommand } from './commands/coins.js';
|
|
17
17
|
import { auditSubmitCommand, auditListCommand, auditViewCommand } from './commands/audit.js';
|
|
18
18
|
import { planSubmitCommand, planListCommand } from './commands/plan.js';
|
|
19
|
+
import { updateCommand } from './commands/update.js';
|
|
19
20
|
import { trackCommand } from './utils/analytics.js';
|
|
20
21
|
// Read version from package.json
|
|
21
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -186,6 +187,12 @@ plan
|
|
|
186
187
|
.action(async (options) => {
|
|
187
188
|
await planListCommand(options);
|
|
188
189
|
});
|
|
190
|
+
program
|
|
191
|
+
.command('update')
|
|
192
|
+
.description('update meltctl to the latest version')
|
|
193
|
+
.action(async () => {
|
|
194
|
+
await updateCommand();
|
|
195
|
+
});
|
|
189
196
|
program
|
|
190
197
|
.command('version')
|
|
191
198
|
.description('show current version')
|
|
@@ -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>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
3
|
import { execSync } from 'child_process';
|
|
3
4
|
import fs from 'fs-extra';
|
|
4
5
|
import path from 'path';
|
|
@@ -72,6 +73,19 @@ export function isCI() {
|
|
|
72
73
|
process.env.MELTCTL_SKIP_UPDATE_CHECK // Custom override
|
|
73
74
|
);
|
|
74
75
|
}
|
|
76
|
+
export function getUpdateSeverity(current, latest) {
|
|
77
|
+
const parseCurrent = current.split('-')[0].split('.').map(Number);
|
|
78
|
+
const parseLatest = latest.split('-')[0].split('.').map(Number);
|
|
79
|
+
const [curMajor = 0, curMinor = 0, curPatch = 0] = parseCurrent;
|
|
80
|
+
const [latMajor = 0, latMinor = 0, latPatch = 0] = parseLatest;
|
|
81
|
+
if (latMajor > curMajor)
|
|
82
|
+
return 'major';
|
|
83
|
+
if (latMajor === curMajor && latMinor > curMinor)
|
|
84
|
+
return 'minor';
|
|
85
|
+
if (latMajor === curMajor && latMinor === curMinor && latPatch > curPatch)
|
|
86
|
+
return 'patch';
|
|
87
|
+
return 'none';
|
|
88
|
+
}
|
|
75
89
|
export async function checkAndEnforceUpdate() {
|
|
76
90
|
// Skip update check in CI environments
|
|
77
91
|
if (isCI()) {
|
|
@@ -84,27 +98,38 @@ export async function checkAndEnforceUpdate() {
|
|
|
84
98
|
if (!latestVersion) {
|
|
85
99
|
return;
|
|
86
100
|
}
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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:'));
|
|
101
|
+
const severity = getUpdateSeverity(currentVersion, latestVersion);
|
|
102
|
+
if (severity === 'none') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (severity === 'patch') {
|
|
106
|
+
// Patch updates: warn but allow continuing
|
|
98
107
|
console.log();
|
|
99
|
-
console.log(chalk.
|
|
108
|
+
console.log(chalk.yellow(` Update available: ${currentVersion} → ${latestVersion} (run: meltctl update)`));
|
|
100
109
|
console.log();
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Minor/major updates: offer to update, block if declined
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(chalk.yellow(` Update required: ${currentVersion} → ${latestVersion}`));
|
|
115
|
+
console.log();
|
|
116
|
+
const shouldUpdate = await confirm({
|
|
117
|
+
message: 'Update now?',
|
|
118
|
+
default: true,
|
|
119
|
+
});
|
|
120
|
+
if (shouldUpdate) {
|
|
121
|
+
const { updateCommand } = await import('../commands/update.js');
|
|
122
|
+
await updateCommand();
|
|
123
|
+
// Re-run the original command after update
|
|
103
124
|
console.log();
|
|
104
|
-
console.log(chalk.
|
|
125
|
+
console.log(chalk.dim(' Please re-run your command.'));
|
|
105
126
|
console.log();
|
|
106
|
-
process.exit(
|
|
127
|
+
process.exit(0);
|
|
107
128
|
}
|
|
129
|
+
console.log();
|
|
130
|
+
console.log(chalk.gray('To skip this check (CI/CD), set MELTCTL_SKIP_UPDATE_CHECK=1'));
|
|
131
|
+
console.log();
|
|
132
|
+
process.exit(1);
|
|
108
133
|
}
|
|
109
134
|
catch {
|
|
110
135
|
// If any error occurs during version check, allow continuing
|
|
@@ -7,9 +7,16 @@ vi.mock('fs-extra', () => ({
|
|
|
7
7
|
readJson: vi.fn(),
|
|
8
8
|
},
|
|
9
9
|
}));
|
|
10
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
11
|
+
confirm: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('../commands/update.js', () => ({
|
|
14
|
+
updateCommand: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
}));
|
|
10
16
|
import { execSync } from 'child_process';
|
|
11
17
|
import fs from 'fs-extra';
|
|
12
|
-
import {
|
|
18
|
+
import { confirm } from '@inquirer/prompts';
|
|
19
|
+
import { getCurrentCliVersion, getLatestCliVersion, compareVersions, getUpdateSeverity, isCI, checkAndEnforceUpdate, } from './version-check.js';
|
|
13
20
|
beforeEach(() => {
|
|
14
21
|
vi.clearAllMocks();
|
|
15
22
|
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
@@ -96,6 +103,26 @@ describe('isCI', () => {
|
|
|
96
103
|
expect(isCI()).toBe(true);
|
|
97
104
|
});
|
|
98
105
|
});
|
|
106
|
+
describe('getUpdateSeverity', () => {
|
|
107
|
+
it('returns major when major version is higher', () => {
|
|
108
|
+
expect(getUpdateSeverity('1.0.0', '2.0.0')).toBe('major');
|
|
109
|
+
});
|
|
110
|
+
it('returns minor when minor version is higher', () => {
|
|
111
|
+
expect(getUpdateSeverity('1.0.0', '1.1.0')).toBe('minor');
|
|
112
|
+
});
|
|
113
|
+
it('returns patch when only patch is higher', () => {
|
|
114
|
+
expect(getUpdateSeverity('1.0.0', '1.0.1')).toBe('patch');
|
|
115
|
+
});
|
|
116
|
+
it('returns none when versions are equal', () => {
|
|
117
|
+
expect(getUpdateSeverity('1.0.0', '1.0.0')).toBe('none');
|
|
118
|
+
});
|
|
119
|
+
it('returns none when current is newer', () => {
|
|
120
|
+
expect(getUpdateSeverity('2.0.0', '1.0.0')).toBe('none');
|
|
121
|
+
});
|
|
122
|
+
it('returns major over minor', () => {
|
|
123
|
+
expect(getUpdateSeverity('1.5.3', '2.0.0')).toBe('major');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
99
126
|
describe('checkAndEnforceUpdate', () => {
|
|
100
127
|
it('skips check in CI environment', async () => {
|
|
101
128
|
process.env.CI = 'true';
|
|
@@ -119,13 +146,40 @@ describe('checkAndEnforceUpdate', () => {
|
|
|
119
146
|
await checkAndEnforceUpdate();
|
|
120
147
|
// Should not throw or exit
|
|
121
148
|
});
|
|
122
|
-
it('
|
|
149
|
+
it('exits when user declines update on minor bump', async () => {
|
|
123
150
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
124
151
|
fs.readJson.mockResolvedValue({ version: '4.25.0' });
|
|
125
152
|
execSync.mockReturnValue('"4.26.0"\n');
|
|
153
|
+
vi.mocked(confirm).mockResolvedValue(false);
|
|
126
154
|
await checkAndEnforceUpdate();
|
|
155
|
+
expect(confirm).toHaveBeenCalled();
|
|
127
156
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
128
157
|
});
|
|
158
|
+
it('runs update when user accepts on minor bump', async () => {
|
|
159
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
160
|
+
fs.readJson.mockResolvedValue({ version: '4.25.0' });
|
|
161
|
+
execSync.mockReturnValue('"4.26.0"\n');
|
|
162
|
+
vi.mocked(confirm).mockResolvedValue(true);
|
|
163
|
+
await checkAndEnforceUpdate();
|
|
164
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
165
|
+
});
|
|
166
|
+
it('exits when user declines update on major bump', async () => {
|
|
167
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
168
|
+
fs.readJson.mockResolvedValue({ version: '4.33.0' });
|
|
169
|
+
execSync.mockReturnValue('"5.0.0"\n');
|
|
170
|
+
vi.mocked(confirm).mockResolvedValue(false);
|
|
171
|
+
await checkAndEnforceUpdate();
|
|
172
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
173
|
+
});
|
|
174
|
+
it('warns but does not block on patch update', async () => {
|
|
175
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
176
|
+
fs.readJson.mockResolvedValue({ version: '4.33.0' });
|
|
177
|
+
execSync.mockReturnValue('"4.33.2"\n');
|
|
178
|
+
await checkAndEnforceUpdate();
|
|
179
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
180
|
+
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
181
|
+
expect(logCalls.some((msg) => msg.includes('Update available'))).toBe(true);
|
|
182
|
+
});
|
|
129
183
|
it('allows continuing when getCurrentCliVersion throws', async () => {
|
|
130
184
|
;
|
|
131
185
|
fs.readJson.mockRejectedValue(new Error('file not found'));
|
package/package.json
CHANGED