@hubspot/cli 7.10.1-experimental.0 → 7.11.0-experimental.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 (32) hide show
  1. package/commands/project/__tests__/deploy.test.js +5 -5
  2. package/commands/project/__tests__/validate.test.js +27 -285
  3. package/commands/project/create.js +20 -14
  4. package/commands/project/deploy.js +6 -14
  5. package/commands/project/dev/index.js +4 -13
  6. package/commands/project/dev/unifiedFlow.js +7 -1
  7. package/commands/project/upload.js +2 -8
  8. package/commands/project/validate.js +12 -72
  9. package/lang/en.d.ts +18 -14
  10. package/lang/en.js +20 -16
  11. package/lib/__tests__/projectProfiles.test.js +32 -273
  12. package/lib/errorHandlers/index.js +8 -3
  13. package/lib/projectProfiles.d.ts +3 -4
  14. package/lib/projectProfiles.js +32 -78
  15. package/lib/projects/__tests__/components.test.js +2 -22
  16. package/lib/projects/__tests__/deploy.test.js +15 -13
  17. package/lib/projects/add/__tests__/legacyAddComponent.test.js +1 -1
  18. package/lib/projects/add/__tests__/v2AddComponent.test.js +30 -4
  19. package/lib/projects/add/legacyAddComponent.js +1 -1
  20. package/lib/projects/add/v2AddComponent.js +16 -5
  21. package/lib/projects/components.d.ts +8 -1
  22. package/lib/projects/components.js +91 -8
  23. package/lib/projects/deploy.js +21 -8
  24. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +9 -1
  25. package/lib/projects/localDev/helpers/process.js +5 -3
  26. package/lib/ui/SpinniesManager.d.ts +5 -7
  27. package/lib/ui/SpinniesManager.js +9 -12
  28. package/lib/ui/__tests__/SpinniesManager.test.d.ts +1 -0
  29. package/lib/ui/__tests__/SpinniesManager.test.js +489 -0
  30. package/mcp-server/utils/config.js +1 -1
  31. package/package.json +4 -4
  32. package/ui/components/BoxWithTitle.js +1 -1
@@ -6,6 +6,8 @@ import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
6
6
  import { ComponentTypes, } from '../../../types/Projects.js';
7
7
  import { lib } from '../../../lang/en.js';
8
8
  import { uiLogger } from '../../ui/logger.js';
