@compilr-dev/cli 0.4.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/README.md +110 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +317 -0
- package/dist/agents/registry.d.ts +66 -0
- package/dist/agents/registry.js +238 -0
- package/dist/agents/types.d.ts +40 -0
- package/dist/agents/types.js +94 -0
- package/dist/commands/custom-registry.d.ts +69 -0
- package/dist/commands/custom-registry.js +246 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/types.d.ts +31 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +324 -0
- package/dist/db/index.d.ts +42 -0
- package/dist/db/index.js +146 -0
- package/dist/db/repositories/document-repository.d.ts +63 -0
- package/dist/db/repositories/document-repository.js +184 -0
- package/dist/db/repositories/index.d.ts +9 -0
- package/dist/db/repositories/index.js +6 -0
- package/dist/db/repositories/project-repository.d.ts +132 -0
- package/dist/db/repositories/project-repository.js +337 -0
- package/dist/db/repositories/work-item-repository.d.ts +115 -0
- package/dist/db/repositories/work-item-repository.js +389 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +143 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +348 -0
- package/dist/index.old.d.ts +7 -0
- package/dist/index.old.js +1014 -0
- package/dist/repl.d.ts +121 -0
- package/dist/repl.js +1878 -0
- package/dist/settings/index.d.ts +80 -0
- package/dist/settings/index.js +195 -0
- package/dist/shared-handlers.d.ts +63 -0
- package/dist/shared-handlers.js +57 -0
- package/dist/slash-autocomplete.d.ts +41 -0
- package/dist/slash-autocomplete.js +638 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.js +130 -0
- package/dist/tabbed-menu.d.ts +11 -0
- package/dist/tabbed-menu.js +328 -0
- package/dist/templates/backlog-md.d.ts +7 -0
- package/dist/templates/backlog-md.js +94 -0
- package/dist/templates/claude-md.d.ts +7 -0
- package/dist/templates/claude-md.js +189 -0
- package/dist/templates/coding-standards.d.ts +7 -0
- package/dist/templates/coding-standards.js +299 -0
- package/dist/templates/compilr-md.d.ts +7 -0
- package/dist/templates/compilr-md.js +189 -0
- package/dist/templates/config-json.d.ts +38 -0
- package/dist/templates/config-json.js +39 -0
- package/dist/templates/gitignore.d.ts +7 -0
- package/dist/templates/gitignore.js +85 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +302 -0
- package/dist/templates/package-json.d.ts +7 -0
- package/dist/templates/package-json.js +111 -0
- package/dist/templates/readme-md.d.ts +7 -0
- package/dist/templates/readme-md.js +161 -0
- package/dist/templates/tsconfig.d.ts +7 -0
- package/dist/templates/tsconfig.js +61 -0
- package/dist/templates/types.d.ts +33 -0
- package/dist/templates/types.js +24 -0
- package/dist/test-autocomplete.d.ts +7 -0
- package/dist/test-autocomplete.js +85 -0
- package/dist/test-tabbed-menu.d.ts +7 -0
- package/dist/test-tabbed-menu.js +25 -0
- package/dist/themes/colors.d.ts +49 -0
- package/dist/themes/colors.js +135 -0
- package/dist/themes/index.d.ts +23 -0
- package/dist/themes/index.js +24 -0
- package/dist/themes/registry.d.ts +60 -0
- package/dist/themes/registry.js +195 -0
- package/dist/themes/types.d.ts +82 -0
- package/dist/themes/types.js +7 -0
- package/dist/tool-selector.d.ts +71 -0
- package/dist/tool-selector.js +184 -0
- package/dist/tools/ask-user-simple.d.ts +19 -0
- package/dist/tools/ask-user-simple.js +86 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +113 -0
- package/dist/tools/backlog.d.ts +53 -0
- package/dist/tools/backlog.js +709 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +121 -0
- package/dist/ui/agents-overlay.d.ts +12 -0
- package/dist/ui/agents-overlay.js +501 -0
- package/dist/ui/arch-type-overlay.d.ts +20 -0
- package/dist/ui/arch-type-overlay.js +229 -0
- package/dist/ui/ask-user-overlay.d.ts +26 -0
- package/dist/ui/ask-user-overlay.js +647 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay.js +242 -0
- package/dist/ui/backlog-overlay.d.ts +17 -0
- package/dist/ui/backlog-overlay.js +786 -0
- package/dist/ui/commands-overlay.d.ts +11 -0
- package/dist/ui/commands-overlay.js +410 -0
- package/dist/ui/config-overlay.d.ts +34 -0
- package/dist/ui/config-overlay.js +977 -0
- package/dist/ui/conversation.d.ts +82 -0
- package/dist/ui/conversation.js +508 -0
- package/dist/ui/diff.d.ts +38 -0
- package/dist/ui/diff.js +182 -0
- package/dist/ui/ephemeral.d.ts +111 -0
- package/dist/ui/ephemeral.js +413 -0
- package/dist/ui/file-autocomplete.d.ts +45 -0
- package/dist/ui/file-autocomplete.js +237 -0
- package/dist/ui/footer.d.ts +153 -0
- package/dist/ui/footer.js +422 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/init-overlay.d.ts +24 -0
- package/dist/ui/init-overlay.js +525 -0
- package/dist/ui/input-prompt-v2.d.ts +179 -0
- package/dist/ui/input-prompt-v2.js +991 -0
- package/dist/ui/input-prompt.d.ts +97 -0
- package/dist/ui/input-prompt.js +800 -0
- package/dist/ui/iteration-limit-overlay.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay.js +150 -0
- package/dist/ui/keys-overlay.d.ts +14 -0
- package/dist/ui/keys-overlay.js +181 -0
- package/dist/ui/model-warning-overlay.d.ts +30 -0
- package/dist/ui/model-warning-overlay.js +171 -0
- package/dist/ui/overlay-controller.d.ts +25 -0
- package/dist/ui/overlay-controller.js +35 -0
- package/dist/ui/overlays.d.ts +47 -0
- package/dist/ui/overlays.js +627 -0
- package/dist/ui/permission-overlay.d.ts +16 -0
- package/dist/ui/permission-overlay.js +494 -0
- package/dist/ui/terminal.d.ts +117 -0
- package/dist/ui/terminal.js +237 -0
- package/dist/ui/todo-zone.d.ts +112 -0
- package/dist/ui/todo-zone.js +353 -0
- package/dist/ui/tools-overlay.d.ts +26 -0
- package/dist/ui/tools-overlay.js +278 -0
- package/dist/ui/tutorial-overlay.d.ts +10 -0
- package/dist/ui/tutorial-overlay.js +936 -0
- package/dist/ui/types.d.ts +103 -0
- package/dist/ui/types.js +33 -0
- package/dist/utils/credentials.d.ts +55 -0
- package/dist/utils/credentials.js +268 -0
- package/dist/utils/model-tiers.d.ts +37 -0
- package/dist/utils/model-tiers.js +118 -0
- package/dist/utils/project-memory.d.ts +47 -0
- package/dist/utils/project-memory.js +117 -0
- package/dist/utils/project-status.d.ts +56 -0
- package/dist/utils/project-status.js +237 -0
- package/package.json +66 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Overlay
|
|
3
|
+
*
|
|
4
|
+
* Modal overlay for tool permission requests.
|
|
5
|
+
* Uses the same pattern as ask-user-simple-overlay for consistent behavior.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import * as terminal from './terminal.js';
|
|
9
|
+
import { getStyles } from '../themes/index.js';
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Helpers
|
|
12
|
+
// =============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Format a value for display, handling nested structures
|
|
15
|
+
* Replaces newlines with ↵ symbol to prevent multi-line rendering
|
|
16
|
+
*/
|
|
17
|
+
function formatValue(value, maxLen) {
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
// Replace all newlines with ↵ to prevent multi-line rendering
|
|
20
|
+
const singleLine = value.replace(/\r?\n|\r/g, '↵');
|
|
21
|
+
return singleLine.length > maxLen ? singleLine.slice(0, maxLen) + '...' : singleLine;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
if (value.length === 0)
|
|
28
|
+
return '[]';
|
|
29
|
+
// Show array contents briefly
|
|
30
|
+
const items = value.slice(0, 3).map((item) => {
|
|
31
|
+
if (typeof item === 'object' && item !== null) {
|
|
32
|
+
// For objects in array, show key highlights
|
|
33
|
+
const obj = item;
|
|
34
|
+
const keys = Object.keys(obj);
|
|
35
|
+
if (keys.length === 0)
|
|
36
|
+
return '{}';
|
|
37
|
+
const preview = keys.slice(0, 2).map(k => {
|
|
38
|
+
const v = obj[k];
|
|
39
|
+
return `${k}: ${formatValue(v, 20)}`;
|
|
40
|
+
}).join(', ');
|
|
41
|
+
return `{${preview}${keys.length > 2 ? ', ...' : ''}}`;
|
|
42
|
+
}
|
|
43
|
+
return formatValue(item, 20);
|
|
44
|
+
});
|
|
45
|
+
const remaining = value.length - 3;
|
|
46
|
+
const suffix = remaining > 0 ? `, ... (+${String(remaining)} more)` : '';
|
|
47
|
+
return `[${items.join(', ')}${suffix}]`;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === 'object' && value !== null) {
|
|
50
|
+
const obj = value;
|
|
51
|
+
const keys = Object.keys(obj);
|
|
52
|
+
if (keys.length === 0)
|
|
53
|
+
return '{}';
|
|
54
|
+
const preview = keys.slice(0, 3).map(k => `${k}: ${formatValue(obj[k], 15)}`).join(', ');
|
|
55
|
+
return keys.length > 3 ? `{${preview}, ...}` : `{${preview}}`;
|
|
56
|
+
}
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Format tool args for display.
|
|
61
|
+
* - For bash commands: show the command prominently
|
|
62
|
+
* - For file operations: show the path
|
|
63
|
+
* - For backlog operations: show action and details clearly
|
|
64
|
+
* - Strip JSON syntax for readability
|
|
65
|
+
*/
|
|
66
|
+
function formatArgs(toolName, args, maxWidth) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
// Special handling for bash commands - show full command
|
|
69
|
+
if (toolName === 'bash' && typeof args.command === 'string') {
|
|
70
|
+
const cmd = args.command;
|
|
71
|
+
const prefix = 'Command: ';
|
|
72
|
+
const availableWidth = maxWidth - prefix.length - 4;
|
|
73
|
+
if (cmd.length <= availableWidth) {
|
|
74
|
+
lines.push(prefix + cmd);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
lines.push(prefix);
|
|
78
|
+
const maxCmdLength = availableWidth * 3;
|
|
79
|
+
const truncatedCmd = cmd.length > maxCmdLength
|
|
80
|
+
? cmd.slice(0, maxCmdLength) + '...'
|
|
81
|
+
: cmd;
|
|
82
|
+
lines.push(' ' + truncatedCmd);
|
|
83
|
+
}
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
// Special handling for backlog_write - show action details clearly
|
|
87
|
+
if (toolName === 'backlog_write') {
|
|
88
|
+
const action = args.action;
|
|
89
|
+
if (action) {
|
|
90
|
+
lines.push(`Action: ${action}`);
|
|
91
|
+
}
|
|
92
|
+
// For updates, show what's being changed
|
|
93
|
+
if (action === 'update' && args.id) {
|
|
94
|
+
lines.push(`Item: ${typeof args.id === 'string' ? args.id : JSON.stringify(args.id)}`);
|
|
95
|
+
if (args.updates && typeof args.updates === 'object') {
|
|
96
|
+
const updates = args.updates;
|
|
97
|
+
const changes = Object.entries(updates)
|
|
98
|
+
.map(([k, v]) => `${k} → ${formatValue(v, 25)}`)
|
|
99
|
+
.join(', ');
|
|
100
|
+
const maxChangesWidth = maxWidth - 14;
|
|
101
|
+
lines.push(`Changes: ${changes.length > maxChangesWidth ? changes.slice(0, maxChangesWidth - 3) + '...' : changes}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// For add, show what's being added
|
|
105
|
+
if (action === 'add' && args.item && typeof args.item === 'object') {
|
|
106
|
+
const item = args.item;
|
|
107
|
+
if (item.id)
|
|
108
|
+
lines.push(`ID: ${typeof item.id === 'string' ? item.id : JSON.stringify(item.id)}`);
|
|
109
|
+
if (item.title) {
|
|
110
|
+
const maxTitleLen = maxWidth - 12;
|
|
111
|
+
const titleStr = formatValue(item.title, maxTitleLen);
|
|
112
|
+
lines.push(`Title: ${titleStr}`);
|
|
113
|
+
}
|
|
114
|
+
if (item.type)
|
|
115
|
+
lines.push(`Type: ${typeof item.type === 'string' ? item.type : JSON.stringify(item.type)}`);
|
|
116
|
+
}
|
|
117
|
+
// For delete, show what's being removed
|
|
118
|
+
if (action === 'delete' && args.id) {
|
|
119
|
+
lines.push(`Deleting: ${typeof args.id === 'string' ? args.id : JSON.stringify(args.id)}`);
|
|
120
|
+
}
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
// Special handling for edit tool - show what's being changed
|
|
124
|
+
if (toolName === 'edit') {
|
|
125
|
+
const path = args.path ?? args.file_path ?? args.filePath;
|
|
126
|
+
if (typeof path === 'string') {
|
|
127
|
+
const maxPathLen = maxWidth - 10;
|
|
128
|
+
const truncPath = path.length > maxPathLen ? '...' + path.slice(-(maxPathLen - 3)) : path;
|
|
129
|
+
lines.push(`File: ${truncPath}`);
|
|
130
|
+
}
|
|
131
|
+
const oldText = args.old_text ?? args.oldText ?? args.old_string ?? args.oldString;
|
|
132
|
+
const newText = args.new_text ?? args.newText ?? args.new_string ?? args.newString;
|
|
133
|
+
if (typeof oldText === 'string' && typeof newText === 'string') {
|
|
134
|
+
const maxPreview = maxWidth - 8; // "- " or "+ " plus buffer
|
|
135
|
+
lines.push(`- ${formatValue(oldText, maxPreview)}`);
|
|
136
|
+
lines.push(`+ ${formatValue(newText, maxPreview)}`);
|
|
137
|
+
}
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
// Special handling for write_file - show path and content preview
|
|
141
|
+
if (toolName === 'write_file') {
|
|
142
|
+
const path = args.path ?? args.file_path ?? args.filePath;
|
|
143
|
+
if (typeof path === 'string') {
|
|
144
|
+
const maxPathLen = maxWidth - 10; // "File: " + buffer
|
|
145
|
+
const truncPath = path.length > maxPathLen ? '...' + path.slice(-(maxPathLen - 3)) : path;
|
|
146
|
+
lines.push(`File: ${truncPath}`);
|
|
147
|
+
}
|
|
148
|
+
const content = args.content;
|
|
149
|
+
if (typeof content === 'string') {
|
|
150
|
+
const maxContentLen = maxWidth - 15; // "Content: " + buffer
|
|
151
|
+
// Use formatValue to handle newline replacement
|
|
152
|
+
lines.push(`Content: ${formatValue(content, maxContentLen)}`);
|
|
153
|
+
}
|
|
154
|
+
return lines;
|
|
155
|
+
}
|
|
156
|
+
// For file operations, show path prominently
|
|
157
|
+
const path = args.path ?? args.file_path ?? args.filePath;
|
|
158
|
+
if (typeof path === 'string') {
|
|
159
|
+
const maxPathLen = maxWidth - 10;
|
|
160
|
+
const truncPath = path.length > maxPathLen ? '...' + path.slice(-(maxPathLen - 3)) : path;
|
|
161
|
+
lines.push('Path: ' + truncPath);
|
|
162
|
+
}
|
|
163
|
+
// Show other args in a clean format (key: value), one per line for clarity
|
|
164
|
+
const skipKeys = ['path', 'file_path', 'filePath', 'command'];
|
|
165
|
+
for (const [key, value] of Object.entries(args)) {
|
|
166
|
+
if (skipKeys.includes(key))
|
|
167
|
+
continue;
|
|
168
|
+
const valueStr = formatValue(value, maxWidth - key.length - 6);
|
|
169
|
+
const line = `${key}: ${valueStr}`;
|
|
170
|
+
// Truncate if too long
|
|
171
|
+
if (line.length > maxWidth - 4) {
|
|
172
|
+
lines.push(line.slice(0, maxWidth - 7) + '...');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
lines.push(line);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return lines;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Format args for detail view (no truncation, proper word wrapping)
|
|
182
|
+
*/
|
|
183
|
+
function formatArgsForDetail(toolName, args, maxWidth) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
lines.push(`Tool: ${toolName}`);
|
|
186
|
+
lines.push('');
|
|
187
|
+
for (const [key, value] of Object.entries(args)) {
|
|
188
|
+
if (typeof value === 'string') {
|
|
189
|
+
// For strings, show full content with word wrapping
|
|
190
|
+
lines.push(`${key}:`);
|
|
191
|
+
// Split by newlines first, then wrap each line
|
|
192
|
+
const valueLines = value.split(/\r?\n/);
|
|
193
|
+
for (const valueLine of valueLines) {
|
|
194
|
+
if (valueLine.length <= maxWidth - 4) {
|
|
195
|
+
lines.push(` ${valueLine}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Word wrap long lines
|
|
199
|
+
let remaining = valueLine;
|
|
200
|
+
while (remaining.length > 0) {
|
|
201
|
+
const chunk = remaining.slice(0, maxWidth - 4);
|
|
202
|
+
lines.push(` ${chunk}`);
|
|
203
|
+
remaining = remaining.slice(maxWidth - 4);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
lines.push('');
|
|
208
|
+
}
|
|
209
|
+
else if (Array.isArray(value)) {
|
|
210
|
+
lines.push(`${key}: [${String(value.length)} items]`);
|
|
211
|
+
for (let i = 0; i < value.length; i++) {
|
|
212
|
+
const item = value[i];
|
|
213
|
+
if (typeof item === 'object' && item !== null) {
|
|
214
|
+
lines.push(` [${String(i)}]:`);
|
|
215
|
+
const itemObj = item;
|
|
216
|
+
for (const [k, v] of Object.entries(itemObj)) {
|
|
217
|
+
const vStr = typeof v === 'string' ? v : JSON.stringify(v);
|
|
218
|
+
lines.push(` ${k}: ${vStr.slice(0, maxWidth - 10)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
lines.push(` [${String(i)}]: ${String(item)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
lines.push('');
|
|
226
|
+
}
|
|
227
|
+
else if (typeof value === 'object' && value !== null) {
|
|
228
|
+
lines.push(`${key}:`);
|
|
229
|
+
for (const [k, v] of Object.entries(value)) {
|
|
230
|
+
const vStr = typeof v === 'string' ? v : JSON.stringify(v);
|
|
231
|
+
lines.push(` ${k}: ${vStr.slice(0, maxWidth - 6)}`);
|
|
232
|
+
}
|
|
233
|
+
lines.push('');
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
lines.push(`${key}: ${String(value)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return lines;
|
|
240
|
+
}
|
|
241
|
+
// Fixed height for detail view overlay (predictable rendering)
|
|
242
|
+
const DETAIL_VIEW_CONTENT_LINES = 15;
|
|
243
|
+
// Note: Total = CONTENT_LINES + header(4) + footer(3) = 22 lines
|
|
244
|
+
/**
|
|
245
|
+
* Render the detail view as an overlay (fixed height)
|
|
246
|
+
*/
|
|
247
|
+
function renderDetailView(options, state, contentLines, previousLineCount = 0) {
|
|
248
|
+
const s = getStyles();
|
|
249
|
+
const cols = terminal.getTerminalWidth();
|
|
250
|
+
// Clear previous render
|
|
251
|
+
if (previousLineCount > 0) {
|
|
252
|
+
terminal.clearLinesAbove(previousLineCount);
|
|
253
|
+
}
|
|
254
|
+
const lines = [];
|
|
255
|
+
const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
|
|
256
|
+
// Header
|
|
257
|
+
lines.push(border);
|
|
258
|
+
lines.push(' ' + s.warning('⚠') + ' ' + chalk.bold('Permission Details') + s.muted(` (${options.toolName})`));
|
|
259
|
+
lines.push(border);
|
|
260
|
+
lines.push('');
|
|
261
|
+
// Fixed visible area
|
|
262
|
+
state.visibleLines = DETAIL_VIEW_CONTENT_LINES;
|
|
263
|
+
state.totalLines = contentLines.length;
|
|
264
|
+
// Show content with scroll
|
|
265
|
+
const endLine = Math.min(state.scrollOffset + state.visibleLines, contentLines.length);
|
|
266
|
+
for (let i = state.scrollOffset; i < endLine; i++) {
|
|
267
|
+
const line = contentLines[i];
|
|
268
|
+
// Truncate to prevent wrapping
|
|
269
|
+
const safeLine = line.length > cols - 4 ? line.slice(0, cols - 7) + '...' : line;
|
|
270
|
+
lines.push(' ' + safeLine);
|
|
271
|
+
}
|
|
272
|
+
// Pad to fixed height
|
|
273
|
+
const renderedLines = endLine - state.scrollOffset;
|
|
274
|
+
for (let i = renderedLines; i < DETAIL_VIEW_CONTENT_LINES; i++) {
|
|
275
|
+
lines.push('');
|
|
276
|
+
}
|
|
277
|
+
// Footer with scroll indicator
|
|
278
|
+
lines.push('');
|
|
279
|
+
const scrollInfo = contentLines.length > state.visibleLines
|
|
280
|
+
? s.muted(` [${String(state.scrollOffset + 1)}-${String(endLine)}/${String(contentLines.length)}]`)
|
|
281
|
+
: '';
|
|
282
|
+
lines.push(border);
|
|
283
|
+
lines.push(s.muted(' ↑↓/PgUp/PgDn Scroll · q/Esc Back') + scrollInfo);
|
|
284
|
+
// Render all lines
|
|
285
|
+
terminal.write(lines.join('\n'));
|
|
286
|
+
return lines.length;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Show the detail view (blocking, returns when user presses q/Esc)
|
|
290
|
+
* Returns the line count so caller can clear it properly
|
|
291
|
+
*/
|
|
292
|
+
async function showDetailView(options) {
|
|
293
|
+
const cols = terminal.getTerminalWidth();
|
|
294
|
+
const contentLines = formatArgsForDetail(options.toolName, options.args, cols - 4);
|
|
295
|
+
const state = {
|
|
296
|
+
scrollOffset: 0,
|
|
297
|
+
totalLines: contentLines.length,
|
|
298
|
+
visibleLines: DETAIL_VIEW_CONTENT_LINES,
|
|
299
|
+
};
|
|
300
|
+
let lineCount = 0;
|
|
301
|
+
// Initial render (no previous lines to clear)
|
|
302
|
+
lineCount = renderDetailView(options, state, contentLines, 0);
|
|
303
|
+
return new Promise((resolve) => {
|
|
304
|
+
const handleData = (data) => {
|
|
305
|
+
const isEscape = data.length === 1 && data[0] === 0x1b;
|
|
306
|
+
const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
|
|
307
|
+
const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
|
|
308
|
+
const isPageUp = data.length === 4 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x35 && data[3] === 0x7e;
|
|
309
|
+
const isPageDown = data.length === 4 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x36 && data[3] === 0x7e;
|
|
310
|
+
const key = data.toString().toLowerCase();
|
|
311
|
+
// q or Escape = go back
|
|
312
|
+
if (key === 'q' || isEscape) {
|
|
313
|
+
process.stdin.removeListener('data', handleData);
|
|
314
|
+
// Clear the detail view before returning
|
|
315
|
+
terminal.clearLinesAbove(lineCount);
|
|
316
|
+
resolve(lineCount);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Scroll up
|
|
320
|
+
if (isUpArrow) {
|
|
321
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - 1);
|
|
322
|
+
}
|
|
323
|
+
else if (isPageUp) {
|
|
324
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - state.visibleLines);
|
|
325
|
+
}
|
|
326
|
+
// Scroll down
|
|
327
|
+
else if (isDownArrow) {
|
|
328
|
+
const maxOffset = Math.max(0, state.totalLines - state.visibleLines);
|
|
329
|
+
state.scrollOffset = Math.min(maxOffset, state.scrollOffset + 1);
|
|
330
|
+
}
|
|
331
|
+
else if (isPageDown) {
|
|
332
|
+
const maxOffset = Math.max(0, state.totalLines - state.visibleLines);
|
|
333
|
+
state.scrollOffset = Math.min(maxOffset, state.scrollOffset + state.visibleLines);
|
|
334
|
+
}
|
|
335
|
+
// Re-render with previous line count
|
|
336
|
+
lineCount = renderDetailView(options, state, contentLines, lineCount);
|
|
337
|
+
};
|
|
338
|
+
process.stdin.on('data', handleData);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// =============================================================================
|
|
342
|
+
// Rendering
|
|
343
|
+
// =============================================================================
|
|
344
|
+
function render(options, state, previousLineCount = 0, targetLineCount = 0) {
|
|
345
|
+
const s = getStyles();
|
|
346
|
+
const lines = [];
|
|
347
|
+
const cols = terminal.getTerminalWidth();
|
|
348
|
+
const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
|
|
349
|
+
// Clear previous render
|
|
350
|
+
if (previousLineCount > 0) {
|
|
351
|
+
terminal.clearLinesAbove(previousLineCount);
|
|
352
|
+
}
|
|
353
|
+
// Header
|
|
354
|
+
lines.push(border);
|
|
355
|
+
lines.push(' ' + s.warning('⚠') + ' ' + chalk.bold('Permission Required'));
|
|
356
|
+
lines.push('');
|
|
357
|
+
// Tool info
|
|
358
|
+
lines.push(' Tool: ' + s.primary(options.toolName));
|
|
359
|
+
// Format and display args
|
|
360
|
+
// Use cols - 6 to account for ' ' indent and buffer for safety
|
|
361
|
+
const maxArgWidth = cols - 6;
|
|
362
|
+
const argLines = formatArgs(options.toolName, options.args, maxArgWidth);
|
|
363
|
+
for (const argLine of argLines) {
|
|
364
|
+
// Final safety truncation to prevent any line from wrapping
|
|
365
|
+
const safeArg = argLine.length > maxArgWidth ? argLine.slice(0, maxArgWidth - 3) + '...' : argLine;
|
|
366
|
+
lines.push(' ' + s.muted(safeArg));
|
|
367
|
+
}
|
|
368
|
+
lines.push('');
|
|
369
|
+
// Options
|
|
370
|
+
const optionLabels = ['Yes, allow this', 'No, deny', 'Always allow this tool'];
|
|
371
|
+
const optionKeys = ['y', 'n', 'a'];
|
|
372
|
+
for (let i = 0; i < optionLabels.length; i++) {
|
|
373
|
+
const isCursor = state.selectedIndex === i;
|
|
374
|
+
const prefix = isCursor ? ' ❯ ' : ' ';
|
|
375
|
+
const key = `[${optionKeys[i]}] `;
|
|
376
|
+
if (isCursor) {
|
|
377
|
+
lines.push(s.primary(prefix + key + optionLabels[i]));
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
lines.push(s.muted(prefix + key + optionLabels[i]));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Footer
|
|
384
|
+
lines.push('');
|
|
385
|
+
lines.push(s.muted(' ↑↓ Navigate · Enter Select · y/n/a Quick select · ') + s.primary('d') + s.muted(' Details'));
|
|
386
|
+
// Bottom border
|
|
387
|
+
lines.push(border);
|
|
388
|
+
// Pad with empty lines to maintain consistent height
|
|
389
|
+
while (lines.length < targetLineCount) {
|
|
390
|
+
lines.push('');
|
|
391
|
+
}
|
|
392
|
+
// Render all lines
|
|
393
|
+
terminal.write(lines.join('\n'));
|
|
394
|
+
return lines.length;
|
|
395
|
+
}
|
|
396
|
+
// =============================================================================
|
|
397
|
+
// Main Export
|
|
398
|
+
// =============================================================================
|
|
399
|
+
/**
|
|
400
|
+
* Show the permission overlay
|
|
401
|
+
*/
|
|
402
|
+
export async function showPermissionOverlay(options) {
|
|
403
|
+
const state = {
|
|
404
|
+
selectedIndex: 0, // Default to "Yes"
|
|
405
|
+
};
|
|
406
|
+
let lineCount = 0;
|
|
407
|
+
let maxLineCount = 0;
|
|
408
|
+
// NOTE: Footer is already paused by the caller (index.ts handler calls sharedState.pauseFooter)
|
|
409
|
+
// Do NOT call pauseForOverlay() here - it causes double-pause issues
|
|
410
|
+
// Ensure we start from a fresh line
|
|
411
|
+
terminal.writeLine('');
|
|
412
|
+
terminal.hideCursor();
|
|
413
|
+
const wasRawMode = process.stdin.isRaw;
|
|
414
|
+
terminal.enableRawMode();
|
|
415
|
+
// Initial render
|
|
416
|
+
lineCount = render(options, state, 0);
|
|
417
|
+
maxLineCount = Math.max(maxLineCount, lineCount);
|
|
418
|
+
return new Promise((resolve) => {
|
|
419
|
+
const cleanup = (result) => {
|
|
420
|
+
terminal.clearLinesAbove(maxLineCount);
|
|
421
|
+
// Show result summary
|
|
422
|
+
const s = getStyles();
|
|
423
|
+
const resultText = result === 'allow'
|
|
424
|
+
? s.success('Allowed')
|
|
425
|
+
: result === 'allow-always'
|
|
426
|
+
? s.primary('Always allowed')
|
|
427
|
+
: s.error('Denied');
|
|
428
|
+
terminal.writeLine(s.muted(`Permission: ${resultText}`));
|
|
429
|
+
terminal.writeLine(''); // Blank line for separation
|
|
430
|
+
terminal.showCursor();
|
|
431
|
+
if (!wasRawMode) {
|
|
432
|
+
terminal.disableRawMode();
|
|
433
|
+
}
|
|
434
|
+
process.stdin.removeListener('data', handleData);
|
|
435
|
+
resolve(result);
|
|
436
|
+
};
|
|
437
|
+
const handleData = (data) => {
|
|
438
|
+
const isEscape = data.length === 1 && data[0] === 0x1b;
|
|
439
|
+
const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
|
|
440
|
+
const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
|
|
441
|
+
const isCtrlC = data.length === 1 && data[0] === 0x03;
|
|
442
|
+
const isEnter = data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
|
|
443
|
+
// Ctrl+C or Escape = deny
|
|
444
|
+
if (isCtrlC || isEscape) {
|
|
445
|
+
cleanup('deny');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// Quick keys
|
|
449
|
+
const key = data.toString().toLowerCase();
|
|
450
|
+
if (key === 'y') {
|
|
451
|
+
cleanup('allow');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (key === 'n') {
|
|
455
|
+
cleanup('deny');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (key === 'a') {
|
|
459
|
+
cleanup('allow-always');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (key === 'd') {
|
|
463
|
+
// Show detail view, then return to permission overlay
|
|
464
|
+
process.stdin.removeListener('data', handleData);
|
|
465
|
+
// First clear the current permission overlay
|
|
466
|
+
terminal.clearLinesAbove(maxLineCount);
|
|
467
|
+
void showDetailView(options).then(() => {
|
|
468
|
+
// Detail view already cleared itself, just re-render permission overlay
|
|
469
|
+
lineCount = render(options, state, 0);
|
|
470
|
+
maxLineCount = lineCount;
|
|
471
|
+
process.stdin.on('data', handleData);
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
// Arrow navigation
|
|
476
|
+
if (isUpArrow) {
|
|
477
|
+
state.selectedIndex = Math.max(0, state.selectedIndex - 1);
|
|
478
|
+
}
|
|
479
|
+
else if (isDownArrow) {
|
|
480
|
+
state.selectedIndex = Math.min(2, state.selectedIndex + 1);
|
|
481
|
+
}
|
|
482
|
+
else if (isEnter) {
|
|
483
|
+
// Select based on current index
|
|
484
|
+
const results = ['allow', 'deny', 'allow-always'];
|
|
485
|
+
cleanup(results[state.selectedIndex]);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// Re-render
|
|
489
|
+
lineCount = render(options, state, maxLineCount, maxLineCount);
|
|
490
|
+
maxLineCount = Math.max(maxLineCount, lineCount);
|
|
491
|
+
};
|
|
492
|
+
process.stdin.on('data', handleData);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Utilities
|
|
3
|
+
*
|
|
4
|
+
* Low-level terminal operations using ANSI escape codes.
|
|
5
|
+
* Pure functions with no state.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Set terminal window title
|
|
9
|
+
*/
|
|
10
|
+
export declare function setTitle(title: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Get terminal width (columns)
|
|
13
|
+
*/
|
|
14
|
+
export declare function getTerminalWidth(): number;
|
|
15
|
+
/**
|
|
16
|
+
* Get terminal height (rows)
|
|
17
|
+
*/
|
|
18
|
+
export declare function getTerminalHeight(): number;
|
|
19
|
+
/**
|
|
20
|
+
* Move cursor up N lines
|
|
21
|
+
*/
|
|
22
|
+
export declare function moveCursorUp(n: number): void;
|
|
23
|
+
/**
|
|
24
|
+
* Move cursor down N lines
|
|
25
|
+
*/
|
|
26
|
+
export declare function moveCursorDown(n: number): void;
|
|
27
|
+
/**
|
|
28
|
+
* Move cursor to column (1-indexed)
|
|
29
|
+
*/
|
|
30
|
+
export declare function moveCursorToColumn(col: number): void;
|
|
31
|
+
/**
|
|
32
|
+
* Move cursor to beginning of line
|
|
33
|
+
*/
|
|
34
|
+
export declare function moveCursorToLineStart(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Save cursor position
|
|
37
|
+
*/
|
|
38
|
+
export declare function saveCursor(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Restore cursor position
|
|
41
|
+
*/
|
|
42
|
+
export declare function restoreCursor(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Hide cursor
|
|
45
|
+
*/
|
|
46
|
+
export declare function hideCursor(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Show cursor
|
|
49
|
+
*/
|
|
50
|
+
export declare function showCursor(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Clear current line
|
|
53
|
+
*/
|
|
54
|
+
export declare function clearLine(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Clear from cursor to end of screen
|
|
57
|
+
*/
|
|
58
|
+
export declare function clearToEndOfScreen(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Clear N lines above cursor (including current line)
|
|
61
|
+
* Moves cursor up, clears to end of screen, cursor ends at top
|
|
62
|
+
*/
|
|
63
|
+
export declare function clearLinesAbove(count: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* Clear entire screen
|
|
66
|
+
*/
|
|
67
|
+
export declare function clearScreen(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Calculate how many physical (visual) lines a string occupies
|
|
70
|
+
* given terminal width and starting column.
|
|
71
|
+
*
|
|
72
|
+
* @param text - The text to measure (may contain newlines)
|
|
73
|
+
* @param termWidth - Terminal width in columns
|
|
74
|
+
* @param startCol - Starting column position (0-indexed)
|
|
75
|
+
* @returns Number of physical lines occupied
|
|
76
|
+
*/
|
|
77
|
+
export declare function calculatePhysicalLines(text: string, termWidth: number, startCol?: number): number;
|
|
78
|
+
/**
|
|
79
|
+
* Calculate physical layout of text with cursor position
|
|
80
|
+
*
|
|
81
|
+
* @param text - The text to analyze
|
|
82
|
+
* @param cursorPos - Cursor position in the text (character index)
|
|
83
|
+
* @param termWidth - Terminal width
|
|
84
|
+
* @param startCol - Starting column (for prompt prefix)
|
|
85
|
+
*/
|
|
86
|
+
export declare function calculateCursorPosition(text: string, cursorPos: number, termWidth: number, startCol?: number): {
|
|
87
|
+
row: number;
|
|
88
|
+
col: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Create a horizontal line spanning terminal width
|
|
92
|
+
*
|
|
93
|
+
* @param char - Character to use (default: '─')
|
|
94
|
+
* @returns String of repeated characters
|
|
95
|
+
*/
|
|
96
|
+
export declare function horizontalLine(char?: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* Enable raw mode for stdin
|
|
99
|
+
* In raw mode, input is available character by character
|
|
100
|
+
*/
|
|
101
|
+
export declare function enableRawMode(): void;
|
|
102
|
+
/**
|
|
103
|
+
* Disable raw mode for stdin and pause it
|
|
104
|
+
*/
|
|
105
|
+
export declare function disableRawMode(): void;
|
|
106
|
+
/**
|
|
107
|
+
* Write to stdout without newline
|
|
108
|
+
*/
|
|
109
|
+
export declare function write(text: string): void;
|
|
110
|
+
/**
|
|
111
|
+
* Write line to stdout with newline
|
|
112
|
+
*/
|
|
113
|
+
export declare function writeLine(text?: string): void;
|
|
114
|
+
/**
|
|
115
|
+
* Ring terminal bell
|
|
116
|
+
*/
|
|
117
|
+
export declare function bell(): void;
|