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