geeto 0.9.1 → 0.10.1

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 (162) hide show
  1. package/README.md +77 -18
  2. package/lib/api/copilot-sdk.d.ts.map +1 -1
  3. package/lib/api/copilot-sdk.js +29 -24
  4. package/lib/api/copilot-sdk.js.map +1 -1
  5. package/lib/api/copilot.d.ts.map +1 -1
  6. package/lib/api/copilot.js +6 -6
  7. package/lib/api/copilot.js.map +1 -1
  8. package/lib/api/gemini.d.ts.map +1 -1
  9. package/lib/api/gemini.js +2 -2
  10. package/lib/api/gemini.js.map +1 -1
  11. package/lib/api/groq-sdk.d.ts +11 -0
  12. package/lib/api/groq-sdk.d.ts.map +1 -0
  13. package/lib/api/groq-sdk.js +131 -0
  14. package/lib/api/groq-sdk.js.map +1 -0
  15. package/lib/api/groq.d.ts +10 -0
  16. package/lib/api/groq.d.ts.map +1 -0
  17. package/lib/api/groq.js +36 -0
  18. package/lib/api/groq.js.map +1 -0
  19. package/lib/api/openrouter-sdk.d.ts.map +1 -1
  20. package/lib/api/openrouter-sdk.js +58 -73
  21. package/lib/api/openrouter-sdk.js.map +1 -1
  22. package/lib/api/openrouter.d.ts.map +1 -1
  23. package/lib/api/openrouter.js +2 -2
  24. package/lib/api/openrouter.js.map +1 -1
  25. package/lib/api/trello.d.ts +16 -1
  26. package/lib/api/trello.d.ts.map +1 -1
  27. package/lib/api/trello.js +90 -2
  28. package/lib/api/trello.js.map +1 -1
  29. package/lib/cli/input.d.ts +1 -9
  30. package/lib/cli/input.d.ts.map +1 -1
  31. package/lib/cli/input.js +439 -169
  32. package/lib/cli/input.js.map +1 -1
  33. package/lib/cli/menu.d.ts.map +1 -1
  34. package/lib/cli/menu.js +33 -20
  35. package/lib/cli/menu.js.map +1 -1
  36. package/lib/core/copilot-setup.d.ts +5 -0
  37. package/lib/core/copilot-setup.d.ts.map +1 -1
  38. package/lib/core/copilot-setup.js +18 -0
  39. package/lib/core/copilot-setup.js.map +1 -1
  40. package/lib/core/gemini-setup.d.ts.map +1 -1
  41. package/lib/core/gemini-setup.js +7 -13
  42. package/lib/core/gemini-setup.js.map +1 -1
  43. package/lib/core/github-setup.d.ts.map +1 -1
  44. package/lib/core/github-setup.js +13 -7
  45. package/lib/core/github-setup.js.map +1 -1
  46. package/lib/core/gitlab-setup.d.ts.map +1 -1
  47. package/lib/core/gitlab-setup.js +13 -6
  48. package/lib/core/gitlab-setup.js.map +1 -1
  49. package/lib/core/groq-setup.d.ts +5 -0
  50. package/lib/core/groq-setup.d.ts.map +1 -0
  51. package/lib/core/groq-setup.js +67 -0
  52. package/lib/core/groq-setup.js.map +1 -0
  53. package/lib/core/openrouter-setup.d.ts.map +1 -1
  54. package/lib/core/openrouter-setup.js +11 -21
  55. package/lib/core/openrouter-setup.js.map +1 -1
  56. package/lib/core/setup.d.ts +2 -1
  57. package/lib/core/setup.d.ts.map +1 -1
  58. package/lib/core/setup.js +44 -7
  59. package/lib/core/setup.js.map +1 -1
  60. package/lib/index.js +8 -0
  61. package/lib/index.js.map +1 -1
  62. package/lib/types/index.d.ts +9 -1
  63. package/lib/types/index.d.ts.map +1 -1
  64. package/lib/utils/ai-workflow.d.ts +10 -2
  65. package/lib/utils/ai-workflow.d.ts.map +1 -1
  66. package/lib/utils/ai-workflow.js +15 -0
  67. package/lib/utils/ai-workflow.js.map +1 -1
  68. package/lib/utils/branch-naming.d.ts +2 -1
  69. package/lib/utils/branch-naming.d.ts.map +1 -1
  70. package/lib/utils/branch-naming.js +114 -68
  71. package/lib/utils/branch-naming.js.map +1 -1
  72. package/lib/utils/config.d.ts +11 -3
  73. package/lib/utils/config.d.ts.map +1 -1
  74. package/lib/utils/config.js +46 -18
  75. package/lib/utils/config.js.map +1 -1
  76. package/lib/utils/git-ai-errors.d.ts.map +1 -1
  77. package/lib/utils/git-ai-errors.js +5 -1
  78. package/lib/utils/git-ai-errors.js.map +1 -1
  79. package/lib/utils/git-ai.d.ts +8 -7
  80. package/lib/utils/git-ai.d.ts.map +1 -1
  81. package/lib/utils/git-ai.js +239 -117
  82. package/lib/utils/git-ai.js.map +1 -1
  83. package/lib/utils/logging.d.ts.map +1 -1
  84. package/lib/utils/logging.js +14 -3
  85. package/lib/utils/logging.js.map +1 -1
  86. package/lib/utils/menu-builders.js +1 -1
  87. package/lib/utils/menu-builders.js.map +1 -1
  88. package/lib/utils/prompts-embedded.d.ts.map +1 -1
  89. package/lib/utils/prompts-embedded.js +0 -81
  90. package/lib/utils/prompts-embedded.js.map +1 -1
  91. package/lib/utils/scramble.d.ts.map +1 -1
  92. package/lib/utils/scramble.js +14 -3
  93. package/lib/utils/scramble.js.map +1 -1
  94. package/lib/utils/state.d.ts.map +1 -1
  95. package/lib/utils/state.js +12 -1
  96. package/lib/utils/state.js.map +1 -1
  97. package/lib/version.d.ts +1 -1
  98. package/lib/version.d.ts.map +1 -1
  99. package/lib/version.js +1 -1
  100. package/lib/version.js.map +1 -1
  101. package/lib/workflows/ai-provider.d.ts +3 -1
  102. package/lib/workflows/ai-provider.d.ts.map +1 -1
  103. package/lib/workflows/ai-provider.js +8 -2
  104. package/lib/workflows/ai-provider.js.map +1 -1
  105. package/lib/workflows/branch-helpers.d.ts.map +1 -1
  106. package/lib/workflows/branch-helpers.js +142 -71
  107. package/lib/workflows/branch-helpers.js.map +1 -1
  108. package/lib/workflows/branch.d.ts.map +1 -1
  109. package/lib/workflows/branch.js +97 -58
  110. package/lib/workflows/branch.js.map +1 -1
  111. package/lib/workflows/commit.d.ts +1 -1
  112. package/lib/workflows/commit.d.ts.map +1 -1
  113. package/lib/workflows/commit.js +99 -37
  114. package/lib/workflows/commit.js.map +1 -1
  115. package/lib/workflows/dry-run.d.ts.map +1 -1
  116. package/lib/workflows/dry-run.js +6 -0
  117. package/lib/workflows/dry-run.js.map +1 -1
  118. package/lib/workflows/issue.d.ts.map +1 -1
  119. package/lib/workflows/issue.js +12 -12
  120. package/lib/workflows/issue.js.map +1 -1
  121. package/lib/workflows/main-helpers.d.ts +3 -1
  122. package/lib/workflows/main-helpers.d.ts.map +1 -1
  123. package/lib/workflows/main-helpers.js +30 -26
  124. package/lib/workflows/main-helpers.js.map +1 -1
  125. package/lib/workflows/main-steps.d.ts.map +1 -1
  126. package/lib/workflows/main-steps.js +8 -0
  127. package/lib/workflows/main-steps.js.map +1 -1
  128. package/lib/workflows/main.d.ts.map +1 -1
  129. package/lib/workflows/main.js +7 -15
  130. package/lib/workflows/main.js.map +1 -1
  131. package/lib/workflows/pr.d.ts.map +1 -1
  132. package/lib/workflows/pr.js +12 -12
  133. package/lib/workflows/pr.js.map +1 -1
  134. package/lib/workflows/release-merge.d.ts.map +1 -1
  135. package/lib/workflows/release-merge.js +40 -10
  136. package/lib/workflows/release-merge.js.map +1 -1
  137. package/lib/workflows/release-sync.d.ts.map +1 -1
  138. package/lib/workflows/release-sync.js +25 -7
  139. package/lib/workflows/release-sync.js.map +1 -1
  140. package/lib/workflows/release.d.ts.map +1 -1
  141. package/lib/workflows/release.js +130 -15
  142. package/lib/workflows/release.js.map +1 -1
  143. package/lib/workflows/repo-settings.d.ts.map +1 -1
  144. package/lib/workflows/repo-settings.js +35 -8
  145. package/lib/workflows/repo-settings.js.map +1 -1
  146. package/lib/workflows/reword.d.ts.map +1 -1
  147. package/lib/workflows/reword.js +60 -17
  148. package/lib/workflows/reword.js.map +1 -1
  149. package/lib/workflows/settings.d.ts +2 -1
  150. package/lib/workflows/settings.d.ts.map +1 -1
  151. package/lib/workflows/settings.js +364 -76
  152. package/lib/workflows/settings.js.map +1 -1
  153. package/lib/workflows/trello-menu.d.ts +0 -3
  154. package/lib/workflows/trello-menu.d.ts.map +1 -1
  155. package/lib/workflows/trello-menu.js +349 -19
  156. package/lib/workflows/trello-menu.js.map +1 -1
  157. package/package.json +13 -12
  158. package/lib/workflows/security-gate.d.ts +0 -8
  159. package/lib/workflows/security-gate.d.ts.map +0 -1
  160. package/lib/workflows/security-gate.js +0 -455
  161. package/lib/workflows/security-gate.js.map +0 -1
  162. package/prompts/security-gate-prompt.md +0 -80
