geeto 0.4.3 → 0.6.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 (116) hide show
  1. package/README.md +1 -1
  2. package/lib/api/copilot.d.ts +3 -3
  3. package/lib/api/copilot.js +3 -3
  4. package/lib/cli/input.d.ts +21 -18
  5. package/lib/cli/input.d.ts.map +1 -1
  6. package/lib/cli/input.js +174 -372
  7. package/lib/cli/input.js.map +1 -1
  8. package/lib/cli/menu.d.ts.map +1 -1
  9. package/lib/cli/menu.js +83 -4
  10. package/lib/cli/menu.js.map +1 -1
  11. package/lib/core/copilot-setup.d.ts +2 -2
  12. package/lib/core/copilot-setup.d.ts.map +1 -1
  13. package/lib/core/copilot-setup.js +16 -19
  14. package/lib/core/copilot-setup.js.map +1 -1
  15. package/lib/core/setup.d.ts +1 -1
  16. package/lib/core/setup.js +1 -1
  17. package/lib/index.js +203 -3
  18. package/lib/index.js.map +1 -1
  19. package/lib/utils/branch-naming.d.ts.map +1 -1
  20. package/lib/utils/branch-naming.js +17 -7
  21. package/lib/utils/branch-naming.js.map +1 -1
  22. package/lib/utils/dry-run.d.ts +21 -0
  23. package/lib/utils/dry-run.d.ts.map +1 -0
  24. package/lib/utils/dry-run.js +102 -0
  25. package/lib/utils/dry-run.js.map +1 -0
  26. package/lib/utils/exec.d.ts.map +1 -1
  27. package/lib/utils/exec.js +13 -0
  28. package/lib/utils/exec.js.map +1 -1
  29. package/lib/utils/git-ai.d.ts.map +1 -1
  30. package/lib/utils/git-ai.js +40 -21
  31. package/lib/utils/git-ai.js.map +1 -1
  32. package/lib/utils/git-errors.d.ts.map +1 -1
  33. package/lib/utils/git-errors.js +15 -2
  34. package/lib/utils/git-errors.js.map +1 -1
  35. package/lib/utils/menu-builders.js +1 -1
  36. package/lib/utils/menu-builders.js.map +1 -1
  37. package/lib/utils/scramble.d.ts +117 -0
  38. package/lib/utils/scramble.d.ts.map +1 -0
  39. package/lib/utils/scramble.js +317 -0
  40. package/lib/utils/scramble.js.map +1 -0
  41. package/lib/version.d.ts +1 -1
  42. package/lib/version.js +1 -1
  43. package/lib/workflows/abort.d.ts +9 -0
  44. package/lib/workflows/abort.d.ts.map +1 -0
  45. package/lib/workflows/abort.js +158 -0
  46. package/lib/workflows/abort.js.map +1 -0
  47. package/lib/workflows/ai-provider.js +1 -1
  48. package/lib/workflows/ai-provider.js.map +1 -1
  49. package/lib/workflows/alias.d.ts +6 -0
  50. package/lib/workflows/alias.d.ts.map +1 -0
  51. package/lib/workflows/alias.js +420 -0
  52. package/lib/workflows/alias.js.map +1 -0
  53. package/lib/workflows/amend.d.ts.map +1 -1
  54. package/lib/workflows/amend.js +9 -4
  55. package/lib/workflows/amend.js.map +1 -1
  56. package/lib/workflows/branch-helpers.js +2 -2
  57. package/lib/workflows/branch-helpers.js.map +1 -1
  58. package/lib/workflows/branch-utils.d.ts.map +1 -1
  59. package/lib/workflows/branch-utils.js +4 -3
  60. package/lib/workflows/branch-utils.js.map +1 -1
  61. package/lib/workflows/branch.d.ts.map +1 -1
  62. package/lib/workflows/branch.js +7 -4
  63. package/lib/workflows/branch.js.map +1 -1
  64. package/lib/workflows/cleanup.js +3 -3
  65. package/lib/workflows/cleanup.js.map +1 -1
  66. package/lib/workflows/commit.d.ts.map +1 -1
  67. package/lib/workflows/commit.js +58 -6
  68. package/lib/workflows/commit.js.map +1 -1
  69. package/lib/workflows/dry-run.d.ts +5 -0
  70. package/lib/workflows/dry-run.d.ts.map +1 -0
  71. package/lib/workflows/dry-run.js +127 -0
  72. package/lib/workflows/dry-run.js.map +1 -0
  73. package/lib/workflows/fetch.d.ts +9 -0
  74. package/lib/workflows/fetch.d.ts.map +1 -0
  75. package/lib/workflows/fetch.js +118 -0
  76. package/lib/workflows/fetch.js.map +1 -0
  77. package/lib/workflows/issue.d.ts.map +1 -1
  78. package/lib/workflows/issue.js +317 -72
  79. package/lib/workflows/issue.js.map +1 -1
  80. package/lib/workflows/main-steps.d.ts.map +1 -1
  81. package/lib/workflows/main-steps.js +144 -99
  82. package/lib/workflows/main-steps.js.map +1 -1
  83. package/lib/workflows/main.d.ts.map +1 -1
  84. package/lib/workflows/main.js +14 -6
  85. package/lib/workflows/main.js.map +1 -1
  86. package/lib/workflows/pr.d.ts.map +1 -1
  87. package/lib/workflows/pr.js +307 -39
  88. package/lib/workflows/pr.js.map +1 -1
  89. package/lib/workflows/prune.d.ts +9 -0
  90. package/lib/workflows/prune.d.ts.map +1 -0
  91. package/lib/workflows/prune.js +116 -0
  92. package/lib/workflows/prune.js.map +1 -0
  93. package/lib/workflows/pull.d.ts +9 -0
  94. package/lib/workflows/pull.d.ts.map +1 -0
  95. package/lib/workflows/pull.js +281 -0
  96. package/lib/workflows/pull.js.map +1 -0
  97. package/lib/workflows/release.d.ts.map +1 -1
  98. package/lib/workflows/release.js +50 -38
  99. package/lib/workflows/release.js.map +1 -1
  100. package/lib/workflows/repo-settings.js +2 -2
  101. package/lib/workflows/repo-settings.js.map +1 -1
  102. package/lib/workflows/revert.d.ts +9 -0
  103. package/lib/workflows/revert.d.ts.map +1 -0
  104. package/lib/workflows/revert.js +77 -0
  105. package/lib/workflows/revert.js.map +1 -0
  106. package/lib/workflows/reword.d.ts +9 -0
  107. package/lib/workflows/reword.d.ts.map +1 -0
  108. package/lib/workflows/reword.js +722 -0
  109. package/lib/workflows/reword.js.map +1 -0
  110. package/lib/workflows/settings.js +1 -1
  111. package/lib/workflows/settings.js.map +1 -1
  112. package/lib/workflows/status.d.ts +9 -0
  113. package/lib/workflows/status.d.ts.map +1 -0
  114. package/lib/workflows/status.js +164 -0
  115. package/lib/workflows/status.js.map +1 -0
  116. package/package.json +1 -1
