@meltstudio/meltctl 4.38.0 → 4.39.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.
Files changed (81) hide show
  1. package/dist/index.js +2178 -210
  2. package/package.json +4 -3
  3. package/dist/commands/audit.d.ts +0 -10
  4. package/dist/commands/audit.js +0 -191
  5. package/dist/commands/audit.test.d.ts +0 -1
  6. package/dist/commands/audit.test.js +0 -324
  7. package/dist/commands/coins.d.ts +0 -5
  8. package/dist/commands/coins.js +0 -51
  9. package/dist/commands/coins.test.d.ts +0 -1
  10. package/dist/commands/coins.test.js +0 -113
  11. package/dist/commands/feedback.d.ts +0 -7
  12. package/dist/commands/feedback.js +0 -90
  13. package/dist/commands/feedback.test.d.ts +0 -1
  14. package/dist/commands/feedback.test.js +0 -177
  15. package/dist/commands/init.d.ts +0 -8
  16. package/dist/commands/init.js +0 -520
  17. package/dist/commands/init.test.d.ts +0 -1
  18. package/dist/commands/init.test.js +0 -478
  19. package/dist/commands/login.d.ts +0 -1
  20. package/dist/commands/login.js +0 -90
  21. package/dist/commands/login.test.d.ts +0 -1
  22. package/dist/commands/login.test.js +0 -194
  23. package/dist/commands/logout.d.ts +0 -1
  24. package/dist/commands/logout.js +0 -12
  25. package/dist/commands/logout.test.d.ts +0 -1
  26. package/dist/commands/logout.test.js +0 -59
  27. package/dist/commands/plan.d.ts +0 -6
  28. package/dist/commands/plan.js +0 -123
  29. package/dist/commands/plan.test.d.ts +0 -1
  30. package/dist/commands/plan.test.js +0 -246
  31. package/dist/commands/standup.d.ts +0 -7
  32. package/dist/commands/standup.js +0 -74
  33. package/dist/commands/standup.test.d.ts +0 -1
  34. package/dist/commands/standup.test.js +0 -218
  35. package/dist/commands/templates.d.ts +0 -1
  36. package/dist/commands/templates.js +0 -37
  37. package/dist/commands/templates.test.d.ts +0 -1
  38. package/dist/commands/templates.test.js +0 -89
  39. package/dist/commands/update.d.ts +0 -2
  40. package/dist/commands/update.js +0 -74
  41. package/dist/commands/update.test.d.ts +0 -1
  42. package/dist/commands/update.test.js +0 -93
  43. package/dist/commands/version.d.ts +0 -1
  44. package/dist/commands/version.js +0 -43
  45. package/dist/commands/version.test.d.ts +0 -1
  46. package/dist/commands/version.test.js +0 -86
  47. package/dist/index.d.ts +0 -2
  48. package/dist/utils/analytics.d.ts +0 -1
  49. package/dist/utils/analytics.js +0 -54
  50. package/dist/utils/analytics.test.d.ts +0 -1
  51. package/dist/utils/analytics.test.js +0 -91
  52. package/dist/utils/api.d.ts +0 -3
  53. package/dist/utils/api.js +0 -23
  54. package/dist/utils/api.test.d.ts +0 -1
  55. package/dist/utils/api.test.js +0 -76
  56. package/dist/utils/auth.d.ts +0 -12
  57. package/dist/utils/auth.js +0 -54
  58. package/dist/utils/auth.test.d.ts +0 -1
  59. package/dist/utils/auth.test.js +0 -165
  60. package/dist/utils/banner.d.ts +0 -1
  61. package/dist/utils/banner.js +0 -22
  62. package/dist/utils/banner.test.d.ts +0 -1
  63. package/dist/utils/banner.test.js +0 -34
  64. package/dist/utils/debug.d.ts +0 -1
  65. package/dist/utils/debug.js +0 -6
  66. package/dist/utils/git.d.ts +0 -9
  67. package/dist/utils/git.js +0 -76
  68. package/dist/utils/git.test.d.ts +0 -1
  69. package/dist/utils/git.test.js +0 -184
  70. package/dist/utils/package-manager.d.ts +0 -7
  71. package/dist/utils/package-manager.js +0 -55
  72. package/dist/utils/package-manager.test.d.ts +0 -1
  73. package/dist/utils/package-manager.test.js +0 -76
  74. package/dist/utils/templates.d.ts +0 -2
  75. package/dist/utils/templates.js +0 -5
  76. package/dist/utils/templates.test.d.ts +0 -1
  77. package/dist/utils/templates.test.js +0 -38
  78. package/dist/utils/version-check.d.ts +0 -7
  79. package/dist/utils/version-check.js +0 -139
  80. package/dist/utils/version-check.test.d.ts +0 -1
  81. package/dist/utils/version-check.test.js +0 -189