package/lib/cli/input.js CHANGED
@@ -4,12 +4,36 @@
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import os from 'node:os';
7
- import path from 'node:path';
8
7
  import readline from 'node:readline';
9
8
  const rl = readline.createInterface({
10
9
  input: process.stdin,
11
10
  output: process.stdout,
12
11
  });
12
+ const readStdinText = () => {
13
+ try {
14
+ return fs.readFileSync(0, 'utf8');
15
+ }
16
+ catch {
17
+ return '';
18
+ }
19
+ };
20
+ const supportsStickyTerminalLayout = () => {
21
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
22
+ return false;
23
+ const term = process.env.TERM?.toLowerCase();
24
+ if (term === 'dumb')
25
+ return false;
26
+ if (process.platform !== 'win32')
27
+ return term !== undefined;
28
+ return [
29
+ process.env.WT_SESSION,
30
+ // cspell:ignore ANSICON
31
+ process.env.ANSICON,
32
+ process.env.ConEmuANSI === 'ON' ? '1' : undefined,
33
+ process.env.TERM_PROGRAM === 'vscode' ? '1' : undefined,
34
+ term !== undefined && term !== 'dumb' ? term : undefined,
35
+ ].some(Boolean);
36
+ };
13
37
  /**
14
38
  * Ask a question and return user input
15
39
  */
