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.
- package/CHANGELOG.md +439 -0
- package/LICENSE +22 -0
- package/README.md +401 -0
- package/images/icon.png +0 -0
- package/out/ansi.js +123 -0
- package/out/argumentHint.js +43 -0
- package/out/bukkitHelpParsing.js +62 -0
- package/out/cli.js +253 -0
- package/out/cliConfig.js +141 -0
- package/out/commandLine.js +28 -0
- package/out/commandSuggestions.js +202 -0
- package/out/commandTree.js +46 -0
- package/out/commandTreeCache.js +171 -0
- package/out/commandTreeCrawler.js +583 -0
- package/out/commandTreeParsingBrigadier.js +426 -0
- package/out/commandTreeParsingBukkit.js +116 -0
- package/out/commandTreeSuggestions.js +142 -0
- package/out/completionBackend.js +94 -0
- package/out/completionEngine.js +376 -0
- package/out/completionQueries.js +86 -0
- package/out/completionsBackend.js +97 -0
- package/out/connectionManager.js +209 -0
- package/out/displayArgumentHint.js +43 -0
- package/out/displayCommandTree.js +115 -0
- package/out/displaySuggestion.js +282 -0
- package/out/extension.js +190 -0
- package/out/helpTextParsing.js +445 -0
- package/out/historySearch.js +46 -0
- package/out/historyStore.js +126 -0
- package/out/lineEditor.js +525 -0
- package/out/localCommandTree.js +541 -0
- package/out/logger.js +14 -0
- package/out/minercon +253 -0
- package/out/pager.js +168 -0
- package/out/pagination.js +142 -0
- package/out/rconClient.js +97 -0
- package/out/rconConnectionManager.js +238 -0
- package/out/rconProtocol.js +421 -0
- package/out/rconSession.js +920 -0
- package/out/rconTerminal.js +80 -0
- package/out/suggestionDisplay.js +286 -0
- package/out/terminalOutput.js +110 -0
- package/out/unpaginate.js +30 -0
- 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
|