centaurus-cli 2.7.3 → 2.8.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 (97) hide show
  1. package/dist/cli-adapter.d.ts +10 -6
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +613 -154
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/slash-commands.d.ts.map +1 -1
  6. package/dist/config/slash-commands.js +1 -0
  7. package/dist/config/slash-commands.js.map +1 -1
  8. package/dist/context/context-manager.d.ts +4 -1
  9. package/dist/context/context-manager.d.ts.map +1 -1
  10. package/dist/context/context-manager.js +30 -7
  11. package/dist/context/context-manager.js.map +1 -1
  12. package/dist/context/handlers/wsl-handler.d.ts +10 -0
  13. package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
  14. package/dist/context/handlers/wsl-handler.js +31 -2
  15. package/dist/context/handlers/wsl-handler.js.map +1 -1
  16. package/dist/index.js +33 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/services/ai-service-client.d.ts +1 -0
  19. package/dist/services/ai-service-client.d.ts.map +1 -1
  20. package/dist/services/ai-service-client.js +20 -0
  21. package/dist/services/ai-service-client.js.map +1 -1
  22. package/dist/tools/command.d.ts.map +1 -1
  23. package/dist/tools/command.js +136 -21
  24. package/dist/tools/command.js.map +1 -1
  25. package/dist/tools/file-ops.d.ts +1 -0
  26. package/dist/tools/file-ops.d.ts.map +1 -1
  27. package/dist/tools/file-ops.js +144 -3
  28. package/dist/tools/file-ops.js.map +1 -1
  29. package/dist/tools/inspect-symbol.js +27 -27
  30. package/dist/tools/inspect-symbol.js.map +1 -1
  31. package/dist/tools/plan-mode.d.ts +55 -19
  32. package/dist/tools/plan-mode.d.ts.map +1 -1
  33. package/dist/tools/plan-mode.js +204 -123
  34. package/dist/tools/plan-mode.js.map +1 -1
  35. package/dist/tools/types.d.ts +1 -1
  36. package/dist/tools/types.d.ts.map +1 -1
  37. package/dist/types/index.d.ts +11 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/ui/components/App.d.ts +6 -5
  40. package/dist/ui/components/App.d.ts.map +1 -1
  41. package/dist/ui/components/App.js +277 -125
  42. package/dist/ui/components/App.js.map +1 -1
  43. package/dist/ui/components/InputBox.d.ts.map +1 -1
  44. package/dist/ui/components/InputBox.js +24 -5
  45. package/dist/ui/components/InputBox.js.map +1 -1
  46. package/dist/ui/components/InteractiveShell.d.ts +2 -1
  47. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  48. package/dist/ui/components/InteractiveShell.js +41 -106
  49. package/dist/ui/components/InteractiveShell.js.map +1 -1
  50. package/dist/ui/components/MarkdownRenderer.d.ts.map +1 -1
  51. package/dist/ui/components/MarkdownRenderer.js +12 -8
  52. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  53. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  54. package/dist/ui/components/MessageDisplay.js +11 -3
  55. package/dist/ui/components/MessageDisplay.js.map +1 -1
  56. package/dist/ui/components/PlanAcceptedMessage.d.ts +12 -0
  57. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -0
  58. package/dist/ui/components/PlanAcceptedMessage.js +22 -0
  59. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -0
  60. package/dist/ui/components/PlanReviewScreen.d.ts +14 -0
  61. package/dist/ui/components/PlanReviewScreen.d.ts.map +1 -0
  62. package/dist/ui/components/PlanReviewScreen.js +52 -0
  63. package/dist/ui/components/PlanReviewScreen.js.map +1 -0
  64. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  65. package/dist/ui/components/StreamingMessageDisplay.js +5 -5
  66. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  67. package/dist/ui/components/TaskCompletedMessage.d.ts +14 -0
  68. package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -0
  69. package/dist/ui/components/TaskCompletedMessage.js +25 -0
  70. package/dist/ui/components/TaskCompletedMessage.js.map +1 -0
  71. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  72. package/dist/ui/components/ToolExecutionMessage.js +174 -17
  73. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  74. package/dist/utils/conversation-logger.d.ts +127 -0
  75. package/dist/utils/conversation-logger.d.ts.map +1 -0
  76. package/dist/utils/conversation-logger.js +283 -0
  77. package/dist/utils/conversation-logger.js.map +1 -0
  78. package/dist/utils/editor-utils.d.ts +87 -0
  79. package/dist/utils/editor-utils.d.ts.map +1 -0
  80. package/dist/utils/editor-utils.js +712 -0
  81. package/dist/utils/editor-utils.js.map +1 -0
  82. package/dist/utils/input-classifier.d.ts.map +1 -1
  83. package/dist/utils/input-classifier.js +12 -4
  84. package/dist/utils/input-classifier.js.map +1 -1
  85. package/dist/utils/markdown-parser.d.ts.map +1 -1
  86. package/dist/utils/markdown-parser.js +4 -2
  87. package/dist/utils/markdown-parser.js.map +1 -1
  88. package/dist/utils/shell.d.ts +32 -1
  89. package/dist/utils/shell.d.ts.map +1 -1
  90. package/dist/utils/shell.js +97 -161
  91. package/dist/utils/shell.js.map +1 -1
  92. package/dist/utils/syntax-checker.d.ts +24 -0
  93. package/dist/utils/syntax-checker.d.ts.map +1 -0
  94. package/dist/utils/syntax-checker.js +320 -0
  95. package/dist/utils/syntax-checker.js.map +1 -0
  96. package/package.json +4 -3
  97. package/prompts/system-prompt-autonomous.md +0 -377