@@ -62,7 +86,9 @@ export const confirm = (question, defaultYes = true) => {
62
86
  if (process.stdin.isTTY && process.stdin.isRaw) {
63
87
  process.stdin.setRawMode(false);
64
88
  }
65
- process.stdout.write('\u001B[?25h');
89
+ if (supportsStickyTerminalLayout()) {
90
+ process.stdout.write('\u001B[?25h');
91
+ }
66
92
  // Non-TTY fallback — plain text input
67
93
  if (!process.stdin.isTTY) {
68
94
  const suffix = defaultYes ? ' (Y/n): ' : ' (y/N): ';
@@ -98,6 +124,7 @@ export const confirm = (question, defaultYes = true) => {
98
124
  rl.pause();
99
125
  process.stdin.setRawMode(true);
100
126
  const buf = Buffer.alloc(16);
127
+ let rawModeOK = false;
101
128
  try {
102
129
  for (;;) {
103
130
  let n;
@@ -109,6 +136,7 @@ export const confirm = (question, defaultYes = true) => {
109
136
  }
110
137
  if (n === 0)
111
138
  break;
139
+ rawModeOK = true;
112
140
  const b = buf[0];
113
141
  if (b === undefined)
114
142
  break;
@@ -163,6 +191,15 @@ export const confirm = (question, defaultYes = true) => {
163
191
  process.stdout.write('\u001B[?25h'); // Show cursor
164
192
  rl.resume();
165
193
  }
194
+ // Raw mode loop didn't get any input (e.g. fs.readSync returned 0 or threw).
195
+ // Fall back to askQuestion so we don't silently accept the default.
196
+ if (!rawModeOK) {
197
+ const suffix = defaultYes ? ' (Y/n): ' : ' (y/N): ';
198
+ const answer = askQuestion(`${cleanQuestion} ${suffix}`);
199
+ if (answer === '')
200
+ return defaultYes;
201
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
202
+ }
166
203
  return selected ?? defaultYes;
167
204
  };
168
205
  /**
@@ -179,203 +216,436 @@ export const confirm = (question, defaultYes = true) => {
179
216
  *
180
217
  * Returns trimmed text or `null` when cancelled.
181
218
  */
182
- export const askMultiline = (question, initialText = '') => {
219
+ export const editMultiline = async (question, initialText = '') => {
183
220
  if (process.stdin.isTTY && process.stdin.isRaw) {
184
221
  process.stdin.setRawMode(false);
185
222
  }
186
- process.stdout.write('\u001B[?25h');
187
223
  const isMac = process.platform === 'darwin';
188
224
  const delWordHint = isMac ? '⌥⌫' : 'Ctrl+W';
189
- const showHeader = () => {
190
- console.log(`\n\u001B[36m?\u001B[0m ${question}`);
191
- console.log(` \u001B[90mEnter=newline | Ctrl+D=submit | Ctrl+C=cancel\u001B[0m`);
192
- console.log(` \u001B[90m${delWordHint}=del word | Ctrl+U=del line | Ctrl+L=clear all\u001B[0m`);
193
- };
194
- showHeader();
195
- if (initialText.trim()) {
196
- console.log(' \u001B[90m── current ──\u001B[0m');
197
- for (const l of initialText.split('\n')) {
198
- console.log(` \u001B[90m${l}\u001B[0m`);
225
+ const cleanQuestion = question.trim();
226
+ const hintText1 = 'Enter=newline | Ctrl+D=submit | Ctrl+C=cancel';
227
+ const hintText2 = `${delWordHint}=del word | Ctrl+U=del line | Ctrl+L=clear all`;
228
+ const compactHintText = `Enter=newline | Ctrl+D=submit | Ctrl+C=cancel | ${delWordHint}=del word | Ctrl+U=line | Ctrl+L=clear`;
229
+ const printPlainIntro = () => {
230
+ console.log(`\n? ${cleanQuestion}`);
231
+ if (initialText.trim()) {
232
+ console.log(' -- current --');
233
+ for (const l of initialText.split('\n')) {
234
+ console.log(` ${l}`);
235
+ }
236
+ console.log(' -- type below (empty = keep) --');
199
237
  }
200
- console.log(' \u001B[90m── type below (Ctrl+D empty = keep) ──\u001B[0m');
201
- }
202
- // Non-TTY fallback (piped input)
203
- if (!process.stdin.isTTY) {
204
- const r = spawnSync('cat', [], {
205
- stdio: ['inherit', 'pipe', 'inherit'],
206
- encoding: 'utf8',
207
- });
208
- const t = r.stdout?.trim() ?? '';
238
+ };
239
+ const getTextResult = (rawText) => {
240
+ const t = rawText.trim();
209
241
  if (!t && initialText.trim())
210
242
  return initialText.trim();
211
243
  return t || null;
244
+ };
245
+ // Non-TTY fallback (piped input)
246
+ if (!process.stdin.isTTY) {
247
+ printPlainIntro();
248
+ return getTextResult(readStdinText());
212
249
  }
250
+ if (!supportsStickyTerminalLayout()) {
251
+ printPlainIntro();
252
+ console.log(' Submit with EOF: Ctrl+D on Unix/macOS, Ctrl+Z then Enter on Windows.');
253
+ rl.pause();
254
+ const text = getTextResult(readStdinText());
255
+ rl.resume();
256
+ return text;
257
+ }
258
+ process.stdout.write('\u001B[?25h');
213
259
  // Pause readline so fs.readSync can use fd 0
214
260
  rl.pause();
215
261
  process.stdin.setRawMode(true);
216
- const lines = [''];
217
- let li = 0;
218
- const buf = Buffer.alloc(16);
219
- /** Redraw all text from scratch (clears screen) */
220
- const fullRedraw = () => {
221
- process.stdout.write('\u001B[2J\u001B[H');
222
- showHeader();
223
- for (let i = 0; i <= li; i++) {
224
- process.stdout.write(lines[i] ?? '');
225
- if (i < li)
226
- process.stdout.write('\n');
262
+ let rows = process.stdout.rows ?? 24;
263
+ let columns = process.stdout.columns ?? 80;
264
+ let footerRows = rows < 6 ? 1 : 2;
265
+ let scrollTop = rows < 6 ? 2 : 3;
266
+ let scrollBottom = Math.max(scrollTop, rows - footerRows);
267
+ let viewportTop = 0;
268
+ let resizeTimer;
269
+ let stickyTimer;
270
+ const updateTerminalSize = () => {
271
+ rows = process.stdout.rows ?? 24;
272
+ columns = process.stdout.columns ?? 80;
273
+ footerRows = rows < 6 ? 1 : 2;
274
+ scrollTop = rows < 6 ? 2 : 3;
275
+ scrollBottom = Math.max(scrollTop, rows - footerRows);
276
+ };
277
+ const printHeader = () => {
278
+ process.stdout.write(`\u001B[1;1H\u001B[2K\u001B[36m?\u001B[0m ${cleanQuestion}`);
279
+ };
280
+ const printHints = () => {
281
+ const formatHint = (text) => {
282
+ const maxLength = Math.max(1, columns - 2);
283
+ const trimmed = text.length > maxLength
284
+ ? maxLength <= 3
285
+ ? '.'.repeat(maxLength)
286
+ : text.slice(0, maxLength - 3) + '...'
287
+ : text;
288
+ return ` \u001B[90m${trimmed}\u001B[0m`;
289
+ };
290
+ if (footerRows === 1) {
291
+ process.stdout.write(`\u001B[${rows};1H\u001B[2K${formatHint(compactHintText)}`);
292
+ return;
293
+ }
294
+ process.stdout.write(`\u001B[${rows - 1};1H\u001B[2K${formatHint(hintText1)}`);
295
+ process.stdout.write(`\u001B[${rows};1H\u001B[2K${formatHint(hintText2)}`);
296
+ };
297
+ const refreshStickyChrome = () => {
298
+ const currentRows = process.stdout.rows ?? 24;
299
+ const currentColumns = process.stdout.columns ?? 80;
300
+ if (currentRows !== rows || currentColumns !== columns) {
301
+ fullRedraw(true);
302
+ }
303
+ };
304
+ const lines = initialText.trim() ? initialText.split('\n') : [''];
305
+ let li = lines.length - 1;
306
+ let ci = (lines[li] ?? '').length;
307
+ const currentLine = () => lines[li] ?? '';
308
+ const isWhitespace = (ch) => /\s/.test(ch);
309
+ const contentHeight = () => Math.max(1, scrollBottom - scrollTop + 1);
310
+ // Keep one column free so redraws never trigger terminal auto-wrap by writing
311
+ // exactly at the right edge. That keeps cursor math stable for long AI text.
312
+ const wrapWidth = () => Math.max(1, columns - 1);
313
+ const visualRowsForLine = (line) => Math.max(1, Math.ceil(line.length / wrapWidth()));
314
+ const visualRowBeforeLine = (lineIndex) => {
315
+ let total = 0;
316
+ for (let i = 0; i < lineIndex; i++) {
317
+ total += visualRowsForLine(lines[i] ?? '');
318
+ }
319
+ return total;
320
+ };
321
+ const cursorVisualRow = () => visualRowBeforeLine(li) + Math.floor(ci / wrapWidth());
322
+ const cursorVisualColumn = () => ci % wrapWidth();
323
+ const ensureCursorVisible = () => {
324
+ const row = cursorVisualRow();
325
+ if (row < viewportTop) {
326
+ viewportTop = row;
327
+ }
328
+ else if (row >= viewportTop + contentHeight()) {
329
+ viewportTop = row - contentHeight() + 1;
330
+ }
331
+ viewportTop = Math.max(0, viewportTop);
332
+ };
333
+ const getVisualLine = (targetRow) => {
334
+ let row = 0;
335
+ const width = wrapWidth();
336
+ for (const line of lines) {
337
+ const visualRows = visualRowsForLine(line);
338
+ if (targetRow < row + visualRows) {
339
+ const offset = (targetRow - row) * width;
340
+ return line.slice(offset, offset + width);
341
+ }
342
+ row += visualRows;
343
+ }
344
+ return '';
345
+ };
346
+ const totalVisualRows = () => {
347
+ let total = 0;
348
+ for (const line of lines) {
349
+ total += visualRowsForLine(line);
350
+ }
351
+ return Math.max(1, total);
352
+ };
353
+ const setCursorFromVisualPosition = (targetRow, targetColumn) => {
354
+ let row = 0;
355
+ const width = wrapWidth();
356
+ for (const [lineIndex, line] of lines.entries()) {
357
+ const visualRows = visualRowsForLine(line);
358
+ if (targetRow < row + visualRows) {
359
+ const offset = (targetRow - row) * width;
360
+ li = lineIndex;
361
+ ci = Math.min(line.length, offset + targetColumn);
362
+ return;
363
+ }
364
+ row += visualRows;
365
+ }
366
+ li = lines.length - 1;
367
+ ci = currentLine().length;
368
+ };
369
+ const moveCursorToInput = () => {
370
+ ensureCursorVisible();
371
+ const row = Math.min(scrollBottom, scrollTop + cursorVisualRow() - viewportTop);
372
+ const col = Math.max(1, cursorVisualColumn() + 1);
373
+ process.stdout.write(`\u001B[${row};${col}H`);
374
+ };
375
+ /** Redraw editor layout; header+hints stay fixed around the scrollable content area. */
376
+ const fullRedraw = (clearScreen = false) => {
377
+ updateTerminalSize();
378
+ process.stdout.write('\u001B[r');
379
+ if (clearScreen)
380
+ process.stdout.write('\u001B[2J');
381
+ ensureCursorVisible();
382
+ printHeader();
383
+ for (let row = 0; row < contentHeight(); row++) {
384
+ process.stdout.write(`\u001B[${scrollTop + row};1H\u001B[2K`);
385
+ process.stdout.write(getVisualLine(viewportTop + row));
227
386
  }
387
+ printHints();
388
+ moveCursorToInput();
228
389
  };
390
+ const handleResize = () => {
391
+ if (resizeTimer)
392
+ clearTimeout(resizeTimer);
393
+ resizeTimer = setTimeout(() => {
394
+ fullRedraw(true);
395
+ }, 25);
396
+ };
397
+ process.on('SIGWINCH', handleResize);
398
+ // Use the alternate screen so sticky multiline editing does not fight shell scrollback.
399
+ process.stdout.write('\u001B[?1049h');
400
+ fullRedraw(true);
401
+ stickyTimer = setInterval(refreshStickyChrome, 150);
229
402
  /** Delete word backwards on current line */
230
403
  const deleteWord = () => {
231
- const cur = lines[li] ?? '';
232
- if (!cur)
404
+ const cur = currentLine();
405
+ if (!cur || ci === 0)
233
406
  return;
234
- const stripped = cur.replace(/\s+$/, '');
407
+ const before = cur.slice(0, ci);
408
+ const after = cur.slice(ci);
409
+ const stripped = before.replace(/\s+$/, '');
235
410
  const sp = stripped.lastIndexOf(' ');
236
- lines[li] = sp === -1 ? '' : stripped.slice(0, sp + 1);
237
- process.stdout.write(`\r\u001B[K${lines[li]}`);
411
+ const nextBefore = sp === -1 ? '' : stripped.slice(0, sp + 1);
412
+ lines[li] = nextBefore + after;
413
+ ci = nextBefore.length;
414
+ fullRedraw();
238
415
  };
239
- try {
240
- for (;;) {
241
- let n;
242
- try {
243
- n = fs.readSync(0, buf, 0, buf.length, null);
244
- }
245
- catch {
246
- break;
416
+ const moveWordLeft = () => {
417
+ if (ci === 0) {
418
+ if (li > 0) {
419
+ li--;
420
+ ci = currentLine().length;
247
421
  }
248
- if (n === 0)
249
- break;
250
- const b = buf[0];
251
- if (b === undefined)
252
- break;
253
- // Ctrl+C cancel
254
- if (b === 3) {
255
- process.stdout.write('\n');
256
- return null;
257
- }
258
- // Escape (standalone, not arrow sequence) → cancel
259
- if (b === 27 && (n === 1 || buf[1] !== 0x5b)) {
260
- process.stdout.write('\n');
261
- return null;
262
- }
263
- // Ctrl+D submit
264
- if (b === 4) {
265
- process.stdout.write('\n');
266
- const text = lines.join('\n').trim();
267
- if (!text && initialText.trim())
268
- return initialText.trim();
269
- return text || null;
270
- }
271
- // Enter → new line
272
- if (b === 13 || b === 10) {
273
- process.stdout.write('\n');
422
+ moveCursorToInput();
423
+ return;
424
+ }
425
+ const cur = currentLine();
426
+ let nextCursor = ci;
427
+ while (nextCursor > 0 && isWhitespace(cur[nextCursor - 1] ?? ''))
428
+ nextCursor--;
429
+ while (nextCursor > 0 && !isWhitespace(cur[nextCursor - 1] ?? ''))
430
+ nextCursor--;
431
+ ci = nextCursor;
432
+ moveCursorToInput();
433
+ };
434
+ const moveWordRight = () => {
435
+ const cur = currentLine();
436
+ if (ci >= cur.length) {
437
+ if (li < lines.length - 1) {
274
438
  li++;
275
- lines.splice(li, 0, '');
276
- continue;
439
+ ci = 0;
277
440
  }
278
- // Backspace
279
- if (b === 127 || b === 8) {
280
- const cur = lines[li] ?? '';
281
- if (cur.length > 0) {
282
- // Delete within current line
283
- lines[li] = cur.slice(0, -1);
284
- process.stdout.write('\b \b');
441
+ moveCursorToInput();
442
+ return;
443
+ }
444
+ let nextCursor = ci;
445
+ while (nextCursor < cur.length && !isWhitespace(cur[nextCursor] ?? ''))
446
+ nextCursor++;
447
+ while (nextCursor < cur.length && isWhitespace(cur[nextCursor] ?? ''))
448
+ nextCursor++;
449
+ ci = nextCursor;
450
+ moveCursorToInput();
451
+ };
452
+ const moveVisualLine = (delta) => {
453
+ const nextRow = Math.min(Math.max(0, cursorVisualRow() + delta), totalVisualRows() - 1);
454
+ setCursorFromVisualPosition(nextRow, cursorVisualColumn());
455
+ moveCursorToInput();
456
+ };
457
+ const getFinalText = () => {
458
+ const text = lines.join('\n').trim();
459
+ if (!text && initialText.trim())
460
+ return initialText.trim();
461
+ return text || null;
462
+ };
463
+ return await new Promise((resolve) => {
464
+ let done = false;
465
+ const finish = (value) => {
466
+ if (done)
467
+ return;
468
+ done = true;
469
+ process.stdin.off('data', handleData);
470
+ if (process.stdin.isTTY) {
471
+ process.stdin.setRawMode(false);
472
+ }
473
+ if (resizeTimer)
474
+ clearTimeout(resizeTimer);
475
+ if (stickyTimer)
476
+ clearInterval(stickyTimer);
477
+ process.off('SIGWINCH', handleResize);
478
+ process.stdout.write('\u001B[r\u001B[?1049l');
479
+ process.stdin.pause();
480
+ resolve(value);
481
+ };
482
+ function handleData(chunk) {
483
+ const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
484
+ const n = input.length;
485
+ for (let offset = 0; offset < n; offset++) {
486
+ const b = input[offset];
487
+ if (b === undefined)
488
+ break;
489
+ // Ctrl+C → cancel
490
+ if (b === 3) {
491
+ process.stdout.write('\n');
492
+ finish(null);
493
+ return;
494
+ }
495
+ // Ctrl+D → submit
496
+ if (b === 4) {
497
+ process.stdout.write('\n');
498
+ finish(getFinalText());
499
+ return;
285
500
  }
286
- else if (li > 0) {
287
- // At start of line merge with previous line
288
- lines.splice(li, 1);
289
- li--;
501
+ // Enter new line
502
+ if (b === 13 || b === 10) {
503
+ const cur = currentLine();
504
+ lines[li] = cur.slice(0, ci);
505
+ li++;
506
+ lines.splice(li, 0, cur.slice(ci));
507
+ ci = 0;
290
508
  fullRedraw();
509
+ continue;
291
510
  }
292
- continue;
293
- }
294
- // Ctrl+W delete word
295
- if (b === 23) {
296
- deleteWord();
297
- continue;
298
- }
299
- // Ctrl+U → clear current line
300
- if (b === 21) {
301
- lines[li] = '';
302
- process.stdout.write('\r\u001B[K');
303
- continue;
304
- }
305
- // Ctrl+L → clear all, restart
306
- if (b === 12) {
307
- lines.length = 0;
308
- lines.push('');
309
- li = 0;
310
- fullRedraw();
311
- continue;
312
- }
313
- // ESC sequences → Option+Backspace (ESC+DEL) = del word
314
- if (b === 27) {
315
- if (n >= 2 && buf[1] === 0x7f) {
511
+ // Backspace
512
+ if (b === 127 || b === 8) {
513
+ const cur = currentLine();
514
+ if (ci > 0) {
515
+ // Delete within current line
516
+ lines[li] = cur.slice(0, ci - 1) + cur.slice(ci);
517
+ ci--;
518
+ fullRedraw();
519
+ }
520
+ else if (li > 0) {
521
+ // At start of line → merge with previous line
522
+ const previousLength = (lines[li - 1] ?? '').length;
523
+ lines[li - 1] = (lines[li - 1] ?? '') + cur;
524
+ lines.splice(li, 1);
525
+ li--;
526
+ ci = previousLength;
527
+ fullRedraw();
528
+ }
529
+ continue;
530
+ }
531
+ // Ctrl+W → delete word
532
+ if (b === 23) {
316
533
  deleteWord();
534
+ continue;
535
+ }
536
+ // Ctrl+U → clear current line
537
+ if (b === 21) {
538
+ lines[li] = '';
539
+ ci = 0;
540
+ fullRedraw();
541
+ continue;
542
+ }
543
+ // Ctrl+L → clear all, restart
544
+ if (b === 12) {
545
+ lines.length = 0;
546
+ lines.push('');
547
+ li = 0;
548
+ ci = 0;
549
+ fullRedraw();
550
+ continue;
551
+ }
552
+ // ESC sequences → Option+Backspace / Option+Arrow / Ctrl+Arrow
553
+ if (b === 27) {
554
+ const next = input[offset + 1];
555
+ if (next === 0x7f) {
556
+ deleteWord();
557
+ offset += 1;
558
+ continue;
559
+ }
560
+ if (next === 0x62) {
561
+ moveWordLeft();
562
+ offset += 1;
563
+ continue;
564
+ }
565
+ if (next === 0x66) {
566
+ moveWordRight();
567
+ offset += 1;
568
+ continue;
569
+ }
570
+ if (next === 0x5b) {
571
+ let sequenceEnd = offset + 2;
572
+ while (sequenceEnd < n) {
573
+ const value = input[sequenceEnd];
574
+ if (value !== undefined && value >= 0x40 && value <= 0x7e)
575
+ break;
576
+ sequenceEnd++;
577
+ }
578
+ const code = input[sequenceEnd];
579
+ const sequence = input.toString('utf8', offset, sequenceEnd + 1);
580
+ const isWordArrow = sequence.includes(';3') ||
581
+ sequence.includes(';5') ||
582
+ /^\u001B\[[35][CD]$/.test(sequence);
583
+ switch (code) {
584
+ case 0x44: {
585
+ if (isWordArrow) {
586
+ moveWordLeft();
587
+ }
588
+ else if (ci > 0) {
589
+ ci--;
590
+ }
591
+ else if (li > 0) {
592
+ li--;
593
+ ci = currentLine().length;
594
+ }
595
+ moveCursorToInput();
596
+ break;
597
+ }
598
+ case 0x43: {
599
+ if (isWordArrow) {
600
+ moveWordRight();
601
+ }
602
+ else if (ci < currentLine().length) {
603
+ ci++;
604
+ }
605
+ else if (li < lines.length - 1) {
606
+ li++;
607
+ ci = 0;
608
+ }
609
+ moveCursorToInput();
610
+ break;
611
+ }
612
+ case 0x41: {
613
+ moveVisualLine(-1);
614
+ break;
615
+ }
616
+ case 0x42: {
617
+ moveVisualLine(1);
618
+ break;
619
+ }
620
+ }
621
+ offset = sequenceEnd;
622
+ continue;
623
+ }
624
+ process.stdout.write('\n');
625
+ finish(null);
626
+ return;
627
+ }
628
+ // Printable text segment. A raw read can contain text plus control keys.
629
+ if (b >= 32) {
630
+ let end = offset + 1;
631
+ while (end < n) {
632
+ const next = input[end];
633
+ if (next === undefined || next < 32 || next === 127 || next === 27)
634
+ break;
635
+ end++;
636
+ }
637
+ const ch = input.toString('utf8', offset, end);
638
+ const cur = currentLine();
639
+ lines[li] = cur.slice(0, ci) + ch + cur.slice(ci);
640
+ ci += ch.length;
641
+ fullRedraw();
642
+ offset = end - 1;
317
643
  }
318
- continue;
319
- }
320
- // Printable characters
321
- if (b >= 32) {
322
- const ch = buf.toString('utf8', 0, n);
323
- lines[li] = (lines[li] ?? '') + ch;
324
- process.stdout.write(ch);
325
644
  }
326
645
  }
327
- }
328
- finally {
329
- if (process.stdin.isTTY) {
330
- process.stdin.setRawMode(false);
331
- }
332
- rl.resume();
333
- }
334
- const text = lines.join('\n').trim();
335
- if (!text && initialText.trim())
336
- return initialText.trim();
337
- return text || null;
338
- };
339
- /**
340
- * Inline multi-line text editor using the system's terminal editor.
341
- * Opens vim (macOS/Linux) or notepad (Windows) with the initial text.
342
- *
343
- * Kept async (returns Promise) so all existing callers using
344
- * `await editInline(...)` continue to work without changes.
345
- */
346
- export const editInline = (initialText, label = 'Edit Message', _syntax = '') => {
347
- if (process.stdin.isTTY && process.stdin.isRaw) {
348
- process.stdin.setRawMode(false);
349
- }
350
- process.stdout.write('\u001B[?25h');
351
- console.log(`\n \u001B[36m${label}\u001B[0m`);
352
- const tmpDir = os.tmpdir();
353
- const tmpPath = path.join(tmpDir, `geeto-${Date.now()}.md`);
354
- try {
355
- fs.writeFileSync(tmpPath, initialText, { encoding: 'utf8' });
356
- }
357
- catch {
358
- return Promise.resolve(null);
359
- }
360
- const editor = process.platform === 'win32' ? 'notepad' : 'vim';
361
- try {
362
- spawnSync(editor, [tmpPath], { stdio: 'inherit' });
363
- const edited = fs.readFileSync(tmpPath, { encoding: 'utf8' }).trim();
364
- if (!edited)
365
- return Promise.resolve(null);
366
- return Promise.resolve(edited);
367
- }
368
- catch {
369
- return Promise.resolve(null);
370
- }
371
- finally {
372
- try {
373
- fs.unlinkSync(tmpPath);
374
- }
375
- catch {
376
- // ignore cleanup errors
377
- }
378
- }
646
+ process.stdin.on('data', handleData);
647
+ process.stdin.resume();
648
+ });
379
649
  };
380
650
  /**
381
651
  * Close the readline interface