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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|