@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.
@@ -0,0 +1,2 @@
1
+ export declare function detectPackageManager(): 'npm' | 'yarn' | 'unknown';
2
+ export declare function updateCommand(): Promise<void>;
@@ -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: ${updateCmd})`));
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 until updated
112
+ // Minor/major updates: offer to update, block if declined
113
113
  console.log();
114
- console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
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
- 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'));
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('blocks on minor version update', async () => {
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('blocks on major version update', async () => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/meltctl",
3
- "version": "4.34.0",
3
+ "version": "4.35.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",