minercon 3.0.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 (44) hide show
  1. package/CHANGELOG.md +439 -0
  2. package/LICENSE +22 -0
  3. package/README.md +401 -0
  4. package/images/icon.png +0 -0
  5. package/out/ansi.js +123 -0
  6. package/out/argumentHint.js +43 -0
  7. package/out/bukkitHelpParsing.js +62 -0
  8. package/out/cli.js +253 -0
  9. package/out/cliConfig.js +141 -0
  10. package/out/commandLine.js +28 -0
  11. package/out/commandSuggestions.js +202 -0
  12. package/out/commandTree.js +46 -0
  13. package/out/commandTreeCache.js +171 -0
  14. package/out/commandTreeCrawler.js +583 -0
  15. package/out/commandTreeParsingBrigadier.js +426 -0
  16. package/out/commandTreeParsingBukkit.js +116 -0
  17. package/out/commandTreeSuggestions.js +142 -0
  18. package/out/completionBackend.js +94 -0
  19. package/out/completionEngine.js +376 -0
  20. package/out/completionQueries.js +86 -0
  21. package/out/completionsBackend.js +97 -0
  22. package/out/connectionManager.js +209 -0
  23. package/out/displayArgumentHint.js +43 -0
  24. package/out/displayCommandTree.js +115 -0
  25. package/out/displaySuggestion.js +282 -0
  26. package/out/extension.js +190 -0
  27. package/out/helpTextParsing.js +445 -0
  28. package/out/historySearch.js +46 -0
  29. package/out/historyStore.js +126 -0
  30. package/out/lineEditor.js +525 -0
  31. package/out/localCommandTree.js +541 -0
  32. package/out/logger.js +14 -0
  33. package/out/minercon +253 -0
  34. package/out/pager.js +168 -0
  35. package/out/pagination.js +142 -0
  36. package/out/rconClient.js +97 -0
  37. package/out/rconConnectionManager.js +238 -0
  38. package/out/rconProtocol.js +421 -0
  39. package/out/rconSession.js +920 -0
  40. package/out/rconTerminal.js +80 -0
  41. package/out/suggestionDisplay.js +286 -0
  42. package/out/terminalOutput.js +110 -0
  43. package/out/unpaginate.js +30 -0
  44. package/package.json +138 -0
