neuro-cli 4.1.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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,649 @@
1
+ // ============================================================
2
+ // NeuroCLI - Interactive Terminal UX (GAP-38)
3
+ // OSC 52 Clipboard, Embedded Interactive Commands, Split Pane
4
+ // TUI, Terminal-native Syntax Highlighting, Inline Diffs,
5
+ // Progress Indicators — using only Node.js built-ins + chalk.
6
+ // ============================================================
7
+ import { spawn } from 'child_process';
8
+ import chalk from 'chalk';
9
+ // ---- Constants ----
10
+ const TUI_COMMANDS = new Set([
11
+ 'vim', 'vi', 'nvim', 'nano', 'emacs',
12
+ 'top', 'htop', 'btop', 'btm',
13
+ 'less', 'more',
14
+ 'tmux', 'screen',
15
+ 'mc', 'ranger',
16
+ 'tig', 'lazygit',
17
+ 'watch',
18
+ ]);
19
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
20
+ const BORDER_CHARS = {
21
+ single: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
22
+ double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
23
+ round: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
24
+ none: { tl: ' ', tr: ' ', bl: ' ', br: ' ', h: ' ', v: ' ' },
25
+ };
26
+ // ---- Syntax Highlighting Token Maps ----
27
+ const KEYWORDS = {
28
+ typescript: new Set([
29
+ 'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch',
30
+ 'class', 'const', 'constructor', 'continue', 'debugger', 'default',
31
+ 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally',
32
+ 'for', 'from', 'function', 'if', 'implements', 'import', 'in',
33
+ 'instanceof', 'interface', 'let', 'new', 'null', 'of', 'package',
34
+ 'private', 'protected', 'public', 'readonly', 'return', 'static',
35
+ 'super', 'switch', 'this', 'throw', 'true', 'try', 'type', 'typeof',
36
+ 'undefined', 'unique', 'unknown', 'var', 'void', 'while', 'with', 'yield',
37
+ ]),
38
+ javascript: new Set([
39
+ 'abstract', 'async', 'await', 'break', 'case', 'catch', 'class', 'const',
40
+ 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'export',
41
+ 'extends', 'false', 'finally', 'for', 'from', 'function', 'if',
42
+ 'implements', 'import', 'in', 'instanceof', 'let', 'new', 'null', 'of',
43
+ 'package', 'private', 'protected', 'public', 'return', 'static', 'super',
44
+ 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'undefined', 'var',
45
+ 'void', 'while', 'with', 'yield',
46
+ ]),
47
+ python: new Set([
48
+ 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue',
49
+ 'def', 'del', 'elif', 'else', 'except', 'False', 'finally', 'for',
50
+ 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'None',
51
+ 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'True', 'try',
52
+ 'while', 'with', 'yield',
53
+ ]),
54
+ rust: new Set([
55
+ 'as', 'async', 'await', 'break', 'const', 'continue', 'crate', 'dyn',
56
+ 'else', 'enum', 'extern', 'false', 'fn', 'for', 'if', 'impl', 'in',
57
+ 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'return',
58
+ 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true', 'type',
59
+ 'unsafe', 'use', 'where', 'while', 'yield',
60
+ ]),
61
+ go: new Set([
62
+ 'break', 'case', 'chan', 'const', 'continue', 'default', 'defer',
63
+ 'else', 'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import',
64
+ 'interface', 'map', 'package', 'range', 'return', 'select', 'struct',
65
+ 'switch', 'type', 'var',
66
+ ]),
67
+ bash: new Set([
68
+ 'if', 'then', 'else', 'elif', 'fi', 'case', 'esac', 'for', 'while',
69
+ 'until', 'do', 'done', 'in', 'function', 'select', 'time', 'coproc',
70
+ 'return', 'exit', 'export', 'local', 'readonly', 'declare', 'typeset',
71
+ 'unset', 'shift', 'source', 'alias', 'bg', 'fg', 'jobs', 'kill',
72
+ 'wait', 'read', 'echo', 'printf', 'set', 'trap', 'true', 'false',
73
+ ]),
74
+ sql: new Set([
75
+ 'SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET',
76
+ 'DELETE', 'CREATE', 'TABLE', 'ALTER', 'DROP', 'INDEX', 'JOIN', 'INNER',
77
+ 'LEFT', 'RIGHT', 'OUTER', 'ON', 'AND', 'OR', 'NOT', 'NULL', 'IS',
78
+ 'IN', 'BETWEEN', 'LIKE', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT',
79
+ 'OFFSET', 'UNION', 'ALL', 'AS', 'DISTINCT', 'COUNT', 'SUM', 'AVG',
80
+ 'MIN', 'MAX', 'EXISTS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
81
+ 'BEGIN', 'COMMIT', 'ROLLBACK', 'GRANT', 'REVOKE', 'PRIMARY', 'KEY',
82
+ 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'CHECK', 'UNIQUE',
83
+ ]),
84
+ };
85
+ const COMMENT_PATTERNS = {
86
+ typescript: { single: '//', multiStart: '/*', multiEnd: '*/' },
87
+ javascript: { single: '//', multiStart: '/*', multiEnd: '*/' },
88
+ python: { single: '#', multiStart: '"""', multiEnd: '"""' },
89
+ rust: { single: '//', multiStart: '/*', multiEnd: '*/' },
90
+ go: { single: '//', multiStart: '/*', multiEnd: '*/' },
91
+ bash: { single: '#' },
92
+ sql: { single: '--', multiStart: '/*', multiEnd: '*/' },
93
+ };
94
+ const STRING_DELIMITERS = {
95
+ typescript: ["'", '"', '`'],
96
+ javascript: ["'", '"', '`'],
97
+ python: ["'", '"'],
98
+ rust: ['"'],
99
+ go: ['"', '`'],
100
+ bash: ["'", '"'],
101
+ sql: ["'"],
102
+ };
103
+ // ============================================================
104
+ // TerminalUX Class
105
+ // ============================================================
106
+ export class TerminalUX {
107
+ capabilities;
108
+ spinnerInterval = null;
109
+ spinnerActive = false;
110
+ constructor() {
111
+ this.capabilities = this.detectCapabilities();
112
+ }
113
+ // ============================================================
114
+ // OSC 52 Clipboard
115
+ // ============================================================
116
+ /**
117
+ * Copy text to the terminal clipboard using the OSC 52 escape sequence.
118
+ * Works in remote SSH sessions when the terminal emulator supports it.
119
+ * Returns true if the sequence was written, false if OSC 52 is unsupported.
120
+ */
121
+ copyToClipboard(text) {
122
+ if (!this.capabilities.osc52) {
123
+ return false;
124
+ }
125
+ const encoded = Buffer.from(text, 'utf-8').toString('base64');
126
+ // OSC 52: ESC ] 52 ; c ; <base64-data> BEL
127
+ const sequence = `\x1b]52;c;${encoded}\x07`;
128
+ try {
129
+ process.stdout.write(sequence);
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ /**
137
+ * Check whether the current terminal supports OSC 52 clipboard operations.
138
+ */
139
+ isOSC52Supported() {
140
+ return this.capabilities.osc52;
141
+ }
142
+ // ============================================================
143
+ // Embedded Interactive Commands
144
+ // ============================================================
145
+ /**
146
+ * Launch an interactive TUI command (e.g. vim, top, htop) and capture its
147
+ * exit code and any output. The child process inherits stdio so the user
148
+ * can interact with the TUI directly.
149
+ */
150
+ async launchInteractive(command, options) {
151
+ const parts = this.parseCommand(command);
152
+ const cmd = parts[0];
153
+ const args = parts.slice(1);
154
+ const rows = options?.rows ?? process.stdout.rows ?? 24;
155
+ const cols = options?.cols ?? process.stdout.columns ?? 80;
156
+ // Set terminal size via COLUMNS/LINES env vars for the child
157
+ const env = {
158
+ ...process.env,
159
+ COLUMNS: String(cols),
160
+ LINES: String(rows),
161
+ TERM: process.env.TERM || 'xterm-256color',
162
+ };
163
+ return new Promise((resolve) => {
164
+ const outputChunks = [];
165
+ const child = spawn(cmd, args, {
166
+ stdio: ['pipe', 'pipe', 'pipe'],
167
+ env,
168
+ // Provide a pseudo-TTY–like experience by setting COLUMNS/LINES.
169
+ // In production, node-pty would be used; here we capture output.
170
+ });
171
+ child.stdout?.on('data', (chunk) => {
172
+ outputChunks.push(chunk);
173
+ });
174
+ child.stderr?.on('data', (chunk) => {
175
+ outputChunks.push(chunk);
176
+ });
177
+ child.on('close', (code) => {
178
+ const output = Buffer.concat(outputChunks).toString('utf-8');
179
+ resolve({ exitCode: code ?? 0, output });
180
+ });
181
+ child.on('error', (err) => {
182
+ resolve({ exitCode: 1, output: err.message });
183
+ });
184
+ });
185
+ }
186
+ /**
187
+ * Detect whether a command is a known TUI / interactive application.
188
+ */
189
+ isTUIApp(command) {
190
+ const base = command.split(/\s+/)[0];
191
+ const name = base.split('/').pop() ?? base;
192
+ return TUI_COMMANDS.has(name);
193
+ }
194
+ // ============================================================
195
+ // Split Pane TUI
196
+ // ============================================================
197
+ /**
198
+ * Render a split-pane layout to a string. Each pane has a label and content
199
+ * lines, rendered side-by-side with borders.
200
+ */
201
+ renderSplitPane(layout) {
202
+ const { panes, gap, borderStyle } = layout;
203
+ if (panes.length === 0)
204
+ return '';
205
+ const border = BORDER_CHARS[borderStyle] ?? BORDER_CHARS.single;
206
+ const totalWidth = process.stdout.columns ?? 80;
207
+ const totalGapWidth = gap * (panes.length - 1);
208
+ const availableWidth = totalWidth - totalGapWidth;
209
+ const paneWidth = Math.floor(availableWidth / panes.length);
210
+ const contentWidth = Math.max(1, paneWidth - 2); // minus borders
211
+ const rows = process.stdout.rows ?? 24;
212
+ const contentHeight = Math.max(1, rows - 2); // minus top/bottom borders
213
+ const renderedPanes = [];
214
+ for (const pane of panes) {
215
+ const lines = [];
216
+ // Top border with label
217
+ const labelText = ` ${pane.label} `;
218
+ const labelPadLeft = Math.max(0, Math.floor((contentWidth - labelText.length) / 2));
219
+ const labelPadRight = Math.max(0, contentWidth - labelText.length - labelPadLeft);
220
+ const topLine = border.tl + border.h.repeat(labelPadLeft) + labelText + border.h.repeat(labelPadRight) + border.tr;
221
+ lines.push(this.truncateToWidth(topLine, paneWidth));
222
+ // Content lines
223
+ const visibleLines = pane.content.slice(pane.scrollOffset, pane.scrollOffset + contentHeight);
224
+ for (let i = 0; i < contentHeight; i++) {
225
+ const line = i < visibleLines.length ? visibleLines[i] : '';
226
+ const truncated = this.truncateToWidth(line, contentWidth);
227
+ const padded = truncated.padEnd(contentWidth);
228
+ lines.push(border.v + padded + border.v);
229
+ }
230
+ // Bottom border
231
+ const bottomLine = border.bl + border.h.repeat(contentWidth) + border.br;
232
+ lines.push(this.truncateToWidth(bottomLine, paneWidth));
233
+ renderedPanes.push(lines);
234
+ }
235
+ // Combine panes side-by-side
236
+ const maxLines = Math.max(...renderedPanes.map((p) => p.length));
237
+ const outputLines = [];
238
+ for (let row = 0; row < maxLines; row++) {
239
+ const parts = [];
240
+ for (let p = 0; p < renderedPanes.length; p++) {
241
+ const paneLines = renderedPanes[p];
242
+ const line = row < paneLines.length ? paneLines[row] : ' '.repeat(paneWidth);
243
+ parts.push(line);
244
+ if (p < renderedPanes.length - 1) {
245
+ parts.push(' '.repeat(gap));
246
+ }
247
+ }
248
+ outputLines.push(parts.join(''));
249
+ }
250
+ return outputLines.join('\n');
251
+ }
252
+ // ============================================================
253
+ // Syntax Highlighting
254
+ // ============================================================
255
+ /**
256
+ * Apply chalk-based syntax highlighting to a code string for the given language.
257
+ * Returns a chalk-colored string suitable for terminal output.
258
+ */
259
+ highlight(code, language) {
260
+ const lang = language.toLowerCase();
261
+ const keywords = KEYWORDS[lang];
262
+ const commentInfo = COMMENT_PATTERNS[lang];
263
+ const stringDels = STRING_DELIMITERS[lang] ?? ['"', "'"];
264
+ if (!keywords && !commentInfo) {
265
+ // No rules for this language — return as-is with minimal styling
266
+ return chalk.white(code);
267
+ }
268
+ const lines = code.split('\n');
269
+ const highlighted = [];
270
+ for (const line of lines) {
271
+ highlighted.push(this.highlightLine(line, keywords, commentInfo, stringDels));
272
+ }
273
+ return highlighted.join('\n');
274
+ }
275
+ /**
276
+ * Highlight a unified diff string with color coding.
277
+ * - Added lines: green
278
+ * - Removed lines: red
279
+ * - Header lines (---, +++, @@): cyan
280
+ * - Context lines: dim gray
281
+ */
282
+ highlightDiff(diff) {
283
+ const lines = diff.split('\n');
284
+ const result = [];
285
+ for (const line of lines) {
286
+ if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@')) {
287
+ result.push(chalk.cyan.bold(line));
288
+ }
289
+ else if (line.startsWith('+')) {
290
+ result.push(chalk.green(line));
291
+ }
292
+ else if (line.startsWith('-')) {
293
+ result.push(chalk.red(line));
294
+ }
295
+ else if (line.startsWith('diff ') || line.startsWith('index ')) {
296
+ result.push(chalk.cyan.dim(line));
297
+ }
298
+ else {
299
+ result.push(chalk.gray(line));
300
+ }
301
+ }
302
+ return result.join('\n');
303
+ }
304
+ // ============================================================
305
+ // Terminal Detection
306
+ // ============================================================
307
+ /**
308
+ * Return detected terminal capabilities.
309
+ */
310
+ getTerminalCapabilities() {
311
+ return { ...this.capabilities };
312
+ }
313
+ /**
314
+ * Return the current terminal dimensions.
315
+ */
316
+ getTerminalSize() {
317
+ return {
318
+ rows: process.stdout.rows ?? 24,
319
+ cols: process.stdout.columns ?? 80,
320
+ };
321
+ }
322
+ /**
323
+ * Check whether the terminal supports 24-bit true color.
324
+ */
325
+ supportsTrueColor() {
326
+ return this.capabilities.trueColor;
327
+ }
328
+ /**
329
+ * Check whether the terminal supports Unicode characters (beyond ASCII).
330
+ */
331
+ supportsUnicode() {
332
+ return this.capabilities.unicode;
333
+ }
334
+ // ============================================================
335
+ // Progress Indicators
336
+ // ============================================================
337
+ /**
338
+ * Render a progress bar as a string.
339
+ */
340
+ renderProgressBar(options) {
341
+ const { total, current, width = 30, label = '', completeChar = '█', incompleteChar = '░', showPercent = true, showETA = false, startTime, } = options;
342
+ const ratio = total > 0 ? Math.min(current / total, 1) : 0;
343
+ const filled = Math.round(ratio * width);
344
+ const empty = width - filled;
345
+ const bar = chalk.green(completeChar.repeat(filled)) + chalk.gray(incompleteChar.repeat(empty));
346
+ const percent = showPercent ? ` ${Math.round(ratio * 100)}%` : '';
347
+ const count = ` ${current}/${total}`;
348
+ let eta = '';
349
+ if (showETA && startTime && current > 0 && current < total) {
350
+ const elapsed = Date.now() - startTime;
351
+ const perItem = elapsed / current;
352
+ const remaining = perItem * (total - current);
353
+ eta = ` ETA: ${this.formatDuration(remaining)}`;
354
+ }
355
+ const labelStr = label ? chalk.bold(label) + ' ' : '';
356
+ return `${labelStr}${bar}${percent}${count}${eta}`;
357
+ }
358
+ /**
359
+ * Start a spinner animation on the terminal.
360
+ * Returns a stop function.
361
+ */
362
+ startSpinner(options) {
363
+ if (this.spinnerActive) {
364
+ return { stop: () => { } };
365
+ }
366
+ const frames = options?.frames ?? SPINNER_FRAMES;
367
+ const interval = options?.interval ?? 80;
368
+ const label = options?.label ?? '';
369
+ const color = options?.color ?? 'cyan';
370
+ let frameIndex = 0;
371
+ this.spinnerActive = true;
372
+ const colorFn = chalk[color] ?? chalk.cyan;
373
+ this.spinnerInterval = setInterval(() => {
374
+ const frame = frames[frameIndex % frames.length];
375
+ process.stdout.write(`\r${colorFn(frame)} ${label}`);
376
+ frameIndex++;
377
+ }, interval);
378
+ return {
379
+ stop: (finalMessage) => {
380
+ if (this.spinnerInterval) {
381
+ clearInterval(this.spinnerInterval);
382
+ this.spinnerInterval = null;
383
+ }
384
+ this.spinnerActive = false;
385
+ // Clear the spinner line
386
+ process.stdout.write('\r' + ' '.repeat((label.length || 20) + 4) + '\r');
387
+ if (finalMessage) {
388
+ process.stdout.write(finalMessage + '\n');
389
+ }
390
+ },
391
+ };
392
+ }
393
+ /**
394
+ * Render a multi-step progress indicator (checklist).
395
+ */
396
+ renderStepProgress(steps) {
397
+ const lines = [];
398
+ for (const step of steps) {
399
+ switch (step.status) {
400
+ case 'done':
401
+ lines.push(chalk.green(' ✓ ') + chalk.white(step.label));
402
+ break;
403
+ case 'running':
404
+ lines.push(chalk.cyan(' ◉ ') + chalk.cyan(step.label));
405
+ break;
406
+ case 'failed':
407
+ lines.push(chalk.red(' ✗ ') + chalk.red(step.label));
408
+ break;
409
+ case 'pending':
410
+ lines.push(chalk.dim(' ○ ') + chalk.dim(step.label));
411
+ break;
412
+ }
413
+ }
414
+ return lines.join('\n');
415
+ }
416
+ // ============================================================
417
+ // Private Helpers — Capability Detection
418
+ // ============================================================
419
+ detectCapabilities() {
420
+ const term = (process.env.TERM ?? '').toLowerCase();
421
+ const termProgram = (process.env.TERM_PROGRAM ?? '').toLowerCase();
422
+ const colorterm = (process.env.COLORTERM ?? '').toLowerCase();
423
+ const kittyWindowId = process.env.KITTY_WINDOW_ID;
424
+ const itermSessionId = process.env.ITERM_SESSION_ID;
425
+ const wtSession = process.env.WT_SESSION;
426
+ // OSC 52: supported by most modern terminals, but some (like screen) block it.
427
+ // tmux passes it through if allow-passthrough is on.
428
+ // We check for known-good environments and default to true for modern terminals.
429
+ let osc52 = false;
430
+ if (kittyWindowId) {
431
+ osc52 = true;
432
+ }
433
+ else if (itermSessionId) {
434
+ osc52 = true;
435
+ }
436
+ else if (termProgram.includes('wezterm')) {
437
+ osc52 = true;
438
+ }
439
+ else if (termProgram.includes('alacritty')) {
440
+ osc52 = true;
441
+ }
442
+ else if (termProgram.includes('foot')) {
443
+ osc52 = true;
444
+ }
445
+ else if (wtSession) {
446
+ osc52 = true; // Windows Terminal
447
+ }
448
+ else if (term.includes('xterm') || term.includes('vte') || colorterm.includes('truecolor')) {
449
+ osc52 = true;
450
+ }
451
+ // True color: check COLORTERM or known terminals
452
+ const trueColor = colorterm.includes('truecolor') || colorterm.includes('24bit') ||
453
+ !!kittyWindowId || !!itermSessionId ||
454
+ termProgram.includes('wezterm') || termProgram.includes('alacritty') ||
455
+ termProgram.includes('foot') || !!wtSession;
456
+ // Unicode: most modern terminals support it; assume true except for very old TERM values
457
+ const unicode = !term.includes('ascii') && !term.includes('dumb');
458
+ // iTerm2 specific
459
+ const iterm2 = !!itermSessionId || termProgram === 'iterm.app';
460
+ // Kitty specific
461
+ const kitty = !!kittyWindowId;
462
+ // Sixel graphics: only a few terminals
463
+ const sixel = termProgram.includes('wezterm') || !!kittyWindowId ||
464
+ term.includes('sixel');
465
+ // Window title: most xterm-compatible terminals support it
466
+ const title = term.includes('xterm') || term.includes('screen') || term.includes('tmux') ||
467
+ !!itermSessionId || !!kittyWindowId || !!wtSession;
468
+ // Hyperlinks (OSC 8): supported by many modern terminals
469
+ const hyperlinks = !!kittyWindowId || !!itermSessionId ||
470
+ termProgram.includes('wezterm') || termProgram.includes('alacritty') ||
471
+ !!wtSession || termProgram.includes('foot');
472
+ return {
473
+ osc52,
474
+ trueColor,
475
+ unicode,
476
+ iterm2,
477
+ kitty,
478
+ sixel,
479
+ title,
480
+ hyperlinks,
481
+ };
482
+ }
483
+ // ============================================================
484
+ // Private Helpers — Syntax Highlighting
485
+ // ============================================================
486
+ highlightLine(line, keywords, commentInfo, stringDels) {
487
+ // Simple tokeniser approach: scan character by character to detect
488
+ // strings, comments, and keywords.
489
+ const result = [];
490
+ let i = 0;
491
+ let inSingleComment = false;
492
+ while (i < line.length) {
493
+ // ---- Single-line comment ----
494
+ if (commentInfo && !inSingleComment) {
495
+ const single = commentInfo.single;
496
+ if (line.substring(i, i + single.length) === single) {
497
+ // Rest of line is a comment
498
+ result.push(chalk.gray(line.substring(i)));
499
+ inSingleComment = true;
500
+ break;
501
+ }
502
+ }
503
+ // ---- String literals ----
504
+ if (!inSingleComment) {
505
+ let matchedDelim = null;
506
+ for (const delim of stringDels) {
507
+ if (line[i] === delim[0]) {
508
+ // Check full delimiter (e.g. """ for python)
509
+ if (line.substring(i, i + delim.length) === delim) {
510
+ matchedDelim = delim;
511
+ break;
512
+ }
513
+ }
514
+ }
515
+ if (matchedDelim) {
516
+ const start = i;
517
+ i += matchedDelim.length;
518
+ // Find closing delimiter
519
+ while (i < line.length) {
520
+ if (line[i] === '\\' && i + 1 < line.length) {
521
+ i += 2; // skip escape
522
+ continue;
523
+ }
524
+ if (line.substring(i, i + matchedDelim.length) === matchedDelim) {
525
+ i += matchedDelim.length;
526
+ break;
527
+ }
528
+ i++;
529
+ }
530
+ result.push(chalk.yellow(line.substring(start, i)));
531
+ continue;
532
+ }
533
+ }
534
+ // ---- Numbers ----
535
+ if (!inSingleComment && /[0-9]/.test(line[i])) {
536
+ const start = i;
537
+ while (i < line.length && /[0-9.xXa-fA-F_]/.test(line[i])) {
538
+ i++;
539
+ }
540
+ result.push(chalk.magenta(line.substring(start, i)));
541
+ continue;
542
+ }
543
+ // ---- Identifiers / keywords ----
544
+ if (!inSingleComment && /[a-zA-Z_$@]/.test(line[i])) {
545
+ const start = i;
546
+ while (i < line.length && /[a-zA-Z0-9_$]/.test(line[i])) {
547
+ i++;
548
+ }
549
+ const word = line.substring(start, i);
550
+ if (keywords?.has(word)) {
551
+ result.push(chalk.blue.bold(word));
552
+ }
553
+ else if (word === 'true' || word === 'false' || word === 'null' || word === 'True' || word === 'False' || word === 'None') {
554
+ result.push(chalk.magenta(word));
555
+ }
556
+ else if (word.startsWith('@') || word.startsWith('$')) {
557
+ result.push(chalk.cyan(word));
558
+ }
559
+ else {
560
+ result.push(chalk.white(word));
561
+ }
562
+ continue;
563
+ }
564
+ // ---- Operators / punctuation ----
565
+ if (!inSingleComment && /[=+\-*/<>!&|^~%?:]/.test(line[i])) {
566
+ result.push(chalk.red(line[i]));
567
+ i++;
568
+ continue;
569
+ }
570
+ // ---- Everything else (whitespace, brackets, etc.) ----
571
+ result.push(line[i]);
572
+ i++;
573
+ }
574
+ return result.join('');
575
+ }
576
+ // ============================================================
577
+ // Private Helpers — General Utilities
578
+ // ============================================================
579
+ parseCommand(command) {
580
+ // Simple shell-like command parser that respects basic quoting
581
+ const parts = [];
582
+ let current = '';
583
+ let inSingle = false;
584
+ let inDouble = false;
585
+ for (let i = 0; i < command.length; i++) {
586
+ const ch = command[i];
587
+ if (ch === "'" && !inDouble) {
588
+ inSingle = !inSingle;
589
+ }
590
+ else if (ch === '"' && !inSingle) {
591
+ inDouble = !inDouble;
592
+ }
593
+ else if (ch === ' ' && !inSingle && !inDouble) {
594
+ if (current.length > 0) {
595
+ parts.push(current);
596
+ current = '';
597
+ }
598
+ }
599
+ else {
600
+ current += ch;
601
+ }
602
+ }
603
+ if (current.length > 0) {
604
+ parts.push(current);
605
+ }
606
+ return parts;
607
+ }
608
+ truncateToWidth(str, maxWidth) {
609
+ // Strip ANSI escape codes for width calculation
610
+ const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
611
+ if (stripped.length <= maxWidth)
612
+ return str;
613
+ // Truncate visible characters while preserving ANSI codes
614
+ let visibleLen = 0;
615
+ let result = '';
616
+ let i = 0;
617
+ while (i < str.length && visibleLen < maxWidth) {
618
+ if (str[i] === '\x1b') {
619
+ // Consume the full escape sequence
620
+ const seqStart = i;
621
+ i++;
622
+ while (i < str.length && str[i] !== 'm')
623
+ i++;
624
+ if (i < str.length)
625
+ i++;
626
+ result += str.substring(seqStart, i);
627
+ }
628
+ else {
629
+ result += str[i];
630
+ visibleLen++;
631
+ i++;
632
+ }
633
+ }
634
+ return result;
635
+ }
636
+ formatDuration(ms) {
637
+ const seconds = Math.floor(ms / 1000);
638
+ if (seconds < 60)
639
+ return `${seconds}s`;
640
+ const minutes = Math.floor(seconds / 60);
641
+ const remainingSeconds = seconds % 60;
642
+ if (minutes < 60)
643
+ return `${minutes}m${remainingSeconds}s`;
644
+ const hours = Math.floor(minutes / 60);
645
+ const remainingMinutes = minutes % 60;
646
+ return `${hours}h${remainingMinutes}m`;
647
+ }
648
+ }
649
+ //# sourceMappingURL=terminal-ux.js.map