mepcli 1.1.0 → 1.3.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.
@@ -0,0 +1,25 @@
1
+ export type ShellType = 'bash' | 'powershell' | 'cmd';
2
+ export interface ShellStrategy {
3
+ binary: string;
4
+ wrapper: string;
5
+ continuation: string;
6
+ escape(value: string): string;
7
+ }
8
+ export declare class BashStrategy implements ShellStrategy {
9
+ readonly binary = "curl";
10
+ readonly wrapper = "'";
11
+ readonly continuation = " \\";
12
+ escape(value: string): string;
13
+ }
14
+ export declare class PowerShellStrategy implements ShellStrategy {
15
+ readonly binary = "curl.exe";
16
+ readonly wrapper = "'";
17
+ readonly continuation = " `";
18
+ escape(value: string): string;
19
+ }
20
+ export declare class CmdStrategy implements ShellStrategy {
21
+ readonly binary = "curl";
22
+ readonly wrapper = "\"";
23
+ readonly continuation = " ^";
24
+ escape(value: string): string;
25
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CmdStrategy = exports.PowerShellStrategy = exports.BashStrategy = void 0;
4
+ class BashStrategy {
5
+ constructor() {
6
+ this.binary = 'curl';
7
+ this.wrapper = "'";
8
+ this.continuation = ' \\';
9
+ }
10
+ escape(value) {
11
+ // e.g. "It's me" -> 'It'\''s me'
12
+ return `'${value.replace(/'/g, "'\\''")}'`;
13
+ }
14
+ }
15
+ exports.BashStrategy = BashStrategy;
16
+ class PowerShellStrategy {
17
+ constructor() {
18
+ this.binary = 'curl.exe';
19
+ this.wrapper = "'";
20
+ this.continuation = ' `';
21
+ }
22
+ escape(value) {
23
+ // e.g. "It's me" -> 'It''s me'
24
+ return `'${value.replace(/'/g, "''")}'`;
25
+ }
26
+ }
27
+ exports.PowerShellStrategy = PowerShellStrategy;
28
+ class CmdStrategy {
29
+ constructor() {
30
+ this.binary = 'curl';
31
+ this.wrapper = '"';
32
+ this.continuation = ' ^';
33
+ }
34
+ escape(value) {
35
+ // e.g. {"key": "value"} -> "{\"key\": \"value\"}"
36
+ // e.g. value with " -> "value with \""
37
+ const flattened = value.replace(/[\r\n]+/g, ' ');
38
+ return `"${flattened.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
39
+ }
40
+ }
41
+ exports.CmdStrategy = CmdStrategy;
@@ -16,24 +16,25 @@ export interface CurlResult {
16
16
  export declare class CurlPrompt extends Prompt<CurlResult, CurlOptions> {
17
17
  private section;
18
18
  private methodIndex;
19
- private url;
19
+ private urlSegments;
20
+ private urlCursor;
20
21
  private headers;
21
22
  private body;
22
- private urlCursor;
23
23
  private lastLinesUp;
24
+ private shell;
25
+ private strategies;
24
26
  constructor(options: CurlOptions);
25
27
  private get currentMethod();
26
28
  private get hasBody();
27
- /**
28
- * Escape a string for safe inclusion inside a double-quoted shell argument.
29
- * This escapes backslashes first, then double quotes.
30
- */
31
- private shellEscapeDoubleQuoted;
29
+ private get url();
30
+ private cycleShell;
32
31
  private generateCommand;
33
32
  protected render(firstRender: boolean): void;
34
33
  protected cleanup(): void;
35
34
  protected handleInput(char: string, _buffer: Buffer): void;
35
+ private handleUrlInput;
36
36
  private cycleSection;
37
+ private clear;
37
38
  private editHeaders;
38
39
  private editBody;
39
40
  private submitResult;
@@ -7,6 +7,8 @@ const theme_1 = require("../theme");
7
7
  const symbols_1 = require("../symbols");
8
8
  const map_1 = require("./map");
9
9
  const code_1 = require("./code");
10
+ const utils_1 = require("../utils");
11
+ const curl_utils_1 = require("./curl-utils");
10
12
  var Section;
11
13
  (function (Section) {
12
14
  Section[Section["METHOD"] = 0] = "METHOD";
@@ -15,26 +17,45 @@ var Section;
15
17
  Section[Section["BODY"] = 3] = "BODY";
16
18
  })(Section || (Section = {}));
17
19
  const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
20
+ const COMMON_HEADERS = [
21
+ 'Accept', 'Accept-Encoding', 'Accept-Language',
22
+ 'Authorization', 'Cache-Control', 'Connection',
23
+ 'Content-Type', 'Cookie', 'Host',
24
+ 'Origin', 'Pragma', 'Referer',
25
+ 'User-Agent', 'X-Requested-With'
26
+ ];
18
27
  class CurlPrompt extends base_1.Prompt {
19
28
  constructor(options) {
20
29
  super(options);
21
30
  this.section = Section.METHOD;
22
31
  this.methodIndex = 0;
23
- this.url = '';
32
+ // URL State
33
+ this.urlSegments = [];
34
+ this.urlCursor = 0;
24
35
  this.headers = {};
25
36
  this.body = '';
26
- // For URL input
27
- this.urlCursor = 0;
37
+ // Render State
28
38
  this.lastLinesUp = 0;
29
- this.warnExperimental();
39
+ // Shell State
40
+ this.shell = 'bash';
41
+ this.strategies = {
42
+ bash: new curl_utils_1.BashStrategy(),
43
+ powershell: new curl_utils_1.PowerShellStrategy(),
44
+ cmd: new curl_utils_1.CmdStrategy()
45
+ };
46
+ // Auto-detect shell
47
+ if (process.platform === 'win32') {
48
+ this.shell = 'powershell';
49
+ }
30
50
  // Initialize state
31
51
  if (options.defaultMethod) {
32
52
  const idx = METHODS.indexOf(options.defaultMethod.toUpperCase());
33
53
  if (idx >= 0)
34
54
  this.methodIndex = idx;
35
55
  }
36
- this.url = options.defaultUrl || '';
37
- this.urlCursor = this.url.length;
56
+ const initialUrl = options.defaultUrl || '';
57
+ this.urlSegments = (0, utils_1.safeSplit)(initialUrl);
58
+ this.urlCursor = this.urlSegments.length;
38
59
  this.headers = { ...options.defaultHeaders };
39
60
  this.body = options.defaultBody || '';
40
61
  // Auto-select URL if method is GET (default)
@@ -48,32 +69,36 @@ class CurlPrompt extends base_1.Prompt {
48
69
  get hasBody() {
49
70
  return this.currentMethod !== 'GET' && this.currentMethod !== 'HEAD';
50
71
  }
51
- /**
52
- * Escape a string for safe inclusion inside a double-quoted shell argument.
53
- * This escapes backslashes first, then double quotes.
54
- */
55
- shellEscapeDoubleQuoted(value) {
56
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
72
+ get url() {
73
+ return this.urlSegments.join('');
74
+ }
75
+ cycleShell() {
76
+ const shells = ['bash', 'powershell', 'cmd'];
77
+ const currentIdx = shells.indexOf(this.shell);
78
+ this.shell = shells[(currentIdx + 1) % shells.length];
79
+ this.render(false);
57
80
  }
58
- generateCommand() {
59
- let cmd = `curl -X ${this.currentMethod}`;
81
+ generateCommand(multiline = false) {
82
+ // Force single line for CMD
83
+ if (this.shell === 'cmd') {
84
+ multiline = false;
85
+ }
86
+ const strategy = this.strategies[this.shell];
87
+ const continuation = multiline ? `${strategy.continuation}\n ` : ' ';
88
+ let cmd = `${strategy.binary} -X ${this.currentMethod}`;
60
89
  // Headers
61
90
  Object.entries(this.headers).forEach(([k, v]) => {
62
- cmd += ` -H "${k}: ${v}"`;
91
+ cmd += `${continuation}-H ${strategy.escape(`${k}: ${v}`)}`;
63
92
  });
64
93
  // Body
65
94
  if (this.hasBody && this.body) {
66
- // Escape body for shell
67
- const escapedBody = this.shellEscapeDoubleQuoted(this.body);
68
- cmd += ` -d "${escapedBody}"`;
95
+ const escapedBody = strategy.escape(this.body);
96
+ cmd += `${continuation}-d ${escapedBody}`;
69
97
  }
70
98
  // URL
71
- if (this.url) {
72
- cmd += ` "${this.url}"`;
73
- }
74
- else {
75
- cmd += ` "http://localhost..."`;
76
- }
99
+ const urlStr = this.url;
100
+ const displayUrl = urlStr || 'http://localhost...';
101
+ cmd += `${continuation}${strategy.escape(displayUrl)}`;
77
102
  return cmd;
78
103
  }
79
104
  render(firstRender) {
@@ -83,7 +108,8 @@ class CurlPrompt extends base_1.Prompt {
83
108
  this.lastLinesUp = 0;
84
109
  let output = '';
85
110
  // Title
86
- output += `${theme_1.theme.success}? ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}\n`;
111
+ const shellLabel = `${theme_1.theme.muted}[Shell: ${this.shell.toUpperCase()}]${ansi_1.ANSI.RESET}`;
112
+ output += `${theme_1.theme.success}? ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message} ${shellLabel}${ansi_1.ANSI.RESET}\n`;
87
113
  // 1. Method
88
114
  const methodLabel = this.section === Section.METHOD
89
115
  ? `${theme_1.theme.main}${ansi_1.ANSI.REVERSE} ${this.currentMethod} ${ansi_1.ANSI.RESET}`
@@ -92,16 +118,16 @@ class CurlPrompt extends base_1.Prompt {
92
118
  // 2. URL
93
119
  const urlActive = this.section === Section.URL;
94
120
  const urlPrefix = urlActive ? `${theme_1.theme.main}${ansi_1.ANSI.BOLD} URL: ${ansi_1.ANSI.RESET}` : ` URL: `;
95
- let urlDisplay = this.url;
96
- if (!urlDisplay && urlActive) {
121
+ let urlDisplay = '';
122
+ if (this.urlSegments.length === 0 && urlActive) {
123
+ // Placeholder/Empty state when active
124
+ urlDisplay = '';
125
+ }
126
+ else if (this.urlSegments.length === 0 && !urlActive) {
97
127
  urlDisplay = `${theme_1.theme.muted}http://localhost:3000${ansi_1.ANSI.RESET}`;
98
128
  }
