@meltstudio/meltctl 4.34.0 → 4.36.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/audit.js +66 -111
- package/dist/commands/audit.test.js +118 -209
- package/dist/commands/coins.js +30 -28
- package/dist/commands/coins.test.js +23 -43
- package/dist/commands/feedback.js +8 -17
- package/dist/commands/feedback.test.js +38 -103
- package/dist/commands/login.js +15 -20
- package/dist/commands/plan.js +21 -51
- package/dist/commands/plan.test.js +95 -132
- package/dist/commands/standup.js +10 -14
- package/dist/commands/standup.test.js +66 -100
- 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/analytics.js +9 -19
- package/dist/utils/api.d.ts +2 -1
- package/dist/utils/api.js +4 -12
- package/dist/utils/api.test.js +25 -45
- package/dist/utils/templates.d.ts +2 -4
- package/dist/utils/templates.js +3 -7
- package/dist/utils/templates.test.js +14 -26
- package/dist/utils/version-check.js +17 -15
- package/dist/utils/version-check.test.js +20 -2
- package/package.json +3 -2
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
vi.mock('../utils/auth.js', () => ({
|
|
3
3
|
isAuthenticated: vi.fn(),
|
|
4
|
-
|
|
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),
|
|
5
13
|
}));
|
|
6
14
|
vi.mock('@inquirer/prompts', () => ({
|
|
7
15
|
input: vi.fn(),
|
|
8
16
|
editor: vi.fn(),
|
|
9
17
|
}));
|
|
10
|
-
import { isAuthenticated
|
|
18
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
11
19
|
import { input, editor } from '@inquirer/prompts';
|
|
12
20
|
import { standupCommand } from './standup.js';
|
|
13
21
|
beforeEach(() => {
|
|
@@ -28,47 +36,33 @@ describe('standupCommand', () => {
|
|
|
28
36
|
it('returns early when standup already submitted today', async () => {
|
|
29
37
|
;
|
|
30
38
|
isAuthenticated.mockResolvedValue(true);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
blockers: null,
|
|
37
|
-
}),
|
|
38
|
-
};
|
|
39
|
-
authenticatedFetch.mockResolvedValue(statusResponse);
|
|
39
|
+
mockClient.standups.getStatus.mockResolvedValue({
|
|
40
|
+
yesterday: 'Did stuff',
|
|
41
|
+
today: 'More stuff',
|
|
42
|
+
blockers: null,
|
|
43
|
+
});
|
|
40
44
|
await standupCommand({});
|
|
41
|
-
expect(
|
|
42
|
-
// Should not call
|
|
43
|
-
expect(
|
|
45
|
+
expect(mockClient.standups.getStatus).toHaveBeenCalled();
|
|
46
|
+
// Should not call submit
|
|
47
|
+
expect(mockClient.standups.submit).not.toHaveBeenCalled();
|
|
44
48
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
45
49
|
expect(logCalls.some((msg) => msg.includes('already submitted'))).toBe(true);
|
|
46
50
|
});
|
|
47
51
|
it('submits standup with correct payload using option flags', async () => {
|
|
48
52
|
;
|
|
49
53
|
isAuthenticated.mockResolvedValue(true);
|
|
50
|
-
// Status check returns
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ok: true,
|
|
54
|
-
json: vi.fn().mockResolvedValue({}),
|
|
55
|
-
};
|
|
56
|
-
authenticatedFetch
|
|
57
|
-
.mockResolvedValueOnce(statusResponse)
|
|
58
|
-
.mockResolvedValueOnce(submitResponse);
|
|
54
|
+
// Status check returns null (no existing standup)
|
|
55
|
+
mockClient.standups.getStatus.mockResolvedValue(null);
|
|
56
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
59
57
|
await standupCommand({
|
|
60
58
|
yesterday: 'Fixed bugs',
|
|
61
59
|
today: 'Write tests',
|
|
62
60
|
blockers: 'None',
|
|
63
61
|
});
|
|
64
|
-
expect(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
yesterday: 'Fixed bugs',
|
|
69
|
-
today: 'Write tests',
|
|
70
|
-
blockers: 'None',
|
|
71
|
-
}),
|
|
62
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith({
|
|
63
|
+
yesterday: 'Fixed bugs',
|
|
64
|
+
today: 'Write tests',
|
|
65
|
+
blockers: 'None',
|
|
72
66
|
});
|
|
73
67
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
74
68
|
expect(logCalls.some((msg) => msg.includes('Standup submitted'))).toBe(true);
|
|
@@ -76,58 +70,45 @@ describe('standupCommand', () => {
|
|
|
76
70
|
it('submits standup without blockers when empty string', async () => {
|
|
77
71
|
;
|
|
78
72
|
isAuthenticated.mockResolvedValue(true);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
authenticatedFetch
|
|
82
|
-
.mockResolvedValueOnce(statusResponse)
|
|
83
|
-
.mockResolvedValueOnce(submitResponse);
|
|
73
|
+
mockClient.standups.getStatus.mockResolvedValue(null);
|
|
74
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
84
75
|
await standupCommand({
|
|
85
76
|
yesterday: 'Did things',
|
|
86
77
|
today: 'More things',
|
|
87
78
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith({
|
|
80
|
+
yesterday: 'Did things',
|
|
81
|
+
today: 'More things',
|
|
82
|
+
blockers: undefined,
|
|
83
|
+
});
|
|
91
84
|
});
|
|
92
85
|
it('exits with error when API returns failure', async () => {
|
|
93
86
|
;
|
|
94
87
|
isAuthenticated.mockResolvedValue(true);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ok: false,
|
|
98
|
-
statusText: 'Bad Request',
|
|
99
|
-
json: vi.fn().mockResolvedValue({ error: 'Invalid standup' }),
|
|
100
|
-
};
|
|
101
|
-
authenticatedFetch
|
|
102
|
-
.mockResolvedValueOnce(statusResponse)
|
|
103
|
-
.mockResolvedValueOnce(submitResponse);
|
|
88
|
+
mockClient.standups.getStatus.mockResolvedValue(null);
|
|
89
|
+
mockClient.standups.submit.mockRejectedValue(new Error('Invalid standup'));
|
|
104
90
|
await expect(standupCommand({ yesterday: 'x', today: 'y' })).rejects.toThrow('process.exit(1)');
|
|
105
91
|
expect(console.error).toHaveBeenCalled();
|
|
106
92
|
});
|
|
107
93
|
it('continues to submission when status check throws', async () => {
|
|
108
94
|
;
|
|
109
95
|
isAuthenticated.mockResolvedValue(true);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.mockResolvedValueOnce({ ok: true, json: vi.fn().mockResolvedValue({}) });
|
|
96
|
+
mockClient.standups.getStatus.mockRejectedValue(new Error('Network error'));
|
|
97
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
113
98
|
await standupCommand({ yesterday: 'a', today: 'b' });
|
|
114
|
-
expect(
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
100
|
+
yesterday: 'a',
|
|
101
|
+
today: 'b',
|
|
117
102
|
}));
|
|
118
103
|
});
|
|
119
104
|
it('displays existing standup with blockers', async () => {
|
|
120
105
|
;
|
|
121
106
|
isAuthenticated.mockResolvedValue(true);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
blockers: 'Waiting on review',
|
|
128
|
-
}),
|
|
129
|
-
};
|
|
130
|
-
authenticatedFetch.mockResolvedValue(statusResponse);
|
|
107
|
+
mockClient.standups.getStatus.mockResolvedValue({
|
|
108
|
+
yesterday: 'Bug fixes',
|
|
109
|
+
today: 'Feature work',
|
|
110
|
+
blockers: 'Waiting on review',
|
|
111
|
+
});
|
|
131
112
|
await standupCommand({});
|
|
132
113
|
const logCalls = console.log.mock.calls.map((c) => String(c[0]));
|
|
133
114
|
expect(logCalls.some((msg) => msg.includes('already submitted'))).toBe(true);
|
|
@@ -136,44 +117,41 @@ describe('standupCommand', () => {
|
|
|
136
117
|
function setupInteractiveAuth() {
|
|
137
118
|
;
|
|
138
119
|
isAuthenticated.mockResolvedValue(true);
|
|
139
|
-
|
|
120
|
+
// Status check returns null (no existing standup)
|
|
121
|
+
mockClient.standups.getStatus.mockResolvedValue(null);
|
|
140
122
|
}
|
|
141
123
|
it('prompts for yesterday, today, and blockers in interactive mode', async () => {
|
|
142
124
|
setupInteractiveAuth();
|
|
143
|
-
|
|
144
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
125
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
145
126
|
input
|
|
146
127
|
.mockResolvedValueOnce('Worked on feature X')
|
|
147
128
|
.mockResolvedValueOnce('Working on feature Y')
|
|
148
129
|
.mockResolvedValueOnce('No blockers');
|
|
149
130
|
await standupCommand({});
|
|
150
131
|
expect(input).toHaveBeenCalledTimes(3);
|
|
151
|
-
expect(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
blockers: 'No blockers',
|
|
157
|
-
}),
|
|
158
|
-
}));
|
|
132
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith({
|
|
133
|
+
yesterday: 'Worked on feature X',
|
|
134
|
+
today: 'Working on feature Y',
|
|
135
|
+
blockers: 'No blockers',
|
|
136
|
+
});
|
|
159
137
|
});
|
|
160
138
|
it('submits without blockers when left empty in interactive mode', async () => {
|
|
161
139
|
setupInteractiveAuth();
|
|
162
|
-
|
|
163
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
140
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
164
141
|
input
|
|
165
142
|
.mockResolvedValueOnce('Did code review')
|
|
166
143
|
.mockResolvedValueOnce('Deploy to staging')
|
|
167
144
|
.mockResolvedValueOnce('');
|
|
168
145
|
await standupCommand({});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
146
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith({
|
|
147
|
+
yesterday: 'Did code review',
|
|
148
|
+
today: 'Deploy to staging',
|
|
149
|
+
blockers: undefined,
|
|
150
|
+
});
|
|
172
151
|
});
|
|
173
152
|
it('re-prompts when required field is empty', async () => {
|
|
174
153
|
setupInteractiveAuth();
|
|
175
|
-
|
|
176
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
154
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
177
155
|
input
|
|
178
156
|
.mockResolvedValueOnce(' ') // empty yesterday -> re-prompt
|
|
179
157
|
.mockResolvedValueOnce('Fixed a bug') // valid yesterday
|
|
@@ -186,8 +164,7 @@ describe('standupCommand', () => {
|
|
|
186
164
|
});
|
|
187
165
|
it('opens editor when user types \\e', async () => {
|
|
188
166
|
setupInteractiveAuth();
|
|
189
|
-
|
|
190
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
167
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
191
168
|
input
|
|
192
169
|
.mockResolvedValueOnce('\\e') // trigger editor for yesterday
|
|
193
170
|
.mockResolvedValueOnce('Writing docs') // today
|
|
@@ -196,14 +173,13 @@ describe('standupCommand', () => {
|
|
|
196
173
|
editor.mockResolvedValueOnce('Detailed yesterday notes from editor');
|
|
197
174
|
await standupCommand({});
|
|
198
175
|
expect(editor).toHaveBeenCalledTimes(1);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
176
|
+
expect(mockClient.standups.submit).toHaveBeenCalledWith(expect.objectContaining({
|
|
177
|
+
yesterday: 'Detailed yesterday notes from editor',
|
|
178
|
+
}));
|
|
202
179
|
});
|
|
203
180
|
it('falls back to inline input when editor fails', async () => {
|
|
204
181
|
setupInteractiveAuth();
|
|
205
|
-
|
|
206
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
182
|
+
mockClient.standups.submit.mockResolvedValue({});
|
|
207
183
|
input
|
|
208
184
|
.mockResolvedValueOnce('\\e') // trigger editor for yesterday
|
|
209
185
|
.mockResolvedValueOnce('Fallback input') // fallback after editor fails
|
|
@@ -218,12 +194,7 @@ describe('standupCommand', () => {
|
|
|
218
194
|
});
|
|
219
195
|
it('exits with error when API fails in interactive mode', async () => {
|
|
220
196
|
setupInteractiveAuth();
|
|
221
|
-
|
|
222
|
-
ok: false,
|
|
223
|
-
statusText: 'Unprocessable Entity',
|
|
224
|
-
json: vi.fn().mockResolvedValue({ error: 'Missing fields' }),
|
|
225
|
-
};
|
|
226
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
197
|
+
mockClient.standups.submit.mockRejectedValue(new Error('Missing fields'));
|
|
227
198
|
input
|
|
228
199
|
.mockResolvedValueOnce('Yesterday work')
|
|
229
200
|
.mockResolvedValueOnce('Today work')
|
|
@@ -234,12 +205,7 @@ describe('standupCommand', () => {
|
|
|
234
205
|
});
|
|
235
206
|
it('falls back to statusText when API error has no error body', async () => {
|
|
236
207
|
setupInteractiveAuth();
|
|
237
|
-
|
|
238
|
-
ok: false,
|
|
239
|
-
statusText: 'Bad Gateway',
|
|
240
|
-
json: vi.fn().mockResolvedValue({}),
|
|
241
|
-
};
|
|
242
|
-
authenticatedFetch.mockResolvedValueOnce(submitResponse);
|
|
208
|
+
mockClient.standups.submit.mockRejectedValue(new Error('Bad Gateway'));
|
|
243
209
|
input
|
|
244
210
|
.mockResolvedValueOnce('Yesterday')
|
|
245
211
|
.mockResolvedValueOnce('Today')
|
|
@@ -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')
|
package/dist/utils/analytics.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createMeltClient } from '@meltstudio/meltctl-sdk';
|
|
4
5
|
import { getStoredAuth, API_BASE } from './auth.js';
|
|
5
6
|
import { getGitRepository, getGitBranch, getProjectName } from './git.js';
|
|
6
7
|
import { debugLog } from './debug.js';
|
|
@@ -27,15 +28,11 @@ export async function trackCommand(command, success, errorMessage) {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
const repo = getGitRepository();
|
|
31
|
+
const client = createMeltClient({ baseUrl: API_BASE, token: auth.token });
|
|
30
32
|
const controller = new AbortController();
|
|
31
33
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
headers: {
|
|
35
|
-
Authorization: `Bearer ${auth.token}`,
|
|
36
|
-
'Content-Type': 'application/json',
|
|
37
|
-
},
|
|
38
|
-
body: JSON.stringify({
|
|
34
|
+
try {
|
|
35
|
+
await client.events.submit({
|
|
39
36
|
command,
|
|
40
37
|
project: getProjectName(),
|
|
41
38
|
repository: repo?.slug ?? null,
|
|
@@ -43,20 +40,13 @@ export async function trackCommand(command, success, errorMessage) {
|
|
|
43
40
|
version: getVersion(),
|
|
44
41
|
success,
|
|
45
42
|
errorMessage: errorMessage?.slice(0, 500) ?? null,
|
|
46
|
-
})
|
|
47
|
-
signal: controller.signal,
|
|
48
|
-
}).catch(e => {
|
|
49
|
-
debugLog(`Analytics fetch failed: ${e instanceof Error ? e.message : e}`);
|
|
50
|
-
return null;
|
|
51
|
-
});
|
|
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) {
|
|
43
|
+
});
|
|
58
44
|
debugLog(`Analytics event sent for "${command}"`);
|
|
59
45
|
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
debugLog(`Analytics error: ${e instanceof Error ? e.message : e}`);
|
|
48
|
+
}
|
|
49
|
+
clearTimeout(timeout);
|
|
60
50
|
}
|
|
61
51
|
catch (e) {
|
|
62
52
|
debugLog(`Analytics error: ${e instanceof Error ? e.message : e}`);
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import { type MeltClient } from '@meltstudio/meltctl-sdk';
|
|
1
2
|
export declare function getToken(): Promise<string>;
|
|
2
|
-
export declare function
|
|
3
|
+
export declare function getClient(): Promise<MeltClient>;
|
package/dist/utils/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { createMeltClient } from '@meltstudio/meltctl-sdk';
|
|
2
3
|
import { getStoredAuth, API_BASE } from './auth.js';
|
|
3
4
|
export async function getToken() {
|
|
4
5
|
const envToken = process.env['MELTCTL_TOKEN'];
|
|
@@ -16,16 +17,7 @@ export async function getToken() {
|
|
|
16
17
|
}
|
|
17
18
|
return auth.token;
|
|
18
19
|
}
|
|
19
|
-
export async function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
headers: {
|
|
23
|
-
Authorization: `Bearer ${token}`,
|
|
24
|
-
...options.headers,
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
if (response.status === 401) {
|
|
28
|
-
throw new Error('Authentication failed. Run `npx @meltstudio/meltctl@latest login` or check your MELTCTL_TOKEN.');
|
|
29
|
-
}
|
|
30
|
-
return response;
|
|
20
|
+
export async function getClient() {
|
|
21
|
+
const token = await getToken();
|
|
22
|
+
return createMeltClient({ baseUrl: API_BASE, token });
|
|
31
23
|
}
|
package/dist/utils/api.test.js
CHANGED
|
@@ -3,8 +3,12 @@ vi.mock('./auth.js', () => ({
|
|
|
3
3
|
getStoredAuth: vi.fn(),
|
|
4
4
|
API_BASE: 'https://test-api.example.com',
|
|
5
5
|
}));
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
vi.mock('@meltstudio/meltctl-sdk', () => ({
|
|
7
|
+
createMeltClient: vi.fn().mockReturnValue({ mock: true }),
|
|
8
|
+
}));
|
|
9
|
+
import { getStoredAuth } from './auth.js';
|
|
10
|
+
import { createMeltClient } from '@meltstudio/meltctl-sdk';
|
|
11
|
+
import { getToken, getClient } from './api.js';
|
|
8
12
|
beforeEach(() => {
|
|
9
13
|
vi.clearAllMocks();
|
|
10
14
|
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
@@ -47,50 +51,26 @@ describe('getToken', () => {
|
|
|
47
51
|
expect(console.error).toHaveBeenCalled();
|
|
48
52
|
});
|
|
49
53
|
});
|
|
50
|
-
describe('
|
|
51
|
-
it('
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
vi.unstubAllGlobals();
|
|
63
|
-
});
|
|
64
|
-
it('merges custom options and headers', async () => {
|
|
65
|
-
const fetchMock = vi.fn().mockResolvedValue({ status: 200, ok: true });
|
|
66
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
67
|
-
await tokenFetch('tok', '/test', {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: { 'Content-Type': 'application/json' },
|
|
70
|
-
body: '{}',
|
|
54
|
+
describe('getClient', () => {
|
|
55
|
+
it('creates SDK client with token and base URL', async () => {
|
|
56
|
+
;
|
|
57
|
+
getStoredAuth.mockResolvedValue({
|
|
58
|
+
token: 'my-token',
|
|
59
|
+
email: 'dev@meltstudio.co',
|
|
60
|
+
expiresAt: '2099-12-31T00:00:00Z',
|
|
61
|
+
});
|
|
62
|
+
await getClient();
|
|
63
|
+
expect(createMeltClient).toHaveBeenCalledWith({
|
|
64
|
+
baseUrl: 'https://test-api.example.com',
|
|
65
|
+
token: 'my-token',
|
|
71
66
|
});
|
|
72
|
-
expect(fetchMock).toHaveBeenCalledWith(`${API_BASE}/test`, expect.objectContaining({
|
|
73
|
-
method: 'POST',
|
|
74
|
-
body: '{}',
|
|
75
|
-
headers: expect.objectContaining({
|
|
76
|
-
Authorization: 'Bearer tok',
|
|
77
|
-
'Content-Type': 'application/json',
|
|
78
|
-
}),
|
|
79
|
-
}));
|
|
80
|
-
vi.unstubAllGlobals();
|
|
81
|
-
});
|
|
82
|
-
it('throws when API returns 401', async () => {
|
|
83
|
-
const fetchMock = vi.fn().mockResolvedValue({ status: 401, ok: false });
|
|
84
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
85
|
-
await expect(tokenFetch('bad-token', '/test')).rejects.toThrow('Authentication failed');
|
|
86
|
-
vi.unstubAllGlobals();
|
|
87
67
|
});
|
|
88
|
-
it('
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
68
|
+
it('uses env token when MELTCTL_TOKEN is set', async () => {
|
|
69
|
+
process.env['MELTCTL_TOKEN'] = 'env-token';
|
|
70
|
+
await getClient();
|
|
71
|
+
expect(createMeltClient).toHaveBeenCalledWith({
|
|
72
|
+
baseUrl: 'https://test-api.example.com',
|
|
73
|
+
token: 'env-token',
|
|
74
|
+
});
|
|
95
75
|
});
|
|
96
76
|
});
|
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
}
|
|
4
|
-
export declare function fetchTemplates(): Promise<TemplateFiles>;
|
|
1
|
+
export type { TemplateFiles } from '@meltstudio/meltctl-sdk';
|
|
2
|
+
export declare function fetchTemplates(): Promise<Record<string, string>>;
|