@@ -0,0 +1,920 @@
1
+ "use strict";
2
+ // src/rconSession.ts
3
+ //
4
+ // Host-agnostic RCON session orchestrator. Contains all interactive-terminal
5
+ // logic (key dispatch, tab completion, command routing, reconnect handling)
6
+ // behind a narrow `RconSessionHost` seam whose only hard requirement is a
7
+ // function that writes ANSI text to a terminal-shaped output stream.
8
+ //
9
+ // `cli.ts` is the sole host adapter — it wraps process.stdout / raw-mode
10
+ // stdin. The VS Code extension no longer runs a session in-process; it runs
11
+ // the built CLI as the terminal's process (see extension.ts).
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.RconSession = void 0;
47
+ const commandTreeCrawler_1 = require("./commandTreeCrawler");
48
+ const completionEngine_1 = require("./completionEngine");
49
+ const completionBackend_1 = require("./completionBackend");
50
+ const lineEditor_1 = require("./lineEditor");
51
+ const displaySuggestion_1 = require("./displaySuggestion");
52
+ const rconConnectionManager_1 = require("./rconConnectionManager");
53
+ const logger_1 = require("./logger");
54
+ const historyStore_1 = require("./historyStore");
55
+ const ansi = __importStar(require("./ansi"));
56
+ const prompts_1 = require("@clack/prompts");
57
+ const displayCommandTree_1 = require("./displayCommandTree");
58
+ const unpaginate_1 = require("./unpaginate");
59
+ const pagination_1 = require("./pagination");
60
+ const pager_1 = require("./pager");
61
+ /** Phase labels shown on the command-tree-loading progress bar / logged when `logToFile` is set. */
62
+ function progressPhaseLabel(phase) {
63
+ switch (phase) {
64
+ case 'fetching': return 'Fetching commands...';
65
+ case 'loading': return 'Processing subcommands...';
66
+ case 'complete': return 'Commands loaded and cached!';
67
+ case 'cache-hit': return 'Commands loaded from cache!';
68
+ }
69
+ }
70
+ /** A command response taller than this many lines is treated as a screen-disrupting dump that the next prompt/suggestion render must clear past. */
71
+ const LARGE_OUTPUT_LINE_THRESHOLD = 10;
72
+ /** Commands to remember (in-memory and persisted) when the host doesn't specify a `historySize`. */
73
+ const DEFAULT_HISTORY_SIZE = 100;
74
+ /** Substring the TabComplete plugin's `tabcomplete` help text contains — how `detectAndInitialize` recognizes plugin mode. */
75
+ const TAB_COMPLETE_PROBE_MARKER = 'Returns tab completions for a partial command string';
76
+ class RconSession {
77
+ logger;
78
+ sessionHost;
79
+ connectionManager;
80
+ lineEditor;
81
+ serverHost;
82
+ serverPort;
83
+ isExecutingCommand = false;
84
+ commandTree;
85
+ suggestionDisplay;
86
+ pluginMode = false;
87
+ // Whether the detected plugin also exposes the `rcat` unpaginated-output
88
+ // command (Bukkit-family servers; not Fabric, whose probe finds no `rcat`).
89
+ supportsUnpaginate = false;
90
+ // Active output pager, if the last command's output was too tall for the
91
+ // window. While set, keystrokes route to it (see handleInput).
92
+ pager = null;
93
+ engine = (0, completionEngine_1.createMachine)();
94
+ rconBackend;
95
+ localBackend;
96
+ get completionBackend() {
97
+ const inner = this.pluginMode ? this.rconBackend : this.localBackend;
98
+ const builtinNames = this.builtinCommands.map(c => c.name);
99
+ return {
100
+ fetchCompletions: async (line) => {
101
+ if (line.startsWith('.')) {
102
+ return builtinNames.filter(n => n.startsWith(line));
103
+ }
104
+ return inner.fetchCompletions(line);
105
+ },
106
+ fetchUsage: async (line) => {
107
+ if (line.startsWith('.')) {
108
+ return '';
109
+ }
110
+ return inner.fetchUsage(line);
111
+ },
112
+ };
113
+ }
114
+ lastCommandOutputLines = 0;
115
+ historyStore;
116
+ historySearch = null;
117
+ historySearchLabel = '(reverse-i-search): ';
118
+ constructor(controller, host, port, password, logger, sessionHost, controllerFactory) {
119
+ this.logger = logger;
120
+ this.sessionHost = sessionHost;
121
+ this.serverHost = host;
122
+ this.serverPort = port;
123
+ const historySize = sessionHost.historySize ?? DEFAULT_HISTORY_SIZE;
124
+ this.connectionManager = new rconConnectionManager_1.RconConnectionManager(host, port, password, logger, controller, {
125
+ write: (text) => sessionHost.write(text),
126
+ showPrompt: () => this.showPrompt(),
127
+ onReconnected: () => this.initializeCommands(),
128
+ }, controllerFactory);
129
+ this.commandTree = new commandTreeCrawler_1.CommandTreeCrawler((cmd) => this.connectionManager.controller.send(cmd), logger, sessionHost.cacheDir, host, port);
130
+ this.rconBackend = new completionBackend_1.RconCompletionBackend(() => this.connectionManager.controller);
131
+ this.localBackend = new completionBackend_1.LocalCompletionBackend(this.commandTree);
132
+ this.suggestionDisplay = new displaySuggestion_1.SuggestionDisplay({
133
+ write: (text) => sessionHost.write(text),
134
+ cursorColumn: () => {
135
+ // Visual width of the prompt (ANSI codes stripped) plus cursor position
136
+ // within the typed text — gives the cursor's offset from the start of
137
+ // the prompt. When that exceeds the terminal width, the line has
138
+ // wrapped onto a later row, so reduce it mod the column count to get
139
+ // the cursor's actual column on that row.
140
+ const terminalWidth = this.sessionHost.dimensions()?.columns;
141
+ if (this.historySearch) {
142
+ const column = this.historySearchLabel.length + this.historySearch.query.length;
143
+ return terminalWidth ? column % terminalWidth : column;
144
+ }
145
+ const promptWidth = ansi.stripAnsi(this.promptText()).length;
146
+ const column = promptWidth + this.lineEditor.cursor;
147
+ return terminalWidth ? column % terminalWidth : column;
148
+ },
149
+ });
150
+ this.lineEditor = new lineEditor_1.LineEditor({
151
+ write: (text) => sessionHost.write(text),
152
+ promptText: () => this.promptText(),
153
+ onLineChanged: (line) => this.dispatchToEngine({ kind: 'lineChanged', line }),
154
+ beforeLineCleared: () => this.suggestionDisplay.clear(),
155
+ consumeOutputArtifacts: () => {
156
+ if (this.lastCommandOutputLines > LARGE_OUTPUT_LINE_THRESHOLD) {
157
+ this.lastCommandOutputLines = 0;
158
+ return true;
159
+ }
160
+ return false;
161
+ },
162
+ }, historySize);
163
+ this.historyStore = new historyStore_1.HistoryStore(sessionHost.cacheDir, host, port, logger, historySize);
164
+ this.lineEditor.loadHistory(this.historyStore.load());
165
+ this.keyHandlers = this.buildKeyHandlers();
166
+ this.builtinCommands = this.buildBuiltinCommands();
167
+ this.builtinLookup = new Map(this.builtinCommands.flatMap(cmd => [cmd.name, ...(cmd.aliases ?? [])].map(name => [name, cmd])));
168
+ }
169
+ open() {
170
+ this.writeWelcomeBanner();
171
+ this.detectAndInitialize();
172
+ }
173
+ writeWelcomeBanner() {
174
+ this.sessionHost.write(ansi.boldCyan('Minercon Terminal') + '\r\n');
175
+ this.sessionHost.write('Connected to ' + ansi.yellow(this.serverHost + ':' + this.serverPort) + '\r\n\r\n');
176
+ this.sessionHost.write(ansi.dim('Useful shortcuts:') + '\r\n');
177
+ this.sessionHost.write(' ' + ansi.dim('Tab: Autocomplete commands | Type ') + ansi.yellow('.help') + ansi.dim(' for built-in commands') + '\r\n');
178
+ this.sessionHost.write(' ' + ansi.dim('Ctrl+L: Clear screen | Ctrl+C: Cancel input') + '\r\n');
179
+ this.sessionHost.write(' ' + ansi.dim('Up/Down: Command history | Ctrl+R: Search history | Esc: Clear line') + '\r\n\r\n');
180
+ }
181
+ async detectAndInitialize() {
182
+ if (this.sessionHost.disablePlugin) {
183
+ this.sessionHost.write('\r\n' + ansi.yellow('Server tab-complete plugin probe disabled — using local completions') + '\r\n\r\n');
184
+ await this.initializeCommands();
185
+ return;
186
+ }
187
+ try {
188
+ const response = await this.connectionManager.controller.send('tabcomplete');
189
+ if (response && response.includes(TAB_COMPLETE_PROBE_MARKER)) {
190
+ this.pluginMode = true;
191
+ this.commandTree.isReady = true;
192
+ this.sessionHost.write('\r\n' + ansi.green('✓ Server tab-complete plugin detected — using server-side completions') + '\r\n\r\n');
193
+ await this.probeUnpaginateSupport();
194
+ this.showPrompt();
195
+ return;
196
+ }
197
+ }
198
+ catch {
199
+ // probe failed, fall through to normal init
200
+ }
201
+ await this.initializeCommands();
202
+ }
203
+ /**
204
+ * One-shot probe for the plugin's `rcat` command (sent with no args, it
205
+ * echoes RCAT_PROBE_MARKER). Distinct from the `tabcomplete` probe so an
206
+ * older plugin jar — or the Fabric mod, which has no `rcat` — leaves
207
+ * `supportsUnpaginate` false and the client never wraps. Failures are
208
+ * swallowed: de-pagination simply stays off.
209
+ */
210
+ async probeUnpaginateSupport() {
211
+ if (this.sessionHost.unpaginateOutput === false) {
212
+ return;
213
+ }
214
+ try {
215
+ const response = await this.connectionManager.controller.send('rcat');
216
+ this.supportsUnpaginate = (0, unpaginate_1.responseSupportsRcat)(response);
217
+ }
218
+ catch {
219
+ this.supportsUnpaginate = false;
220
+ }
221
+ }
222
+ async initializeCommands(forceRefresh = false) {
223
+ const cacheInfo = this.commandTree.getCacheInfo();
224
+ const willLoadFromCache = !forceRefresh && cacheInfo.exists;
225
+ const reason = forceRefresh ? 'Forcing refresh...' :
226
+ !cacheInfo.exists ? 'No cache found...' :
227
+ 'Cache outdated...';
228
+ // Three progress-reporting strategies for the same underlying crawl,
229
+ // chosen by host/cache state: log lines (file logging), a one-shot cache
230
+ // notice, or a live clack progress bar for a fresh crawl.
231
+ if (this.sessionHost.logToFile) {
232
+ await this.loadCommandsLogged(willLoadFromCache, reason, forceRefresh);
233
+ }
234
+ else if (willLoadFromCache) {
235
+ await this.loadCommandsFromCache(forceRefresh);
236
+ }
237
+ else {
238
+ await this.loadCommandsWithProgressBar(reason, forceRefresh);
239
+ }
240
+ }
241
+ /** Command-tree load for the file-logging host: narrate progress through the logger rather than a progress bar. */
242
+ async loadCommandsLogged(willLoadFromCache, reason, forceRefresh) {
243
+ if (!willLoadFromCache) {
244
+ this.logger.info(`Loading server commands (${reason})`);
245
+ }
246
+ try {
247
+ await this.commandTree.initialize((progressPct, phase) => {
248
+ if (willLoadFromCache) {
249
+ if (progressPct >= 100) {
250
+ this.logger.success('Commands loaded from cache!');
251
+ this.showPrompt();
252
+ }
253
+ return;
254
+ }
255
+ this.logger.info(`${progressPhaseLabel(phase)} (${Math.round(progressPct)}%)`);
256
+ if (progressPct >= 100) {
257
+ this.logger.success('Commands loaded and cached!');
258
+ this.showPrompt();
259
+ }
260
+ }, undefined, forceRefresh);
261
+ }
262
+ catch (error) {
263
+ this.logger.error(`Failed to load commands: ${error}`);
264
+ this.logger.warn('Autocomplete will be limited.');
265
+ this.showPrompt();
266
+ }
267
+ }
268
+ /**
269
+ * Command-tree load on a cache hit (terminal host): resolves in a single
270
+ * synchronous step, so skip the progress bar entirely (and the raw-mode
271
+ * dance its start/stop incurs).
272
+ */
273
+ async loadCommandsFromCache(forceRefresh) {
274
+ try {
275
+ await this.commandTree.initialize((progressPct) => {
276
+ if (progressPct >= 100) {
277
+ this.sessionHost.write(ansi.green('✓ Commands loaded from cache!') + '\r\n\r\n');
278
+ this.showPrompt();
279
+ }
280
+ }, undefined, forceRefresh);
281
+ }
282
+ catch (error) {
283
+ this.sessionHost.write(ansi.red(`✗ Failed to load commands: ${error}`) + '\r\n');
284
+ this.sessionHost.write(ansi.yellow('Autocomplete will be limited.') + '\r\n\r\n');
285
+ this.showPrompt();
286
+ }
287
+ }
288
+ /** Command-tree load for a fresh crawl (terminal host): a clack progress bar narrating phases and per-command messages. */
289
+ async loadCommandsWithProgressBar(reason, forceRefresh) {
290
+ const bar = (0, prompts_1.progress)({ style: 'block' });
291
+ bar.start(`Loading server commands (${reason})`);
292
+ let lastProgress = 0;
293
+ // clack's progress bar, on stop/error, closes the readline interface it
294
+ // created over stdin — which pauses stdin and puts it back into cooked
295
+ // mode. Undo both so the REPL keeps reading input a keystroke at a time.
296
+ const restoreStdin = () => {
297
+ process.stdin.setRawMode(true);
298
+ process.stdin.resume();
299
+ };
300
+ try {
301
+ await this.commandTree.initialize((progressPct, phase) => {
302
+ // During 'loading', leave the message alone — `onMessage` (below)
303
+ // narrates per-command progress there, and immediately overwriting
304
+ // it with the phase label would make that narration flash by unseen.
305
+ if (phase === 'loading') {
306
+ bar.advance(progressPct - lastProgress);
307
+ }
308
+ else {
309
+ bar.advance(progressPct - lastProgress, progressPhaseLabel(phase));
310
+ }
311
+ lastProgress = progressPct;
312
+ if (progressPct >= 100) {
313
+ bar.stop('Commands loaded and cached!');
314
+ restoreStdin();
315
+ this.showPrompt();
316
+ }
317
+ }, (message) => bar.message(message), forceRefresh);
318
+ }
319
+ catch (error) {
320
+ bar.error(`Failed to load commands: ${error}`);
321
+ restoreStdin();
322
+ this.showPrompt();
323
+ }
324
+ }
325
+ close() {
326
+ this.connectionManager.dispose();
327
+ }
328
+ /** The current colored prompt string — the single source of truth for what the prompt looks like in each connection state. */
329
+ promptText() {
330
+ if (this.connectionManager.isReconnecting) {
331
+ return ansi.yellow('[reconnecting]') + ' > ';
332
+ }
333
+ if (!this.connectionManager.isConnected) {
334
+ return ansi.red('[disconnected]') + ' > ';
335
+ }
336
+ return ansi.green('>') + ' ';
337
+ }
338
+ showPrompt() {
339
+ if (this.isExecutingCommand) {
340
+ return;
341
+ }
342
+ this.sessionHost.write(this.promptText());
343
+ }
344
+ keyHandlers;
345
+ buildKeyHandlers() {
346
+ const bindings = [
347
+ { sequences: ['\t'], handler: () => this.handleTabComplete() },
348
+ { sequences: ['\x1b[Z'], handler: () => this.handleShiftTab() },
349
+ { sequences: ['\x1b'], handler: () => this.handleEscape() },
350
+ { sequences: ['\x04'], handler: () => this.handleCtrlD() },
351
+ { sequences: ['\x03'], handler: () => this.handleCtrlC() },
352
+ { sequences: ['\x18'], handler: () => this.handleCut() },
353
+ { sequences: ['\x16', '\x19'], handler: () => this.handlePaste() },
354
+ { sequences: ['\x0c'], handler: () => this.handleClearScreen() },
355
+ { sequences: ['\x12'], handler: () => this.startHistorySearch() },
356
+ { sequences: ['\x1b[A', '\x10'], handler: () => this.handleHistoryOrSuggestionArrow('up') },
357
+ { sequences: ['\x1b[B', '\x0e'], handler: () => this.handleHistoryOrSuggestionArrow('down') },
358
+ { sequences: ['\x1b[5~'], handler: () => this.handlePagePrevious() },
359
+ { sequences: ['\x1b[6~'], handler: () => this.handlePageNext() },
360
+ { sequences: ['\x1b[1;2D'], handler: () => this.lineEditor.selectLeft() },
361
+ { sequences: ['\x1b[1;2C'], handler: () => this.lineEditor.selectRight() },
362
+ { sequences: ['\x1b[1;5D', '\x1b[5D', '\x1bb'], handler: () => this.lineEditor.moveWordLeft() },
363
+ { sequences: ['\x1b[1;5C', '\x1b[5C', '\x1bf'], handler: () => this.lineEditor.moveWordRight() },
364
+ { sequences: ['\x1b[1;6D'], handler: () => this.lineEditor.selectWordLeft() },
365
+ { sequences: ['\x1b[1;6C'], handler: () => this.lineEditor.selectWordRight() },
366
+ { sequences: ['\x1b[1;2H', '\x1b[1;2~'], handler: () => this.lineEditor.selectToStart() },
367
+ { sequences: ['\x1b[1;2F', '\x1b[1;2$'], handler: () => this.lineEditor.selectToEnd() },
368
+ { sequences: ['\x1b[D', '\x02'], handler: () => this.lineEditor.moveLeft() },
369
+ { sequences: ['\x1b[C', '\x06'], handler: () => this.lineEditor.moveRight() },
370
+ { sequences: ['\x1b[H', '\x1bOH', '\x1b[1~', '\x01'], handler: () => this.lineEditor.moveToStart() },
371
+ { sequences: ['\x1b[F', '\x1bOF', '\x1b[4~', '\x05'], handler: () => this.lineEditor.moveToEnd() },
372
+ { sequences: ['\x1b[3~'], handler: () => this.lineEditor.deleteForward() },
373
+ { sequences: ['\x0b'], handler: () => this.killAndStash(() => this.lineEditor.killToEnd()) },
374
+ { sequences: ['\x15'], handler: () => this.killAndStash(() => this.lineEditor.killToStart()) },
375
+ { sequences: ['\x17', '\x1b\x7f', '\x1b\b'], handler: () => this.killAndStash(() => this.lineEditor.killWordBack()) },
376
+ { sequences: ['\x1bd'], handler: () => this.killAndStash(() => this.lineEditor.killWordForward()) },
377
+ { sequences: ['\x14'], handler: () => this.lineEditor.transposeChars() },
378
+ { sequences: ['\r', '\n'], handler: () => this.handleEnter() },
379
+ { sequences: ['\x7f', '\b'], handler: () => this.lineEditor.handleBackspace() },
380
+ ];
381
+ const map = new Map();
382
+ for (const { sequences, handler } of bindings) {
383
+ for (const seq of sequences) {
384
+ map.set(seq, handler);
385
+ }
386
+ }
387
+ return map;
388
+ }
389
+ killAndStash(fn) {
390
+ const text = fn();
391
+ if (text) {
392
+ this.sessionHost.clipboard.writeText(text);
393
+ }
394
+ }
395
+ handleInput(data) {
396
+ // The pager is a post-output interaction, active *after* a command has
397
+ // finished (isExecutingCommand is already false), so it's handled before
398
+ // that guard rather than gated by it.
399
+ if (this.pager) {
400
+ this.pager.handleKey(data);
401
+ return;
402
+ }
403
+ if (this.isExecutingCommand) {
404
+ return;
405
+ }
406
+ if (this.historySearch) {
407
+ this.handleHistorySearchInput(data);
408
+ return;
409
+ }
410
+ const handler = this.keyHandlers.get(data);
411
+ if (handler) {
412
+ handler();
413
+ return;
414
+ }
415
+ if (data.charCodeAt(0) < 32 && data !== '\t') {
416
+ return;
417
+ }
418
+ this.lineEditor.insertText(data);
419
+ }
420
+ handleEscape() {
421
+ if (this.suggestionDisplay.isShowing) {
422
+ this.dispatchToEngine({ kind: 'escape' });
423
+ return;
424
+ }
425
+ this.lineEditor.clearAndReset();
426
+ this.showPrompt();
427
+ }
428
+ handleCtrlD() {
429
+ if (this.lineEditor.line.length > 0) {
430
+ this.lineEditor.deleteForward();
431
+ return;
432
+ }
433
+ this.sessionHost.write('^D\r\n');
434
+ this.sessionHost.write('Disconnecting...\r\n');
435
+ this.sessionHost.close(0);
436
+ }
437
+ handleCtrlC() {
438
+ if (this.lineEditor.hasSelection()) {
439
+ this.sessionHost.clipboard.writeText(this.lineEditor.getSelectedText());
440
+ this.lineEditor.clearSelection();
441
+ this.lineEditor.redraw();
442
+ }
443
+ else if (this.lineEditor.line.length > 0) {
444
+ this.sessionHost.write('^C\r\n');
445
+ this.lineEditor.clearAndReset();
446
+ this.showPrompt();
447
+ }
448
+ }
449
+ handleCut() {
450
+ if (!this.lineEditor.hasSelection()) {
451
+ return;
452
+ }
453
+ this.sessionHost.clipboard.writeText(this.lineEditor.getSelectedText());
454
+ this.lineEditor.deleteSelection();
455
+ this.lineEditor.redraw();
456
+ }
457
+ handlePaste() {
458
+ this.sessionHost.clipboard.readText().then(text => {
459
+ if (text) {
460
+ this.lineEditor.insertText(text);
461
+ }
462
+ });
463
+ }
464
+ handleClearScreen() {
465
+ this.sessionHost.write('\x1b[2J\x1b[H');
466
+ this.showPrompt();
467
+ this.sessionHost.write(this.lineEditor.line);
468
+ if (this.lineEditor.cursor < this.lineEditor.line.length) {
469
+ const moveBack = this.lineEditor.line.length - this.lineEditor.cursor;
470
+ this.sessionHost.write('\x1b[' + moveBack + 'D');
471
+ }
472
+ }
473
+ handleHistoryOrSuggestionArrow(direction) {
474
+ if (this.suggestionDisplay.isShowing) {
475
+ this.dispatchToEngine({ kind: 'arrow', direction });
476
+ return;
477
+ }
478
+ if (this.lineEditor.hasSelection()) {
479
+ this.lineEditor.clearSelection();
480
+ this.lineEditor.redraw();
481
+ }
482
+ this.lineEditor.navigateHistory(direction);
483
+ }
484
+ // ── Ctrl+R: reverse history search ──
485
+ //
486
+ // While active, keystrokes edit the search query (not the line) and the
487
+ // matching history entries are shown in the same popup tab-completion uses
488
+ // (SuggestionDisplay), most-recently-used first. handleInput routes
489
+ // everything here instead of through keyHandlers until the search ends.
490
+ startHistorySearch() {
491
+ if (this.suggestionDisplay.isShowing) {
492
+ this.dispatchToEngine({ kind: 'escape' });
493
+ }
494
+ this.historySearch = (0, historyStore_1.startHistorySearch)(this.lineEditor.historyEntries, this.lineEditor.line);
495
+ this.renderHistorySearch();
496
+ }
497
+ handleHistorySearchInput(data) {
498
+ switch (data) {
499
+ case '\x12': // Ctrl+R again: cycle to the next-older match
500
+ case '\t': // Tab: cycle to the next-older match
501
+ this.cycleHistorySearch(1);
502
+ return;
503
+ case '\x1b[A':
504
+ case '\x10': // Up / Ctrl+P: move visual selection up (newer)
505
+ this.cycleHistorySearch(-1);
506
+ return;
507
+ case '\x1b[B':
508
+ case '\x0e': // Down / Ctrl+N: move visual selection down (older)
509
+ this.cycleHistorySearch(1);
510
+ return;
511
+ case '\x1b[Z': // Shift-Tab: cycle to the next-newer match
512
+ this.cycleHistorySearch(-1);
513
+ return;
514
+ case '\x1b':
515
+ case '\x07':
516
+ case '\x03': // Escape / Ctrl+G / Ctrl+C: cancel
517
+ this.cancelHistorySearch();
518
+ return;
519
+ case '\r':
520
+ case '\n': // Enter: load the match for further editing
521
+ this.acceptHistorySearch();
522
+ return;
523
+ case '\x7f':
524
+ case '\b': // Backspace: shrink the query
525
+ this.setHistorySearchQuery(this.historySearch.query.slice(0, -1));
526
+ return;
527
+ default:
528
+ if (data.charCodeAt(0) >= 32) {
529
+ this.setHistorySearchQuery(this.historySearch.query + data);
530
+ }
531
+ return;
532
+ }
533
+ }
534
+ setHistorySearchQuery(query) {
535
+ this.historySearch = (0, historyStore_1.setHistorySearchQuery)(this.lineEditor.historyEntries, this.historySearch, query);
536
+ this.renderHistorySearch();
537
+ }
538
+ cycleHistorySearch(delta) {
539
+ this.historySearch = (0, historyStore_1.cycleHistorySearch)(this.historySearch, delta);
540
+ this.renderHistorySearch();
541
+ }
542
+ acceptHistorySearch() {
543
+ const search = this.historySearch;
544
+ const selected = search.items[search.selectedIndex] ?? search.originalLine;
545
+ this.suggestionDisplay.hide();
546
+ this.historySearch = null;
547
+ this.lineEditor.replaceLine(selected);
548
+ }
549
+ cancelHistorySearch() {
550
+ const search = this.historySearch;
551
+ this.suggestionDisplay.hide();
552
+ this.historySearch = null;
553
+ this.lineEditor.replaceLine(search.originalLine);
554
+ }
555
+ renderHistorySearch() {
556
+ const search = this.historySearch;
557
+ this.sessionHost.write('\r\x1b[K');
558
+ this.sessionHost.write(ansi.cyan(this.historySearchLabel) + search.query);
559
+ this.suggestionDisplay.render(search.items, search.selectedIndex, null, '');
560
+ }
561
+ handlePagePrevious() {
562
+ const i = this.suggestionDisplay.previousPageIndex();
563
+ if (i !== null) {
564
+ this.dispatchToEngine({ kind: 'selectIndex', index: i });
565
+ }
566
+ }
567
+ handlePageNext() {
568
+ const i = this.suggestionDisplay.nextPageIndex();
569
+ if (i !== null) {
570
+ this.dispatchToEngine({ kind: 'selectIndex', index: i });
571
+ }
572
+ }
573
+ dispatchToEngine(event) {
574
+ const { machine, effects } = (0, completionEngine_1.step)(this.engine, event);
575
+ this.engine = machine;
576
+ for (const effect of effects) {
577
+ this.executeEngineEffect(effect);
578
+ }
579
+ }
580
+ executeEngineEffect(effect) {
581
+ switch (effect.kind) {
582
+ case 'fetchCompletions':
583
+ this.runEngineCompletionsFetch(effect.requestId);
584
+ break;
585
+ case 'fetchUsage':
586
+ this.runEngineUsageFetch(effect.requestId);
587
+ break;
588
+ case 'applySuggestion': {
589
+ const query = this.engine.phase.kind === 'open' ? this.engine.phase.query : this.lineEditor.line;
590
+ this.applySuggestionText(query, effect.text);
591
+ break;
592
+ }
593
+ case 'render':
594
+ this.suggestionDisplay.render(effect.items, effect.selectedIndex, effect.usage, this.lineEditor.line);
595
+ break;
596
+ case 'hide':
597
+ this.suggestionDisplay.hide();
598
+ break;
599
+ case 'restoreLine':
600
+ this.lineEditor.replaceLine(effect.text);
601
+ break;
602
+ }
603
+ }
604
+ /** The input line the current in-flight fetch is resolving for (or the live line if somehow idle). */
605
+ fetchLine() {
606
+ return this.engine.fetch.kind === 'busy' ? this.engine.fetch.forLine : this.lineEditor.line;
607
+ }
608
+ /** Await `p`, falling back to `fallback` if it rejects — a failed fetch is just "no result". */
609
+ async settleOr(p, fallback) {
610
+ try {
611
+ return await p;
612
+ }
613
+ catch {
614
+ return fallback;
615
+ }
616
+ }
617
+ async runEngineCompletionsFetch(requestId) {
618
+ const items = await this.settleOr(this.completionBackend.fetchCompletions(this.fetchLine()), []);
619
+ this.dispatchToEngine({ kind: 'completionsResult', requestId, items });
620
+ }
621
+ async runEngineUsageFetch(requestId) {
622
+ const text = await this.settleOr(this.completionBackend.fetchUsage(this.fetchLine()), '');
623
+ this.dispatchToEngine({ kind: 'usageResult', requestId, text });
624
+ }
625
+ applySuggestionText(query, suggestionText) {
626
+ this.lineEditor.replaceLine((0, completionEngine_1.applySuggestion)(query, suggestionText));
627
+ }
628
+ handleTabComplete() {
629
+ this.dispatchToEngine({ kind: 'tab', line: this.lineEditor.line });
630
+ }
631
+ handleShiftTab() {
632
+ this.dispatchToEngine({ kind: 'shiftTab', line: this.lineEditor.line });
633
+ }
634
+ handleEnter() {
635
+ if (this.suggestionDisplay.isShowing) {
636
+ this.suggestionDisplay.hide();
637
+ }
638
+ this.suggestionDisplay.clear();
639
+ this.sessionHost.write('\r\n');
640
+ const command = this.lineEditor.line.trim();
641
+ this.lineEditor.resetLine();
642
+ this.lineEditor.resetHistoryCursor();
643
+ this.dispatchToEngine({ kind: 'lineChanged', line: '' });
644
+ if (command) {
645
+ // Record every command, including minercon's own built-ins, so they're
646
+ // recallable via Up/Ctrl+R and listed by .history — just like a shell's
647
+ // history includes "history" itself. .history is the one exception:
648
+ // it's recorded after displaying, so its own listing reflects the
649
+ // history *before* this invocation, not including itself.
650
+ if (command !== '.history') {
651
+ this.lineEditor.pushHistory(command);
652
+ this.historyStore.save(this.lineEditor.historyEntries);
653
+ }
654
+ const spaceIdx = command.indexOf(' ');
655
+ const cmdName = spaceIdx === -1 ? command : command.slice(0, spaceIdx);
656
+ const cmdArgs = spaceIdx === -1 ? '' : command.slice(spaceIdx + 1).trim();
657
+ const builtin = this.builtinLookup.get(cmdName);
658
+ if (builtin) {
659
+ builtin.run(cmdArgs);
660
+ }
661
+ else {
662
+ this.executeCommand(command);
663
+ }
664
+ }
665
+ else {
666
+ this.showPrompt();
667
+ }
668
+ }
669
+ // ── built-in `.` commands ──
670
+ //
671
+ // One table drives both dispatch (handleEnter → builtinLookup) and the
672
+ // command list .help prints — adding a command here is the whole job.
673
+ // Same lookup-table pattern as buildKeyHandlers; like there, the run()
674
+ // closures resolve collaborators lazily at call time.
675
+ builtinCommands;
676
+ builtinLookup;
677
+ buildBuiltinCommands() {
678
+ /** Writes the "this is plugin mode, there's no local cache" notice — shared by every cache-related command. */
679
+ const pluginModeNotice = (text) => {
680
+ this.sessionHost.write(ansi.yellow(text) + '\r\n\r\n');
681
+ this.showPrompt();
682
+ };
683
+ return [
684
+ {
685
+ name: '.help', description: 'Show this help message',
686
+ run: (_) => this.showHelp(),
687
+ },
688
+ {
689
+ name: '.clear', description: 'Clear the terminal screen',
690
+ run: (_) => {
691
+ this.sessionHost.write('\x1b[2J\x1b[H');
692
+ this.showPrompt();
693
+ },
694
+ },
695
+ {
696
+ name: '.history', description: 'Show command history',
697
+ run: (_) => {
698
+ this.showHistory();
699
+ this.lineEditor.pushHistory('.history');
700
+ this.historyStore.save(this.lineEditor.historyEntries);
701
+ },
702
+ },
703
+ {
704
+ name: '.reconnect', description: 'Reconnect to the server',
705
+ run: (_) => { this.connectionManager.manualReconnect(); },
706
+ },
707
+ {
708
+ name: '.disconnect', description: 'Disconnect from the server',
709
+ run: (_) => this.connectionManager.disconnect(),
710
+ },
711
+ {
712
+ name: '.reload-commands', description: 'Force reload command database from server',
713
+ aliases: ['.refresh-commands'],
714
+ run: (_) => {
715
+ if (this.pluginMode) {
716
+ pluginModeNotice('Using server-side tab completion — no command cache to reload.');
717
+ }
718
+ else {
719
+ this.initializeCommands(true);
720
+ }
721
+ },
722
+ },
723
+ {
724
+ name: '.clear-cache', description: 'Clear cached command database',
725
+ run: (_) => {
726
+ if (this.pluginMode) {
727
+ pluginModeNotice('Using server-side tab completion — no cache.');
728
+ }
729
+ else {
730
+ this.commandTree.clearCache();
731
+ this.sessionHost.write(ansi.yellow('Command cache cleared.') + '\r\n\r\n');
732
+ this.showPrompt();
733
+ }
734
+ },
735
+ },
736
+ {
737
+ name: '.cache-info', description: 'Show command cache information',
738
+ run: (_) => {
739
+ if (this.pluginMode) {
740
+ pluginModeNotice('Using server-side tab completion — no cache.');
741
+ return;
742
+ }
743
+ const info = this.commandTree.getCacheInfo();
744
+ if (info.exists) {
745
+ this.sessionHost.write(ansi.cyan('Cache Status:') + '\r\n');
746
+ this.sessionHost.write(' Age: ' + info.age + '\r\n');
747
+ this.sessionHost.write(' Last updated: ' + info.lastUpdated?.toLocaleString() + '\r\n\r\n');
748
+ }
749
+ else {
750
+ this.sessionHost.write(ansi.yellow('No cache found.') + '\r\n\r\n');
751
+ }
752
+ this.showPrompt();
753
+ },
754
+ },
755
+ {
756
+ name: '.tree', description: 'Print command tree. Usage: .tree [<command>]',
757
+ run: (args) => {
758
+ if (this.pluginMode) {
759
+ pluginModeNotice('Using server-side tab completion — no local command tree.');
760
+ return;
761
+ }
762
+ if (!this.commandTree.isReady) {
763
+ this.sessionHost.write(ansi.yellow('Command tree not yet loaded.') + '\r\n\r\n');
764
+ this.showPrompt();
765
+ return;
766
+ }
767
+ const cmdArg = args.trim() || undefined;
768
+ const output = (0, displayCommandTree_1.formatCommandTree)(this.commandTree.commands, cmdArg);
769
+ output.split('\n').forEach(line => this.sessionHost.write(line + '\r\n'));
770
+ if (cmdArg) {
771
+ const name = cmdArg.startsWith('/') ? cmdArg.slice(1) : cmdArg;
772
+ const logOutput = (0, displayCommandTree_1.formatCommandLog)(this.commandTree.getCommandLog(name));
773
+ if (logOutput) {
774
+ logOutput.split('\n').forEach(line => this.sessionHost.write(line + '\r\n'));
775
+ }
776
+ }
777
+ this.showPrompt();
778
+ },
779
+ },
780
+ ];
781
+ }
782
+ showHistory() {
783
+ const entries = this.lineEditor.historyEntries;
784
+ if (entries.length === 0) {
785
+ this.sessionHost.write(ansi.gray('(no history yet)') + '\r\n');
786
+ }
787
+ else {
788
+ const width = String(entries.length).length;
789
+ entries.forEach((entry, index) => {
790
+ const number = String(index + 1).padStart(width, ' ');
791
+ this.sessionHost.write(ansi.gray(number) + ' ' + entry + '\r\n');
792
+ });
793
+ }
794
+ this.showPrompt();
795
+ }
796
+ showHelp() {
797
+ this.sessionHost.write(ansi.boldCyan('Built-in Commands:') + '\r\n');
798
+ for (const { name, description } of this.builtinCommands) {
799
+ this.sessionHost.write(' ' + ansi.yellow(name) + ' - ' + description + '\r\n');
800
+ }
801
+ this.sessionHost.write('\r\n');
802
+ this.sessionHost.write(ansi.boldCyan('Keyboard Shortcuts:') + '\r\n');
803
+ this.sessionHost.write(' ' + ansi.dim('Tab - Autocomplete commands and cycle suggestions') + '\r\n');
804
+ this.sessionHost.write(' ' + ansi.dim('Up/Down or Ctrl+P/Ctrl+N - Navigate command history') + '\r\n');
805
+ this.sessionHost.write(' ' + ansi.dim('Ctrl+R - Reverse search command history') + '\r\n');
806
+ this.sessionHost.write(' ' + ansi.dim('Ctrl+A/Ctrl+E - Start/end of line | Ctrl+B/Ctrl+F - Move by character') + '\r\n');
807
+ this.sessionHost.write(' ' + ansi.dim('Alt+B/Alt+F - Move by word | Ctrl+T - Transpose characters') + '\r\n');
808
+ this.sessionHost.write(' ' + ansi.dim('Ctrl+K - Kill to end of line | Ctrl+U - Kill to start of line') + '\r\n');
809
+ this.sessionHost.write(' ' + ansi.dim('Ctrl+W/Alt+Backspace - Delete word back | Alt+D - Delete word forward') + '\r\n');
810
+ this.sessionHost.write(' ' + ansi.dim('Ctrl+Y - Yank | Ctrl+L - Clear screen | Esc - Clear current line') + '\r\n');
811
+ this.sessionHost.write(' ' + ansi.dim('Ctrl+C - Cancel input | Ctrl+D - Disconnect') + '\r\n');
812
+ this.sessionHost.write('\r\n');
813
+ this.showPrompt();
814
+ }
815
+ async executeCommand(command) {
816
+ if (this.connectionManager.isReconnecting) {
817
+ this.sessionHost.write(ansi.yellow('Reconnecting... Please wait.') + '\r\n\r\n');
818
+ this.showPrompt();
819
+ return;
820
+ }
821
+ if (!this.connectionManager.isConnected) {
822
+ this.sessionHost.write(ansi.red('Not connected. Type ' + ansi.yellow('.reconnect') + ' to reconnect.') + '\r\n\r\n');
823
+ this.showPrompt();
824
+ return;
825
+ }
826
+ this.isExecutingCommand = true;
827
+ let outputLineCount = 0;
828
+ try {
829
+ // In plugin mode, route the command through the server-side `rcat`
830
+ // de-pagination helper so its output comes back unpaginated in one
831
+ // response (no-op unless the plugin supports it and the toggle is on).
832
+ const unpaginateActive = this.pluginMode && this.supportsUnpaginate && this.sessionHost.unpaginateOutput !== false;
833
+ const toSend = (0, unpaginate_1.wrapForUnpagination)(command, unpaginateActive);
834
+ const response = await this.connectionManager.controller.send(toSend);
835
+ // Some plugins paginate their OWN output (e.g. Multiverse-Core's
836
+ // "Page X of Y"), returning a single page per call regardless of `rcat`.
837
+ // When de-pagination is active, recognise that chrome and stitch every
838
+ // page into one response. Best-effort: any failure falls back to the
839
+ // single page we already have.
840
+ let combined = response;
841
+ if (unpaginateActive && response && response.trim()) {
842
+ try {
843
+ const stitched = await (0, pagination_1.stitchPaginated)(response, command, (pageCommand) => this.connectionManager.controller.send((0, unpaginate_1.wrapForUnpagination)(pageCommand, true)), { log: (message) => this.logger.debug(message) });
844
+ if (stitched !== undefined) {
845
+ combined = stitched;
846
+ }
847
+ }
848
+ catch (err) {
849
+ this.logger.debug(`pagination stitch failed: ${(0, logger_1.errorMessage)(err)}`);
850
+ }
851
+ }
852
+ if (combined && combined.trim()) {
853
+ const formatted = ansi.formatMinecraftColors(combined);
854
+ const lines = formatted.split('\n');
855
+ outputLineCount = lines.length;
856
+ const rows = this.sessionHost.dimensions()?.rows ?? pager_1.FALLBACK_ROWS;
857
+ const shouldPage = this.sessionHost.terminalPager !== false && lines.length > rows - 1;
858
+ if (shouldPage) {
859
+ // The pager prints its own lines, draws/erases its status prompt, and
860
+ // restores the prompt itself when the user exits — so we skip the
861
+ // trailing newline + showPrompt below (guarded on `this.pager`).
862
+ this.startPager(lines);
863
+ }
864
+ else {
865
+ lines.forEach(line => {
866
+ this.sessionHost.write(`${line}\r\n`);
867
+ });
868
+ this.sessionHost.write('\r\n');
869
+ outputLineCount++;
870
+ }
871
+ }
872
+ else {
873
+ this.sessionHost.write(ansi.dim('(no response)') + '\r\n');
874
+ outputLineCount = 1;
875
+ this.sessionHost.write('\r\n');
876
+ outputLineCount++;
877
+ }
878
+ }
879
+ catch (err) {
880
+ const message = (0, logger_1.errorMessage)(err);
881
+ this.sessionHost.write(ansi.red(`Error: ${message}`) + '\r\n');
882
+ outputLineCount = 1;
883
+ // Ask the controller whether the connection actually died rather than
884
+ // sniffing the error text for socket-ish substrings — a slow command's
885
+ // "Command timeout" used to match 'timeout' and tear down a perfectly
886
+ // healthy connection.
887
+ if (!this.connectionManager.controller.isConnected()) {
888
+ this.sessionHost.write(ansi.yellow('⚠ Connection lost. Auto-reconnecting...') + '\r\n');
889
+ outputLineCount++;
890
+ this.connectionManager.reportConnectionLost();
891
+ }
892
+ else {
893
+ this.sessionHost.write('\r\n');
894
+ outputLineCount++;
895
+ }
896
+ }
897
+ finally {
898
+ this.lastCommandOutputLines = outputLineCount;
899
+ if (outputLineCount > LARGE_OUTPUT_LINE_THRESHOLD) {
900
+ this.suggestionDisplay.markNeedsClearOnNextRender();
901
+ }
902
+ this.isExecutingCommand = false;
903
+ // When paging, the pager owns the prompt — it restores it on exit.
904
+ if (!this.pager) {
905
+ this.showPrompt();
906
+ }
907
+ }
908
+ }
909
+ /** Starts an append-only pager over `lines`; restores the prompt when it exits. */
910
+ startPager(lines) {
911
+ this.pager = new pager_1.Pager({ write: (text) => this.sessionHost.write(text), dimensions: () => this.sessionHost.dimensions() }, new pager_1.ArrayLineSource(lines), () => {
912
+ this.pager = null;
913
+ this.sessionHost.write('\r\n');
914
+ this.showPrompt();
915
+ });
916
+ this.pager.start();
917
+ }
918
+ }
919
+ exports.RconSession = RconSession;
920
+ //# sourceMappingURL=rconSession.js.map