@meltstudio/meltctl 4.34.0 → 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.js +17 -15
- package/dist/utils/version-check.test.js +20 -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')
|
|
@@ -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';
|
|
@@ -101,29 +102,30 @@ export async function checkAndEnforceUpdate() {
|
|
|
101
102
|
if (severity === 'none') {
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
|
-
const updateCmd = 'npm install -g @meltstudio/meltctl@latest';
|
|
105
105
|
if (severity === 'patch') {
|
|
106
106
|
// Patch updates: warn but allow continuing
|
|
107
107
|
console.log();
|
|
108
|
-
console.log(chalk.yellow(` Update available: ${currentVersion} → ${latestVersion} (run:
|
|
108
|
+
console.log(chalk.yellow(` Update available: ${currentVersion} → ${latestVersion} (run: meltctl update)`));
|
|
109
109
|
console.log();
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
-
// Minor/major updates: block
|
|
112
|
+
// Minor/major updates: offer to update, block if declined
|
|
113
113
|
console.log();
|
|
114
|
-
console.log(chalk.
|
|
115
|
-
console.log(chalk.red.bold(' ⚠️ Update Required'));
|
|
116
|
-
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
114
|
+
console.log(chalk.yellow(` Update required: ${currentVersion} → ${latestVersion}`));
|
|
117
115
|
console.log();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
124
|
+
console.log();
|
|
125
|
+
console.log(chalk.dim(' Please re-run your command.'));
|
|
126
|
+
console.log();
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
127
129
|
console.log();
|
|
128
130
|
console.log(chalk.gray('To skip this check (CI/CD), set MELTCTL_SKIP_UPDATE_CHECK=1'));
|
|
129
131
|
console.log();
|
|
@@ -7,8 +7,15 @@ 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';
|
|
18
|
+
import { confirm } from '@inquirer/prompts';
|
|
12
19
|
import { getCurrentCliVersion, getLatestCliVersion, compareVersions, getUpdateSeverity, isCI, checkAndEnforceUpdate, } from './version-check.js';
|
|
13
20
|
beforeEach(() => {
|
|
14
21
|
vi.clearAllMocks();
|
|
@@ -139,17 +146,28 @@ describe('checkAndEnforceUpdate', () => {
|
|
|
139
146
|
await checkAndEnforceUpdate();
|
|
140
147
|
// Should not throw or exit
|
|
141
148
|
});
|
|
142
|
-
it('
|
|
149
|
+
it('exits when user declines update on minor bump', async () => {
|
|
143
150
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
144
151
|
fs.readJson.mockResolvedValue({ version: '4.25.0' });
|
|
145
152
|
execSync.mockReturnValue('"4.26.0"\n');
|
|
153
|
+
vi.mocked(confirm).mockResolvedValue(false);
|
|
146
154
|
await checkAndEnforceUpdate();
|
|
155
|
+
expect(confirm).toHaveBeenCalled();
|
|
147
156
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
148
157
|
});
|
|
149
|
-
it('
|
|
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 () => {
|
|
150
167
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
151
168
|
fs.readJson.mockResolvedValue({ version: '4.33.0' });
|
|
152
169
|
execSync.mockReturnValue('"5.0.0"\n');
|
|
170
|
+
vi.mocked(confirm).mockResolvedValue(false);
|
|
153
171
|
await checkAndEnforceUpdate();
|
|
154
172
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
155
173
|
});
|
package/package.json
CHANGED