package/lib/cli/input.js CHANGED
@@ -21,7 +21,7 @@ export const askQuestion = (question, defaultValue) => {
21
21
  process.stdout.write('\u001B[?25h');
22
22
  const platform = os.platform();
23
23
  const fullQuestion = defaultValue ? `${question} (${defaultValue}) ` : question;
24
- process.stdout.write(fullQuestion);
24
+ process.stdout.write(`\n${fullQuestion}`);
25
25
  const result = platform === 'win32'
26
26
  ? spawnSync('powershell', ['-Command', '$input = Read-Host; Write-Output $input'], {
27
27
  stdio: ['inherit', 'pipe', 'inherit'],
@@ -34,38 +34,6 @@ export const askQuestion = (question, defaultValue) => {
34
34
  const input = result.stdout?.trim() ?? '';
35
35
  return input ?? defaultValue ?? '';
36
36
  };
37
- /**
38
- * Progress bar utility for long operations
39
- */
40
- export class ProgressBar {
41
- constructor(total, title = 'Progress', width = 40) {
42
- this.total = total;
43
- this.current = 0;
44
- this.width = width;
45
- this.title = title;
46
- }
47
- update(current) {
48
- this.current = Math.min(current, this.total);
49
- this.render();
50
- }
51
- increment(amount = 1) {
52
- this.current = Math.min(this.current + amount, this.total);
53
- this.render();
54
- }
55
- complete() {
56
- this.current = this.total;
57
- this.render();
58
- console.log(''); // New line after completion
59
- }
60
- render() {
61
- const percentage = this.total > 0 ? Math.round((this.current / this.total) * 100) : 0;
62
- const filled = Math.round((this.current / this.total) * this.width);
63
- const empty = this.width - filled;
64
- const bar = '█'.repeat(filled) + '░'.repeat(empty);
65
- const status = `${this.current}/${this.total} (${percentage}%)`;
66
- process.stdout.write(`\r${this.title}: [${bar}] ${status}`);
67
- }
68
- }
69
37
  /**
70
38
  * Syntax highlighting for git diff output
71
39
  */
@@ -88,377 +56,211 @@ export const confirm = (question, defaultYes = true) => {
88
56
  }
89
57
  return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
90
58
  };
91
- const buildKwRegex = (kw) => new RegExp(String.raw `\b(${kw})\b`, 'g'); // eslint-disable-line
92
- const SH_RE = buildKwRegex('if|then|else|elif|fi|for|do|done|while|case|' +
93
- 'esac|in|function|return|export|source|alias|local|readonly');
94
- const JS_RE = buildKwRegex('const|let|var|function|return|if|else|for|while|' +
95
- 'do|switch|case|break|continue|import|export|from|' +
96
- 'default|class|extends|new|this|async|await|try|' +
97
- 'catch|throw|typeof|instanceof|of|in|true|false|null|undefined');
98
- const PY_RE = buildKwRegex('def|class|if|elif|else|for|while|return|import|' +
99
- 'from|as|try|except|finally|with|yield|lambda|' +
100
- 'pass|break|continue|and|or|not|in|is|True|False|' +
101
- 'None|self|async|await');
102
- const rulesFor = (ext) => {
103
- const g = '\u001B[32m'; // green
104
- const y = '\u001B[33m'; // yellow
105
- const c = '\u001B[36m'; // cyan
106
- const gr = '\u001B[90m'; // gray
107
- const m = '\u001B[35m'; // magenta
108
- const str = { re: /(['"`])(?:(?!\1).)*\1/g, c: g };
109
- const num = { re: /\b\d+\.?\d*\b/g, c: m };
110
- if (/^\.(sh|bash|bashrc|zshrc|zsh|profile|bash_profile)$/.test(ext)) {
111
- return [{ re: /#.*/g, c: gr }, str, { re: /\$\{?\w+\}?/g, c: y }, { re: SH_RE, c }];
112
- }
113
- if (/^\.(js|ts|jsx|tsx|mjs|cjs)$/.test(ext)) {
114
- return [{ re: /\/\/.*/g, c: gr }, str, { re: JS_RE, c }, num];
115
- }
116
- if (ext === '.py') {
117
- return [{ re: /#.*/g, c: gr }, str, { re: PY_RE, c }, num];
118
- }
119
- if (ext === '.json') {
120
- return [str, num, { re: /\b(true|false|null)\b/g, c }];
121
- }
122
- if (ext === '.md') {
123
- return [
124
- { re: /^#{1,6}\s.*/g, c },
125
- { re: /\*\*[^*]+\*\*/g, c: y },
126
- { re: /`[^`]+`/g, c: g },
127
- ];
128
- }
129
- if (/^\.(ya?ml|toml)$/.test(ext)) {
130
- return [
131
- { re: /#.*/g, c: gr },
132
- str,
133
- { re: /^[\w.-]+(?=\s*[=:])/gm, c },
134
- { re: /\b(true|false)\b/g, c: m },
135
- num,
136
- ];
59
+ /**
60
+ * Interactive multiline text input with editing support.
61
+ *
62
+ * Shortcuts:
63
+ * - Enter → new line
64
+ * - Backspace → delete character
65
+ * - Ctrl+W → delete word
66
+ * - Ctrl+U → clear current line
67
+ * - Ctrl+L → clear all text
68
+ * - Ctrl+D → submit
69
+ * - Ctrl+C → cancel
70
+ *
71
+ * Returns trimmed text or `null` when cancelled.
72
+ */
73
+ export const askMultiline = (question, initialText = '') => {
74
+ if (process.stdin.isTTY && process.stdin.isRaw) {
75
+ process.stdin.setRawMode(false);
137
76
  }
138
- if (/^\.(s?css)$/.test(ext)) {
139
- return [{ re: /\/\*.+?\*\//g, c: gr }, str, { re: /[.#][\w-]+/g, c: y }, num];
77
+ process.stdout.write('\u001B[?25h');
78
+ const isMac = process.platform === 'darwin';
79
+ const delWordHint = isMac ? '⌥⌫' : 'Ctrl+W';
80
+ const showHeader = () => {
81
+ console.log(`\n\u001B[36m?\u001B[0m ${question}`);
82
+ console.log(` \u001B[90mEnter=newline | Ctrl+D=submit | Ctrl+C=cancel\u001B[0m`);
83
+ console.log(` \u001B[90m${delWordHint}=del word | Ctrl+U=del line | Ctrl+L=clear all\u001B[0m`);
84
+ };
85
+ showHeader();
86
+ if (initialText.trim()) {
87
+ console.log(' \u001B[90m── current ──\u001B[0m');
88
+ for (const l of initialText.split('\n')) {
89
+ console.log(` \u001B[90m${l}\u001B[0m`);
90
+ }
91
+ console.log(' \u001B[90m── type below (Ctrl+D empty = keep) ──\u001B[0m');
140
92
  }
141
- return [{ re: /#.*/g, c: gr }, str, num];
142
- };
143
- /** Apply syntax highlighting via sequential replacement */
144
- const colorize = (text, rules) => {
145
- if (rules.length === 0 || text.length === 0)
146
- return text;
147
- let r = text;
148
- for (const rule of rules) {
149
- r = r.replaceAll(rule.re, (m) => `${rule.c}${m}\u001B[0m`);
93
+ // Non-TTY fallback (piped input)
94
+ if (!process.stdin.isTTY) {
95
+ const r = spawnSync('cat', [], {
96
+ stdio: ['inherit', 'pipe', 'inherit'],
97
+ encoding: 'utf8',
98
+ });
99
+ const t = r.stdout?.trim() ?? '';
100
+ if (!t && initialText.trim())
101
+ return initialText.trim();
102
+ return t || null;
150
103
  }
151
- return r;
152
- };
153
- export const editInline = (initialText, label = 'Edit Message', syntax = '') => {
154
- return new Promise((resolve) => {
155
- const synRules = syntax ? rulesFor(syntax) : [];
156
- const lines = initialText.split('\n');
157
- let row = 0;
158
- let col = lines[0]?.length ?? 0;
159
- let rendered = false;
160
- const cols = process.stdout.columns || 80;
161
- const maxRows = Math.max(Math.min((process.stdout.rows || 24) - 6, 20), 3);
162
- // totalLines = maxRows content + 1 footer (no header anymore)
163
- const totalLines = maxRows + 1;
164
- // Use \r\n everywhere — Bun raw mode may disable OPOST
165
- // which means bare \n won't carriage-return
166
- const NL = '\r\n';
167
- /* ── helpers ────────────────────────────────────── */
168
- const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
169
- const lineAt = (r) => lines[r] ?? '';
170
- const render = () => {
171
- let frame = '';
172
- if (rendered) {
173
- // Go up one line at a time (most compatible) and clear each
174
- for (let i = 0; i < totalLines; i++) {
175
- frame += '\u001B[A'; // CUU — cursor up 1
176
- }
177
- frame += '\r'; // ensure column 0
178
- frame += '\u001B[0J'; // ED 0 — clear from cursor to end of screen
179
- }
180
- rendered = true;
181
- // Content lines — reserve 1 extra col for cursor at end of line
182
- const scrollTop = Math.max(0, row - maxRows + 1);
183
- for (let i = 0; i < maxRows; i++) {
184
- const idx = scrollTop + i;
185
- if (idx < lines.length) {
186
- const num = String(idx + 1).padStart(3);
187
- const line = lines[idx] ?? '';
188
- const maxLen = cols - 9;
189
- const visible = line.length > maxLen ? line.slice(0, maxLen - 1) + '…' : line;
190
- if (idx === row) {
191
- const c = clamp(col, 0, visible.length);
192
- const before = colorize(visible.slice(0, c), synRules);
193
- const cursor = visible[c] ?? ' ';
194
- const after = colorize(visible.slice(c + 1), synRules);
195
- frame += ` \u001B[90m${num}\u001B[0m \u001B[36m│\u001B[0m ${before}\u001B[7m${cursor}\u001B[27m${after}${NL}`;
196
- }
197
- else {
198
- const hl = colorize(visible, synRules);
199
- frame += ` \u001B[90m${num}\u001B[0m \u001B[90m│\u001B[0m ${hl}${NL}`;
200
- }
201
- }
202
- else {
203
- frame += ` \u001B[90m │ ~\u001B[0m${NL}`;
204
- }
205
- }
206
- // Footer — title + hints + cursor position
207
- frame += ` \u001B[36m─── ${label} ───\u001B[0m \u001B[90mCtrl+S save · Esc cancel · Ctrl+K del line · ⌥←→ word · Ln ${row + 1}/${lines.length} · Col ${col + 1}\u001B[0m${NL}`;
208
- process.stdout.write(frame);
209
- };
210
- /* ── raw-mode input handling ───────────────────── */
211
- if (process.stdin.isTTY) {
212
- process.stdin.setRawMode(true);
104
+ // Pause readline so fs.readSync can use fd 0
105
+ rl.pause();
106
+ process.stdin.setRawMode(true);
107
+ const lines = [''];
108
+ let li = 0;
109
+ const buf = Buffer.alloc(16);
110
+ /** Redraw all text from scratch (clears screen) */
111
+ const fullRedraw = () => {
112
+ process.stdout.write('\u001B[2J\u001B[H');
113
+ showHeader();
114
+ for (let i = 0; i <= li; i++) {
115
+ process.stdout.write(lines[i] ?? '');
116
+ if (i < li)
117
+ process.stdout.write('\n');
213
118
  }
214
- process.stdin.resume();
215
- // Hide cursor (we draw our own)
216
- process.stdout.write('\u001B[?25l');
217
- render();
218
- // Track escape sequence state to distinguish Esc from arrow keys
219
- let escBuf = '';
220
- let escTimer = null;
221
- const cleanup = () => {
222
- process.stdin.removeListener('data', onData);
223
- if (process.stdin.isTTY) {
224
- process.stdin.setRawMode(false);
225
- }
226
- process.stdin.pause();
227
- process.stdout.write('\u001B[?25h'); // Show cursor again
228
- if (escTimer)
229
- clearTimeout(escTimer);
230
- };
231
- const onData = (buf) => {
232
- const raw = buf.toString('utf8');
233
- // If we have a pending escape, accumulate
234
- if (escBuf) {
235
- escBuf += raw;
236
- if (escTimer)
237
- clearTimeout(escTimer);
238
- }
239
- else if (raw === '\u001B') {
240
- // Start escape sequence
241
- escBuf = '\u001B';
242
- escTimer = setTimeout(() => {
243
- // Standalone Escape — cancel editing
244
- escBuf = '';
245
- cleanup();
246
- resolve(null);
247
- }, 50);
248
- return;
249
- }
250
- const key = escBuf || raw;
251
- escBuf = '';
252
- if (escTimer) {
253
- clearTimeout(escTimer);
254
- escTimer = null;
255
- }
256
- // Ctrl+S (0x13) — save
257
- if (key === '\u0013') {
258
- cleanup();
259
- resolve(lines.join('\n').trim());
260
- return;
261
- }
262
- // Ctrl+C (0x03) — cancel
263
- if (key === '\u0003') {
264
- cleanup();
265
- resolve(null);
266
- return;
267
- }
268
- // Ctrl+K (0x0B) — delete current line
269
- if (key === '\u000B') {
270
- if (lines.length > 1) {
271
- lines.splice(row, 1);
272
- if (row >= lines.length)
273
- row = lines.length - 1;
274
- col = clamp(col, 0, lineAt(row).length);
275
- }
276
- else {
277
- lines[0] = '';
278
- col = 0;
279
- }
280
- render();
281
- return;
282
- }
283
- // Arrow up
284
- if (key === '\u001B[A') {
285
- if (row > 0) {
286
- row--;
287
- col = clamp(col, 0, lineAt(row).length);
288
- }
289
- render();
290
- return;
291
- }
292
- // Arrow down
293
- if (key === '\u001B[B') {
294
- if (row < lines.length - 1) {
295
- row++;
296
- col = clamp(col, 0, lineAt(row).length);
297
- }
298
- render();
299
- return;
300
- }
301
- // Cmd+Right / Option+Right / Ctrl+Right / Arrow right
302
- // macOS: Cmd+Right → \x1B[1;9C → end of line
303
- // Option+Right → \x1Bf or \x1B[1;3C → forward word
304
- // Linux/Win: Ctrl+Right → \x1B[1;5C → forward word
305
- if (key === '\u001B[1;9C') {
306
- col = lineAt(row).length;
307
- render();
308
- return;
119
+ };
120
+ /** Delete word backwards on current line */
121
+ const deleteWord = () => {
122
+ const cur = lines[li] ?? '';
123
+ if (!cur)
124
+ return;
125
+ const stripped = cur.replace(/\s+$/, '');
126
+ const sp = stripped.lastIndexOf(' ');
127
+ lines[li] = sp === -1 ? '' : stripped.slice(0, sp + 1);
128
+ process.stdout.write(`\r\u001B[K${lines[li]}`);
129
+ };
130
+ try {
131
+ for (;;) {
132
+ let n;
133
+ try {
134
+ n = fs.readSync(0, buf, 0, buf.length, null);
309
135
  }
310
- if (key === '\u001Bf' || key === '\u001B[1;3C' || key === '\u001B[1;5C') {
311
- const line = lineAt(row);
312
- let c = col;
313
- while (c < line.length && /\w/.test(line[c] ?? ''))
314
- c++;
315
- while (c < line.length && /\W/.test(line[c] ?? ''))
316
- c++;
317
- col = c;
318
- render();
319
- return;
136
+ catch {
137
+ break;
320
138
  }
321
- if (key === '\u001B[C') {
322
- if (col < lineAt(row).length) {
323
- col++;
324
- }
325
- else if (row < lines.length - 1) {
326
- row++;
327
- col = 0;
328
- }
329
- render();
330
- return;
139
+ if (n === 0)
140
+ break;
141
+ const b = buf[0];
142
+ if (b === undefined)
143
+ break;
144
+ // Ctrl+C → cancel
145
+ if (b === 3) {
146
+ process.stdout.write('\n');
147
+ return null;
331
148
  }
332
- // Cmd+Left / Option+Left / Ctrl+Left / Arrow left
333
- // macOS: Cmd+Left \x1B[1;9D → start of line
334
- // Option+Left → \x1Bb or \x1B[1;3D → backward word
335
- // Linux/Win: Ctrl+Left \x1B[1;5D → backward word
336
- if (key === '\u001B[1;9D') {
337
- col = 0;
338
- render();
339
- return;
149
+ // Ctrl+D submit
150
+ if (b === 4) {
151
+ process.stdout.write('\n');
152
+ const text = lines.join('\n').trim();
153
+ if (!text && initialText.trim())
154
+ return initialText.trim();
155
+ return text || null;
340
156
  }
341
- if (key === '\u001Bb' || key === '\u001B[1;3D' || key === '\u001B[1;5D') {
342
- const line = lineAt(row);
343
- let c = col;
344
- while (c > 0 && /\W/.test(line[c - 1] ?? ''))
345
- c--;
346
- while (c > 0 && /\w/.test(line[c - 1] ?? ''))
347
- c--;
348
- col = c;
349
- render();
350
- return;
157
+ // Enter new line
158
+ if (b === 13 || b === 10) {
159
+ process.stdout.write('\n');
160
+ li++;
161
+ lines.splice(li, 0, '');
162
+ continue;
351
163
  }
352
- if (key === '\u001B[D') {
353
- if (col > 0) {
354
- col--;
164
+ // Backspace
165
+ if (b === 127 || b === 8) {
166
+ const cur = lines[li] ?? '';
167
+ if (cur.length > 0) {
168
+ // Delete within current line
169
+ lines[li] = cur.slice(0, -1);
170
+ process.stdout.write('\b \b');
355
171
  }
356
- else if (row > 0) {
357
- row--;
358
- col = lineAt(row).length;
172
+ else if (li > 0) {
173
+ // At start of line → merge with previous line
174
+ lines.splice(li, 1);
175
+ li--;
176
+ fullRedraw();
359
177
  }
360
- render();
361
- return;
178
+ continue;
362
179
  }
363
- // Home — also \x1B[1;2D (Shift+Left in some terminals)
364
- if (key === '\u001B[H' || key === '\u001BOH') {
365
- col = 0;
366
- render();
367
- return;
180
+ // Ctrl+W delete word
181
+ if (b === 23) {
182
+ deleteWord();
183
+ continue;
368
184
  }
369
- // End
370
- if (key === '\u001B[F' || key === '\u001BOF') {
371
- col = lineAt(row).length;
372
- render();
373
- return;
185
+ // Ctrl+U → clear current line
186
+ if (b === 21) {
187
+ lines[li] = '';
188
+ process.stdout.write('\r\u001B[K');
189
+ continue;
374
190
  }
375
- // Delete
376
- if (key === '\u001B[3~') {
377
- const line = lineAt(row);
378
- if (col < line.length) {
379
- lines[row] = line.slice(0, col) + line.slice(col + 1);
380
- }
381
- else if (row < lines.length - 1) {
382
- // Join with next line
383
- lines[row] = line + (lines[row + 1] ?? '');
384
- lines.splice(row + 1, 1);
385
- }
386
- render();
387
- return;
191
+ // Ctrl+L → clear all, restart
192
+ if (b === 12) {
193
+ lines.length = 0;
194
+ lines.push('');
195
+ li = 0;
196
+ fullRedraw();
197
+ continue;
388
198
  }
389
- // Backspace
390
- if (key === '\u007F' || key === '\b') {
391
- if (col > 0) {
392
- const line = lineAt(row);
393
- lines[row] = line.slice(0, col - 1) + line.slice(col);
394
- col--;
199
+ // ESC sequences → Option+Backspace (ESC+DEL) = del word
200
+ if (b === 27) {
201
+ if (n >= 2 && buf[1] === 0x7f) {
202
+ deleteWord();
395
203
  }
396
- else if (row > 0) {
397
- // Join with previous line
398
- col = lineAt(row - 1).length;
399
- lines[row - 1] = lineAt(row - 1) + lineAt(row);
400
- lines.splice(row, 1);
401
- row--;
402
- }
403
- render();
404
- return;
405
- }
406
- // Enter
407
- if (key === '\r' || key === '\n') {
408
- const line = lineAt(row);
409
- const before = line.slice(0, col);
410
- const after = line.slice(col);
411
- lines[row] = before;
412
- lines.splice(row + 1, 0, after);
413
- row++;
414
- col = 0;
415
- render();
416
- return;
417
- }
418
- // Tab → 2 spaces
419
- if (key === '\t') {
420
- const line = lineAt(row);
421
- lines[row] = line.slice(0, col) + ' ' + line.slice(col);
422
- col += 2;
423
- render();
424
- return;
204
+ continue;
425
205
  }
426
206
  // Printable characters
427
- for (const ch of raw) {
428
- const code = ch.codePointAt(0) ?? 0;
429
- if (code >= 32) {
430
- const line = lineAt(row);
431
- lines[row] = line.slice(0, col) + ch + line.slice(col);
432
- col++;
433
- }
207
+ if (b >= 32) {
208
+ const ch = buf.toString('utf8', 0, n);
209
+ lines[li] = (lines[li] ?? '') + ch;
210
+ process.stdout.write(ch);
434
211
  }
435
- render();
436
- };
437
- process.stdin.on('data', onData);
438
- });
212
+ }
213
+ }
214
+ finally {
215
+ if (process.stdin.isTTY) {
216
+ process.stdin.setRawMode(false);
217
+ }
218
+ rl.resume();
219
+ }
220
+ const text = lines.join('\n').trim();
221
+ if (!text && initialText.trim())
222
+ return initialText.trim();
223
+ return text || null;
439
224
  };
440
225
  /**
441
- * Edit multi-line content in the user's editor (from $EDITOR).
442
- * Writes initialText to a temp file, opens $EDITOR, and returns the edited content.
226
+ * Inline multi-line text editor using the system's built-in terminal editor.
227
+ * Opens nano (macOS/Linux) or notepad (Windows) with the initial text.
228
+ *
229
+ * Kept async (returns Promise) so all existing callers using
230
+ * `await editInline(...)` continue to work without changes.
443
231
  */
444
- export const editInEditor = (initialText = '', filenameHint = 'geeto-commit.txt') => {
232
+ export const editInline = (initialText, label = 'Edit Message', _syntax = '') => {
233
+ if (process.stdin.isTTY && process.stdin.isRaw) {
234
+ process.stdin.setRawMode(false);
235
+ }
236
+ process.stdout.write('\u001B[?25h');
237
+ console.log(`\n \u001B[36m${label}\u001B[0m`);
445
238
  const tmpDir = os.tmpdir();
446
- const tmpPath = path.join(tmpDir, `${Date.now()}-${filenameHint}`);
239
+ const tmpPath = path.join(tmpDir, `geeto-${Date.now()}.md`);
447
240
  try {
448
241
  fs.writeFileSync(tmpPath, initialText, { encoding: 'utf8' });
449
242
  }
450
243
  catch {
451
- return initialText;
244
+ return Promise.resolve(null);
452
245
  }
453
- const editor = process.env.EDITOR ?? (process.platform === 'win32' ? 'notepad' : 'vi');
246
+ const editor = process.platform === 'win32' ? 'notepad' : 'nano';
454
247
  try {
455
248
  spawnSync(editor, [tmpPath], { stdio: 'inherit' });
456
- const edited = fs.readFileSync(tmpPath, { encoding: 'utf8' });
457
- return edited.trim();
249
+ const edited = fs.readFileSync(tmpPath, { encoding: 'utf8' }).trim();
250
+ if (!edited)
251
+ return Promise.resolve(null);
252
+ return Promise.resolve(edited);
458
253
  }
459
254
  catch {
460
- // On failure, return initial text
461
- return initialText;
255
+ return Promise.resolve(null);
256
+ }
257
+ finally {
258
+ try {
259
+ fs.unlinkSync(tmpPath);
260
+ }
261
+ catch {
262
+ // ignore cleanup errors
263
+ }
462
264
  }
463
265
  };
464
266
  /**