@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.
Files changed (152) hide show
  1. package/README.md +110 -0
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agent.js +317 -0
  4. package/dist/agents/registry.d.ts +66 -0
  5. package/dist/agents/registry.js +238 -0
  6. package/dist/agents/types.d.ts +40 -0
  7. package/dist/agents/types.js +94 -0
  8. package/dist/commands/custom-registry.d.ts +69 -0
  9. package/dist/commands/custom-registry.js +246 -0
  10. package/dist/commands/index.d.ts +7 -0
  11. package/dist/commands/index.js +7 -0
  12. package/dist/commands/types.d.ts +31 -0
  13. package/dist/commands/types.js +26 -0
  14. package/dist/commands.d.ts +63 -0
  15. package/dist/commands.js +324 -0
  16. package/dist/db/index.d.ts +42 -0
  17. package/dist/db/index.js +146 -0
  18. package/dist/db/repositories/document-repository.d.ts +63 -0
  19. package/dist/db/repositories/document-repository.js +184 -0
  20. package/dist/db/repositories/index.d.ts +9 -0
  21. package/dist/db/repositories/index.js +6 -0
  22. package/dist/db/repositories/project-repository.d.ts +132 -0
  23. package/dist/db/repositories/project-repository.js +337 -0
  24. package/dist/db/repositories/work-item-repository.d.ts +115 -0
  25. package/dist/db/repositories/work-item-repository.js +389 -0
  26. package/dist/db/schema.d.ts +83 -0
  27. package/dist/db/schema.js +143 -0
  28. package/dist/debug.d.ts +8 -0
  29. package/dist/debug.js +48 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +348 -0
  32. package/dist/index.old.d.ts +7 -0
  33. package/dist/index.old.js +1014 -0
  34. package/dist/repl.d.ts +121 -0
  35. package/dist/repl.js +1878 -0
  36. package/dist/settings/index.d.ts +80 -0
  37. package/dist/settings/index.js +195 -0
  38. package/dist/shared-handlers.d.ts +63 -0
  39. package/dist/shared-handlers.js +57 -0
  40. package/dist/slash-autocomplete.d.ts +41 -0
  41. package/dist/slash-autocomplete.js +638 -0
  42. package/dist/state.d.ts +75 -0
  43. package/dist/state.js +130 -0
  44. package/dist/tabbed-menu.d.ts +11 -0
  45. package/dist/tabbed-menu.js +328 -0
  46. package/dist/templates/backlog-md.d.ts +7 -0
  47. package/dist/templates/backlog-md.js +94 -0
  48. package/dist/templates/claude-md.d.ts +7 -0
  49. package/dist/templates/claude-md.js +189 -0
  50. package/dist/templates/coding-standards.d.ts +7 -0
  51. package/dist/templates/coding-standards.js +299 -0
  52. package/dist/templates/compilr-md.d.ts +7 -0
  53. package/dist/templates/compilr-md.js +189 -0
  54. package/dist/templates/config-json.d.ts +38 -0
  55. package/dist/templates/config-json.js +39 -0
  56. package/dist/templates/gitignore.d.ts +7 -0
  57. package/dist/templates/gitignore.js +85 -0
  58. package/dist/templates/index.d.ts +19 -0
  59. package/dist/templates/index.js +302 -0
  60. package/dist/templates/package-json.d.ts +7 -0
  61. package/dist/templates/package-json.js +111 -0
  62. package/dist/templates/readme-md.d.ts +7 -0
  63. package/dist/templates/readme-md.js +161 -0
  64. package/dist/templates/tsconfig.d.ts +7 -0
  65. package/dist/templates/tsconfig.js +61 -0
  66. package/dist/templates/types.d.ts +33 -0
  67. package/dist/templates/types.js +24 -0
  68. package/dist/test-autocomplete.d.ts +7 -0
  69. package/dist/test-autocomplete.js +85 -0
  70. package/dist/test-tabbed-menu.d.ts +7 -0
  71. package/dist/test-tabbed-menu.js +25 -0
  72. package/dist/themes/colors.d.ts +49 -0
  73. package/dist/themes/colors.js +135 -0
  74. package/dist/themes/index.d.ts +23 -0
  75. package/dist/themes/index.js +24 -0
  76. package/dist/themes/registry.d.ts +60 -0
  77. package/dist/themes/registry.js +195 -0
  78. package/dist/themes/types.d.ts +82 -0
  79. package/dist/themes/types.js +7 -0
  80. package/dist/tool-selector.d.ts +71 -0
  81. package/dist/tool-selector.js +184 -0
  82. package/dist/tools/ask-user-simple.d.ts +19 -0
  83. package/dist/tools/ask-user-simple.js +86 -0
  84. package/dist/tools/ask-user.d.ts +32 -0
  85. package/dist/tools/ask-user.js +113 -0
  86. package/dist/tools/backlog.d.ts +53 -0
  87. package/dist/tools/backlog.js +709 -0
  88. package/dist/tools.d.ts +15 -0
  89. package/dist/tools.js +121 -0
  90. package/dist/ui/agents-overlay.d.ts +12 -0
  91. package/dist/ui/agents-overlay.js +501 -0
  92. package/dist/ui/arch-type-overlay.d.ts +20 -0
  93. package/dist/ui/arch-type-overlay.js +229 -0
  94. package/dist/ui/ask-user-overlay.d.ts +26 -0
  95. package/dist/ui/ask-user-overlay.js +647 -0
  96. package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
  97. package/dist/ui/ask-user-simple-overlay.js +242 -0
  98. package/dist/ui/backlog-overlay.d.ts +17 -0
  99. package/dist/ui/backlog-overlay.js +786 -0
  100. package/dist/ui/commands-overlay.d.ts +11 -0
  101. package/dist/ui/commands-overlay.js +410 -0
  102. package/dist/ui/config-overlay.d.ts +34 -0
  103. package/dist/ui/config-overlay.js +977 -0
  104. package/dist/ui/conversation.d.ts +82 -0
  105. package/dist/ui/conversation.js +508 -0
  106. package/dist/ui/diff.d.ts +38 -0
  107. package/dist/ui/diff.js +182 -0
  108. package/dist/ui/ephemeral.d.ts +111 -0
  109. package/dist/ui/ephemeral.js +413 -0
  110. package/dist/ui/file-autocomplete.d.ts +45 -0
  111. package/dist/ui/file-autocomplete.js +237 -0
  112. package/dist/ui/footer.d.ts +153 -0
  113. package/dist/ui/footer.js +422 -0
  114. package/dist/ui/index.d.ts +12 -0
  115. package/dist/ui/index.js +15 -0
  116. package/dist/ui/init-overlay.d.ts +24 -0
  117. package/dist/ui/init-overlay.js +525 -0
  118. package/dist/ui/input-prompt-v2.d.ts +179 -0
  119. package/dist/ui/input-prompt-v2.js +991 -0
  120. package/dist/ui/input-prompt.d.ts +97 -0
  121. package/dist/ui/input-prompt.js +800 -0
  122. package/dist/ui/iteration-limit-overlay.d.ts +21 -0
  123. package/dist/ui/iteration-limit-overlay.js +150 -0
  124. package/dist/ui/keys-overlay.d.ts +14 -0
  125. package/dist/ui/keys-overlay.js +181 -0
  126. package/dist/ui/model-warning-overlay.d.ts +30 -0
  127. package/dist/ui/model-warning-overlay.js +171 -0
  128. package/dist/ui/overlay-controller.d.ts +25 -0
  129. package/dist/ui/overlay-controller.js +35 -0
  130. package/dist/ui/overlays.d.ts +47 -0
  131. package/dist/ui/overlays.js +627 -0
  132. package/dist/ui/permission-overlay.d.ts +16 -0
  133. package/dist/ui/permission-overlay.js +494 -0
  134. package/dist/ui/terminal.d.ts +117 -0
  135. package/dist/ui/terminal.js +237 -0
  136. package/dist/ui/todo-zone.d.ts +112 -0
  137. package/dist/ui/todo-zone.js +353 -0
  138. package/dist/ui/tools-overlay.d.ts +26 -0
  139. package/dist/ui/tools-overlay.js +278 -0
  140. package/dist/ui/tutorial-overlay.d.ts +10 -0
  141. package/dist/ui/tutorial-overlay.js +936 -0
  142. package/dist/ui/types.d.ts +103 -0
  143. package/dist/ui/types.js +33 -0
  144. package/dist/utils/credentials.d.ts +55 -0
  145. package/dist/utils/credentials.js +268 -0
  146. package/dist/utils/model-tiers.d.ts +37 -0
  147. package/dist/utils/model-tiers.js +118 -0
  148. package/dist/utils/project-memory.d.ts +47 -0
  149. package/dist/utils/project-memory.js +117 -0
  150. package/dist/utils/project-status.d.ts +56 -0
  151. package/dist/utils/project-status.js +237 -0
  152. 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;