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.
- package/README.md +77 -18
- package/lib/api/copilot-sdk.d.ts.map +1 -1
- package/lib/api/copilot-sdk.js +29 -24
- package/lib/api/copilot-sdk.js.map +1 -1
- package/lib/api/copilot.d.ts.map +1 -1
- package/lib/api/copilot.js +6 -6
- package/lib/api/copilot.js.map +1 -1
- package/lib/api/gemini.d.ts.map +1 -1
- package/lib/api/gemini.js +2 -2
- package/lib/api/gemini.js.map +1 -1
- package/lib/api/groq-sdk.d.ts +11 -0
- package/lib/api/groq-sdk.d.ts.map +1 -0
- package/lib/api/groq-sdk.js +131 -0
- package/lib/api/groq-sdk.js.map +1 -0
- package/lib/api/groq.d.ts +10 -0
- package/lib/api/groq.d.ts.map +1 -0
- package/lib/api/groq.js +36 -0
- package/lib/api/groq.js.map +1 -0
- package/lib/api/openrouter-sdk.d.ts.map +1 -1
- package/lib/api/openrouter-sdk.js +58 -73
- package/lib/api/openrouter-sdk.js.map +1 -1
- package/lib/api/openrouter.d.ts.map +1 -1
- package/lib/api/openrouter.js +2 -2
- package/lib/api/openrouter.js.map +1 -1
- package/lib/api/trello.d.ts +16 -1
- package/lib/api/trello.d.ts.map +1 -1
- package/lib/api/trello.js +90 -2
- package/lib/api/trello.js.map +1 -1
- package/lib/cli/input.d.ts +1 -9
- package/lib/cli/input.d.ts.map +1 -1
- package/lib/cli/input.js +439 -169
- package/lib/cli/input.js.map +1 -1
- package/lib/cli/menu.d.ts.map +1 -1
- package/lib/cli/menu.js +33 -20
- package/lib/cli/menu.js.map +1 -1
- package/lib/core/copilot-setup.d.ts +5 -0
- package/lib/core/copilot-setup.d.ts.map +1 -1
- package/lib/core/copilot-setup.js +18 -0
- package/lib/core/copilot-setup.js.map +1 -1
- package/lib/core/gemini-setup.d.ts.map +1 -1
- package/lib/core/gemini-setup.js +7 -13
- package/lib/core/gemini-setup.js.map +1 -1
- package/lib/core/github-setup.d.ts.map +1 -1
- package/lib/core/github-setup.js +13 -7
- package/lib/core/github-setup.js.map +1 -1
- package/lib/core/gitlab-setup.d.ts.map +1 -1
- package/lib/core/gitlab-setup.js +13 -6
- package/lib/core/gitlab-setup.js.map +1 -1
- package/lib/core/groq-setup.d.ts +5 -0
- package/lib/core/groq-setup.d.ts.map +1 -0
- package/lib/core/groq-setup.js +67 -0
- package/lib/core/groq-setup.js.map +1 -0
- package/lib/core/openrouter-setup.d.ts.map +1 -1
- package/lib/core/openrouter-setup.js +11 -21
- package/lib/core/openrouter-setup.js.map +1 -1
- package/lib/core/setup.d.ts +2 -1
- package/lib/core/setup.d.ts.map +1 -1
- package/lib/core/setup.js +44 -7
- package/lib/core/setup.js.map +1 -1
- package/lib/index.js +8 -0
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +9 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/utils/ai-workflow.d.ts +10 -2
- package/lib/utils/ai-workflow.d.ts.map +1 -1
- package/lib/utils/ai-workflow.js +15 -0
- package/lib/utils/ai-workflow.js.map +1 -1
- package/lib/utils/branch-naming.d.ts +2 -1
- package/lib/utils/branch-naming.d.ts.map +1 -1
- package/lib/utils/branch-naming.js +114 -68
- package/lib/utils/branch-naming.js.map +1 -1
- package/lib/utils/config.d.ts +11 -3
- package/lib/utils/config.d.ts.map +1 -1
- package/lib/utils/config.js +46 -18
- package/lib/utils/config.js.map +1 -1
- package/lib/utils/git-ai-errors.d.ts.map +1 -1
- package/lib/utils/git-ai-errors.js +5 -1
- package/lib/utils/git-ai-errors.js.map +1 -1
- package/lib/utils/git-ai.d.ts +8 -7
- package/lib/utils/git-ai.d.ts.map +1 -1
- package/lib/utils/git-ai.js +239 -117
- package/lib/utils/git-ai.js.map +1 -1
- package/lib/utils/logging.d.ts.map +1 -1
- package/lib/utils/logging.js +14 -3
- package/lib/utils/logging.js.map +1 -1
- package/lib/utils/menu-builders.js +1 -1
- package/lib/utils/menu-builders.js.map +1 -1
- package/lib/utils/prompts-embedded.d.ts.map +1 -1
- package/lib/utils/prompts-embedded.js +0 -81
- package/lib/utils/prompts-embedded.js.map +1 -1
- package/lib/utils/scramble.d.ts.map +1 -1
- package/lib/utils/scramble.js +14 -3
- package/lib/utils/scramble.js.map +1 -1
- package/lib/utils/state.d.ts.map +1 -1
- package/lib/utils/state.js +12 -1
- package/lib/utils/state.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.d.ts.map +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/lib/workflows/ai-provider.d.ts +3 -1
- package/lib/workflows/ai-provider.d.ts.map +1 -1
- package/lib/workflows/ai-provider.js +8 -2
- package/lib/workflows/ai-provider.js.map +1 -1
- package/lib/workflows/branch-helpers.d.ts.map +1 -1
- package/lib/workflows/branch-helpers.js +142 -71
- package/lib/workflows/branch-helpers.js.map +1 -1
- package/lib/workflows/branch.d.ts.map +1 -1
- package/lib/workflows/branch.js +97 -58
- package/lib/workflows/branch.js.map +1 -1
- package/lib/workflows/commit.d.ts +1 -1
- package/lib/workflows/commit.d.ts.map +1 -1
- package/lib/workflows/commit.js +99 -37
- package/lib/workflows/commit.js.map +1 -1
- package/lib/workflows/dry-run.d.ts.map +1 -1
- package/lib/workflows/dry-run.js +6 -0
- package/lib/workflows/dry-run.js.map +1 -1
- package/lib/workflows/issue.d.ts.map +1 -1
- package/lib/workflows/issue.js +12 -12
- package/lib/workflows/issue.js.map +1 -1
- package/lib/workflows/main-helpers.d.ts +3 -1
- package/lib/workflows/main-helpers.d.ts.map +1 -1
- package/lib/workflows/main-helpers.js +30 -26
- package/lib/workflows/main-helpers.js.map +1 -1
- package/lib/workflows/main-steps.d.ts.map +1 -1
- package/lib/workflows/main-steps.js +8 -0
- package/lib/workflows/main-steps.js.map +1 -1
- package/lib/workflows/main.d.ts.map +1 -1
- package/lib/workflows/main.js +7 -15
- package/lib/workflows/main.js.map +1 -1
- package/lib/workflows/pr.d.ts.map +1 -1
- package/lib/workflows/pr.js +12 -12
- package/lib/workflows/pr.js.map +1 -1
- package/lib/workflows/release-merge.d.ts.map +1 -1
- package/lib/workflows/release-merge.js +40 -10
- package/lib/workflows/release-merge.js.map +1 -1
- package/lib/workflows/release-sync.d.ts.map +1 -1
- package/lib/workflows/release-sync.js +25 -7
- package/lib/workflows/release-sync.js.map +1 -1
- package/lib/workflows/release.d.ts.map +1 -1
- package/lib/workflows/release.js +130 -15
- package/lib/workflows/release.js.map +1 -1
- package/lib/workflows/repo-settings.d.ts.map +1 -1
- package/lib/workflows/repo-settings.js +35 -8
- package/lib/workflows/repo-settings.js.map +1 -1
- package/lib/workflows/reword.d.ts.map +1 -1
- package/lib/workflows/reword.js +60 -17
- package/lib/workflows/reword.js.map +1 -1
- package/lib/workflows/settings.d.ts +2 -1
- package/lib/workflows/settings.d.ts.map +1 -1
- package/lib/workflows/settings.js +364 -76
- package/lib/workflows/settings.js.map +1 -1
- package/lib/workflows/trello-menu.d.ts +0 -3
- package/lib/workflows/trello-menu.d.ts.map +1 -1
- package/lib/workflows/trello-menu.js +349 -19
- package/lib/workflows/trello-menu.js.map +1 -1
- package/package.json +13 -12
- package/lib/workflows/security-gate.d.ts +0 -8
- package/lib/workflows/security-gate.d.ts.map +0 -1
- package/lib/workflows/security-gate.js +0 -455
- package/lib/workflows/security-gate.js.map +0 -1
- 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
|
-
|
|
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
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
217
|
-
let
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 =
|
|
232
|
-
if (!cur)
|
|
404
|
+
const cur = currentLine();
|
|
405
|
+
if (!cur || ci === 0)
|
|
233
406
|
return;
|
|
234
|
-
const
|
|
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
|
-
|
|
237
|
-
|
|
411
|
+
const nextBefore = sp === -1 ? '' : stripped.slice(0, sp + 1);
|
|
412
|
+
lines[li] = nextBefore + after;
|
|
413
|
+
ci = nextBefore.length;
|
|
414
|
+
fullRedraw();
|
|
238
415
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
276
|
-
continue;
|
|
439
|
+
ci = 0;
|
|
277
440
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|