99
- // Insert cursor for URL
100
- if (urlActive) {
101
- const beforeCursor = urlDisplay.slice(0, this.urlCursor);
102
- const atCursor = urlDisplay.slice(this.urlCursor, this.urlCursor + 1) || ' ';
103
- const afterCursor = urlDisplay.slice(this.urlCursor + 1);
104
- urlDisplay = `${beforeCursor}${theme_1.theme.main}${ansi_1.ANSI.UNDERLINE}${atCursor}${ansi_1.ANSI.RESET}${afterCursor}`;
129
+ else {
130
+ urlDisplay = this.urlSegments.join('');
105
131
  }
106
132
  output += `${urlPrefix}${urlDisplay}\n`;
107
133
  // 3. Headers
@@ -130,26 +156,39 @@ class CurlPrompt extends base_1.Prompt {
130
156
  }
131
157
  // Preview
132
158
  output += `\n${ansi_1.ANSI.BOLD}Preview:${ansi_1.ANSI.RESET}\n`;
133
- const cmd = this.generateCommand();
159
+ // Use multiline mode for preview
160
+ const cmd = this.generateCommand(true);
134
161
  // Syntax highlight command (basic)
135
162
  output += `${ansi_1.ANSI.FG_CYAN}${cmd}${ansi_1.ANSI.RESET}\n`;
136
163
  // Instructions
137
- output += `\n${theme_1.theme.muted}(Tab: Nav, Space: Toggle Method, Enter: Edit/Submit)${ansi_1.ANSI.RESET}`;
164
+ output += `\n${theme_1.theme.muted}(Tab: Nav, 's': Shell, Space: Toggle Method, Enter: Edit/Submit)${ansi_1.ANSI.RESET}`;
138
165
  this.renderFrame(output);
139
166
  // Cursor Positioning
140
167
  if (this.section === Section.URL) {
141
168
  const prefixLen = 6; // " URL: "
142
- // We need to move cursor to (row 3, prefixLen + this.urlCursor)
143
- const lines = output.split('\n');
144
- const urlLineIndex = lines.findIndex(l => l.includes(' URL: '));
145
- const linesFromBottom = lines.length - 1 - urlLineIndex;
146
- this.print(ansi_1.ANSI.SHOW_CURSOR);
147
- if (linesFromBottom > 0) {
148
- this.print(`\x1b[${linesFromBottom}A`);
149
- this.lastLinesUp = linesFromBottom;
169
+ // Use lastRenderLines to find the exact visual line index
170
+ let urlLineIndex = -1;
171
+ for (let i = 0; i < this.lastRenderLines.length; i++) {
172
+ if (this.lastRenderLines[i].includes(' URL: ')) {
173
+ urlLineIndex = i;
174
+ break;
175
+ }
176
+ }
177
+ if (urlLineIndex !== -1) {
178
+ const linesFromBottom = this.lastRenderLines.length - 1 - urlLineIndex;
179
+ this.print(ansi_1.ANSI.SHOW_CURSOR);
180
+ if (linesFromBottom > 0) {
181
+ this.print(`\x1b[${linesFromBottom}A`);
182
+ this.lastLinesUp = linesFromBottom;
183
+ }
184
+ // Calculate visual cursor position
185
+ let targetCol = prefixLen;
186
+ // Add width of segments up to cursor
187
+ for (let i = 0; i < this.urlCursor; i++) {
188
+ targetCol += (0, utils_1.stringWidth)(this.urlSegments[i]);
189
+ }
190
+ this.print(`\r\x1b[${targetCol}C`);
150
191
  }
151
- const targetCol = prefixLen + this.urlCursor;
152
- this.print(`\r\x1b[${targetCol}C`);
153
192
  }
154
193
  else {
155
194
  this.print(ansi_1.ANSI.HIDE_CURSOR);
@@ -163,6 +202,11 @@ class CurlPrompt extends base_1.Prompt {
163
202
  super.cleanup();
164
203
  }
165
204
  handleInput(char, _buffer) {
205
+ // Toggle Shell (only when not editing URL)
206
+ if (char === 's' && this.section !== Section.URL) {
207
+ this.cycleShell();
208
+ return;
209
+ }
166
210
  // Navigation
167
211
  if (char === '\t') {
168
212
  this.cycleSection(1);
@@ -186,40 +230,11 @@ class CurlPrompt extends base_1.Prompt {
186
230
  this.render(false);
187
231
  }
188
232
  else if (char === '\r' || char === '\n') {
189
- // Enter on Method -> Submit
190
233
  this.submitResult();
191
234
  }
192
235
  break;
193
236
  case Section.URL:
194
- if (char === '\r' || char === '\n') {
195
- this.submitResult();
196
- return;
197
- }
198
- // Typing
199
- if (char === '\u0008' || char === '\x7f') { // Backspace
200
- if (this.urlCursor > 0) {
201
- this.url = this.url.slice(0, this.urlCursor - 1) + this.url.slice(this.urlCursor);
202
- this.urlCursor--;
203
- this.render(false);
204
- }
205
- }
206
- else if (this.isLeft(char)) {
207
- if (this.urlCursor > 0) {
208
- this.urlCursor--;
209
- this.render(false);
210
- }
211
- }
212
- else if (this.isRight(char)) {
213
- if (this.urlCursor < this.url.length) {
214
- this.urlCursor++;
215
- this.render(false);
216
- }
217
- }
218
- else if (!/^[\x00-\x1F]/.test(char)) {
219
- this.url = this.url.slice(0, this.urlCursor) + char + this.url.slice(this.urlCursor);
220
- this.urlCursor += char.length;
221
- this.render(false);
222
- }
237
+ this.handleUrlInput(char);
223
238
  break;
224
239
  case Section.HEADERS:
225
240
  if (char === '\r' || char === '\n') {
@@ -233,36 +248,139 @@ class CurlPrompt extends base_1.Prompt {
233
248
  break;
234
249
  }
235
250
  }
251
+ handleUrlInput(char) {
252
+ // Submit
253
+ if (char === '\r' || char === '\n') {
254
+ this.submitResult();
255
+ return;
256
+ }
257
+ // Home
258
+ if (char === '\x1b[H' || char === '\x1bOH' || char === '\x1b[1~') {
259
+ this.urlCursor = 0;
260
+ this.render(false);
261
+ return;
262
+ }
263
+ // End
264
+ if (char === '\x1b[F' || char === '\x1bOF' || char === '\x1b[4~') {
265
+ this.urlCursor = this.urlSegments.length;
266
+ this.render(false);
267
+ return;
268
+ }
269
+ // Ctrl+U (Delete to start)
270
+ if (char === '\x15') {
271
+ if (this.urlCursor > 0) {
272
+ this.urlSegments.splice(0, this.urlCursor);
273
+ this.urlCursor = 0;
274
+ this.render(false);
275
+ }
276
+ return;
277
+ }
278
+ // Ctrl+W (Delete word backwards)
279
+ if (char === '\x17') {
280
+ if (this.urlCursor > 0) {
281
+ // Find previous word boundary
282
+ let i = this.urlCursor - 1;
283
+ // Skip trailing spaces
284
+ while (i >= 0 && this.urlSegments[i] === ' ')
285
+ i--;
286
+ // Skip word characters
287
+ while (i >= 0 && this.urlSegments[i] !== ' ')
288
+ i--;
289
+ const deleteCount = this.urlCursor - (i + 1);
290
+ this.urlSegments.splice(i + 1, deleteCount);
291
+ this.urlCursor = i + 1;
292
+ this.render(false);
293
+ }
294
+ return;
295
+ }
296
+ // Backspace
297
+ if (char === '\u0008' || char === '\x7f') {
298
+ if (this.urlCursor > 0) {
299
+ this.urlSegments.splice(this.urlCursor - 1, 1);
300
+ this.urlCursor--;
301
+ this.render(false);
302
+ }
303
+ return;
304
+ }
305
+ // Delete
306
+ if (char === '\u001b[3~') {
307
+ if (this.urlCursor < this.urlSegments.length) {
308
+ this.urlSegments.splice(this.urlCursor, 1);
309
+ this.render(false);
310
+ }
311
+ return;
312
+ }
313
+ // Left
314
+ if (this.isLeft(char)) {
315
+ if (this.urlCursor > 0) {
316
+ this.urlCursor--;
317
+ this.render(false);
318
+ }
319
+ return;
320
+ }
321
+ // Right
322
+ if (this.isRight(char)) {
323
+ if (this.urlCursor < this.urlSegments.length) {
324
+ this.urlCursor++;
325
+ this.render(false);
326
+ }
327
+ return;
328
+ }
329
+ // Regular Typing
330
+ if (!/^[\x00-\x1F]/.test(char) && !char.startsWith('\x1b')) {
331
+ const newSegments = (0, utils_1.safeSplit)(char);
332
+ this.urlSegments.splice(this.urlCursor, 0, ...newSegments);
333
+ this.urlCursor += newSegments.length;
334
+ this.render(false);
335
+ }
336
+ }
236
337
  cycleSection(direction) {
237
- // Logic to skip disabled Body
238
338
  let next = this.section + direction;
239
- // Loop
240
339
  if (next > Section.BODY)
241
340
  next = Section.METHOD;
242
341
  if (next < Section.METHOD)
243
342
  next = Section.BODY;
244
- // If Body is disabled and we landed on it
245
343
  if (next === Section.BODY && !this.hasBody) {
246
344
  next = direction === 1 ? Section.METHOD : Section.HEADERS;
247
345
  }
248
346
  this.section = next;
249
347
  }
348
+ clear() {
349
+ // 1. Restore cursor to bottom if it was moved up (for URL editing)
350
+ if (this.lastLinesUp > 0) {
351
+ this.print(`\x1b[${this.lastLinesUp}B`);
352
+ this.lastLinesUp = 0;
353
+ }
354
+ // 2. Erase the prompt content
355
+ // We move up (height - 1) lines to the top line, then erase everything below
356
+ if (this.lastRenderHeight > 0) {
357
+ this.print(`\x1b[${this.lastRenderHeight - 1}A`); // Go to top line
358
+ this.print('\r'); // Go to start of line
359
+ this.print(ansi_1.ANSI.ERASE_DOWN); // Erase everything below
360
+ // Reset state so next render is treated as fresh
361
+ this.lastRenderLines = [];
362
+ this.lastRenderHeight = 0;
363
+ }
364
+ }
250
365
  async editHeaders() {
366
+ this.clear(); // Clear UI to prevent artifacts
251
367
  this.pauseInput();
252
368
  try {
253
369
  const result = await new map_1.MapPrompt({
254
370
  message: 'Edit Headers',
255
371
  initial: this.headers,
372
+ suggestions: COMMON_HEADERS
256
373
  }).run();
257
374
  this.headers = result;
258
375
  }
259
376
  catch (_e) {
260
- // Cancelled or error
377
+ // Cancelled
261
378
  }
262
379
  this.resumeInput();
263
- this.render(false); // Re-render our UI
380
+ this.render(true); // Force full re-render
264
381
  }
265
382
  async editBody() {
383
+ this.clear(); // Clear UI to prevent artifacts
266
384
  this.pauseInput();
267
385
  try {
268
386
  const result = await new code_1.CodePrompt({
@@ -277,7 +395,7 @@ class CurlPrompt extends base_1.Prompt {
277
395
  // Cancelled
278
396
  }
279
397
  this.resumeInput();
280
- this.render(false);
398
+ this.render(true); // Force full re-render
281
399
  }
282
400
  submitResult() {
283
401
  this.submit({
@@ -285,7 +403,7 @@ class CurlPrompt extends base_1.Prompt {
285
403
  url: this.url,
286
404
  headers: this.headers,
287
405
  body: this.hasBody ? this.body : undefined,
288
- command: this.generateCommand()
406
+ command: this.generateCommand(false) // Single line for clipboard/usage
289
407
  });
290
408
  }
291
409
  }
@@ -4,8 +4,12 @@ export declare class ExecPrompt extends Prompt<void, ExecOptions> {
4
4
  private child?;
5
5
  private status;
6
6
  private timer?;
7
+ private stdoutBuffer;
8
+ private stderrBuffer;
9
+ private lastLogLine;
7
10
  constructor(options: ExecOptions);
8
11
  run(): Promise<void>;
12
+ private updateLastLogLine;
9
13
  private killChild;
10
14
  protected cleanup(): void;
11
15
  protected render(_firstRender: boolean): void;
@@ -10,20 +10,42 @@ class ExecPrompt extends base_1.Prompt {
10
10
  constructor(options) {
11
11
  super(options);
12
12
  this.status = 'running';
13
- this.warnExperimental();
13
+ this.stdoutBuffer = '';
14
+ this.stderrBuffer = '';
15
+ this.lastLogLine = '';
16
+ // Experimental warning removed
14
17
  }
15
18
  run() {
16
19
  this.child = (0, child_process_1.spawn)(this.options.command, [], {
17
20
  cwd: this.options.cwd || process.cwd(),
18
21
  shell: true,
19
- stdio: this.options.streamOutput ? 'inherit' : 'ignore'
22
+ // Use 'ignore' for stdin so parent keeps control (and raw mode).
23
+ // Use 'pipe' for stdout/stderr to capture output.
24
+ stdio: ['ignore', 'pipe', 'pipe']
20
25
  });
26
+ // Capture stdout
27
+ if (this.child.stdout) {
28
+ this.child.stdout.on('data', (data) => {
29
+ const chunk = data.toString();
30
+ this.stdoutBuffer += chunk;
31
+ this.updateLastLogLine(chunk);
32
+ });
33
+ }
34
+ // Capture stderr
35
+ if (this.child.stderr) {
36
+ this.child.stderr.on('data', (data) => {
37
+ const chunk = data.toString();
38
+ this.stderrBuffer += chunk;
39
+ this.updateLastLogLine(chunk);
40
+ });
41
+ }
21
42
  if (this.options.timeout && this.options.timeout > 0) {
22
43
  this.timer = setTimeout(() => {
23
44
  if (this.status !== 'running')
24
45
  return;
25
46
  this.status = 'error';
26
47
  this.render(false);
48
+ this.killChild();
27
49
  this.cancel(new Error(`Timeout after ${this.options.timeout}ms`));
28
50
  }, this.options.timeout);
29
51
  }
@@ -38,7 +60,15 @@ class ExecPrompt extends base_1.Prompt {
38
60
  else {
39
61
  this.status = 'error';
40
62
  this.render(false);
41
- this.cancel(new Error(`Command failed with exit code ${code}`));
63
+ const errorMessage = this.stderrBuffer.trim() || `Command failed with exit code ${code}`;
64
+ const err = new Error(errorMessage);
65
+ // Attach details
66
+ Object.assign(err, {
67
+ code,
68
+ stdout: this.stdoutBuffer,
69
+ stderr: this.stderrBuffer
70
+ });
71
+ this.cancel(err);
42
72
  }
43
73
  });
44
74
  this.child.on('error', (err) => {
@@ -50,6 +80,19 @@ class ExecPrompt extends base_1.Prompt {
50
80
  });
51
81
  return super.run();
52
82
  }
83
+ updateLastLogLine(chunk) {
84
+ // We only want the last non-empty line
85
+ const lines = chunk.split('\n');
86
+ // Iterate backwards
87
+ for (let i = lines.length - 1; i >= 0; i--) {
88
+ const line = lines[i].trim();
89
+ if (line) {
90
+ this.lastLogLine = line;
91
+ this.render(false);
92
+ break;
93
+ }
94
+ }
95
+ }
53
96
  killChild() {
54
97
  if (this.child && !this.child.killed) {
55
98
  this.child.kill();
@@ -73,11 +116,22 @@ class ExecPrompt extends base_1.Prompt {
73
116
  else if (this.status === 'error') {
74
117
  symbol = theme_1.theme.error + symbols_1.symbols.cross + ansi_1.ANSI.RESET;
75
118
  }
76
- const output = `${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${symbol}`;
119
+ let details = '';
120
+ if (this.status === 'running' && this.lastLogLine) {
121
+ // Truncate for display
122
+ const maxLen = 50;
123
+ let line = this.stripAnsi(this.lastLogLine);
124
+ if (line.length > maxLen) {
125
+ line = line.substring(0, maxLen - 3) + '...';
126
+ }
127
+ details = ` ${theme_1.theme.muted}${line}${ansi_1.ANSI.RESET}`;
128
+ }
129
+ const output = `${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${symbol}${details}`;
77
130
  this.renderFrame(output);
78
131
  }
79
132
  handleInput(_char, _key) {
80
- // Ignore input
133
+ // Prompt base class handles Ctrl+C (SIGINT) in _onKeyHandler
134
+ // which calls cleanup() -> killChild().
81
135
  }
82
136
  }
83
137
  exports.ExecPrompt = ExecPrompt;
@@ -7,9 +7,11 @@ export declare class MapPrompt extends Prompt<Record<string, string>, MapOptions
7
7
  private scrollTop;
8
8
  private readonly pageSize;
9
9
  private errorMsg;
10
+ private ghost;
10
11
  constructor(options: MapOptions);
11
12
  protected render(_firstRender: boolean): void;
12
13
  private pad;
14
+ private updateGhost;
13
15
  protected handleInput(char: string): void;
14
16
  protected handleMouse(event: MouseEvent): void;
15
17
  }