9
+ import { logError } from '../../errorHandlers/index.js';
10
+ import { EXIT_CODES } from '../../enums/exitCodes.js';
9
11
  const SERVER_KEYS = {
10
12
  privateApp: 'privateApp',
11
13
  publicApp: 'publicApp',
@@ -62,7 +64,13 @@ class DevServerManager_DEPRECATED {
62
64
  if (accountConfig) {
63
65
  env = accountConfig.env;
64
66
  }
65
- await startPortManagerServer();
67
+ try {
68
+ await startPortManagerServer();
69
+ }
70
+ catch (e) {
71
+ logError(e);
72
+ process.exit(EXIT_CODES.ERROR);
73
+ }
66
74
  await this.iterateDevServers(async (serverInterface, compatibleComponents) => {
67
75
  if (serverInterface.setup) {
68
76
  await serverInterface.setup({
@@ -5,9 +5,11 @@ import { uiLogger } from '../../../ui/logger.js';
5
5
  import { commands } from '../../../../lang/en.js';
6
6
  export async function confirmLocalDevIsNotRunning() {
7
7
  try {
8
- await getServerPortByInstanceId(LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID);
9
- uiLogger.error(commands.project.dev.errors.localDevAlreadyRunning);
10
- process.exit(EXIT_CODES.ERROR);
8
+ const existingPortInUse = await getServerPortByInstanceId(LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID);
9
+ if (existingPortInUse) {
10
+ uiLogger.error(commands.project.dev.errors.localDevAlreadyRunning);
11
+ process.exit(EXIT_CODES.ERROR);
12
+ }
11
13
  }
12
14
  catch (error) {
13
15
  return;
@@ -17,13 +17,11 @@ declare class SpinniesManager {
17
17
  private resetState;
18
18
  setDisableOutput(disableOutput: boolean): void;
19
19
  pick(name: string): SpinnerState | undefined;
20
- add(name: string, options?: Partial<SpinnerState>): SpinnerState & {
21
- name: string;
22
- };
23
- update(name: string, options?: Partial<SpinnerState>): SpinnerState;
24
- succeed(name: string, options?: Partial<SpinnerState>): SpinnerState;
25
- fail(name: string, options?: Partial<SpinnerState>): SpinnerState;
26
- remove(name: string): SpinnerState;
20
+ add(name: string, options?: Partial<SpinnerState>): void;
21
+ update(name: string, options?: Partial<SpinnerState>): void;
22
+ succeed(name: string, options?: Partial<SpinnerState>): void;
23
+ fail(name: string, options?: Partial<SpinnerState>): void;
24
+ remove(name: string): void;
27
25
  stopAll(newStatus?: (typeof VALID_STATUSES)[number]): {
28
26
  [key: string]: SpinnerState;
29
27
  };
@@ -10,7 +10,8 @@ The above copyright notice and this permission notice shall be included in all c
10
10
  import readline from 'readline';
11
11
  import chalk from 'chalk';
12
12
  import cliCursor from 'cli-cursor';
13
- import { breakText, cleanStream, colorOptions, getLinesLength, purgeSpinnerOptions, purgeSpinnersOptions, SPINNERS, terminalSupportsUnicode, writeStream, prefixOptions, } from './spinniesUtils.js';
13
+ import { breakText, cleanStream, colorOptions, getLinesLength, prefixOptions, purgeSpinnerOptions, purgeSpinnersOptions, SPINNERS, terminalSupportsUnicode, writeStream, } from './spinniesUtils.js';
14
+ import { uiLogger } from './logger.js';
14
15
  function safeColor(text, color) {
15
16
  const chalkFn = chalk[color];
16
17
  if (typeof chalkFn === 'function') {
@@ -74,40 +75,34 @@ class SpinniesManager {
74
75
  if (!options.text) {
75
76
  options.text = resolvedName;
76
77
  }
77
- const spinnerProperties = {
78
+ this.spinners[resolvedName] = {
78
79
  ...colorOptions(this.options),
79
80
  succeedPrefix: this.options.succeedPrefix,
80
81
  failPrefix: this.options.failPrefix,
81
82
  status: 'spinning',
82
83
  ...purgeSpinnerOptions(options),
83
84
  };
84
- this.spinners[resolvedName] = spinnerProperties;
85
85
  this.updateSpinnerState();
86
- return { name: resolvedName, ...spinnerProperties };
87
86
  }
88
87
  update(name, options = {}) {
89
88
  const { status } = options;
90
89
  this.setSpinnerProperties(name, options, status);
91
90
  this.updateSpinnerState();
92
- return this.spinners[name];
93
91
  }
94
92
  succeed(name, options = {}) {
95
93
  this.setSpinnerProperties(name, options, 'succeed');
96
94
  this.updateSpinnerState();
97
- return this.spinners[name];
98
95
  }
99
96
  fail(name, options = {}) {
100
97
  this.setSpinnerProperties(name, options, 'fail');
101
98
  this.updateSpinnerState();
102
- return this.spinners[name];
103
99
  }
104
100
  remove(name) {
105
101
  if (typeof name !== 'string') {
106
- throw Error('A spinner reference name must be specified');
102
+ uiLogger.debug('A spinner reference name must be specified');
103
+ return;
107
104
  }
108
- const spinner = this.spinners[name];
109
105
  delete this.spinners[name];
110
- return spinner;
111
106
  }
112
107
  stopAll(newStatus = 'stopped') {
113
108
  Object.keys(this.spinners).forEach(name => {
@@ -136,10 +131,12 @@ class SpinniesManager {
136
131
  }
137
132
  setSpinnerProperties(name, options, status) {
138
133
  if (typeof name !== 'string') {
139
- throw Error('A spinner reference name must be specified');
134
+ uiLogger.debug('A spinner reference name must be specified');
135
+ return;
140
136
  }
141
137
  if (!this.spinners[name]) {
142
- throw Error(`No spinner initialized with name ${name}`);
138
+ uiLogger.debug(`No spinner initialized with name ${name}`);
139
+ return;
143
140
  }
144
141
  options = purgeSpinnerOptions(options);
145
142
  status = status || 'spinning';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,489 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import SpinniesManager from '../SpinniesManager.js';
3
+ // Mock dependencies
4
+ vi.mock('readline', () => ({
5
+ default: {
6
+ clearScreenDown: vi.fn(),
7
+ moveCursor: vi.fn(),
8
+ clearLine: vi.fn(),
9
+ },
10
+ }));
11
+ vi.mock('cli-cursor', () => ({
12
+ default: {
13
+ hide: vi.fn(),
14
+ show: vi.fn(),
15
+ },
16
+ }));
17
+ vi.mock('../logger.js', () => ({
18
+ uiLogger: {
19
+ debug: vi.fn(),
20
+ log: vi.fn(),
21
+ },
22
+ }));
23
+ describe('SpinniesManager', () => {
24
+ let spinniesManager;
25
+ let mockUiLogger;
26
+ beforeEach(async () => {
27
+ // Reset the singleton instance before each test
28
+ vi.clearAllMocks();
29
+ // Get the mocked logger
30
+ const loggerModule = await import('../logger.js');
31
+ mockUiLogger = loggerModule.uiLogger;
32
+ // Mock process.stderr
33
+ Object.defineProperty(process, 'stderr', {
34
+ value: {
35
+ write: vi.fn(),
36
+ isTTY: true,
37
+ columns: 80,
38
+ },
39
+ writable: true,
40
+ });
41
+ // Mock process.env
42
+ delete process.env.CI;
43
+ spinniesManager = SpinniesManager;
44
+ });
45
+ afterEach(() => {
46
+ vi.restoreAllMocks();
47
+ });
48
+ describe('initialization', () => {
49
+ it('should initialize with default options', () => {
50
+ spinniesManager.init();
51
+ expect(spinniesManager.hasActiveSpinners()).toBe(false);
52
+ });
53
+ it('should initialize with custom options', () => {
54
+ const customOptions = {
55
+ spinnerColor: 'red',
56
+ succeedColor: 'blue',
57
+ failColor: 'yellow',
58
+ };
59
+ spinniesManager.init(customOptions);
60
+ spinniesManager.add('test', { text: 'Test spinner' });
61
+ const spinner = spinniesManager.pick('test');
62
+ expect(spinner).toBeDefined();
63
+ });
64
+ it('should set disableSpins to true in CI environment', () => {
65
+ process.env.CI = 'true';
66
+ spinniesManager.init();
67
+ spinniesManager.add('test', { text: 'Test spinner' });
68
+ const spinner = spinniesManager.pick('test');
69
+ expect(spinner).toBeDefined();
70
+ });
71
+ it('should use fallback spinner when terminal does not support unicode', () => {
72
+ Object.defineProperty(process, 'platform', {
73
+ value: 'win32',
74
+ writable: true,
75
+ });
76
+ delete process.env.TERM_PROGRAM;
77
+ delete process.env.WT_SESSION;
78
+ spinniesManager.init();
79
+ spinniesManager.add('test', { text: 'Test spinner' });
80
+ const spinner = spinniesManager.pick('test');
81
+ expect(spinner).toBeDefined();
82
+ });
83
+ });
84
+ describe('spinner management', () => {
85
+ beforeEach(() => {
86
+ spinniesManager.init();
87
+ });
88
+ describe('add', () => {
89
+ it('should add a spinner with name', () => {
90
+ spinniesManager.add('test-spinner', {
91
+ text: 'Loading...',
92
+ });
93
+ const spinner = spinniesManager.pick('test-spinner');
94
+ expect(spinner).toBeDefined();
95
+ expect(spinner?.text).toBe('Loading...');
96
+ expect(spinner?.status).toBe('spinning');
97
+ expect(spinniesManager.hasActiveSpinners()).toBe(true);
98
+ });
99
+ it('should add a spinner without name and generate one', () => {
100
+ spinniesManager.add('', { text: 'Loading...' });
101
+ // Since we can't get the generated name directly, check that a spinner was added
102
+ expect(spinniesManager.hasActiveSpinners()).toBe(true);
103
+ });
104
+ it('should use spinner name as text if no text provided', () => {
105
+ spinniesManager.add('test-spinner');
106
+ const spinner = spinniesManager.pick('test-spinner');
107
+ expect(spinner?.text).toBe('test-spinner');
108
+ });
109
+ it('should add multiple spinners', () => {
110
+ spinniesManager.add('spinner1', { text: 'First' });
111
+ spinniesManager.add('spinner2', { text: 'Second' });
112
+ const spinner1 = spinniesManager.pick('spinner1');
113
+ const spinner2 = spinniesManager.pick('spinner2');
114
+ expect(spinner1).toBeDefined();
115
+ expect(spinner2).toBeDefined();
116
+ expect(spinniesManager.hasActiveSpinners()).toBe(true);
117
+ });
118
+ });
119
+ describe('pick', () => {
120
+ it('should return spinner by name', () => {
121
+ spinniesManager.add('test-spinner', { text: 'Loading...' });
122
+ const picked = spinniesManager.pick('test-spinner');
123
+ expect(picked).toBeDefined();
124
+ expect(picked?.text).toBe('Loading...');
125
+ });
126
+ it('should return undefined for non-existent spinner', () => {
127
+ const picked = spinniesManager.pick('non-existent');
128
+ expect(picked).toBeUndefined();
129
+ });
130
+ });
131
+ describe('update', () => {
132
+ it('should update spinner properties', () => {
133
+ spinniesManager.add('test-spinner', { text: 'Loading...' });
134
+ spinniesManager.update('test-spinner', {
135
+ text: 'Updated text',
136
+ color: 'red',
137
+ });
138
+ const spinner = spinniesManager.pick('test-spinner');
139
+ expect(spinner?.text).toBe('Updated text');
140
+ expect(spinner?.color).toBe('red');
141
+ });
142
+ it('should update spinner status', () => {
143
+ spinniesManager.add('test-spinner', { text: 'Loading...' });
144
+ spinniesManager.update('test-spinner', {
145
+ status: 'succeed',
146
+ });
147
+ const spinner = spinniesManager.pick('test-spinner');
148
+ expect(spinner).toBeDefined();
149
+ expect(spinner?.status).toBe('succeed');
150
+ });
151
+ it('should log and return early for invalid spinner name', async () => {
152
+ // @ts-expect-error testing bad input
153
+ spinniesManager.update(123, { text: 'test' });
154
+ expect(mockUiLogger.debug).toHaveBeenCalledWith('A spinner reference name must be specified');
155
+ });
156
+ });
157
+ describe('succeed', () => {
158
+ it('should mark spinner as succeeded', () => {
159
+ spinniesManager.add('test-spinner', { text: 'Loading...' });
160
+ spinniesManager.succeed('test-spinner', { text: 'Success!' });
161
+ const spinner = spinniesManager.pick('test-spinner');
162
+ expect(spinner?.status).toBe('succeed');
163
+ expect(spinner?.text).toBe('Success!');
164
+ });
165
+ it('should succeed without updating text', () => {
166
+ spinniesManager.add('test-spinner', { text: 'Loading...' });
167
+ spinniesManager.succeed('test-spinner');
168
+ const spinner = spinniesManager.pick('test-spinner');
169
+ expect(spinner?.status).toBe('succeed');
170
+ expect(spinner?.text).toBe('Loading...');
171
+ });
172
+ });
173
+ describe('fail', () => {
174
+ it('should mark spinner as failed', () => {
175
+ spinniesManager.add('test-spinner', { text: 'Loading...' });
176
+ spinniesManager.fail('test-spinner', { text: 'Failed!' });
177
+ const spinner = spinniesManager.pick('test-spinner');
178
+ expect(spinner?.status).toBe('fail');
179
+ expect(spinner?.text).toBe('Failed!');
180
+ });
181
+ it('should fail without updating text', () => {
182
+ spinniesManager.add('test-spinner', { text: 'Loading...' });
183
+ spinniesManager.fail('test-spinner');
184
+ const spinner = spinniesManager.pick('test-spinner');
185
+ expect(spinner?.status).toBe('fail');
186
+ expect(spinner?.text).toBe('Loading...');
187
+ });
188
+ });
189
+ describe('remove', () => {
190
+ it('should remove spinner by name', () => {
191
+ spinniesManager.add('test-spinner', { text: 'Loading...' });
192
+ expect(spinniesManager.pick('test-spinner')).toBeDefined();
193
+ spinniesManager.remove('test-spinner');
194
+ expect(spinniesManager.pick('test-spinner')).toBeUndefined();
195
+ });
196
+ it('should log debug message for invalid name', async () => {
197
+ // @ts-expect-error Testing bad case
198
+ spinniesManager.remove(123);
199
+ expect(mockUiLogger.debug).toHaveBeenCalledWith('A spinner reference name must be specified');
200
+ });
201
+ it('should handle undefined name', async () => {
202
+ // @ts-expect-error bad input
203
+ spinniesManager.remove(undefined);
204
+ expect(mockUiLogger.debug).toHaveBeenCalledWith('A spinner reference name must be specified');
205
+ });
206
+ });
207
+ describe('stopAll', () => {
208
+ beforeEach(() => {
209
+ // @ts-expect-error private
210
+ spinniesManager.spinners = {};
211
+ spinniesManager.add('spinner1', { text: 'First' });
212
+ spinniesManager.add('spinner2', { text: 'Second' });
213
+ });
214
+ it('should stop all active spinners with default status', () => {
215
+ // Check spinners exist before stopping
216
+ expect(spinniesManager.pick('spinner1')).toBeDefined();
217
+ expect(spinniesManager.pick('spinner2')).toBeDefined();
218
+ // Mock checkIfActiveSpinners to prevent clearing of spinners
219
+ const originalCheckIfActiveSpinners =
220
+ // @ts-expect-error private method
221
+ spinniesManager.checkIfActiveSpinners;
222
+ // @ts-expect-error private method
223
+ spinniesManager.checkIfActiveSpinners = vi.fn();
224
+ const result = spinniesManager.stopAll();
225
+ // The result should contain the updated spinners
226
+ expect(result.spinner1?.status).toBe('stopped');
227
+ expect(result.spinner2?.status).toBe('stopped');
228
+ expect(result.spinner1?.color).toBe('gray');
229
+ expect(result.spinner2?.color).toBe('gray');
230
+ // @ts-expect-error private method
231
+ spinniesManager.checkIfActiveSpinners = originalCheckIfActiveSpinners;
232
+ });
233
+ it('should stop all active spinners with succeed status', () => {
234
+ // Check spinners exist before stopping
235
+ expect(spinniesManager.pick('spinner1')).toBeDefined();
236
+ expect(spinniesManager.pick('spinner2')).toBeDefined();
237
+ // Mock checkIfActiveSpinners to prevent clearing of spinners
238
+ const originalCheckIfActiveSpinners =
239
+ // @ts-expect-error private method
240
+ spinniesManager.checkIfActiveSpinners;
241
+ // @ts-expect-error private method
242
+ spinniesManager.checkIfActiveSpinners = vi.fn();
243
+ const result = spinniesManager.stopAll('succeed');
244
+ expect(result.spinner1?.status).toBe('succeed');
245
+ expect(result.spinner2?.status).toBe('succeed');
246
+ // @ts-expect-error private method
247
+ spinniesManager.checkIfActiveSpinners = originalCheckIfActiveSpinners;
248
+ });
249
+ it('should stop all active spinners with fail status', () => {
250
+ // Check spinners exist before stopping
251
+ expect(spinniesManager.pick('spinner1')).toBeDefined();
252
+ expect(spinniesManager.pick('spinner2')).toBeDefined();
253
+ // Mock checkIfActiveSpinners to prevent clearing of spinners
254
+ const originalCheckIfActiveSpinners =
255
+ // @ts-expect-error private method
256
+ spinniesManager.checkIfActiveSpinners;
257
+ // @ts-expect-error private method
258
+ spinniesManager.checkIfActiveSpinners = vi.fn();
259
+ const result = spinniesManager.stopAll('fail');
260
+ expect(result.spinner1?.status).toBe('fail');
261
+ expect(result.spinner2?.status).toBe('fail');
262
+ // @ts-expect-error private method
263
+ spinniesManager.checkIfActiveSpinners = originalCheckIfActiveSpinners;
264
+ });
265
+ it('should not change already completed spinners', () => {
266
+ // Add a spinner that is already completed
267
+ spinniesManager.add('completed', { text: 'Done' });
268
+ spinniesManager.succeed('completed');
269
+ // Verify it exists and is succeeded
270
+ expect(spinniesManager.pick('completed')?.status).toBe('succeed');
271
+ // Mock checkIfActiveSpinners to prevent clearing of spinners
272
+ const originalCheckIfActiveSpinners =
273
+ // @ts-expect-error private method
274
+ spinniesManager.checkIfActiveSpinners;
275
+ // @ts-expect-error private method
276
+ spinniesManager.checkIfActiveSpinners = vi.fn();
277
+ const result = spinniesManager.stopAll();
278
+ // Completed spinners should not change status
279
+ expect(result.completed?.status).toBe('succeed');
280
+ // @ts-expect-error private method
281
+ spinniesManager.checkIfActiveSpinners = originalCheckIfActiveSpinners;
282
+ });
283
+ it('should handle non-spinnable status correctly', () => {
284
+ // Add spinner with non-spinnable status
285
+ spinniesManager.add('non-spin', {
286
+ text: 'Non-spinnable',
287
+ status: 'non-spinnable',
288
+ });
289
+ // Verify it exists with correct status
290
+ expect(spinniesManager.pick('non-spin')?.status).toBe('non-spinnable');
291
+ // Mock checkIfActiveSpinners to prevent clearing of spinners
292
+ const originalCheckIfActiveSpinners =
293
+ // @ts-expect-error private method
294
+ spinniesManager.checkIfActiveSpinners;
295
+ // @ts-expect-error private method
296
+ spinniesManager.checkIfActiveSpinners = vi.fn();
297
+ const result = spinniesManager.stopAll();
298
+ // Non-spinnable status should not change
299
+ expect(result['non-spin']?.status).toBe('non-spinnable');
300
+ // @ts-expect-error Testing private method
301
+ spinniesManager.checkIfActiveSpinners = originalCheckIfActiveSpinners;
302
+ });
303
+ });
304
+ });
305
+ describe('active spinner detection', () => {
306
+ beforeEach(() => {
307
+ spinniesManager.init();
308
+ });
309
+ it('should detect active spinners', () => {
310
+ expect(spinniesManager.hasActiveSpinners()).toBe(false);
311
+ spinniesManager.add('active', { text: 'Loading...' });
312
+ expect(spinniesManager.hasActiveSpinners()).toBe(true);
313
+ spinniesManager.succeed('active');
314
+ expect(spinniesManager.hasActiveSpinners()).toBe(false);
315
+ });
316
+ it('should detect multiple active spinners', () => {
317
+ spinniesManager.add('active1', { text: 'Loading 1...' });
318
+ spinniesManager.add('active2', { text: 'Loading 2...' });
319
+ expect(spinniesManager.hasActiveSpinners()).toBe(true);
320
+ spinniesManager.succeed('active1');
321
+ expect(spinniesManager.hasActiveSpinners()).toBe(true);
322
+ spinniesManager.succeed('active2');
323
+ expect(spinniesManager.hasActiveSpinners()).toBe(false);
324
+ });
325
+ });
326
+ describe('output control', () => {
327
+ beforeEach(() => {
328
+ spinniesManager.init();
329
+ });
330
+ it('should disable output when requested', () => {
331
+ spinniesManager.setDisableOutput(true);
332
+ spinniesManager.add('test', { text: 'Test' });
333
+ // Output should be disabled, so no writes to stderr should occur
334
+ expect(process.stderr.write).not.toHaveBeenCalled();
335
+ });
336
+ it('should enable output by default', () => {
337
+ // Mock process.stderr.write to be a spy
338
+ const mockWrite = vi.fn();
339
+ Object.defineProperty(process.stderr, 'write', {
340
+ value: mockWrite,
341
+ writable: true,
342
+ });
343
+ // Set stderr.isTTY to false to trigger raw output mode
344
+ Object.defineProperty(process.stderr, 'isTTY', {
345
+ value: false,
346
+ writable: true,
347
+ });
348
+ spinniesManager.setDisableOutput(false);
349
+ spinniesManager.init(); // Re-init to pick up the new isTTY value
350
+ spinniesManager.add('test', { text: 'Test' });
351
+ // In non-TTY mode, it should write with raw output
352
+ expect(mockWrite).toHaveBeenCalledWith('- Test\n');
353
+ });
354
+ });
355
+ describe('error handling', () => {
356
+ beforeEach(() => {
357
+ spinniesManager.init();
358
+ });
359
+ it('should handle missing spinner in update', async () => {
360
+ spinniesManager.update('non-existent', { text: 'test' });
361
+ expect(mockUiLogger.debug).toHaveBeenCalledWith('No spinner initialized with name non-existent');
362
+ });
363
+ it('should handle missing spinner in setSpinnerProperties', async () => {
364
+ // @ts-expect-error Testing private method
365
+ spinniesManager.setSpinnerProperties('missing', {
366
+ text: 'test',
367
+ });
368
+ expect(mockUiLogger.debug).toHaveBeenCalledWith('No spinner initialized with name missing');
369
+ });
370
+ it('should handle invalid spinner name types', async () => {
371
+ // @ts-expect-error Testing private method
372
+ spinniesManager.setSpinnerProperties(null, { text: 'test' });
373
+ expect(mockUiLogger.debug).toHaveBeenCalledWith('A spinner reference name must be specified');
374
+ // @ts-expect-error Testing private method
375
+ spinniesManager.setSpinnerProperties(undefined, {
376
+ text: 'test',
377
+ });
378
+ expect(mockUiLogger.debug).toHaveBeenCalledWith('A spinner reference name must be specified');
379
+ // @ts-expect-error Testing private method
380
+ spinniesManager.setSpinnerProperties(123, { text: 'test' });
381
+ expect(mockUiLogger.debug).toHaveBeenCalledWith('A spinner reference name must be specified');
382
+ });
383
+ });
384
+ describe('SIGINT handling', () => {
385
+ beforeEach(() => {
386
+ spinniesManager.init();
387
+ });
388
+ it('should bind SIGINT handler on init', () => {
389
+ const processOnSpy = vi.spyOn(process, 'on');
390
+ const processRemoveAllListenersSpy = vi.spyOn(process, 'removeAllListeners');
391
+ spinniesManager.init();
392
+ expect(processRemoveAllListenersSpy).toHaveBeenCalledWith('SIGINT');
393
+ expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
394
+ });
395
+ });
396
+ describe('spinner options validation', () => {
397
+ beforeEach(() => {
398
+ spinniesManager.init();
399
+ });
400
+ it('should handle custom spinner frames', () => {
401
+ const customOptions = {
402
+ spinner: {
403
+ frames: ['|', '/', '-', '\\'],
404
+ interval: 100,
405
+ },
406
+ };
407
+ spinniesManager.init(customOptions);
408
+ spinniesManager.add('custom', { text: 'Custom spinner' });
409
+ const spinner = spinniesManager.pick('custom');
410
+ expect(spinner).toBeDefined();
411
+ expect(spinner?.text).toBe('Custom spinner');
412
+ });
413
+ it('should handle custom colors', () => {
414
+ spinniesManager.add('colored', {
415
+ text: 'Colored spinner',
416
+ color: 'red',
417
+ spinnerColor: 'blue',
418
+ succeedColor: 'green',
419
+ failColor: 'yellow',
420
+ });
421
+ const spinner = spinniesManager.pick('colored');
422
+ expect(spinner?.color).toBe('red');
423
+ expect(spinner?.spinnerColor).toBe('blue');
424
+ expect(spinner?.succeedColor).toBe('green');
425
+ expect(spinner?.failColor).toBe('yellow');
426
+ });
427
+ it('should handle indentation', () => {
428
+ spinniesManager.add('indented', {
429
+ text: 'Indented spinner',
430
+ indent: 4,
431
+ });
432
+ const spinner = spinniesManager.pick('indented');
433
+ expect(spinner?.indent).toBe(4);
434
+ });
435
+ it('should handle custom prefixes', () => {
436
+ spinniesManager.init({
437
+ succeedPrefix: '[OK]',
438
+ failPrefix: '[ERR]',
439
+ });
440
+ spinniesManager.add('prefixed', {
441
+ text: 'Prefixed spinner',
442
+ });
443
+ const spinner = spinniesManager.pick('prefixed');
444
+ expect(spinner?.succeedPrefix).toBe('[OK]');
445
+ expect(spinner?.failPrefix).toBe('[ERR]');
446
+ });
447
+ });
448
+ describe('edge cases', () => {
449
+ beforeEach(() => {
450
+ spinniesManager.init();
451
+ });
452
+ it('should handle empty spinner name gracefully', () => {
453
+ spinniesManager.add('', { text: 'No name spinner' });
454
+ // Since we can't get the generated name directly, just verify a spinner was added
455
+ expect(spinniesManager.hasActiveSpinners()).toBe(true);
456
+ });
457
+ it('should handle null/undefined text', () => {
458
+ // @ts-expect-error Testing bad case
459
+ spinniesManager.add('null-text', { text: null });
460
+ const spinner = spinniesManager.pick('null-text');
461
+ expect(spinner?.text).toBe('null-text');
462
+ });
463
+ it('should handle non-TTY environments', () => {
464
+ const mockWrite = vi.fn();
465
+ Object.defineProperty(process.stderr, 'write', {
466
+ value: mockWrite,
467
+ writable: true,
468
+ });
469
+ Object.defineProperty(process.stderr, 'isTTY', {
470
+ value: false,
471
+ writable: true,
472
+ });
473
+ spinniesManager.init();
474
+ spinniesManager.add('no-tty', { text: 'No TTY' });
475
+ const spinner = spinniesManager.pick('no-tty');
476
+ expect(spinner).toBeDefined();
477
+ expect(mockWrite).toHaveBeenCalledWith('- No TTY\n');
478
+ });
479
+ it('should handle missing process.stderr.columns', () => {
480
+ // @ts-expect-error
481
+ delete process.stderr.columns;
482
+ spinniesManager.add('no-columns', {
483
+ text: 'A very long text that would normally be broken into multiple lines but now has to handle missing columns gracefully',
484
+ });
485
+ const spinner = spinniesManager.pick('no-columns');
486
+ expect(spinner).toBeDefined();
487
+ });
488
+ });
489
+ });
@@ -3,7 +3,7 @@ export function setupHubSpotConfig(absoluteCurrentWorkingDirectory) {
3
3
  if (!absoluteCurrentWorkingDirectory) {
4
4
  return;
5
5
  }
6
- const configPath = getLocalConfigFilePathIfExists();
6
+ const configPath = getLocalConfigFilePathIfExists(absoluteCurrentWorkingDirectory);
7
7
  if (configPath) {
8
8
  process.env.HUBSPOT_CONFIG_PATH = configPath;
9
9
  }
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "7.10.1-experimental.0",
3
+ "version": "7.11.0-experimental.0",
4
4
  "description": "The official CLI for developing on HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/HubSpot/hubspot-cli",
7
7
  "type": "module",
8
8
  "dependencies": {
9
9
  "@hubspot/local-dev-lib": "4.0.1",
10
- "@hubspot/project-parsing-lib": "0.10.3",
10
+ "@hubspot/project-parsing-lib": "0.10.2",
11
11
  "@hubspot/serverless-dev-runtime": "7.0.7",
12
12
  "@hubspot/theme-preview-dev-server": "0.0.12",
13
- "@hubspot/ui-extensions-dev-server": "1.0.0",
13
+ "@hubspot/ui-extensions-dev-server": "1.0.1",
14
14
  "archiver": "7.0.1",
15
15
  "chalk": "5.4.1",
16
16
  "chokidar": "3.6.0",
@@ -35,7 +35,7 @@
35
35
  "yargs-parser": "21.1.1"
36
36
  },
37
37
  "devDependencies": {
38
- "@hubspot/npm-scripts": "0.0.6-beta.0",
38
+ "@hubspot/npm-scripts": "0.0.6",
39
39
  "@types/archiver": "^6.0.3",
40
40
  "@types/cli-progress": "^3.11.6",
41
41
  "@types/express": "^5.0.0",