spck 0.3.1

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 (155) hide show
  1. package/.oxlintrc.json +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +631 -0
  4. package/bin/cli.js +20 -0
  5. package/bin/validate-cwd.js +41 -0
  6. package/dist/config/__tests__/config.test.d.ts +2 -0
  7. package/dist/config/__tests__/config.test.js +262 -0
  8. package/dist/config/__tests__/credentials.test.d.ts +2 -0
  9. package/dist/config/__tests__/credentials.test.js +360 -0
  10. package/dist/config/config.d.ts +33 -0
  11. package/dist/config/config.js +185 -0
  12. package/dist/config/credentials.d.ts +75 -0
  13. package/dist/config/credentials.js +259 -0
  14. package/dist/config/server-selection.d.ts +40 -0
  15. package/dist/config/server-selection.js +130 -0
  16. package/dist/connection/__tests__/firebase-auth.test.d.ts +2 -0
  17. package/dist/connection/__tests__/firebase-auth.test.js +96 -0
  18. package/dist/connection/__tests__/hmac.test.d.ts +2 -0
  19. package/dist/connection/__tests__/hmac.test.js +372 -0
  20. package/dist/connection/auth.d.ts +13 -0
  21. package/dist/connection/auth.js +91 -0
  22. package/dist/connection/firebase-auth.d.ts +40 -0
  23. package/dist/connection/firebase-auth.js +429 -0
  24. package/dist/connection/hmac.d.ts +24 -0
  25. package/dist/connection/hmac.js +109 -0
  26. package/dist/i18n/index.d.ts +25 -0
  27. package/dist/i18n/index.js +101 -0
  28. package/dist/i18n/locales/en.json +313 -0
  29. package/dist/i18n/locales/es.json +302 -0
  30. package/dist/i18n/locales/fr.json +302 -0
  31. package/dist/i18n/locales/id.json +302 -0
  32. package/dist/i18n/locales/ja.json +302 -0
  33. package/dist/i18n/locales/ko.json +302 -0
  34. package/dist/i18n/locales/locales/en.json +309 -0
  35. package/dist/i18n/locales/locales/es.json +302 -0
  36. package/dist/i18n/locales/locales/fr.json +302 -0
  37. package/dist/i18n/locales/locales/id.json +302 -0
  38. package/dist/i18n/locales/locales/ja.json +302 -0
  39. package/dist/i18n/locales/locales/ko.json +302 -0
  40. package/dist/i18n/locales/locales/pt.json +302 -0
  41. package/dist/i18n/locales/locales/zh-Hans.json +302 -0
  42. package/dist/i18n/locales/pt.json +302 -0
  43. package/dist/i18n/locales/zh-Hans.json +302 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.js +493 -0
  46. package/dist/proxy/ProxyClient.d.ts +125 -0
  47. package/dist/proxy/ProxyClient.js +781 -0
  48. package/dist/proxy/ProxySocketWrapper.d.ts +43 -0
  49. package/dist/proxy/ProxySocketWrapper.js +98 -0
  50. package/dist/proxy/__tests__/ProxyClient.test.d.ts +2 -0
  51. package/dist/proxy/__tests__/ProxyClient.test.js +445 -0
  52. package/dist/proxy/__tests__/ProxySocketWrapper.test.d.ts +2 -0
  53. package/dist/proxy/__tests__/ProxySocketWrapper.test.js +190 -0
  54. package/dist/proxy/__tests__/handshake-validation.test.d.ts +2 -0
  55. package/dist/proxy/__tests__/handshake-validation.test.js +282 -0
  56. package/dist/proxy/__tests__/token-refresh-race.test.d.ts +14 -0
  57. package/dist/proxy/__tests__/token-refresh-race.test.js +173 -0
  58. package/dist/proxy/chunking.d.ts +53 -0
  59. package/dist/proxy/chunking.js +127 -0
  60. package/dist/proxy/handshake-validation.d.ts +21 -0
  61. package/dist/proxy/handshake-validation.js +49 -0
  62. package/dist/rpc/__tests__/router.test.d.ts +2 -0
  63. package/dist/rpc/__tests__/router.test.js +262 -0
  64. package/dist/rpc/router.d.ts +37 -0
  65. package/dist/rpc/router.js +132 -0
  66. package/dist/services/BrowserProxyService.d.ts +13 -0
  67. package/dist/services/BrowserProxyService.js +139 -0
  68. package/dist/services/FilesystemService.d.ts +99 -0
  69. package/dist/services/FilesystemService.js +742 -0
  70. package/dist/services/GitService.d.ts +243 -0
  71. package/dist/services/GitService.js +1439 -0
  72. package/dist/services/SearchService.d.ts +93 -0
  73. package/dist/services/SearchService.js +670 -0
  74. package/dist/services/TerminalService.d.ts +62 -0
  75. package/dist/services/TerminalService.js +337 -0
  76. package/dist/services/__tests__/BrowserProxyService.test.d.ts +2 -0
  77. package/dist/services/__tests__/BrowserProxyService.test.js +145 -0
  78. package/dist/services/__tests__/FilesystemService.test.d.ts +2 -0
  79. package/dist/services/__tests__/FilesystemService.test.js +609 -0
  80. package/dist/services/__tests__/GitService.test.d.ts +2 -0
  81. package/dist/services/__tests__/GitService.test.js +953 -0
  82. package/dist/services/__tests__/SearchService.test.d.ts +2 -0
  83. package/dist/services/__tests__/SearchService.test.js +384 -0
  84. package/dist/services/__tests__/TerminalService.test.d.ts +2 -0
  85. package/dist/services/__tests__/TerminalService.test.js +513 -0
  86. package/dist/setup/wizard.d.ts +10 -0
  87. package/dist/setup/wizard.js +172 -0
  88. package/dist/types.d.ts +196 -0
  89. package/dist/types.js +44 -0
  90. package/dist/utils/__tests__/gitignore.test.d.ts +2 -0
  91. package/dist/utils/__tests__/gitignore.test.js +127 -0
  92. package/dist/utils/gitignore.d.ts +24 -0
  93. package/dist/utils/gitignore.js +77 -0
  94. package/dist/utils/logger.d.ts +96 -0
  95. package/dist/utils/logger.js +456 -0
  96. package/dist/utils/project-dir.d.ts +51 -0
  97. package/dist/utils/project-dir.js +191 -0
  98. package/dist/utils/ripgrep.d.ts +34 -0
  99. package/dist/utils/ripgrep.js +148 -0
  100. package/dist/utils/tool-detection.d.ts +17 -0
  101. package/dist/utils/tool-detection.js +126 -0
  102. package/dist/watcher/FileWatcher.d.ts +10 -0
  103. package/dist/watcher/FileWatcher.js +42 -0
  104. package/package.json +70 -0
  105. package/src/config/__tests__/config.test.ts +318 -0
  106. package/src/config/__tests__/credentials.test.ts +494 -0
  107. package/src/config/config.ts +206 -0
  108. package/src/config/credentials.ts +302 -0
  109. package/src/config/server-selection.ts +150 -0
  110. package/src/connection/__tests__/firebase-auth.test.ts +121 -0
  111. package/src/connection/__tests__/hmac.test.ts +509 -0
  112. package/src/connection/auth.ts +140 -0
  113. package/src/connection/firebase-auth.ts +504 -0
  114. package/src/connection/hmac.ts +139 -0
  115. package/src/i18n/index.ts +119 -0
  116. package/src/i18n/locales/en.json +313 -0
  117. package/src/i18n/locales/es.json +302 -0
  118. package/src/i18n/locales/fr.json +302 -0
  119. package/src/i18n/locales/id.json +302 -0
  120. package/src/i18n/locales/ja.json +302 -0
  121. package/src/i18n/locales/ko.json +302 -0
  122. package/src/i18n/locales/pt.json +302 -0
  123. package/src/i18n/locales/zh-Hans.json +302 -0
  124. package/src/index.ts +542 -0
  125. package/src/proxy/ProxyClient.ts +968 -0
  126. package/src/proxy/ProxySocketWrapper.ts +113 -0
  127. package/src/proxy/__tests__/ProxyClient.test.ts +575 -0
  128. package/src/proxy/__tests__/ProxySocketWrapper.test.ts +251 -0
  129. package/src/proxy/__tests__/handshake-validation.test.ts +367 -0
  130. package/src/proxy/chunking.ts +162 -0
  131. package/src/proxy/handshake-validation.ts +64 -0
  132. package/src/rpc/__tests__/router.test.ts +400 -0
  133. package/src/rpc/router.ts +183 -0
  134. package/src/services/BrowserProxyService.ts +179 -0
  135. package/src/services/FilesystemService.ts +841 -0
  136. package/src/services/GitService.ts +1639 -0
  137. package/src/services/SearchService.ts +809 -0
  138. package/src/services/TerminalService.ts +413 -0
  139. package/src/services/__tests__/BrowserProxyService.test.ts +155 -0
  140. package/src/services/__tests__/FilesystemService.test.ts +1002 -0
  141. package/src/services/__tests__/GitService.test.ts +1552 -0
  142. package/src/services/__tests__/SearchService.test.ts +484 -0
  143. package/src/services/__tests__/TerminalService.test.ts +702 -0
  144. package/src/setup/wizard.ts +242 -0
  145. package/src/types/fossil-delta.d.ts +4 -0
  146. package/src/types.ts +287 -0
  147. package/src/utils/__tests__/gitignore.test.ts +174 -0
  148. package/src/utils/gitignore.ts +91 -0
  149. package/src/utils/logger.ts +578 -0
  150. package/src/utils/project-dir.ts +218 -0
  151. package/src/utils/ripgrep.ts +180 -0
  152. package/src/utils/tool-detection.ts +141 -0
  153. package/src/watcher/FileWatcher.ts +53 -0
  154. package/tsconfig.json +24 -0
  155. package/vitest.config.ts +19 -0
