@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.
@@ -1,9 +1,17 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  vi.mock('../utils/auth.js', () => ({
3
3
  isAuthenticated: vi.fn(),
4
- authenticatedFetch: vi.fn(),
5
4
  }));
6
- import { isAuthenticated, authenticatedFetch } from '../utils/auth.js';
5
+ const mockClient = vi.hoisted(() => ({
6
+ coins: {
7
+ getBalance: vi.fn(),
8
+ getLeaderboard: vi.fn(),
9
+ },
10
+ }));
11
+ vi.mock('../utils/api.js', () => ({
12
+ getClient: vi.fn().mockResolvedValue(mockClient),
13
+ }));
14
+ import { isAuthenticated } from '../utils/auth.js';
7
15
  import { coinsCommand } from './coins.js';
8
16
  beforeEach(() => {
9
17
  vi.clearAllMocks();
@@ -24,23 +32,16 @@ describe('coinsCommand', () => {
24
32
  it('displays coin balance on success', async () => {
25
33
  ;
26
34
  isAuthenticated.mockResolvedValue(true);
27
- const mockResponse = {
28
- ok: true,
29
- json: vi.fn().mockResolvedValue({ coins: 5, period: '28d' }),
30
- };
31
- authenticatedFetch.mockResolvedValue(mockResponse);
35
+ mockClient.coins.getBalance.mockResolvedValue({ coins: 5, period: '28d' });
32
36
  await coinsCommand({});
33
- expect(authenticatedFetch).toHaveBeenCalledWith('/coins');
37
+ expect(mockClient.coins.getBalance).toHaveBeenCalled();
34
38
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
35
39
  expect(logCalls.some((msg) => msg.includes('5'))).toBe(true);
36
40
  });
37
41
  it('displays singular coin text for 1 coin', async () => {
38
42
  ;
39
43
  isAuthenticated.mockResolvedValue(true);
40
- authenticatedFetch.mockResolvedValue({
41
- ok: true,
42
- json: vi.fn().mockResolvedValue({ coins: 1, period: '28d' }),
43
- });
44
+ mockClient.coins.getBalance.mockResolvedValue({ coins: 1, period: '28d' });
44
45
  await coinsCommand({});
45
46
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
46
47
  expect(logCalls.some((msg) => msg.includes('coin') && !msg.includes('coins'))).toBe(true);
@@ -48,10 +49,7 @@ describe('coinsCommand', () => {
48
49
  it('displays plural coin text for multiple coins', async () => {
49
50
  ;
50
51
  isAuthenticated.mockResolvedValue(true);
51
- authenticatedFetch.mockResolvedValue({
52
- ok: true,
53
- json: vi.fn().mockResolvedValue({ coins: 3, period: '28d' }),
54
- });
52
+ mockClient.coins.getBalance.mockResolvedValue({ coins: 3, period: '28d' });
55
53
  await coinsCommand({});
56
54
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
57
55
  expect(logCalls.some((msg) => msg.includes('coins'))).toBe(true);
@@ -59,12 +57,7 @@ describe('coinsCommand', () => {
59
57
  it('exits with error when API returns failure', async () => {
60
58
  ;
61
59
  isAuthenticated.mockResolvedValue(true);
62
- const mockResponse = {
63
- ok: false,
64
- statusText: 'Internal Server Error',
65
- json: vi.fn().mockResolvedValue({ error: 'Something went wrong' }),
66
- };
67
- authenticatedFetch.mockResolvedValue(mockResponse);
60
+ mockClient.coins.getBalance.mockRejectedValue(new Error('Something went wrong'));
68
61
  await expect(coinsCommand({})).rejects.toThrow('process.exit(1)');
69
62
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
70
63
  expect(errorCalls.some((msg) => msg.includes('Something went wrong'))).toBe(true);
@@ -84,12 +77,9 @@ describe('coinsCommand', () => {
84
77
  { name: 'Bob', coins: 7 },
85
78
  { name: 'Charlie', coins: 3 },
86
79
  ];
87
- authenticatedFetch.mockResolvedValue({
88
- ok: true,
89
- json: vi.fn().mockResolvedValue(entries),
90
- });
80
+ mockClient.coins.getLeaderboard.mockResolvedValue(entries);
91
81
  await coinsCommand({ leaderboard: true });
92
- expect(authenticatedFetch).toHaveBeenCalledWith('/coins/leaderboard');
82
+ expect(mockClient.coins.getLeaderboard).toHaveBeenCalled();
93
83
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
94
84
  expect(logCalls.some((msg) => msg.includes('Leaderboard'))).toBe(true);
95
85
  expect(logCalls.some((msg) => msg.includes('Alice'))).toBe(true);
@@ -98,10 +88,7 @@ describe('coinsCommand', () => {
98
88
  it('shows message when no coins have been sent', async () => {
99
89
  ;
100
90
  isAuthenticated.mockResolvedValue(true);
101
- authenticatedFetch.mockResolvedValue({
102
- ok: true,
103
- json: vi.fn().mockResolvedValue([]),
104
- });
91
+ mockClient.coins.getLeaderboard.mockResolvedValue([]);
105
92
  await coinsCommand({ leaderboard: true });
106
93
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
107
94
  expect(logCalls.some((msg) => msg.includes('No coins'))).toBe(true);
@@ -109,25 +96,18 @@ describe('coinsCommand', () => {
109
96
  it('exits with error when leaderboard API fails', async () => {
110
97
  ;
111
98
  isAuthenticated.mockResolvedValue(true);
112
- authenticatedFetch.mockResolvedValue({
113
- ok: false,
114
- statusText: 'Forbidden',
115
- json: vi.fn().mockResolvedValue({ error: 'Access denied' }),
116
- });
99
+ mockClient.coins.getLeaderboard.mockRejectedValue(new Error('Access denied'));
117
100
  await expect(coinsCommand({ leaderboard: true })).rejects.toThrow('process.exit(1)');
118
101
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
119
102
  expect(errorCalls.some((msg) => msg.includes('Access denied'))).toBe(true);
120
103
  });
121
- it('calls /coins/leaderboard endpoint not /coins', async () => {
104
+ it('calls getLeaderboard not getBalance', async () => {
122
105
  ;
123
106
  isAuthenticated.mockResolvedValue(true);
124
- authenticatedFetch.mockResolvedValue({
125
- ok: true,
126
- json: vi.fn().mockResolvedValue([{ name: 'Test', coins: 1 }]),
127
- });
107
+ mockClient.coins.getLeaderboard.mockResolvedValue([{ name: 'Test', coins: 1 }]);
128
108
  await coinsCommand({ leaderboard: true });
129
- expect(authenticatedFetch).toHaveBeenCalledWith('/coins/leaderboard');
130
- expect(authenticatedFetch).not.toHaveBeenCalledWith('/coins');
109
+ expect(mockClient.coins.getLeaderboard).toHaveBeenCalled();
110
+ expect(mockClient.coins.getBalance).not.toHaveBeenCalled();
131
111
  });
132
112
  });
133
113
  });
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { input, select, editor } from '@inquirer/prompts';
3
- import { isAuthenticated, authenticatedFetch } from '../utils/auth.js';
3
+ import { isAuthenticated } from '../utils/auth.js';
4
+ import { getClient } from '../utils/api.js';
4
5
  const EDITOR_HINT = chalk.dim('(type \\\\e to open your editor)');
5
6
  async function promptDescription() {
6
7
  const value = await input({
@@ -35,6 +36,7 @@ export async function feedbackCommand(options) {
35
36
  console.error(chalk.red('Not authenticated. Run `npx @meltstudio/meltctl@latest login` first.'));
36
37
  process.exit(1);
37
38
  }
39
+ const client = await getClient();
38
40
  let toUserId;
39
41
  let coins;
40
42
  let description;
@@ -49,13 +51,7 @@ export async function feedbackCommand(options) {
49
51
  // Fetch recipients
50
52
  let recipients;
51
53
  try {
52
- const res = await authenticatedFetch('/feedback/recipients');
53
- if (!res.ok) {
54
- const body = (await res.json());
55
- console.error(chalk.red(`Failed to load recipients: ${body.error ?? res.statusText}`));
56
- process.exit(1);
57
- }
58
- recipients = (await res.json());
54
+ recipients = await client.feedback.getRecipients();
59
55
  }
60
56
  catch {
61
57
  console.error(chalk.red('Failed to connect to the server.'));
@@ -83,17 +79,12 @@ export async function feedbackCommand(options) {
83
79
  });
84
80
  description = await promptDescription();
85
81
  }
86
- const res = await authenticatedFetch('/feedback', {
87
- method: 'POST',
88
- headers: { 'Content-Type': 'application/json' },
89
- body: JSON.stringify({ toUserId, coins, description }),
90
- });
91
- if (res.ok) {
82
+ try {
83
+ await client.feedback.submit({ toUserId, coins, description });
92
84
  console.log(chalk.green(`\n ✓ Feedback sent! You gave ${coins} coin${coins > 1 ? 's' : ''}.\n`));
93
85
  }
94
- else {
95
- const body = (await res.json());
96
- console.error(chalk.red(`\nFailed to send feedback: ${body.error ?? res.statusText}`));
86
+ catch (error) {
87
+ console.error(chalk.red(`\nFailed to send feedback: ${error instanceof Error ? error.message : 'Unknown error'}`));
97
88
  process.exit(1);
98
89
  }
99
90
  }
@@ -1,14 +1,22 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  vi.mock('../utils/auth.js', () => ({
3
3
  isAuthenticated: vi.fn(),
4
- authenticatedFetch: vi.fn(),
4
+ }));
5
+ const mockClient = vi.hoisted(() => ({
6
+ feedback: {
7
+ submit: vi.fn(),
8
+ getRecipients: 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
  select: vi.fn(),
9
17
  editor: vi.fn(),
10
18
  }));
11
- import { isAuthenticated, authenticatedFetch } from '../utils/auth.js';
19
+ import { isAuthenticated } from '../utils/auth.js';
12
20
  import { input, select } from '@inquirer/prompts';
13
21
  import { feedbackCommand } from './feedback.js';
14
22
  beforeEach(() => {
@@ -29,24 +37,16 @@ describe('feedbackCommand', () => {
29
37
  it('submits feedback with correct payload using option flags', async () => {
30
38
  ;
31
39
  isAuthenticated.mockResolvedValue(true);
32
- const submitResponse = {
33
- ok: true,
34
- json: vi.fn().mockResolvedValue({}),
35
- };
36
- authenticatedFetch.mockResolvedValue(submitResponse);
40
+ mockClient.feedback.submit.mockResolvedValue({});
37
41
  await feedbackCommand({
38
42
  to: '42',
39
43
  coins: '2',
40
44
  description: 'Great job on the feature implementation!',
41
45
  });
42
- expect(authenticatedFetch).toHaveBeenCalledWith('/feedback', {
43
- method: 'POST',
44
- headers: { 'Content-Type': 'application/json' },
45
- body: JSON.stringify({
46
- toUserId: 42,
47
- coins: 2,
48
- description: 'Great job on the feature implementation!',
49
- }),
46
+ expect(mockClient.feedback.submit).toHaveBeenCalledWith({
47
+ toUserId: 42,
48
+ coins: 2,
49
+ description: 'Great job on the feature implementation!',
50
50
  });
51
51
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
52
52
  expect(logCalls.some((msg) => msg.includes('Feedback sent'))).toBe(true);
@@ -54,10 +54,7 @@ describe('feedbackCommand', () => {
54
54
  it('displays singular coin text when sending 1 coin', async () => {
55
55
  ;
56
56
  isAuthenticated.mockResolvedValue(true);
57
- authenticatedFetch.mockResolvedValue({
58
- ok: true,
59
- json: vi.fn().mockResolvedValue({}),
60
- });
57
+ mockClient.feedback.submit.mockResolvedValue({});
61
58
  await feedbackCommand({ to: '1', coins: '1', description: 'Nice work' });
62
59
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
63
60
  expect(logCalls.some((msg) => msg.includes('1 coin') && !msg.includes('1 coins'))).toBe(true);
@@ -65,10 +62,7 @@ describe('feedbackCommand', () => {
65
62
  it('displays plural coin text when sending multiple coins', async () => {
66
63
  ;
67
64
  isAuthenticated.mockResolvedValue(true);
68
- authenticatedFetch.mockResolvedValue({
69
- ok: true,
70
- json: vi.fn().mockResolvedValue({}),
71
- });
65
+ mockClient.feedback.submit.mockResolvedValue({});
72
66
  await feedbackCommand({ to: '1', coins: '3', description: 'Excellent work' });
73
67
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
74
68
  expect(logCalls.some((msg) => msg.includes('3 coins'))).toBe(true);
@@ -76,12 +70,7 @@ describe('feedbackCommand', () => {
76
70
  it('exits with error when API returns failure', async () => {
77
71
  ;
78
72
  isAuthenticated.mockResolvedValue(true);
79
- const submitResponse = {
80
- ok: false,
81
- statusText: 'Bad Request',
82
- json: vi.fn().mockResolvedValue({ error: 'Insufficient coins' }),
83
- };
84
- authenticatedFetch.mockResolvedValue(submitResponse);
73
+ mockClient.feedback.submit.mockRejectedValue(new Error('Insufficient coins'));
85
74
  await expect(feedbackCommand({ to: '42', coins: '2', description: 'Good work' })).rejects.toThrow('process.exit(1)');
86
75
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
87
76
  expect(errorCalls.some((msg) => msg.includes('Insufficient coins'))).toBe(true);
@@ -89,12 +78,7 @@ describe('feedbackCommand', () => {
89
78
  it('exits with error when API returns failure without error body', async () => {
90
79
  ;
91
80
  isAuthenticated.mockResolvedValue(true);
92
- const submitResponse = {
93
- ok: false,
94
- statusText: 'Internal Server Error',
95
- json: vi.fn().mockResolvedValue({}),
96
- };
97
- authenticatedFetch.mockResolvedValue(submitResponse);
81
+ mockClient.feedback.submit.mockRejectedValue(new Error('Internal Server Error'));
98
82
  await expect(feedbackCommand({ to: '42', coins: '2', description: 'Good work' })).rejects.toThrow('process.exit(1)');
99
83
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
100
84
  expect(errorCalls.some((msg) => msg.includes('Internal Server Error'))).toBe(true);
@@ -102,20 +86,13 @@ describe('feedbackCommand', () => {
102
86
  it('sends correct numeric types in payload', async () => {
103
87
  ;
104
88
  isAuthenticated.mockResolvedValue(true);
105
- authenticatedFetch.mockResolvedValue({
106
- ok: true,
107
- json: vi.fn().mockResolvedValue({}),
108
- });
89
+ mockClient.feedback.submit.mockResolvedValue({});
109
90
  await feedbackCommand({ to: '7', coins: '3', description: 'Awesome' });
110
- const call = authenticatedFetch.mock.calls[0];
111
- const body = JSON.parse(call[1].body);
112
- expect(body).toEqual({
91
+ expect(mockClient.feedback.submit).toHaveBeenCalledWith({
113
92
  toUserId: 7,
114
93
  coins: 3,
115
94
  description: 'Awesome',
116
95
  });
117
- expect(typeof body.toUserId).toBe('number');
118
- expect(typeof body.coins).toBe('number');
119
96
  });
120
97
  describe('interactive flow', () => {
121
98
  const mockRecipients = [
@@ -128,73 +105,45 @@ describe('feedbackCommand', () => {
128
105
  }
129
106
  it('prompts for recipient, coins, and description in interactive mode', async () => {
130
107
  setupInteractiveMocks();
131
- const recipientsResponse = {
132
- ok: true,
133
- json: vi.fn().mockResolvedValue(mockRecipients),
134
- };
135
- const submitResponse = {
136
- ok: true,
137
- json: vi.fn().mockResolvedValue({}),
138
- };
139
- authenticatedFetch
140
- .mockResolvedValueOnce(recipientsResponse)
141
- .mockResolvedValueOnce(submitResponse);
108
+ mockClient.feedback.getRecipients.mockResolvedValue(mockRecipients);
109
+ mockClient.feedback.submit.mockResolvedValue({});
142
110
  select.mockResolvedValueOnce(1).mockResolvedValueOnce(2);
143
111
  input.mockResolvedValueOnce('This is a great piece of feedback that is long enough to pass validation easily.');
144
112
  await feedbackCommand({});
145
- expect(authenticatedFetch).toHaveBeenCalledWith('/feedback/recipients');
113
+ expect(mockClient.feedback.getRecipients).toHaveBeenCalled();
146
114
  expect(select).toHaveBeenCalledTimes(2);
147
115
  expect(input).toHaveBeenCalledTimes(1);
148
- expect(authenticatedFetch).toHaveBeenCalledWith('/feedback', expect.objectContaining({
149
- method: 'POST',
150
- body: expect.stringContaining('"toUserId":1'),
116
+ expect(mockClient.feedback.submit).toHaveBeenCalledWith(expect.objectContaining({
117
+ toUserId: 1,
151
118
  }));
152
119
  });
153
120
  it('shows no recipients message when list is empty', async () => {
154
121
  setupInteractiveMocks();
155
- const recipientsResponse = {
156
- ok: true,
157
- json: vi.fn().mockResolvedValue([]),
158
- };
159
- authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
122
+ mockClient.feedback.getRecipients.mockResolvedValue([]);
160
123
  await feedbackCommand({});
161
124
  const logCalls = console.log.mock.calls.map((c) => String(c[0]));
162
125
  expect(logCalls.some((msg) => msg.includes('No recipients'))).toBe(true);
163
- // Should not call POST /feedback
164
- expect(authenticatedFetch).toHaveBeenCalledTimes(1);
126
+ // Should not call submit
127
+ expect(mockClient.feedback.submit).not.toHaveBeenCalled();
165
128
  });
166
129
  it('exits with error when recipients API fails', async () => {
167
130
  setupInteractiveMocks();
168
- const recipientsResponse = {
169
- ok: false,
170
- statusText: 'Forbidden',
171
- json: vi.fn().mockResolvedValue({ error: 'Not allowed' }),
172
- };
173
- authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
131
+ mockClient.feedback.getRecipients.mockRejectedValue(new Error('Not allowed'));
174
132
  await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
175
133
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
176
- expect(errorCalls.some((msg) => msg.includes('Not allowed'))).toBe(true);
134
+ expect(errorCalls.some((msg) => msg.includes('Failed to connect'))).toBe(true);
177
135
  });
178
136
  it('exits with error when recipients fetch throws network error', async () => {
179
137
  setupInteractiveMocks();
180
- authenticatedFetch.mockRejectedValueOnce(new Error('Network error'));
138
+ mockClient.feedback.getRecipients.mockRejectedValue(new Error('Network error'));
181
139
  await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
182
140
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
183
141
  expect(errorCalls.some((msg) => msg.includes('Failed to connect'))).toBe(true);
184
142
  });
185
143
  it('re-prompts when description is too short', async () => {
186
144
  setupInteractiveMocks();
187
- const recipientsResponse = {
188
- ok: true,
189
- json: vi.fn().mockResolvedValue(mockRecipients),
190
- };
191
- const submitResponse = {
192
- ok: true,
193
- json: vi.fn().mockResolvedValue({}),
194
- };
195
- authenticatedFetch
196
- .mockResolvedValueOnce(recipientsResponse)
197
- .mockResolvedValueOnce(submitResponse);
145
+ mockClient.feedback.getRecipients.mockResolvedValue(mockRecipients);
146
+ mockClient.feedback.submit.mockResolvedValue({});
198
147
  select.mockResolvedValueOnce(2).mockResolvedValueOnce(3);
199
148
  input
200
149
  .mockResolvedValueOnce('Too short')
@@ -206,17 +155,8 @@ describe('feedbackCommand', () => {
206
155
  });
207
156
  it('re-prompts when description is too long', async () => {
208
157
  setupInteractiveMocks();
209
- const recipientsResponse = {
210
- ok: true,
211
- json: vi.fn().mockResolvedValue(mockRecipients),
212
- };
213
- const submitResponse = {
214
- ok: true,
215
- json: vi.fn().mockResolvedValue({}),
216
- };
217
- authenticatedFetch
218
- .mockResolvedValueOnce(recipientsResponse)
219
- .mockResolvedValueOnce(submitResponse);
158
+ mockClient.feedback.getRecipients.mockResolvedValue(mockRecipients);
159
+ mockClient.feedback.submit.mockResolvedValue({});
220
160
  select.mockResolvedValueOnce(1).mockResolvedValueOnce(1);
221
161
  input
222
162
  .mockResolvedValueOnce('x'.repeat(501))
@@ -228,15 +168,10 @@ describe('feedbackCommand', () => {
228
168
  });
229
169
  it('falls back to recipients error statusText when no error body', async () => {
230
170
  setupInteractiveMocks();
231
- const recipientsResponse = {
232
- ok: false,
233
- statusText: 'Service Unavailable',
234
- json: vi.fn().mockResolvedValue({}),
235
- };
236
- authenticatedFetch.mockResolvedValueOnce(recipientsResponse);
171
+ mockClient.feedback.getRecipients.mockRejectedValue(new Error('Service Unavailable'));
237
172
  await expect(feedbackCommand({})).rejects.toThrow('process.exit(1)');
238
173
  const errorCalls = console.error.mock.calls.map((c) => String(c[0]));
239
- expect(errorCalls.some((msg) => msg.includes('Service Unavailable'))).toBe(true);
174
+ expect(errorCalls.some((msg) => msg.includes('Failed to connect'))).toBe(true);
240
175
  });
241
176
  });
242
177
  });
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import http from 'http';
3
3
  import { URL } from 'url';
4
4
  import { exec } from 'child_process';
5
+ import { createMeltClient } from '@meltstudio/meltctl-sdk';
5
6
  import { API_BASE, storeAuth } from '../utils/auth.js';
6
7
  import { printBanner } from '../utils/banner.js';
7
8
  const LOGIN_TIMEOUT_MS = 60_000;
@@ -70,26 +71,20 @@ export async function loginCommand() {
70
71
  });
71
72
  });
72
73
  console.log(chalk.dim('Exchanging authorization code...'));
73
- const response = await fetch(`${API_BASE}/auth/token`, {
74
- method: 'POST',
75
- headers: { 'Content-Type': 'application/json' },
76
- body: JSON.stringify({ code: authCode, redirect_uri: redirectUri }),
77
- });
78
- if (!response.ok) {
79
- const error = (await response.json());
80
- if (response.status === 403) {
81
- console.error(chalk.red('Only @meltstudio.co accounts can use this tool.'));
82
- process.exit(1);
83
- }
84
- console.error(chalk.red(`Authentication failed: ${error.error ?? 'Unknown error'}`));
74
+ // Use SDK with empty token for the unauthenticated token exchange
75
+ const client = createMeltClient({ baseUrl: API_BASE, token: '' });
76
+ try {
77
+ const data = await client.auth.exchangeToken(authCode, redirectUri);
78
+ await storeAuth({
79
+ token: data.token,
80
+ email: data.email,
81
+ expiresAt: data.expiresAt,
82
+ });
83
+ console.log();
84
+ console.log(chalk.green(`Logged in as ${data.email}`));
85
+ }
86
+ catch (error) {
87
+ console.error(chalk.red(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
85
88
  process.exit(1);
86
89
  }
87
- const data = (await response.json());
88
- await storeAuth({
89
- token: data.token,
90
- email: data.email,
91
- expiresAt: data.expiresAt,
92
- });
93
- console.log();
94
- console.log(chalk.green(`Logged in as ${data.email}`));
95
90
  }
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { getToken, tokenFetch } from '../utils/api.js';
4
+ import { getClient } from '../utils/api.js';
5
5
  import { getGitBranch, getGitCommit, getGitRepository, getProjectName, extractTicketId, findMdFiles, } from '../utils/git.js';
6
6
  function extractFrontmatterStatus(content) {
7
7
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -29,7 +29,7 @@ async function autoDetectPlanFile() {
29
29
  return mdFiles[0] ?? null;
30
30
  }
31
31
  export async function planSubmitCommand(file) {
32
- const token = await getToken();
32
+ const client = await getClient();
33
33
  let filePath;
34
34
  if (file) {
35
35
  filePath = path.resolve(file);
@@ -55,38 +55,23 @@ export async function planSubmitCommand(file) {
55
55
  const repo = getGitRepository();
56
56
  const ticket = extractTicketId(branch) ?? extractTicketId(filename);
57
57
  const status = extractFrontmatterStatus(content);
58
- const payload = {
59
- project,
60
- repository: repo?.slug ?? null,
61
- repositoryUrl: repo?.url ?? null,
62
- branch,
63
- commit,
64
- content,
65
- metadata: { filename },
66
- };
67
- if (ticket)
68
- payload.ticket = ticket;
69
- if (status)
70
- payload.status = status;
71
58
  try {
72
- const res = await tokenFetch(token, '/plans', {
73
- method: 'POST',
74
- headers: { 'Content-Type': 'application/json' },
75
- body: JSON.stringify(payload),
59
+ const result = await client.plans.submit({
60
+ project,
61
+ repository: repo?.slug ?? null,
62
+ repositoryUrl: repo?.url ?? null,
63
+ branch,
64
+ commit,
65
+ content,
66
+ ticket: ticket ?? null,
67
+ status: status ?? null,
68
+ metadata: { filename },
76
69
  });
77
- if (res.ok) {
78
- const body = (await res.json());
79
- if (body.created) {
80
- console.log(chalk.green(`\n ✓ Plan submitted! ID: ${body.id}\n`));
81
- }
82
- else {
83
- console.log(chalk.green(`\n ✓ Plan updated! ID: ${body.id}\n`));
84
- }
70
+ if (result.created) {
71
+ console.log(chalk.green(`\n ✓ Plan submitted! ID: ${result.id}\n`));
85
72
  }
86
73
  else {
87
- const body = (await res.json());
88
- console.error(chalk.red(`\nFailed to submit plan: ${body.error ?? res.statusText}`));
89
- process.exit(1);
74
+ console.log(chalk.green(`\n ✓ Plan updated! ID: ${result.id}\n`));
90
75
  }
91
76
  }
92
77
  catch (error) {
@@ -95,28 +80,13 @@ export async function planSubmitCommand(file) {
95
80
  }
96
81
  }
97
82
  export async function planListCommand(options) {
98
- const token = await getToken();
99
- const params = new URLSearchParams();
100
- if (options.repository)
101
- params.set('repository', options.repository);
102
- if (options.author)
103
- params.set('author', options.author);
104
- if (options.limit)
105
- params.set('limit', options.limit);
106
- const query = params.toString();
107
- const urlPath = `/plans${query ? `?${query}` : ''}`;
83
+ const client = await getClient();
108
84
  try {
109
- const res = await tokenFetch(token, urlPath);
110
- if (res.status === 403) {
111
- console.error(chalk.red('Access denied. Only Team Managers can list plans.'));
112
- process.exit(1);
113
- }
114
- if (!res.ok) {
115
- const body = (await res.json());
116
- console.error(chalk.red(`Failed to list plans: ${body.error ?? res.statusText}`));
117
- process.exit(1);
118
- }
119
- const body = (await res.json());
85
+ const body = await client.plans.list({
86
+ repository: options.repository,
87
+ author: options.author,
88
+ limit: options.limit ? parseInt(options.limit, 10) : undefined,
89
+ });
120
90
  if (body.plans.length === 0) {
121
91
  console.log(chalk.dim('\n No plans found.\n'));
122
92
  return;