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
package/out/minercon ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // src/cli.ts — standalone CLI entry point for the Minercon terminal.
4
+ // Compiles to out/cli.js; the build script copies it to out/minercon.
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const util_1 = require("util");
43
+ const consola_1 = require("consola");
44
+ const prompts_1 = require("@clack/prompts");
45
+ const rconClient_1 = require("./rconClient");
46
+ const rconSession_1 = require("./rconSession");
47
+ const cliConfig_1 = require("./cliConfig");
48
+ // ── Config file ──────────────────────────────────────────────────────────────
49
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'minercon');
50
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
51
+ // ── Prompt cancellation ──────────────────────────────────────────────────────
52
+ /** Unwraps a clack prompt result, exiting cleanly if the user cancelled (Ctrl+C/Esc). */
53
+ function unwrap(result) {
54
+ if ((0, prompts_1.isCancel)(result)) {
55
+ (0, prompts_1.cancel)('Cancelled.');
56
+ process.exit(0);
57
+ }
58
+ return result;
59
+ }
60
+ // ── Main ─────────────────────────────────────────────────────────────────────
61
+ async function main() {
62
+ const { values, positionals } = (0, util_1.parseArgs)({
63
+ args: process.argv.slice(2),
64
+ options: {
65
+ password: { type: 'string', short: 'p' },
66
+ save: { type: 'boolean', default: false },
67
+ 'log-file': { type: 'string' },
68
+ 'log-level': { type: 'string' },
69
+ 'history-size': { type: 'string' },
70
+ 'no-plugin': { type: 'boolean', default: false },
71
+ 'no-unpaginate': { type: 'boolean', default: false },
72
+ 'no-pager': { type: 'boolean', default: false },
73
+ help: { type: 'boolean', short: 'h', default: false },
74
+ version: { type: 'boolean', short: 'V', default: false },
75
+ },
76
+ allowPositionals: true,
77
+ strict: false,
78
+ });
79
+ if (values.version) {
80
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
81
+ process.stdout.write(`minercon ${pkg.version}\n`);
82
+ process.exit(0);
83
+ }
84
+ if (values.help) {
85
+ process.stdout.write([
86
+ 'Usage: minercon [host] [port] [options]',
87
+ '',
88
+ 'Options:',
89
+ ' -p, --password <pw> RCON password (also: MCRCON_PASSWORD env var)',
90
+ ' --save Save host/port/history-size to ~/.config/minercon/config.json',
91
+ ' --log-file <path> Write log output to a file instead of the console',
92
+ ' --log-level <level> consola log level, e.g. debug, info, warn, error (default: info)',
93
+ ' --history-size <n> Number of commands to remember in history (default: 100)',
94
+ ' --no-plugin Skip the server-side tab-complete plugin probe (manual testing only;',
95
+ ' not persisted to config)',
96
+ ' --no-unpaginate Do not request unpaginated output via the plugin (keep server pages)',
97
+ ' --no-pager Do not page tall output; print it all at once',
98
+ ' -V, --version Print version and exit',
99
+ ' -h, --help Show this help',
100
+ '',
101
+ 'Environment:',
102
+ ' MCRCON_PASSWORD RCON password (used if --password is not given)',
103
+ ' MCRCON_LOG_FILE Log file path (used if --log-file is not given)',
104
+ ' MCRCON_LOG_LEVEL Log level (used if --log-level is not given)',
105
+ ' MCRCON_HISTORY_SIZE History size (used if --history-size is not given)',
106
+ ' MCRCON_UNPAGINATE Set to 0 to disable unpaginated output (default on)',
107
+ ' MCRCON_PAGER Set to 0 to disable the output pager (default on)',
108
+ '',
109
+ ].join('\n'));
110
+ process.exit(0);
111
+ }
112
+ if (!process.stdin.isTTY) {
113
+ process.stderr.write('Error: minercon is an interactive terminal and does not support piped input\n');
114
+ process.exit(1);
115
+ }
116
+ const logLevelResolution = (0, cliConfig_1.resolveLogLevel)(values['log-level'], process.env['MCRCON_LOG_LEVEL']);
117
+ if ('error' in logLevelResolution) {
118
+ process.stderr.write(`Error: ${logLevelResolution.error}\n`);
119
+ process.exit(1);
120
+ }
121
+ const logFilePath = values['log-file'] ?? process.env['MCRCON_LOG_FILE'];
122
+ const logStream = logFilePath
123
+ ? fs.createWriteStream(logFilePath, { flags: 'a' })
124
+ : undefined;
125
+ (0, prompts_1.updateSettings)({ withGuide: false });
126
+ const logger = (0, consola_1.createConsola)({
127
+ level: logLevelResolution.level,
128
+ ...(logStream ? { stdout: logStream, stderr: logStream } : {}),
129
+ });
130
+ const savedConfig = (0, cliConfig_1.readConfig)(CONFIG_FILE);
131
+ // Resolve host
132
+ let host = (0, cliConfig_1.resolveHost)(positionals[0], savedConfig);
133
+ if (!host) {
134
+ host = unwrap(await (0, prompts_1.text)({
135
+ message: 'RCON host (e.g. 127.0.0.1):',
136
+ validate: (value) => {
137
+ if (!value || value.trim() === '') {
138
+ return 'host is required';
139
+ }
140
+ },
141
+ })).trim();
142
+ }
143
+ // Resolve port
144
+ let port;
145
+ const portResolution = (0, cliConfig_1.resolvePort)(positionals[1], savedConfig);
146
+ if (portResolution && 'error' in portResolution) {
147
+ process.stderr.write(`Error: ${portResolution.error}\n`);
148
+ process.exit(1);
149
+ }
150
+ else if (portResolution) {
151
+ port = portResolution.port;
152
+ }
153
+ else {
154
+ const raw = unwrap(await (0, prompts_1.text)({
155
+ message: 'RCON port',
156
+ placeholder: '25575',
157
+ defaultValue: '25575',
158
+ validate: (value) => {
159
+ if (value && (0, cliConfig_1.parsePort)(value) === null) {
160
+ return `invalid port: ${value}`;
161
+ }
162
+ },
163
+ }));
164
+ const parsed = (0, cliConfig_1.parsePort)(raw);
165
+ if (parsed === null) {
166
+ process.stderr.write(`Error: invalid port: ${raw}\n`);
167
+ process.exit(1);
168
+ }
169
+ port = parsed;
170
+ }
171
+ // Resolve password — never saved to disk
172
+ let password = (0, cliConfig_1.resolvePassword)(values.password, process.env['MCRCON_PASSWORD']);
173
+ if (!password) {
174
+ password = unwrap(await (0, prompts_1.password)({ message: `RCON password for ${host}:${port}:` }));
175
+ }
176
+ // Resolve history size
177
+ const historySizeResolution = (0, cliConfig_1.resolveHistorySize)(values['history-size'], process.env['MCRCON_HISTORY_SIZE'], savedConfig);
178
+ if ('error' in historySizeResolution) {
179
+ process.stderr.write(`Error: ${historySizeResolution.error}\n`);
180
+ process.exit(1);
181
+ }
182
+ const historySize = historySizeResolution.historySize;
183
+ if (values.save) {
184
+ (0, cliConfig_1.writeConfig)(CONFIG_FILE, { host, port, historySize });
185
+ logger.info(`Saved ${host}:${port} (history size ${historySize}) to ${CONFIG_FILE}`);
186
+ }
187
+ // ── Establish connection ──────────────────────────────────────────────────
188
+ const controller = new rconClient_1.RconController(host, port, password, logger);
189
+ logger.info(`Connecting to ${host}:${port}...`);
190
+ try {
191
+ await controller.connect();
192
+ }
193
+ catch (err) {
194
+ logger.error(`Failed to connect: ${err}`);
195
+ process.exit(1);
196
+ }
197
+ // ── Build session host ────────────────────────────────────────────────────
198
+ let pasteboard = '';
199
+ // Cache lives in the same config dir as config.json
200
+ const cacheDir = CONFIG_DIR;
201
+ const sessionHost = {
202
+ write: (text) => process.stdout.write(text),
203
+ close: (code) => {
204
+ teardown();
205
+ process.exit(code);
206
+ },
207
+ clipboard: {
208
+ readText: () => Promise.resolve(pasteboard),
209
+ writeText: (text) => { pasteboard = text; return Promise.resolve(); },
210
+ },
211
+ cacheDir,
212
+ dimensions: () => process.stdout.columns && process.stdout.rows
213
+ ? { columns: process.stdout.columns, rows: process.stdout.rows }
214
+ : undefined,
215
+ historySize,
216
+ disablePlugin: values['no-plugin'],
217
+ // Both default-on: disabled only by their --no-* flag or env var set to '0'.
218
+ unpaginateOutput: !values['no-unpaginate'] && process.env['MCRCON_UNPAGINATE'] !== '0',
219
+ terminalPager: !values['no-pager'] && process.env['MCRCON_PAGER'] !== '0',
220
+ logToFile: logFilePath !== undefined,
221
+ };
222
+ const session = new rconSession_1.RconSession(controller, host, port, password, logger, sessionHost);
223
+ // ── TTY / signal setup ────────────────────────────────────────────────────
224
+ let tornDown = false;
225
+ function teardown() {
226
+ if (tornDown) {
227
+ return;
228
+ }
229
+ tornDown = true;
230
+ process.stdin.setRawMode(false);
231
+ process.stdin.pause();
232
+ session.close();
233
+ }
234
+ process.on('exit', teardown);
235
+ process.on('SIGINT', () => { teardown(); process.exit(0); });
236
+ process.on('SIGTERM', () => { teardown(); process.exit(0); });
237
+ process.stdin.setRawMode(true);
238
+ process.stdin.resume();
239
+ process.stdin.setEncoding('utf8');
240
+ process.stdout.on('resize', () => {
241
+ // dimensions() reads process.stdout.columns/rows live — nothing to do here
242
+ });
243
+ process.stdin.on('data', (chunk) => {
244
+ session.handleInput(chunk);
245
+ });
246
+ // Open the session (writes the welcome banner and starts the plugin probe)
247
+ session.open();
248
+ }
249
+ main().catch((err) => {
250
+ process.stderr.write(`Error: ${err}\n`);
251
+ process.exit(1);
252
+ });
253
+ //# sourceMappingURL=cli.js.map
package/out/pager.js ADDED
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ // src/pager.ts
3
+ //
4
+ // A `more`-style, append-only terminal pager for large command output.
5
+ //
6
+ // Why append-only (and not a `less`-style repaint): the server-side
7
+ // de-pagination (the `rcat` plugin wrap) hands us the *full* output in one
8
+ // response, which can be hundreds of lines. We page it at the terminal's real
9
+ // height — but the paged content MUST remain in the terminal's scrollback after
10
+ // the pager exits (an alternate-screen / repaint pager would restore the screen
11
+ // and wipe it, a regression). So we only ever print *forward*, below what's
12
+ // already shown, and draw a one-line status prompt that is erased in place
13
+ // before the next batch. Backward viewing is the terminal's own scrollback,
14
+ // which works precisely because nothing is ever cleared.
15
+ //
16
+ // The pager reads from a `LineSource` rather than a fixed array so a future
17
+ // just-in-time "fetch the next page as you scroll" source (the deferred
18
+ // no-plugin option C) can drop into the same UI unchanged.
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.Pager = exports.FALLBACK_COLS = exports.FALLBACK_ROWS = exports.ArrayLineSource = void 0;
21
+ exports.visualRowCount = visualRowCount;
22
+ const ansi_1 = require("./ansi");
23
+ /** The trivial in-memory source: the whole response, split into lines. */
24
+ class ArrayLineSource {
25
+ lines;
26
+ constructor(lines) {
27
+ this.lines = lines;
28
+ }
29
+ length() { return this.lines.length; }
30
+ lineAt(index) { return this.lines[index]; }
31
+ }
32
+ exports.ArrayLineSource = ArrayLineSource;
33
+ exports.FALLBACK_ROWS = 24;
34
+ exports.FALLBACK_COLS = 80;
35
+ /**
36
+ * Number of visual terminal rows a single (possibly ANSI-styled) source line
37
+ * occupies once the terminal soft-wraps it at `columns`. ANSI-aware: color/
38
+ * style escapes are stripped before measuring so they don't count toward width,
39
+ * and — because we only *count* here and let the terminal do the actual wrap —
40
+ * no escape sequence is ever split. An empty line still occupies one row.
41
+ */
42
+ function visualRowCount(line, columns) {
43
+ const width = (0, ansi_1.stripAnsi)(line).length;
44
+ if (width === 0) {
45
+ return 1;
46
+ }
47
+ return Math.ceil(width / Math.max(1, columns));
48
+ }
49
+ /**
50
+ * Drives one paging interaction over `source`. Created and `start()`ed once the
51
+ * session decides the output is too tall for the window; fed keystrokes via
52
+ * `handleKey` until it calls `onDone` (which restores the prompt).
53
+ */
54
+ class Pager {
55
+ host;
56
+ source;
57
+ onDone;
58
+ // Index of the next not-yet-printed source line.
59
+ next = 0;
60
+ // Whether a status prompt line is currently drawn on the cursor's row.
61
+ statusShown = false;
62
+ finished = false;
63
+ constructor(host, source, onDone) {
64
+ this.host = host;
65
+ this.source = source;
66
+ this.onDone = onDone;
67
+ }
68
+ /** True once the pager has exited (so the session can drop its reference). */
69
+ get isFinished() { return this.finished; }
70
+ /** Prints the first screenful and the status prompt. */
71
+ start() {
72
+ this.advancePage();
73
+ }
74
+ rows() { return this.host.dimensions()?.rows ?? exports.FALLBACK_ROWS; }
75
+ columns() { return this.host.dimensions()?.columns ?? exports.FALLBACK_COLS; }
76
+ /** Reserve one row for the status prompt; never less than one content row. */
77
+ pageHeight() { return Math.max(1, this.rows() - 1); }
78
+ clearStatus() {
79
+ if (this.statusShown) {
80
+ // Return to column 0 and clear the line in place — never clears the
81
+ // screen, never touches the alternate buffer, so scrollback is retained.
82
+ this.host.write('\r\x1b[K');
83
+ this.statusShown = false;
84
+ }
85
+ }
86
+ drawStatus() {
87
+ const shown = this.next;
88
+ const total = this.source.length();
89
+ this.host.write((0, ansi_1.dim)(`-- More -- (${shown}/${total}) Space: more · G: all · q: quit`));
90
+ this.statusShown = true;
91
+ }
92
+ /** Prints source lines (recomputing height each call) until `maxRows` visual rows are used or the source is exhausted. */
93
+ printRows(maxRows) {
94
+ const cols = this.columns();
95
+ let used = 0;
96
+ while (this.next < this.source.length() && used < maxRows) {
97
+ const line = this.source.lineAt(this.next);
98
+ this.host.write(`${line}\r\n`);
99
+ used += visualRowCount(line, cols);
100
+ this.next++;
101
+ }
102
+ }
103
+ atEnd() { return this.next >= this.source.length(); }
104
+ /** After printing a batch: either redraw the status prompt, or finish if done. */
105
+ afterBatch() {
106
+ if (this.atEnd()) {
107
+ this.finish();
108
+ }
109
+ else {
110
+ this.drawStatus();
111
+ }
112
+ }
113
+ advancePage() {
114
+ this.clearStatus();
115
+ this.printRows(this.pageHeight());
116
+ this.afterBatch();
117
+ }
118
+ advanceLine() {
119
+ this.clearStatus();
120
+ this.printRows(1);
121
+ this.afterBatch();
122
+ }
123
+ printAllRemaining() {
124
+ this.clearStatus();
125
+ this.printRows(Number.POSITIVE_INFINITY);
126
+ this.finish();
127
+ }
128
+ finish() {
129
+ this.clearStatus();
130
+ this.finished = true;
131
+ this.onDone();
132
+ }
133
+ /**
134
+ * Handles one input chunk while the pager is active. Forward-only by design;
135
+ * scrolling back up is the terminal's native scrollback.
136
+ */
137
+ handleKey(data) {
138
+ if (this.finished) {
139
+ return;
140
+ }
141
+ switch (data) {
142
+ case ' ':
143
+ case 'f':
144
+ case '\x1b[6~': // PageDown
145
+ this.advancePage();
146
+ break;
147
+ case '\r':
148
+ case '\n':
149
+ case '\x1b[B': // Down arrow
150
+ case 'j':
151
+ this.advanceLine();
152
+ break;
153
+ case 'G':
154
+ this.printAllRemaining();
155
+ break;
156
+ case 'q':
157
+ case '\x03': // Ctrl+C
158
+ this.finish();
159
+ break;
160
+ default:
161
+ // Ignore everything else (incl. attempts to scroll up — use the
162
+ // terminal's own scrollback).
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ exports.Pager = Pager;
168
+ //# sourceMappingURL=pager.js.map
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ // src/pagination.ts
3
+ //
4
+ // Client-side "stitching" of plugin commands that paginate their OWN output,
5
+ // independently of the server-side `rcat` de-pagination. Some plugins — most
6
+ // notably Multiverse-Core — only ever return a single page per RCON call, with
7
+ // a "Page X of Y" header and their own page argument, so `rcat` alone still
8
+ // leaves you on page 1. When de-pagination is active we recognise that chrome,
9
+ // re-issue the command once per page, strip the headers, and concatenate the
10
+ // bodies into one response that flows into the pager like any other output.
11
+ //
12
+ // The set of recognised formats lives in DEFAULT_PATTERNS and is intentionally
13
+ // open: add a PaginationPattern for each plugin family whose page navigation we
14
+ // can actually drive from the command line. Plugins with no usable "go to page
15
+ // N" command have no pattern and fall through unchanged — e.g.
16
+ // Multiverse-Portals, whose `mvp list [filter] [page]` treats the first
17
+ // positional as a name filter, so there is no way to request page 2 of the
18
+ // *unfiltered* list. If a future version exposes a real page flag, add a
19
+ // pattern here and it will be picked up automatically.
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.DEFAULT_PATTERNS = void 0;
22
+ exports.stitchPaginated = stitchPaginated;
23
+ const ansi_1 = require("./ansi");
24
+ // --- Multiverse-Core -------------------------------------------------------
25
+ //
26
+ // ContentDisplay output looks like:
27
+ //
28
+ // ====[ Multiverse World List ]====
29
+ // [Page 2 of 3]
30
+ // elficka - NORMAL
31
+ // ...
32
+ //
33
+ // A specific page is fetched with `--page N` (also accepts `--page=N`).
34
+ const MV_PAGE_LINE = /\[Page (\d+) of (\d+)\]/i;
35
+ const MV_TITLE_LINE = /^={2,}\[.*\]={2,}$/;
36
+ const MV_PAGE_ARG = /\s--page(?:=|\s+)\d+\b/i;
37
+ /** Hard cap on pages we'll fetch — a clamp against bogus/huge page counts. */
38
+ const MAX_PAGES = 1000;
39
+ const multiverseCore = {
40
+ name: 'multiverse-core',
41
+ detect(response) {
42
+ const match = (0, ansi_1.stripColors)(response).match(MV_PAGE_LINE);
43
+ if (!match) {
44
+ return undefined;
45
+ }
46
+ const page = Number(match[1]);
47
+ // totalPages comes from untrusted server output; clamp it to MAX_PAGES so a
48
+ // bogus or huge value can't drive an unbounded fetch walk.
49
+ const totalPages = Math.min(Number(match[2]), MAX_PAGES);
50
+ return { page, totalPages };
51
+ },
52
+ pageCommand(command, page) {
53
+ const base = command.replace(MV_PAGE_ARG, '').trimEnd();
54
+ return `${base} --page ${page}`;
55
+ },
56
+ contentLines(response) {
57
+ return response.split('\n').filter(line => {
58
+ const bare = (0, ansi_1.stripColors)(line).trim();
59
+ return !MV_PAGE_LINE.test(bare) && !MV_TITLE_LINE.test(bare);
60
+ });
61
+ },
62
+ titleLines(response) {
63
+ return response.split('\n').filter(line => MV_TITLE_LINE.test((0, ansi_1.stripColors)(line).trim()));
64
+ },
65
+ hasExplicitPage(command) {
66
+ return MV_PAGE_ARG.test(command);
67
+ },
68
+ };
69
+ /** Recognised paginated-output formats, tried in order. */
70
+ exports.DEFAULT_PATTERNS = [multiverseCore];
71
+ /**
72
+ * If `firstResponse` is a recognised paginated output and `command` did not
73
+ * already request a specific page, fetch every page via `fetchPage`, strip the
74
+ * chrome, and return the concatenated result. Returns `undefined` when nothing
75
+ * was stitched (not paginated, single page, explicit page requested, or no
76
+ * pattern matched) so the caller can use the original response unchanged.
77
+ *
78
+ * Resilient by design: a page fetch that throws, or whose response no longer
79
+ * matches the pattern, stops/skips rather than corrupting the output — so the
80
+ * result is never worse than the single page the caller already holds.
81
+ */
82
+ async function stitchPaginated(firstResponse, command, fetchPage, options = {}) {
83
+ const patterns = options.patterns ?? exports.DEFAULT_PATTERNS;
84
+ for (const pattern of patterns) {
85
+ if (pattern.hasExplicitPage(command)) {
86
+ continue;
87
+ }
88
+ const detected = pattern.detect(firstResponse);
89
+ if (!detected) {
90
+ continue;
91
+ }
92
+ // detect() already clamps totalPages to a safe maximum.
93
+ const totalPages = detected.totalPages;
94
+ if (totalPages <= 1) {
95
+ return undefined; // single page — nothing to stitch
96
+ }
97
+ const pages = new Map();
98
+ pages.set(detected.page, trimBlankEdges(pattern.contentLines(firstResponse)));
99
+ for (let page = 1; page <= totalPages; page++) {
100
+ if (pages.has(page)) {
101
+ continue;
102
+ }
103
+ let pageResponse;
104
+ try {
105
+ pageResponse = await fetchPage(pattern.pageCommand(command, page));
106
+ }
107
+ catch (err) {
108
+ options.log?.(`pagination: ${pattern.name} page ${page} fetch failed: ${String(err)}`);
109
+ break; // give up the walk; emit what we have
110
+ }
111
+ // A non-matching page (error text, out of range) is skipped rather than
112
+ // appended verbatim.
113
+ if (!pattern.detect(pageResponse)) {
114
+ options.log?.(`pagination: ${pattern.name} page ${page} did not match; skipping`);
115
+ continue;
116
+ }
117
+ pages.set(page, trimBlankEdges(pattern.contentLines(pageResponse)));
118
+ }
119
+ const out = [...pattern.titleLines(firstResponse)];
120
+ for (let page = 1; page <= totalPages; page++) {
121
+ const content = pages.get(page);
122
+ if (content) {
123
+ out.push(...content);
124
+ }
125
+ }
126
+ return out.join('\n');
127
+ }
128
+ return undefined;
129
+ }
130
+ /** Drop leading/trailing blank lines so stitched pages don't accrue gaps. */
131
+ function trimBlankEdges(lines) {
132
+ let start = 0;
133
+ let end = lines.length;
134
+ while (start < end && (0, ansi_1.stripColors)(lines[start]).trim() === '') {
135
+ start++;
136
+ }
137
+ while (end > start && (0, ansi_1.stripColors)(lines[end - 1]).trim() === '') {
138
+ end--;
139
+ }
140
+ return lines.slice(start, end);
141
+ }
142
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RconController = void 0;
4
+ const rconProtocol_1 = require("./rconProtocol");
5
+ const logger_1 = require("./logger");
6
+ class RconController {
7
+ host;
8
+ port;
9
+ password;
10
+ client = null;
11
+ logger;
12
+ // Serializes every `send` through this controller — completions/usage
13
+ // fetches and actual command execution all funnel through here (see
14
+ // RconCompletionBackend, CommandTreeCrawler, RconSession.executeCommand).
15
+ // Without this, e.g. hitting Enter on "/mvp list" while its argument-hint
16
+ // "cmdusage mvp list" round trip is still in flight fires two concurrent
17
+ // RCON exchanges over the same socket — which the server doesn't tolerate
18
+ // and answers by closing the connection. Chaining onto this promise (and
19
+ // swallowing its rejection so one failed command doesn't wedge the queue)
20
+ // guarantees at most one command is ever outstanding at a time.
21
+ sendQueue = Promise.resolve();
22
+ createProtocol;
23
+ unexpectedCloseHandler = null;
24
+ constructor(host, port, password, logger,
25
+ // Defaults to a real RconProtocol; tests substitute a fake here so the
26
+ // queue-serialization/error-handling logic can be exercised without a
27
+ // live server (mirrors RconProtocol's own `createSocket` seam).
28
+ createProtocol = (h, p, pw, l) => new rconProtocol_1.RconProtocol(h, p, pw, l)) {
29
+ this.host = host;
30
+ this.port = port;
31
+ this.password = password;
32
+ this.logger = logger;
33
+ this.createProtocol = createProtocol;
34
+ }
35
+ /** Called by `RconConnectionManager` so it can react to unexpected socket closes without polling. */
36
+ setUnexpectedCloseHandler(handler) {
37
+ this.unexpectedCloseHandler = handler;
38
+ }
39
+ async connect() {
40
+ this.client = this.createProtocol(this.host, this.port, this.password, this.logger);
41
+ // Set up error handler
42
+ this.client.on('error', (error) => {
43
+ this.logger.error(`RCON error: ${error.message}`);
44
+ });
45
+ // Set up close handler
46
+ this.client.on('close', () => {
47
+ this.logger.info('RCON connection closed');
48
+ this.client = null;
49
+ this.unexpectedCloseHandler?.();
50
+ });
51
+ await this.client.connect();
52
+ this.logger.info('RCON session established.');
53
+ }
54
+ send(cmd) {
55
+ const result = this.sendQueue.then(() => this.sendNow(cmd));
56
+ this.sendQueue = result.catch(() => undefined);
57
+ return result;
58
+ }
59
+ async sendNow(cmd) {
60
+ if (!this.client) {
61
+ throw new Error('Not connected');
62
+ }
63
+ const sentAt = Date.now();
64
+ this.logger.debug(`send: ${cmd}`);
65
+ try {
66
+ const res = await this.client.send(cmd);
67
+ const elapsedMs = Date.now() - sentAt;
68
+ this.logger.debug(`recv (+${elapsedMs}ms): ${cmd} -> ${res.length} chars`);
69
+ return res;
70
+ }
71
+ catch (err) {
72
+ this.logger.error('Error sending command: ' + (0, logger_1.errorMessage)(err));
73
+ throw err;
74
+ }
75
+ }
76
+ async disconnect() {
77
+ if (!this.client) {
78
+ return;
79
+ }
80
+ try {
81
+ await this.client.disconnect();
82
+ }
83
+ catch (e) {
84
+ // ignore
85
+ }
86
+ this.client = null;
87
+ }
88
+ // The live socket-level truth: do we currently have an authenticated
89
+ // connection? `RconConnectionManager` tracks a separate, intent-level
90
+ // `isConnected` (whether the session *believes* it's connected) that can
91
+ // briefly diverge from this during reconnects — see that class.
92
+ isConnected() {
93
+ return this.client !== null && this.client.isConnected();
94
+ }
95
+ }
96
+ exports.RconController = RconController;
97
+ //# sourceMappingURL=rconClient.js.map