@@ -1,74 +0,0 @@
1
- import chalk from 'chalk';
2
- import { input, editor } from '@inquirer/prompts';
3
- import { isAuthenticated } from '../utils/auth.js';
4
- import { getClient } from '../utils/api.js';
5
- const EDITOR_HINT = chalk.dim('(type \\e to open your editor)');
6
- async function promptField(message, required) {
7
- const value = await input({ message: `${message} ${EDITOR_HINT}` });
8
- if (value === '\\e') {
9
- try {
10
- return await editor({ message });
11
- }
12
- catch {
13
- console.log(chalk.yellow('Editor failed to open. Falling back to inline input.'));
14
- return promptField(message, required);
15
- }
16
- }
17
- if (required && !value.trim()) {
18
- console.log(chalk.yellow('This field is required.'));
19
- return promptField(message, required);
20
- }
21
- return value;
22
- }
23
- export async function standupCommand(options) {
24
- if (!(await isAuthenticated())) {
25
- console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` first.'));
26
- process.exit(1);
27
- }
28
- const client = await getClient();
29
- // Check if already submitted today
30
- try {
31
- const existing = await client.standups.getStatus();
32
- if (existing) {
33
- console.log(chalk.yellow('\nYou already submitted a standup today:\n'));
34
- console.log(chalk.bold('Yesterday:'), existing.yesterday);
35
- console.log(chalk.bold('Today:'), existing.today);
36
- if (existing.blockers) {
37
- console.log(chalk.bold('Blockers:'), existing.blockers);
38
- }
39
- console.log(chalk.dim('\nTo edit your standup, use the Melt Connect frontend.'));
40
- return;
41
- }
42
- }
43
- catch {
44
- // Status check failed — continue to submission
45
- }
46
- let yesterday;
47
- let today;
48
- let blockers;
49
- if (options.yesterday && options.today) {
50
- // Hidden flags for E2E testing
51
- yesterday = options.yesterday;
52
- today = options.today;
53
- blockers = options.blockers ?? '';
54
- }
55
- else {
56
- console.log(chalk.bold.cyan('\n Daily Standup Report\n'));
57
- console.log(chalk.dim(' Tip: Mention tickets, PRs, or features by name.\n'));
58
- yesterday = await promptField('What did you work on yesterday?', true);
59
- today = await promptField('What are you going to work on today?', true);
60
- blockers = await promptField('Any blockers? (leave empty if none)', false);
61
- }
62
- try {
63
- await client.standups.submit({
64
- yesterday,
65
- today,
66
- blockers: blockers || undefined,
67
- });
68
- console.log(chalk.green('\n ✓ Standup submitted!\n'));
69
- }
70
- catch (error) {
71
- console.error(chalk.red(`\nFailed to submit standup: ${error instanceof Error ? error.message : 'Unknown error'}`));
72
- process.exit(1);
73
- }
74
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,218 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- vi.mock('../utils/auth.js', () => ({
3
- isAuthenticated: vi.fn(),
4
- }));
5
- const mockClient = vi.hoisted(() => ({
6
- standups: {
7
- getStatus: vi.fn(),
8
- submit: vi.fn(),
9
- },
10
- }));
11
- vi.mock('../utils/api.js', () => ({
12
- getClient: vi.fn().mockResolvedValue(mockClient),
13
- }));
14
- vi.mock('@inquirer/prompts', () => ({
15
- input: vi.fn(),
16
- editor: vi.fn(),
17
- }));
18
- import { isAuthenticated } from '../utils/auth.js';
19
- import { input, editor } from '@inquirer/prompts';
20
- import { standupCommand } from './standup.js';
21
- beforeEach(() => {
22
- vi.clearAllMocks();
23
- vi.spyOn(console, 'log').mockImplementation(() => { });
24
- vi.spyOn(console, 'error').mockImplementation(() => { });
25
- vi.spyOn(process, 'exit').mockImplementation((code) => {
26
- throw new Error(`process.exit(${code})`);
27
- });
28
- });
29
- describe('standupCommand', () => {
30
- it('exits with error when not authenticated', async () => {
31
- ;
32
- isAuthenticated.mockResolvedValue(false);
33
- await expect(standupCommand({})).rejects.toThrow('process.exit(1)');
34
- expect(console.error).toHaveBeenCalled();
35
- });
36
- it('returns early when standup already submitted today', async () => {
37
- ;
38
- isAuthenticated.mockResolvedValue(true);
39
- mockClient.standups.getStatus.mockResolvedValue({
40
- yesterday: 'Did stuff',
41
- today: 'More stuff',
42
- blockers: null,
43
- });
44
- await standupCommand({});
45
- expect(mockClient.standups.getStatus).toHaveBeenCalled();
46
- // Should not call submit
47
- expect(mockClient.standups.submit).not.toHaveBeenCalled();
48
- const logCalls = console.log.mock.calls.map((c) => String(c[0]));
49
- expect(logCalls.some((msg) => msg.includes('already submitted'))).toBe(true);
50
- });
51
- it('submits standup with correct payload using option flags', async () => {
52
- ;
53
- isAuthenticated.mockResolvedValue(true);
54
- // Status check returns null (no existing standup)
55
- mockClient.standups.getStatus.mockResolvedValue(null);
56
- mockClient.standups.submit.mockResolvedValue({});
57
- await standupCommand({
58
- yesterday: 'Fixed bugs',
59
- today: 'Write tests',
60
- blockers: 'None',
61
- });
62
- expect(mockClient.standups.submit).toHaveBeenCalledWith({
63
- yesterday: 'Fixed bugs',
64
- today: 'Write tests',
65
- blockers: 'None',
66
- });
67
- const logCalls = console.log.mock.calls.map((c) => String(c[0]));
68
- expect(logCalls.some((msg) => msg.includes('Standup submitted'))).toBe(true);
69
- });
70
- it('submits standup without blockers when empty string', async () => {
71
- ;
72
- isAuthenticated.mockResolvedValue(true);
73
- mockClient.standups.getStatus.mockResolvedValue(null);
74
- mockClient.standups.submit.mockResolvedValue({});
75
- await standupCommand({
76
- yesterday: 'Did things',
77
- today: 'More things',
78
- });
79
- expect(mockClient.standups.submit).toHaveBeenCalledWith({
80
- yesterday: 'Did things',
81
- today: 'More things',
82
- blockers: undefined,
83
- });
84
- });
85
- it('exits with error when API returns failure', async () => {
86
- ;
87
- isAuthenticated.mockResolvedValue(true);
88
- mockClient.standups.getStatus.mockResolvedValue(null);
89
- mockClient.standups.submit.mockRejectedValue(new Error('Invalid standup'));
90
- await expect(standupCommand({ yesterday: 'x', today: 'y' })).rejects.toThrow('process.exit(1)');
91
- expect(console.error).toHaveBeenCalled();
92
- });
93
- it('continues to submission when status check throws', async () => {
94
- ;
95
- isAuthenticated.mockResolvedValue(true);
96
- mockClient.standups.getStatus.mockRejectedValue(new Error('Network error'));
97
- mockClient.standups.submit.mockResolvedValue({});
98
- await standupCommand({ yesterday: 'a', today: 'b' });
99
- expect(mockClient.standups.submit).toHaveBeenCalledWith(expect.objectContaining({
100
- yesterday: 'a',
101
- today: 'b',
102
- }));
103
- });
104
- it('displays existing standup with blockers', async () => {
105
- ;
106
- isAuthenticated.mockResolvedValue(true);
107
- mockClient.standups.getStatus.mockResolvedValue({
108
- yesterday: 'Bug fixes',
109
- today: 'Feature work',
110
- blockers: 'Waiting on review',
111
- });
112
- await standupCommand({});
113
- const logCalls = console.log.mock.calls.map((c) => String(c[0]));
114
- expect(logCalls.some((msg) => msg.includes('already submitted'))).toBe(true);
115
- });
116
- describe('interactive flow', () => {
117
- function setupInteractiveAuth() {
118
- ;
119
- isAuthenticated.mockResolvedValue(true);
120
- // Status check returns null (no existing standup)
121
- mockClient.standups.getStatus.mockResolvedValue(null);
122
- }
123
- it('prompts for yesterday, today, and blockers in interactive mode', async () => {
124
- setupInteractiveAuth();
125
- mockClient.standups.submit.mockResolvedValue({});
126
- input
127
- .mockResolvedValueOnce('Worked on feature X')
128
- .mockResolvedValueOnce('Working on feature Y')
129
- .mockResolvedValueOnce('No blockers');
130
- await standupCommand({});
131
- expect(input).toHaveBeenCalledTimes(3);
132
- expect(mockClient.standups.submit).toHaveBeenCalledWith({
133
- yesterday: 'Worked on feature X',
134
- today: 'Working on feature Y',
135
- blockers: 'No blockers',
136
- });
137
- });
138
- it('submits without blockers when left empty in interactive mode', async () => {
139
- setupInteractiveAuth();
140
- mockClient.standups.submit.mockResolvedValue({});
141
- input
142
- .mockResolvedValueOnce('Did code review')
143
- .mockResolvedValueOnce('Deploy to staging')
144
- .mockResolvedValueOnce('');
145
- await standupCommand({});
146
- expect(mockClient.standups.submit).toHaveBeenCalledWith({
147
- yesterday: 'Did code review',
148
- today: 'Deploy to staging',
149
- blockers: undefined,
150
- });
151
- });
152
- it('re-prompts when required field is empty', async () => {
153
- setupInteractiveAuth();
154
- mockClient.standups.submit.mockResolvedValue({});
155
- input
156
- .mockResolvedValueOnce(' ') // empty yesterday -> re-prompt
157
- .mockResolvedValueOnce('Fixed a bug') // valid yesterday
158
- .mockResolvedValueOnce('Write tests') // valid today
159
- .mockResolvedValueOnce(''); // empty blockers (ok, not required)
160
- await standupCommand({});
161
- expect(input).toHaveBeenCalledTimes(4);
162
- const logCalls = console.log.mock.calls.map((c) => String(c[0]));
163
- expect(logCalls.some((msg) => msg.includes('required'))).toBe(true);
164
- });
165
- it('opens editor when user types \\e', async () => {
166
- setupInteractiveAuth();
167
- mockClient.standups.submit.mockResolvedValue({});
168
- input
169
- .mockResolvedValueOnce('\\e') // trigger editor for yesterday
170
- .mockResolvedValueOnce('Writing docs') // today
171
- .mockResolvedValueOnce('') // no blockers
172
- ;
173
- editor.mockResolvedValueOnce('Detailed yesterday notes from editor');
174
- await standupCommand({});
175
- expect(editor).toHaveBeenCalledTimes(1);
176
- expect(mockClient.standups.submit).toHaveBeenCalledWith(expect.objectContaining({
177
- yesterday: 'Detailed yesterday notes from editor',
178
- }));
179
- });
180
- it('falls back to inline input when editor fails', async () => {
181
- setupInteractiveAuth();
182
- mockClient.standups.submit.mockResolvedValue({});
183
- input
184
- .mockResolvedValueOnce('\\e') // trigger editor for yesterday
185
- .mockResolvedValueOnce('Fallback input') // fallback after editor fails
186
- .mockResolvedValueOnce('Today tasks') // today
187
- .mockResolvedValueOnce('') // no blockers
188
- ;
189
- editor.mockRejectedValueOnce(new Error('Editor not found'));
190
- await standupCommand({});
191
- const logCalls = console.log.mock.calls.map((c) => String(c[0]));
192
- expect(logCalls.some((msg) => msg.includes('Editor failed'))).toBe(true);
193
- expect(input).toHaveBeenCalledTimes(4);
194
- });
195
- it('exits with error when API fails in interactive mode', async () => {
196
- setupInteractiveAuth();
197
- mockClient.standups.submit.mockRejectedValue(new Error('Missing fields'));
198
- input
199
- .mockResolvedValueOnce('Yesterday work')
200
- .mockResolvedValueOnce('Today work')
201
- .mockResolvedValueOnce('');
202
- await expect(standupCommand({})).rejects.toThrow('process.exit(1)');
203
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
204
- expect(errorCalls.some((msg) => msg.includes('Missing fields'))).toBe(true);
205
- });
206
- it('falls back to statusText when API error has no error body', async () => {
207
- setupInteractiveAuth();
208
- mockClient.standups.submit.mockRejectedValue(new Error('Bad Gateway'));
209
- input
210
- .mockResolvedValueOnce('Yesterday')
211
- .mockResolvedValueOnce('Today')
212
- .mockResolvedValueOnce('');
213
- await expect(standupCommand({})).rejects.toThrow('process.exit(1)');
214
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
215
- expect(errorCalls.some((msg) => msg.includes('Bad Gateway'))).toBe(true);
216
- });
217
- });
218
- });
@@ -1 +0,0 @@
1
- export declare function templatesCommand(): Promise<void>;
@@ -1,37 +0,0 @@
1
- import chalk from 'chalk';
2
- import fs from 'fs-extra';
3
- import path from 'path';
4
- import os from 'os';
5
- import { isAuthenticated } from '../utils/auth.js';
6
- import { fetchTemplates } from '../utils/templates.js';
7
- export async function templatesCommand() {
8
- if (!(await isAuthenticated())) {
9
- console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` first.'));
10
- process.exit(1);
11
- }
12
- let templates;
13
- try {
14
- templates = await fetchTemplates();
15
- }
16
- catch (error) {
17
- if (error instanceof Error && error.message.includes('expired')) {
18
- console.error(chalk.red('Session expired. Run `npx @meltstudio/meltctl@latest login` to re-authenticate.'));
19
- }
20
- else if (error instanceof Error && error.message.includes('fetch')) {
21
- console.error(chalk.red('Could not reach Melt API. Check your connection.'));
22
- }
23
- else {
24
- console.error(chalk.red(`Failed to fetch templates: ${error instanceof Error ? error.message : 'Unknown error'}`));
25
- }
26
- process.exit(1);
27
- }
28
- const tmpDir = path.join(os.tmpdir(), `melt-templates-${Date.now()}`);
29
- await fs.ensureDir(tmpDir);
30
- for (const [filePath, content] of Object.entries(templates)) {
31
- const fullPath = path.join(tmpDir, filePath);
32
- await fs.ensureDir(path.dirname(fullPath));
33
- await fs.writeFile(fullPath, content, 'utf-8');
34
- }
35
- // Print only the temp dir path to stdout (agent parses this)
36
- console.log(tmpDir);
37
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,89 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- vi.mock('fs-extra', () => ({
3
- default: {
4
- ensureDir: vi.fn(),
5
- writeFile: vi.fn(),
6
- },
7
- }));
8
- vi.mock('../utils/auth.js', () => ({
9
- isAuthenticated: vi.fn(),
10
- }));
11
- vi.mock('../utils/templates.js', () => ({
12
- fetchTemplates: vi.fn(),
13
- }));
14
- import fs from 'fs-extra';
15
- import { isAuthenticated } from '../utils/auth.js';
16
- import { fetchTemplates } from '../utils/templates.js';
17
- import { templatesCommand } from './templates.js';
18
- beforeEach(() => {
19
- vi.clearAllMocks();
20
- vi.spyOn(console, 'log').mockImplementation(() => { });
21
- vi.spyOn(console, 'error').mockImplementation(() => { });
22
- vi.spyOn(process, 'exit').mockImplementation((code) => {
23
- throw new Error(`process.exit(${code})`);
24
- });
25
- });
26
- describe('templatesCommand', () => {
27
- it('exits when not authenticated', async () => {
28
- ;
29
- isAuthenticated.mockResolvedValue(false);
30
- await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
31
- expect(console.error).toHaveBeenCalled();
32
- expect(fetchTemplates).not.toHaveBeenCalled();
33
- });
34
- it('fetches and writes templates to temp dir', async () => {
35
- ;
36
- isAuthenticated.mockResolvedValue(true);
37
- fetchTemplates.mockResolvedValue({
38
- 'AGENTS.md': '# Agents\nContent here',
39
- '.claude/skills/setup.md': '# Setup skill',
40
- });
41
- await templatesCommand();
42
- // Should have called ensureDir for the temp dir and subdirectories
43
- expect(fs.ensureDir).toHaveBeenCalled();
44
- // Should have written both files
45
- expect(fs.writeFile).toHaveBeenCalledTimes(2);
46
- // First file
47
- const writeFileCalls = fs.writeFile.mock.calls;
48
- const filePaths = writeFileCalls.map((c) => c[0]);
49
- expect(filePaths.some((p) => p.endsWith('AGENTS.md'))).toBe(true);
50
- expect(filePaths.some((p) => p.endsWith('setup.md'))).toBe(true);
51
- // Verify content
52
- const agentsCall = writeFileCalls.find((c) => c[0].endsWith('AGENTS.md'));
53
- expect(agentsCall[1]).toBe('# Agents\nContent here');
54
- // Should print the temp dir path
55
- expect(console.log).toHaveBeenCalled();
56
- });
57
- it('exits with error when session expired', async () => {
58
- ;
59
- isAuthenticated.mockResolvedValue(true);
60
- fetchTemplates.mockRejectedValue(new Error('Session expired'));
61
- await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
62
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
63
- expect(errorCalls.some((msg) => msg.includes('expired'))).toBe(true);
64
- });
65
- it('exits with error on fetch/network error', async () => {
66
- ;
67
- isAuthenticated.mockResolvedValue(true);
68
- fetchTemplates.mockRejectedValue(new Error('fetch failed'));
69
- await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
70
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
71
- expect(errorCalls.some((msg) => msg.includes('connection'))).toBe(true);
72
- });
73
- it('exits with generic error for unknown failures', async () => {
74
- ;
75
- isAuthenticated.mockResolvedValue(true);
76
- fetchTemplates.mockRejectedValue(new Error('Something weird'));
77
- await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
78
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
79
- expect(errorCalls.some((msg) => msg.includes('Something weird'))).toBe(true);
80
- });
81
- it('exits with generic message for non-Error thrown values', async () => {
82
- ;
83
- isAuthenticated.mockResolvedValue(true);
84
- fetchTemplates.mockRejectedValue('string error');
85
- await expect(templatesCommand()).rejects.toThrow('process.exit(1)');
86
- const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
87
- expect(errorCalls.some((msg) => msg.includes('Unknown error'))).toBe(true);
88
- });
89
- });
@@ -1,2 +0,0 @@
1
- export declare function detectPackageManager(): 'npm' | 'yarn' | 'unknown';
2
- export declare function updateCommand(): Promise<void>;
@@ -1,74 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,93 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export declare function versionCheckCommand(): Promise<void>;
@@ -1,43 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};