@@ -0,0 +1,513 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ /**
3
+ * Tests for TerminalService
4
+ */
5
+ import { TerminalService } from '../TerminalService.js';
6
+ import { ErrorCode } from '../../types.js';
7
+ // Mock node-pty
8
+ vi.mock('node-pty', () => {
9
+ const mockPty = {
10
+ onData: vi.fn(),
11
+ onExit: vi.fn(),
12
+ write: vi.fn(),
13
+ kill: vi.fn(),
14
+ resize: vi.fn(),
15
+ };
16
+ return {
17
+ spawn: vi.fn(() => mockPty),
18
+ };
19
+ });
20
+ // Mock @xterm/headless
21
+ vi.mock('@xterm/headless', () => {
22
+ const mod = {
23
+ Terminal: vi.fn().mockImplementation(() => ({
24
+ write: vi.fn(),
25
+ resize: vi.fn(),
26
+ loadAddon: vi.fn(),
27
+ })),
28
+ };
29
+ return { default: mod, ...mod };
30
+ });
31
+ // Mock @xterm/addon-serialize
32
+ vi.mock('@xterm/addon-serialize', () => {
33
+ const mod = {
34
+ SerializeAddon: vi.fn().mockImplementation(() => ({
35
+ serialize: vi.fn(() => 'serialized-buffer'),
36
+ })),
37
+ };
38
+ return { default: mod, ...mod };
39
+ });
40
+ import * as pty from 'node-pty';
41
+ import XtermHeadlessModule from '@xterm/headless';
42
+ import SerializeAddonModule from '@xterm/addon-serialize';
43
+ const { Terminal: XtermHeadless } = XtermHeadlessModule;
44
+ const { SerializeAddon } = SerializeAddonModule;
45
+ describe('TerminalService', () => {
46
+ let service;
47
+ let mockSocket;
48
+ let mockPtyProcess;
49
+ let mockXterm;
50
+ let mockSerializeAddon;
51
+ let ptyDataHandler = null;
52
+ let ptyExitHandler = null;
53
+ beforeEach(() => {
54
+ // Reset all mocks
55
+ vi.clearAllMocks();
56
+ // Set up mock socket (with all SocketInterface properties)
57
+ mockSocket = {
58
+ id: 'test-socket',
59
+ data: { uid: 'test-user-123', deviceId: 'test-device-123' },
60
+ emit: vi.fn(),
61
+ on: vi.fn(),
62
+ off: vi.fn(),
63
+ broadcast: {
64
+ emit: vi.fn(),
65
+ },
66
+ };
67
+ // Set up mock PTY process
68
+ mockPtyProcess = {
69
+ onData: vi.fn((handler) => {
70
+ ptyDataHandler = handler;
71
+ }),
72
+ onExit: vi.fn((handler) => {
73
+ ptyExitHandler = handler;
74
+ }),
75
+ write: vi.fn(),
76
+ kill: vi.fn(),
77
+ resize: vi.fn(),
78
+ };
79
+ // Set up mock xterm
80
+ mockXterm = {
81
+ write: vi.fn(),
82
+ resize: vi.fn(),
83
+ loadAddon: vi.fn(),
84
+ };
85
+ // Set up mock serialize addon
86
+ mockSerializeAddon = {
87
+ serialize: vi.fn(() => 'serialized-buffer-content'),
88
+ };
89
+ // Configure mocks
90
+ pty.spawn.mockReturnValue(mockPtyProcess);
91
+ XtermHeadless.mockImplementation(() => mockXterm);
92
+ SerializeAddon.mockImplementation(() => mockSerializeAddon);
93
+ // Create service
94
+ service = new TerminalService(() => mockSocket, 10, 10000, '/test/root');
95
+ // Reset handlers
96
+ ptyDataHandler = null;
97
+ ptyExitHandler = null;
98
+ });
99
+ describe('Terminal Creation', () => {
100
+ it('should create a new terminal session', async () => {
101
+ const result = await service.handle('create', { cols: 80, rows: 24 });
102
+ expect(result).toMatch(/^term-/);
103
+ expect(pty.spawn).toHaveBeenCalledWith(expect.any(String), [], expect.objectContaining({
104
+ name: 'xterm-256color',
105
+ cols: 80,
106
+ rows: 24,
107
+ cwd: '/test/root',
108
+ }));
109
+ expect(XtermHeadless).toHaveBeenCalledWith({
110
+ allowProposedApi: true,
111
+ cols: 80,
112
+ rows: 24,
113
+ scrollback: 10000,
114
+ });
115
+ });
116
+ it('should use default dimensions if not provided', async () => {
117
+ const result = await service.handle('create', {});
118
+ expect(result).toMatch(/^term-/);
119
+ expect(pty.spawn).toHaveBeenCalledWith(expect.any(String), [], expect.objectContaining({
120
+ cols: 80,
121
+ rows: 24,
122
+ }));
123
+ });
124
+ it('should send initial buffer refresh', async () => {
125
+ await service.handle('create', { cols: 80, rows: 24 });
126
+ expect(mockSocket.emit).toHaveBeenCalledWith('rpc', expect.objectContaining({
127
+ jsonrpc: '2.0',
128
+ method: 'terminal.bufferRefresh',
129
+ params: expect.objectContaining({
130
+ buffer: 'serialized-buffer-content',
131
+ }),
132
+ }));
133
+ });
134
+ it('should set up PTY data handler', async () => {
135
+ await service.handle('create', { cols: 80, rows: 24 });
136
+ expect(mockPtyProcess.onData).toHaveBeenCalled();
137
+ });
138
+ it('should set up PTY exit handler', async () => {
139
+ await service.handle('create', { cols: 80, rows: 24 });
140
+ expect(mockPtyProcess.onExit).toHaveBeenCalled();
141
+ });
142
+ it('should enforce terminal limit', async () => {
143
+ // Create service with limit of 2
144
+ service = new TerminalService(() => mockSocket, 2, 10000, '/test/root');
145
+ // Create 2 terminals successfully
146
+ await service.handle('create', { cols: 80, rows: 24 });
147
+ await service.handle('create', { cols: 80, rows: 24 });
148
+ // Third should fail
149
+ await expect(service.handle('create', { cols: 80, rows: 24 })).rejects.toMatchObject({
150
+ code: ErrorCode.TERMINAL_LIMIT_EXCEEDED,
151
+ message: expect.stringContaining('Terminal limit exceeded'),
152
+ });
153
+ });
154
+ });
155
+ describe('Terminal Activation', () => {
156
+ let terminalId;
157
+ beforeEach(async () => {
158
+ terminalId = await service.handle('create', { cols: 80, rows: 24 });
159
+ // Clear emit calls from creation
160
+ mockSocket.emit.mockClear();
161
+ });
162
+ it('should activate terminal', async () => {
163
+ await service.handle('activate', { terminalId });
164
+ // Should send buffer refresh
165
+ expect(mockSocket.emit).toHaveBeenCalledWith('rpc', expect.objectContaining({
166
+ method: 'terminal.bufferRefresh',
167
+ params: expect.objectContaining({
168
+ terminalId,
169
+ buffer: 'serialized-buffer-content',
170
+ }),
171
+ }));
172
+ });
173
+ it('should stream output from active terminal only', async () => {
174
+ // Create unique mocks for each terminal
175
+ let dataHandler1 = null;
176
+ let dataHandler2 = null;
177
+ const mockPty1 = {
178
+ onData: vi.fn((handler) => { dataHandler1 = handler; }),
179
+ onExit: vi.fn(),
180
+ write: vi.fn(),
181
+ kill: vi.fn(),
182
+ resize: vi.fn(),
183
+ };
184
+ const mockPty2 = {
185
+ onData: vi.fn((handler) => { dataHandler2 = handler; }),
186
+ onExit: vi.fn(),
187
+ write: vi.fn(),
188
+ kill: vi.fn(),
189
+ resize: vi.fn(),
190
+ };
191
+ // Return different mocks for each spawn call
192
+ pty.spawn
193
+ .mockReturnValueOnce(mockPty1)
194
+ .mockReturnValueOnce(mockPty2);
195
+ // Create two terminals
196
+ const terminal1 = await service.handle('create', { cols: 80, rows: 24 });
197
+ await service.handle('create', { cols: 80, rows: 24 });
198
+ mockSocket.emit.mockClear();
199
+ // Activate terminal1
200
+ await service.handle('activate', { terminalId: terminal1 });
201
+ mockSocket.emit.mockClear();
202
+ // Simulate output from terminal1 (active)
203
+ dataHandler1('output from terminal 1');
204
+ // Simulate output from terminal2 (inactive)
205
+ dataHandler2('output from terminal 2');
206
+ // Only terminal1 should stream
207
+ const outputCalls = mockSocket.emit.mock.calls.filter((call) => call[1].method === 'terminal.output');
208
+ // Should only have output from terminal1
209
+ expect(outputCalls.length).toBe(1);
210
+ expect(outputCalls[0][1].params.terminalId).toBe(terminal1);
211
+ expect(outputCalls[0][1].params.data).toBe('output from terminal 1');
212
+ });
213
+ it('should throw error for non-existent terminal', async () => {
214
+ await expect(service.handle('activate', { terminalId: 'invalid-id' })).rejects.toMatchObject({
215
+ code: ErrorCode.TERMINAL_NOT_FOUND,
216
+ message: 'Terminal not found',
217
+ });
218
+ });
219
+ it('should prevent activation of other user terminals', async () => {
220
+ // Create terminal for user-123
221
+ const terminal1 = await service.handle('create', { cols: 80, rows: 24 });
222
+ // Create new service for different user
223
+ const otherSocket = {
224
+ ...mockSocket,
225
+ data: { uid: 'different-user', deviceId: 'different-device' },
226
+ };
227
+ const otherService = new TerminalService(() => otherSocket, 10, 10000, '/test/root');
228
+ // Try to activate terminal from different user
229
+ await expect(otherService.handle('activate', { terminalId: terminal1 })).rejects.toMatchObject({
230
+ code: ErrorCode.TERMINAL_NOT_FOUND,
231
+ });
232
+ });
233
+ });
234
+ describe('Sending Input', () => {
235
+ let terminalId;
236
+ beforeEach(async () => {
237
+ terminalId = await service.handle('create', { cols: 80, rows: 24 });
238
+ });
239
+ it('should send input to terminal', async () => {
240
+ await service.handle('send', { terminalId, data: 'ls\n' });
241
+ expect(mockPtyProcess.write).toHaveBeenCalledWith('ls\n');
242
+ });
243
+ it('should throw error for non-existent terminal', async () => {
244
+ await expect(service.handle('send', { terminalId: 'invalid-id', data: 'test' })).rejects.toMatchObject({
245
+ code: ErrorCode.TERMINAL_NOT_FOUND,
246
+ });
247
+ });
248
+ it('should throw error when sending to exited terminal', async () => {
249
+ // Simulate terminal exit
250
+ if (ptyExitHandler) {
251
+ ptyExitHandler({ exitCode: 0 });
252
+ }
253
+ await expect(service.handle('send', { terminalId, data: 'test' })).rejects.toMatchObject({
254
+ code: ErrorCode.TERMINAL_PROCESS_EXITED,
255
+ message: 'Terminal process has exited',
256
+ });
257
+ });
258
+ it('should prevent sending to other user terminals', async () => {
259
+ const otherSocket = {
260
+ ...mockSocket,
261
+ data: { uid: 'different-user', deviceId: 'different-device' },
262
+ };
263
+ const otherService = new TerminalService(() => otherSocket, 10, 10000, '/test/root');
264
+ await expect(otherService.handle('send', { terminalId, data: 'test' })).rejects.toMatchObject({
265
+ code: ErrorCode.TERMINAL_NOT_FOUND,
266
+ });
267
+ });
268
+ });
269
+ describe('Resizing Terminal', () => {
270
+ let terminalId;
271
+ beforeEach(async () => {
272
+ terminalId = await service.handle('create', { cols: 80, rows: 24 });
273
+ });
274
+ it('should resize terminal', async () => {
275
+ await service.handle('resize', { terminalId, cols: 120, rows: 30 });
276
+ expect(mockPtyProcess.resize).toHaveBeenCalledWith(120, 30);
277
+ expect(mockXterm.resize).toHaveBeenCalledWith(120, 30);
278
+ });
279
+ it('should throw error for non-existent terminal', async () => {
280
+ await expect(service.handle('resize', { terminalId: 'invalid-id', cols: 100, rows: 50 })).rejects.toMatchObject({
281
+ code: ErrorCode.TERMINAL_NOT_FOUND,
282
+ });
283
+ });
284
+ it('should prevent resizing other user terminals', async () => {
285
+ const otherSocket = {
286
+ ...mockSocket,
287
+ data: { uid: 'different-user', deviceId: 'different-device' },
288
+ };
289
+ const otherService = new TerminalService(() => otherSocket, 10, 10000, '/test/root');
290
+ await expect(otherService.handle('resize', { terminalId, cols: 100, rows: 50 })).rejects.toMatchObject({
291
+ code: ErrorCode.TERMINAL_NOT_FOUND,
292
+ });
293
+ });
294
+ });
295
+ describe('Terminal Destruction', () => {
296
+ let terminalId;
297
+ beforeEach(async () => {
298
+ terminalId = await service.handle('create', { cols: 80, rows: 24 });
299
+ });
300
+ it('should destroy terminal session', async () => {
301
+ await service.handle('destroy', { terminalId });
302
+ expect(mockPtyProcess.kill).toHaveBeenCalled();
303
+ // Terminal should no longer exist
304
+ await expect(service.handle('send', { terminalId, data: 'test' })).rejects.toMatchObject({
305
+ code: ErrorCode.TERMINAL_NOT_FOUND,
306
+ });
307
+ });
308
+ it('should throw error for non-existent terminal', async () => {
309
+ await expect(service.handle('destroy', { terminalId: 'invalid-id' })).rejects.toMatchObject({
310
+ code: ErrorCode.TERMINAL_NOT_FOUND,
311
+ });
312
+ });
313
+ it('should not kill process if already exited', async () => {
314
+ // Simulate exit
315
+ if (ptyExitHandler) {
316
+ ptyExitHandler({ exitCode: 0 });
317
+ }
318
+ mockPtyProcess.kill.mockClear();
319
+ await service.handle('destroy', { terminalId });
320
+ expect(mockPtyProcess.kill).not.toHaveBeenCalled();
321
+ });
322
+ it('should clear active terminal ID if destroying active terminal', async () => {
323
+ await service.handle('activate', { terminalId });
324
+ await service.handle('destroy', { terminalId });
325
+ // Create and activate new terminal should work
326
+ const newTerminalId = await service.handle('create', { cols: 80, rows: 24 });
327
+ await expect(service.handle('activate', { terminalId: newTerminalId })).resolves.toBeUndefined();
328
+ });
329
+ it('should prevent destroying other user terminals', async () => {
330
+ const otherSocket = {
331
+ ...mockSocket,
332
+ data: { uid: 'different-user', deviceId: 'different-device' },
333
+ };
334
+ const otherService = new TerminalService(() => otherSocket, 10, 10000, '/test/root');
335
+ await expect(otherService.handle('destroy', { terminalId })).rejects.toMatchObject({
336
+ code: ErrorCode.TERMINAL_NOT_FOUND,
337
+ });
338
+ });
339
+ });
340
+ describe('Buffer Refresh', () => {
341
+ let terminalId;
342
+ beforeEach(async () => {
343
+ terminalId = await service.handle('create', { cols: 80, rows: 24 });
344
+ mockSocket.emit.mockClear();
345
+ });
346
+ it('should refresh terminal buffer', async () => {
347
+ await service.handle('refresh', { terminalId });
348
+ expect(mockSocket.emit).toHaveBeenCalledWith('rpc', expect.objectContaining({
349
+ method: 'terminal.bufferRefresh',
350
+ params: expect.objectContaining({
351
+ terminalId,
352
+ buffer: 'serialized-buffer-content',
353
+ }),
354
+ }));
355
+ });
356
+ it('should throw error for non-existent terminal', async () => {
357
+ await expect(service.handle('refresh', { terminalId: 'invalid-id' })).rejects.toMatchObject({
358
+ code: ErrorCode.TERMINAL_NOT_FOUND,
359
+ });
360
+ });
361
+ it('should prevent refreshing other user terminals', async () => {
362
+ const otherSocket = {
363
+ ...mockSocket,
364
+ data: { uid: 'different-user', deviceId: 'different-device' },
365
+ };
366
+ const otherService = new TerminalService(() => otherSocket, 10, 10000, '/test/root');
367
+ await expect(otherService.handle('refresh', { terminalId })).rejects.toMatchObject({
368
+ code: ErrorCode.TERMINAL_NOT_FOUND,
369
+ });
370
+ });
371
+ });
372
+ describe('Terminal List', () => {
373
+ it('should list all terminals for user', async () => {
374
+ const terminal1 = await service.handle('create', { cols: 80, rows: 24 });
375
+ const terminal2 = await service.handle('create', { cols: 80, rows: 24 });
376
+ const result = await service.handle('list', {});
377
+ expect(result).toEqual([terminal1, terminal2]);
378
+ });
379
+ it('should return empty array when no terminals exist', async () => {
380
+ const result = await service.handle('list', {});
381
+ expect(result).toEqual([]);
382
+ });
383
+ it('should only list terminals for current user', async () => {
384
+ // Create terminal for user-123
385
+ await service.handle('create', { cols: 80, rows: 24 });
386
+ // Create service for different user
387
+ const otherSocket = {
388
+ ...mockSocket,
389
+ data: { uid: 'different-user', deviceId: 'different-device' },
390
+ };
391
+ const otherService = new TerminalService(() => otherSocket, 10, 10000, '/test/root');
392
+ // Different user should see empty list
393
+ const result = await otherService.handle('list', {});
394
+ expect(result).toEqual([]);
395
+ });
396
+ });
397
+ describe('PTY Output Handling', () => {
398
+ let terminalId;
399
+ beforeEach(async () => {
400
+ terminalId = await service.handle('create', { cols: 80, rows: 24 });
401
+ mockSocket.emit.mockClear();
402
+ });
403
+ it('should write PTY output to xterm buffer', async () => {
404
+ // Simulate PTY output
405
+ if (ptyDataHandler) {
406
+ ptyDataHandler('hello world');
407
+ }
408
+ expect(mockXterm.write).toHaveBeenCalledWith('hello world', expect.any(Function));
409
+ });
410
+ it('should stream output to client when terminal is active', async () => {
411
+ await service.handle('activate', { terminalId });
412
+ mockSocket.emit.mockClear();
413
+ // Simulate PTY output
414
+ if (ptyDataHandler) {
415
+ ptyDataHandler('hello world');
416
+ }
417
+ expect(mockSocket.emit).toHaveBeenCalledWith('rpc', expect.objectContaining({
418
+ method: 'terminal.output',
419
+ params: {
420
+ terminalId,
421
+ data: 'hello world',
422
+ },
423
+ }));
424
+ });
425
+ it('should not stream output when terminal is inactive', async () => {
426
+ // Don't activate, just simulate output
427
+ if (ptyDataHandler) {
428
+ ptyDataHandler('hello world');
429
+ }
430
+ const outputCalls = mockSocket.emit.mock.calls.filter((call) => call[1].method === 'terminal.output');
431
+ expect(outputCalls.length).toBe(0);
432
+ });
433
+ });
434
+ describe('PTY Exit Handling', () => {
435
+ let terminalId;
436
+ beforeEach(async () => {
437
+ terminalId = await service.handle('create', { cols: 80, rows: 24 });
438
+ mockSocket.emit.mockClear();
439
+ });
440
+ it('should notify client when terminal exits', async () => {
441
+ // Simulate PTY exit
442
+ if (ptyExitHandler) {
443
+ ptyExitHandler({ exitCode: 0 });
444
+ }
445
+ expect(mockSocket.emit).toHaveBeenCalledWith('rpc', expect.objectContaining({
446
+ method: 'terminal.exited',
447
+ params: {
448
+ terminalId,
449
+ exitCode: 0,
450
+ },
451
+ }));
452
+ });
453
+ it('should mark terminal as exited', async () => {
454
+ // Simulate PTY exit
455
+ if (ptyExitHandler) {
456
+ ptyExitHandler({ exitCode: 127 });
457
+ }
458
+ // Sending to exited terminal should fail
459
+ await expect(service.handle('send', { terminalId, data: 'test' })).rejects.toMatchObject({
460
+ code: ErrorCode.TERMINAL_PROCESS_EXITED,
461
+ });
462
+ });
463
+ it('should preserve buffer after exit', async () => {
464
+ // Simulate PTY exit
465
+ if (ptyExitHandler) {
466
+ ptyExitHandler({ exitCode: 0 });
467
+ }
468
+ // Should still be able to refresh buffer
469
+ await expect(service.handle('refresh', { terminalId })).resolves.toBeUndefined();
470
+ expect(mockSocket.emit).toHaveBeenCalledWith('rpc', expect.objectContaining({
471
+ method: 'terminal.bufferRefresh',
472
+ }));
473
+ });
474
+ });
475
+ describe('Error Handling', () => {
476
+ it('should throw error for unknown method', async () => {
477
+ await expect(service.handle('unknownMethod', {})).rejects.toMatchObject({
478
+ code: ErrorCode.METHOD_NOT_FOUND,
479
+ message: expect.stringContaining('Method not found'),
480
+ });
481
+ });
482
+ });
483
+ describe('Cleanup', () => {
484
+ it('should kill all terminals on cleanup', async () => {
485
+ await service.handle('create', { cols: 80, rows: 24 });
486
+ await service.handle('create', { cols: 80, rows: 24 });
487
+ // Clear previous calls
488
+ mockPtyProcess.kill.mockClear();
489
+ service.cleanup();
490
+ // Both terminals should be killed
491
+ expect(mockPtyProcess.kill).toHaveBeenCalledTimes(2);
492
+ });
493
+ it('should not kill already exited terminals', async () => {
494
+ await service.handle('create', { cols: 80, rows: 24 });
495
+ // Simulate exit
496
+ if (ptyExitHandler) {
497
+ ptyExitHandler({ exitCode: 0 });
498
+ }
499
+ mockPtyProcess.kill.mockClear();
500
+ service.cleanup();
501
+ // Should not kill exited terminal
502
+ expect(mockPtyProcess.kill).not.toHaveBeenCalled();
503
+ });
504
+ it('should clear all sessions', async () => {
505
+ await service.handle('create', { cols: 80, rows: 24 });
506
+ await service.handle('create', { cols: 80, rows: 24 });
507
+ service.cleanup();
508
+ const result = await service.handle('list', {});
509
+ expect(result).toEqual([]);
510
+ });
511
+ });
512
+ });
513
+ //# sourceMappingURL=TerminalService.test.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Interactive setup wizard for spck-cli
3
+ * Configures CLI to connect to proxy server
4
+ */
5
+ import { ServerConfig } from '../types.js';
6
+ /**
7
+ * Run the setup wizard
8
+ */
9
+ export declare function runSetup(configPath?: string): Promise<ServerConfig>;
10
+ //# sourceMappingURL=wizard.d.ts.map