@@ -0,0 +1,712 @@
1
+ import * as os from 'os';
2
+ import { createRequire } from 'module';
3
+ // Use createRequire for ESM compatibility with native modules
4
+ const require = createRequire(import.meta.url);
5
+ const nodePty = require('@homebridge/node-pty-prebuilt-multiarch');
6
+ /**
7
+ * List of interactive editor commands that require full terminal control
8
+ */
9
+ const INTERACTIVE_EDITORS = [
10
+ 'vim', 'nvim', 'vi', 'nano', 'emacs', 'micro', 'helix', 'hx',
11
+ 'pico', 'joe', 'ne', 'mcedit'
12
+ ];
13
+ /**
14
+ * Patterns that indicate an editor will be opened
15
+ */
16
+ const EDITOR_PATTERNS = [
17
+ /^git\s+rebase\s+-i/, // git rebase -i (interactive)
18
+ /^crontab\s+-e/, // crontab -e (opens editor)
19
+ /^visudo/, // visudo (opens editor)
20
+ /^vipw/, // vipw (opens editor)
21
+ /^sudoedit/, // sudoedit (opens editor)
22
+ ];
23
+ /**
24
+ * Check if a command is an interactive editor that needs full terminal control
25
+ */
26
+ export function isInteractiveEditorCommand(command) {
27
+ const trimmed = command.trim();
28
+ const parts = trimmed.split(/\s+/);
29
+ const baseCommand = parts[0].toLowerCase();
30
+ // Direct editor commands
31
+ if (INTERACTIVE_EDITORS.includes(baseCommand)) {
32
+ return true;
33
+ }
34
+ // Check patterns
35
+ for (const pattern of EDITOR_PATTERNS) {
36
+ if (pattern.test(trimmed)) {
37
+ return true;
38
+ }
39
+ }
40
+ // Handle sudo/doas prefix
41
+ if (baseCommand === 'sudo' || baseCommand === 'doas') {
42
+ const actualCommand = parts.slice(1).join(' ');
43
+ return isInteractiveEditorCommand(actualCommand);
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Check if the command is specifically for nano editor (the only remote-supported editor currently)
49
+ */
50
+ export function isNanoEditor(command) {
51
+ const trimmed = command.trim();
52
+ const parts = trimmed.split(/\s+/);
53
+ let baseCommand = parts[0].toLowerCase();
54
+ // Handle sudo/doas prefix
55
+ if (baseCommand === 'sudo' || baseCommand === 'doas') {
56
+ baseCommand = parts[1]?.toLowerCase() || '';
57
+ }
58
+ return baseCommand === 'nano' || baseCommand === 'pico'; // pico is often aliased to nano
59
+ }
60
+ /**
61
+ * Get the display name of an editor command for user-friendly messages
62
+ */
63
+ export function getEditorName(command) {
64
+ const trimmed = command.trim();
65
+ const parts = trimmed.split(/\s+/);
66
+ let baseCommand = parts[0].toLowerCase();
67
+ // Handle sudo/doas prefix
68
+ if (baseCommand === 'sudo' || baseCommand === 'doas') {
69
+ baseCommand = parts[1]?.toLowerCase() || baseCommand;
70
+ }
71
+ // Map to display names
72
+ const displayNames = {
73
+ 'vim': 'Vim',
74
+ 'nvim': 'Neovim',
75
+ 'vi': 'Vi',
76
+ 'nano': 'Nano',
77
+ 'emacs': 'Emacs',
78
+ 'micro': 'Micro',
79
+ 'helix': 'Helix',
80
+ 'hx': 'Helix',
81
+ 'pico': 'Pico',
82
+ 'joe': 'Joe',
83
+ 'ne': 'Ne',
84
+ 'mcedit': 'Midnight Commander Editor',
85
+ };
86
+ return displayNames[baseCommand] || baseCommand;
87
+ }
88
+ /**
89
+ * Get the shell command for the current platform.
90
+ * On Windows, prefers Git Bash if available for better terminal emulation.
91
+ */
92
+ function getShellCommand() {
93
+ if (os.platform() !== 'win32') {
94
+ return process.env.SHELL || '/bin/bash';
95
+ }
96
+ // On Windows, try to find Git Bash for better terminal emulation
97
+ const gitBashPath = findGitBash();
98
+ if (gitBashPath) {
99
+ return gitBashPath;
100
+ }
101
+ // Fallback to PowerShell
102
+ return 'powershell.exe';
103
+ }
104
+ /**
105
+ * Find Git Bash on Windows. Returns null if not found.
106
+ * Git Bash provides proper Unix terminal emulation for editors like nano.
107
+ */
108
+ function findGitBash() {
109
+ const possiblePaths = [
110
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
111
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
112
+ process.env.PROGRAMFILES ? `${process.env.PROGRAMFILES}\\Git\\bin\\bash.exe` : null,
113
+ process.env['PROGRAMFILES(X86)'] ? `${process.env['PROGRAMFILES(X86)']}\\Git\\bin\\bash.exe` : null,
114
+ // User-specific installation
115
+ process.env.LOCALAPPDATA ? `${process.env.LOCALAPPDATA}\\Programs\\Git\\bin\\bash.exe` : null,
116
+ ].filter(Boolean);
117
+ for (const bashPath of possiblePaths) {
118
+ try {
119
+ const fs = require('fs');
120
+ if (fs.existsSync(bashPath)) {
121
+ return bashPath;
122
+ }
123
+ }
124
+ catch {
125
+ // Continue to next path
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * Check if we should use bash-style arguments for the shell
132
+ */
133
+ function usesBashArgs(shell) {
134
+ return shell.includes('bash') || shell.includes('sh');
135
+ }
136
+ /**
137
+ * Common terminal reset sequences for cleanup after editor exits
138
+ */
139
+ function getTerminalResetSequences() {
140
+ return [
141
+ '\x1b[?1049l', // Exit alternate screen buffer
142
+ '\x1b[?25l', // HIDE cursor (let Ink show it in the right place)
143
+ '\x1b[0m', // Reset text attributes (colors, bold, etc.)
144
+ '\x1b[?1000l', // Disable mouse tracking (X10)
145
+ '\x1b[?1002l', // Disable mouse button tracking
146
+ '\x1b[?1003l', // Disable mouse any-event tracking
147
+ '\x1b[?1006l', // Disable SGR mouse mode
148
+ '\x1b[?2004l', // Disable bracketed paste mode
149
+ '\x1b[?1u', // Disable CSI u mode (extended key reporting) - some terminals
150
+ '\x1b[>4;0m', // Disable modifyOtherKeys mode
151
+ '\x1b[?66l', // Disable application keypad mode
152
+ '\x1b[?1l', // Disable cursor keys application mode
153
+ '\x1bc', // Full terminal reset (RIS - Reset to Initial State)
154
+ '\x1b[?25l', // Hide cursor again after reset (reset shows it)
155
+ ].join('');
156
+ }
157
+ /**
158
+ * Common setup for interactive editor: raw stdin mode and handlers
159
+ */
160
+ function setupInteractiveMode(ptyProcess, onExit) {
161
+ let isRunning = true;
162
+ // Save original stdin settings
163
+ const wasRaw = process.stdin.isRaw;
164
+ // Set stdin to raw mode for direct key passthrough
165
+ if (process.stdin.isTTY) {
166
+ process.stdin.setRawMode(true);
167
+ }
168
+ process.stdin.resume();
169
+ // Pipe stdin to PTY (user keys -> editor)
170
+ const stdinHandler = (data) => {
171
+ if (isRunning) {
172
+ ptyProcess.write(data.toString());
173
+ }
174
+ };
175
+ process.stdin.on('data', stdinHandler);
176
+ // Pipe PTY output to stdout (editor display -> terminal)
177
+ ptyProcess.onData((data) => {
178
+ if (isRunning) {
179
+ process.stdout.write(data);
180
+ }
181
+ });
182
+ // Handle terminal resize
183
+ const resizeHandler = () => {
184
+ if (isRunning) {
185
+ const newCols = process.stdout.columns || 80;
186
+ const newRows = process.stdout.rows || 24;
187
+ try {
188
+ ptyProcess.resize(newCols, newRows);
189
+ }
190
+ catch (e) {
191
+ // Ignore resize errors
192
+ }
193
+ }
194
+ };
195
+ process.stdout.on('resize', resizeHandler);
196
+ // Handle PTY exit
197
+ ptyProcess.onExit(({ exitCode }) => {
198
+ isRunning = false;
199
+ // CRITICAL: Remove our stdin handler FIRST
200
+ process.stdin.removeListener('data', stdinHandler);
201
+ process.stdout.removeListener('resize', resizeHandler);
202
+ // Restore stdin to original raw mode setting
203
+ if (process.stdin.isTTY) {
204
+ process.stdin.setRawMode(wasRaw ?? false);
205
+ }
206
+ // COMPREHENSIVE TERMINAL RESET
207
+ process.stdout.write(getTerminalResetSequences());
208
+ // Give terminal time to process reset sequences
209
+ setTimeout(() => {
210
+ onExit(exitCode);
211
+ }, 200);
212
+ });
213
+ return {
214
+ cleanup: () => {
215
+ process.stdin.removeListener('data', stdinHandler);
216
+ process.stdout.removeListener('resize', resizeHandler);
217
+ if (process.stdin.isTTY) {
218
+ process.stdin.setRawMode(wasRaw ?? false);
219
+ }
220
+ },
221
+ kill: () => {
222
+ if (isRunning) {
223
+ isRunning = false;
224
+ try {
225
+ ptyProcess.kill();
226
+ }
227
+ catch (e) {
228
+ // Ignore kill errors
229
+ }
230
+ }
231
+ }
232
+ };
233
+ }
234
+ /**
235
+ * Run an interactive editor with full terminal control (LOCAL mode).
236
+ * This function:
237
+ * 1. Clears the screen and enters alternate screen buffer
238
+ * 2. Spawns a PTY process
239
+ * 3. Pipes stdin directly to the PTY (raw mode)
240
+ * 4. Pipes PTY output directly to stdout
241
+ * 5. Handles cleanup on exit
242
+ *
243
+ * Uses alternate screen buffer to prevent chat history from showing through.
244
+ */
245
+ export function runInteractiveEditor(command, cwd, onExit) {
246
+ const shell = getShellCommand();
247
+ const isWindows = os.platform() === 'win32';
248
+ // Use bash-style args for Git Bash, PowerShell-style only if falling back to PowerShell
249
+ const isBash = usesBashArgs(shell);
250
+ const args = isBash ? ['-c', command] : (isWindows ? ['-Command', command] : ['-c', command]);
251
+ // Get terminal dimensions
252
+ const cols = process.stdout.columns || 80;
253
+ const rows = process.stdout.rows || 24;
254
+ // Enter alternate screen buffer and clear screen to hide chat history
255
+ // This combined with cygwin terminal type provides proper display on Windows
256
+ const enterAlternateBuffer = [
257
+ '\x1b[?1049h', // Enter alternate screen buffer
258
+ '\x1b[2J', // Clear entire screen
259
+ '\x1b[H', // Move cursor to home position
260
+ ].join('');
261
+ process.stdout.write(enterAlternateBuffer);
262
+ // Spawn PTY process - use 'cygwin' terminal type on Windows for better compatibility
263
+ // 'xterm-256color' can cause issues with some Windows terminal configurations
264
+ const ptyProcess = nodePty.spawn(shell, args, {
265
+ name: isWindows ? 'cygwin' : 'xterm-256color',
266
+ cols,
267
+ rows,
268
+ cwd,
269
+ env: {
270
+ ...process.env,
271
+ // Use cygwin TERM on Windows for better nano compatibility
272
+ TERM: isWindows ? 'cygwin' : 'xterm-256color',
273
+ COLORTERM: 'truecolor',
274
+ },
275
+ });
276
+ let isRunning = true;
277
+ // Save original stdin settings
278
+ const wasRaw = process.stdin.isRaw;
279
+ // Set stdin to raw mode for direct key passthrough
280
+ if (process.stdin.isTTY) {
281
+ process.stdin.setRawMode(true);
282
+ }
283
+ process.stdin.resume();
284
+ // Pipe stdin to PTY (user keys -> editor)
285
+ const stdinHandler = (data) => {
286
+ if (isRunning) {
287
+ ptyProcess.write(data.toString());
288
+ }
289
+ };
290
+ process.stdin.on('data', stdinHandler);
291
+ // Pipe PTY output to stdout (editor display -> terminal)
292
+ ptyProcess.onData((data) => {
293
+ if (isRunning) {
294
+ process.stdout.write(data);
295
+ }
296
+ });
297
+ // Handle terminal resize
298
+ const resizeHandler = () => {
299
+ if (isRunning) {
300
+ const newCols = process.stdout.columns || 80;
301
+ const newRows = process.stdout.rows || 24;
302
+ try {
303
+ ptyProcess.resize(newCols, newRows);
304
+ }
305
+ catch (e) {
306
+ // Ignore resize errors
307
+ }
308
+ }
309
+ };
310
+ process.stdout.on('resize', resizeHandler);
311
+ // Handle PTY exit
312
+ ptyProcess.onExit(({ exitCode }) => {
313
+ isRunning = false;
314
+ // CRITICAL: Remove our stdin handler FIRST
315
+ process.stdin.removeListener('data', stdinHandler);
316
+ process.stdout.removeListener('resize', resizeHandler);
317
+ // Restore stdin to original raw mode setting
318
+ if (process.stdin.isTTY) {
319
+ process.stdin.setRawMode(wasRaw ?? false);
320
+ }
321
+ // COMPREHENSIVE TERMINAL RESET
322
+ process.stdout.write(getTerminalResetSequences());
323
+ // Give terminal time to process reset sequences
324
+ setTimeout(() => {
325
+ onExit(exitCode);
326
+ }, 200);
327
+ });
328
+ return {
329
+ kill: () => {
330
+ if (isRunning) {
331
+ isRunning = false;
332
+ try {
333
+ ptyProcess.kill();
334
+ }
335
+ catch (e) {
336
+ // Ignore kill errors
337
+ }
338
+ }
339
+ }
340
+ };
341
+ }
342
+ /**
343
+ * Run an interactive editor in WSL session via node-pty.
344
+ * Uses wsl.exe to execute the editor command in the specified distribution.
345
+ */
346
+ export function runWSLEditor(distribution, command, cwd, onExit) {
347
+ // Get terminal dimensions
348
+ const cols = process.stdout.columns || 80;
349
+ const rows = process.stdout.rows || 24;
350
+ // Build WSL command - cd to directory and run editor
351
+ // wsl.exe -d <distro> -- bash -c "cd <path> && <command>"
352
+ const wslArgs = [
353
+ '-d', distribution,
354
+ '--',
355
+ 'bash', '-c',
356
+ `cd "${cwd}" && ${command}`
357
+ ];
358
+ // Spawn PTY process with wsl.exe
359
+ const ptyProcess = nodePty.spawn('wsl.exe', wslArgs, {
360
+ name: 'xterm-256color',
361
+ cols,
362
+ rows,
363
+ cwd: process.cwd(), // Use current Windows cwd
364
+ env: {
365
+ ...process.env,
366
+ TERM: 'xterm-256color',
367
+ COLORTERM: 'truecolor',
368
+ },
369
+ });
370
+ const { kill } = setupInteractiveMode(ptyProcess, onExit);
371
+ return { kill };
372
+ }
373
+ /**
374
+ * Run a command in WSL session via node-pty with streaming output.
375
+ * Unlike runWSLEditor, this doesn't take over the terminal - it streams output to a callback.
376
+ * This is essential for sudo commands that need TTY for password prompts.
377
+ */
378
+ export function runWSLCommand(distribution, command, cwd, onData, onExit) {
379
+ // Get terminal dimensions
380
+ const cols = process.stdout.columns || 80;
381
+ const rows = process.stdout.rows || 24;
382
+ // Build WSL command - cd to directory and run command
383
+ const wslArgs = [
384
+ '-d', distribution,
385
+ '--',
386
+ 'bash', '-c',
387
+ `cd "${cwd}" && ${command}`
388
+ ];
389
+ // Spawn PTY process with wsl.exe
390
+ const ptyProcess = nodePty.spawn('wsl.exe', wslArgs, {
391
+ name: 'xterm-256color',
392
+ cols,
393
+ rows,
394
+ cwd: process.cwd(),
395
+ env: {
396
+ ...process.env,
397
+ TERM: 'xterm-256color',
398
+ COLORTERM: 'truecolor',
399
+ },
400
+ });
401
+ let isRunning = true;
402
+ // Stream output to callback
403
+ ptyProcess.onData((data) => {
404
+ if (isRunning) {
405
+ onData(data);
406
+ }
407
+ });
408
+ // Handle exit
409
+ ptyProcess.onExit(({ exitCode }) => {
410
+ isRunning = false;
411
+ onExit(exitCode);
412
+ });
413
+ return {
414
+ write: (data) => {
415
+ if (isRunning) {
416
+ ptyProcess.write(data);
417
+ }
418
+ },
419
+ kill: () => {
420
+ if (isRunning) {
421
+ isRunning = false;
422
+ try {
423
+ ptyProcess.kill();
424
+ }
425
+ catch (e) {
426
+ // Ignore kill errors
427
+ }
428
+ }
429
+ },
430
+ resize: (cols, rows) => {
431
+ if (isRunning) {
432
+ try {
433
+ ptyProcess.resize(cols, rows);
434
+ }
435
+ catch (e) {
436
+ // Ignore resize errors
437
+ }
438
+ }
439
+ },
440
+ isRunning: () => isRunning
441
+ };
442
+ }
443
+ /**
444
+ * Run a command in Docker container via node-pty with streaming output.
445
+ * Unlike runDockerEditor, this doesn't take over the terminal - it streams output to a callback.
446
+ * This is essential for sudo commands that need TTY for password prompts.
447
+ */
448
+ export function runDockerCommand(containerId, command, cwd, onData, onExit) {
449
+ // Get terminal dimensions
450
+ const cols = process.stdout.columns || 80;
451
+ const rows = process.stdout.rows || 24;
452
+ // Build docker exec command
453
+ const dockerArgs = [
454
+ 'exec',
455
+ '-it',
456
+ containerId,
457
+ 'bash', '-c',
458
+ `cd "${cwd}" && ${command}`
459
+ ];
460
+ // Spawn PTY process with docker
461
+ const ptyProcess = nodePty.spawn('docker', dockerArgs, {
462
+ name: 'xterm-256color',
463
+ cols,
464
+ rows,
465
+ cwd: process.cwd(),
466
+ env: {
467
+ ...process.env,
468
+ TERM: 'xterm-256color',
469
+ COLORTERM: 'truecolor',
470
+ },
471
+ });
472
+ let isRunning = true;
473
+ // Stream output to callback
474
+ ptyProcess.onData((data) => {
475
+ if (isRunning) {
476
+ onData(data);
477
+ }
478
+ });
479
+ // Handle exit
480
+ ptyProcess.onExit(({ exitCode }) => {
481
+ isRunning = false;
482
+ onExit(exitCode);
483
+ });
484
+ return {
485
+ write: (data) => {
486
+ if (isRunning) {
487
+ ptyProcess.write(data);
488
+ }
489
+ },
490
+ kill: () => {
491
+ if (isRunning) {
492
+ isRunning = false;
493
+ try {
494
+ ptyProcess.kill();
495
+ }
496
+ catch (e) {
497
+ // Ignore kill errors
498
+ }
499
+ }
500
+ },
501
+ resize: (cols, rows) => {
502
+ if (isRunning) {
503
+ try {
504
+ ptyProcess.resize(cols, rows);
505
+ }
506
+ catch (e) {
507
+ // Ignore resize errors
508
+ }
509
+ }
510
+ },
511
+ isRunning: () => isRunning
512
+ };
513
+ }
514
+ /**
515
+ * Run an interactive editor in Docker container via node-pty.
516
+ * Uses docker exec -it to execute the editor command in the container.
517
+ */
518
+ export function runDockerEditor(containerId, command, cwd, onExit) {
519
+ // Get terminal dimensions
520
+ const cols = process.stdout.columns || 80;
521
+ const rows = process.stdout.rows || 24;
522
+ // Build docker exec command
523
+ // docker exec -it <container> bash -c "cd <path> && <command>"
524
+ const dockerArgs = [
525
+ 'exec',
526
+ '-it',
527
+ containerId,
528
+ 'bash', '-c',
529
+ `cd "${cwd}" && ${command}`
530
+ ];
531
+ // Spawn PTY process with docker
532
+ const ptyProcess = nodePty.spawn('docker', dockerArgs, {
533
+ name: 'xterm-256color',
534
+ cols,
535
+ rows,
536
+ cwd: process.cwd(),
537
+ env: {
538
+ ...process.env,
539
+ TERM: 'xterm-256color',
540
+ COLORTERM: 'truecolor',
541
+ },
542
+ });
543
+ const { kill } = setupInteractiveMode(ptyProcess, onExit);
544
+ return { kill };
545
+ }
546
+ /**
547
+ * Run an interactive editor in SSH session via ssh2's interactive shell.
548
+ * This is more complex as we need to use the ssh2 client's shell() method
549
+ * instead of exec() to get a proper interactive PTY.
550
+ */
551
+ export function runSSHEditor(sshClient, // ssh2 Client
552
+ command, cwd, onExit) {
553
+ let isRunning = true;
554
+ let stream = null;
555
+ // Save original stdin settings
556
+ const wasRaw = process.stdin.isRaw;
557
+ // Get terminal dimensions
558
+ const cols = process.stdout.columns || 80;
559
+ const rows = process.stdout.rows || 24;
560
+ // Request an interactive shell from the SSH server
561
+ sshClient.shell({
562
+ term: 'xterm-256color',
563
+ cols,
564
+ rows,
565
+ }, (err, shellStream) => {
566
+ if (err) {
567
+ console.error('Failed to open SSH shell:', err.message);
568
+ onExit(1);
569
+ return;
570
+ }
571
+ stream = shellStream;
572
+ // Set stdin to raw mode for direct key passthrough
573
+ if (process.stdin.isTTY) {
574
+ process.stdin.setRawMode(true);
575
+ }
576
+ process.stdin.resume();
577
+ // Pipe stdin to SSH stream (user keys -> remote shell)
578
+ const stdinHandler = (data) => {
579
+ if (isRunning && stream) {
580
+ stream.write(data);
581
+ }
582
+ };
583
+ process.stdin.on('data', stdinHandler);
584
+ // Pipe SSH stream output to stdout (remote -> terminal)
585
+ stream.on('data', (data) => {
586
+ if (isRunning) {
587
+ process.stdout.write(data);
588
+ }
589
+ });
590
+ // Handle terminal resize
591
+ const resizeHandler = () => {
592
+ if (isRunning && stream) {
593
+ const newCols = process.stdout.columns || 80;
594
+ const newRows = process.stdout.rows || 24;
595
+ try {
596
+ stream.setWindow(newRows, newCols, 0, 0);
597
+ }
598
+ catch (e) {
599
+ // Ignore resize errors
600
+ }
601
+ }
602
+ };
603
+ process.stdout.on('resize', resizeHandler);
604
+ // Handle stream close
605
+ stream.on('close', () => {
606
+ isRunning = false;
607
+ // CRITICAL: Remove our stdin handler FIRST
608
+ process.stdin.removeListener('data', stdinHandler);
609
+ process.stdout.removeListener('resize', resizeHandler);
610
+ // Restore stdin to original raw mode setting
611
+ if (process.stdin.isTTY) {
612
+ process.stdin.setRawMode(wasRaw ?? false);
613
+ }
614
+ // COMPREHENSIVE TERMINAL RESET
615
+ process.stdout.write(getTerminalResetSequences());
616
+ // Give terminal time to process reset sequences
617
+ setTimeout(() => {
618
+ onExit(0);
619
+ }, 200);
620
+ });
621
+ // Send the command to the shell: cd to directory and run editor
622
+ // Note: We send the command with exit so shell closes after editor exits
623
+ stream.write(`cd "${cwd}" && ${command}; exit\n`);
624
+ });
625
+ return {
626
+ kill: () => {
627
+ if (isRunning) {
628
+ isRunning = false;
629
+ if (stream) {
630
+ try {
631
+ stream.close();
632
+ }
633
+ catch (e) {
634
+ // Ignore close errors
635
+ }
636
+ }
637
+ }
638
+ }
639
+ };
640
+ }
641
+ /**
642
+ * Run a command in SSH session via ssh2's interactive shell with streaming output.
643
+ * Unlike runSSHEditor, this doesn't take over the terminal - it streams output to a callback.
644
+ * This is essential for sudo commands that need TTY for password prompts.
645
+ */
646
+ export function runSSHCommand(sshClient, // ssh2 Client
647
+ command, cwd, onData, onExit) {
648
+ let isRunning = true;
649
+ let stream = null;
650
+ // Get terminal dimensions
651
+ const cols = process.stdout.columns || 80;
652
+ const rows = process.stdout.rows || 24;
653
+ // Request an interactive shell from the SSH server
654
+ sshClient.shell({
655
+ term: 'xterm-256color',
656
+ cols,
657
+ rows,
658
+ }, (err, shellStream) => {
659
+ if (err) {
660
+ console.error('Failed to open SSH shell:', err.message);
661
+ onExit(1);
662
+ return;
663
+ }
664
+ stream = shellStream;
665
+ // Stream output to callback (instead of process.stdout)
666
+ stream.on('data', (data) => {
667
+ if (isRunning) {
668
+ onData(data.toString());
669
+ }
670
+ });
671
+ // Handle stream close
672
+ stream.on('close', () => {
673
+ isRunning = false;
674
+ onExit(0);
675
+ });
676
+ // Send the command to the shell: cd to directory and run command
677
+ // Note: We send the command with exit so shell closes after command exits
678
+ stream.write(`cd "${cwd}" && ${command}; exit\n`);
679
+ });
680
+ return {
681
+ write: (data) => {
682
+ if (isRunning && stream) {
683
+ stream.write(data);
684
+ }
685
+ },
686
+ kill: () => {
687
+ if (isRunning) {
688
+ isRunning = false;
689
+ if (stream) {
690
+ try {
691
+ stream.close();
692
+ }
693
+ catch (e) {
694
+ // Ignore close errors
695
+ }
696
+ }
697
+ }
698
+ },
699
+ resize: (cols, rows) => {
700
+ if (isRunning && stream) {
701
+ try {
702
+ stream.setWindow(rows, cols, 0, 0);
703
+ }
704
+ catch (e) {
705
+ // Ignore resize errors
706
+ }
707
+ }
708
+ },
709
+ isRunning: () => isRunning
710
+ };
711
+ }
712
+ //# sourceMappingURL=editor-utils.js.map