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,426 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/commandTreeParsingBrigadier.ts
|
|
3
|
+
//
|
|
4
|
+
// Pure parsing of Minecraft's Brigadier-shaped `/help` output (flat
|
|
5
|
+
// `/cmd <args>` blobs, as returned by `minecraft:help` and vanilla's plain
|
|
6
|
+
// `help`) into a `Parameter` tree, plus the root-listing parser
|
|
7
|
+
// (`parseHelpResponse`) and the one-time namespace-support probe
|
|
8
|
+
// (`isUnsupportedNamespaceError`). No state, no IO — every export here is a
|
|
9
|
+
// deterministic function of its arguments, which is what makes them directly
|
|
10
|
+
// unit-testable without constructing a `CommandTreeCrawler`.
|
|
11
|
+
//
|
|
12
|
+
// Bukkit's hand-written `Description:`/`Usage:`/`Aliases:` `/help <command>`
|
|
13
|
+
// pages are a different grammar entirely - their extraction lives in
|
|
14
|
+
// `commandTreeParsingBukkit.ts`.
|
|
15
|
+
//
|
|
16
|
+
// `stripColors` (used throughout to normalize input before parsing) and the
|
|
17
|
+
// `formatMinecraftColors`/ANSI rendering side live in ansi.ts.
|
|
18
|
+
//
|
|
19
|
+
// The `Parameter`/`ParameterType` tree this file builds is defined in
|
|
20
|
+
// `commandTree.ts`.
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.tokenizeParameterString = tokenizeParameterString;
|
|
23
|
+
exports.parseParameter = parseParameter;
|
|
24
|
+
exports.classifyParameterTokens = classifyParameterTokens;
|
|
25
|
+
exports.splitConcatenatedHelpLines = splitConcatenatedHelpLines;
|
|
26
|
+
exports.parseAliasRedirect = parseAliasRedirect;
|
|
27
|
+
exports.parseHelpLines = parseHelpLines;
|
|
28
|
+
exports.isGenericArgsPlaceholder = isGenericArgsPlaceholder;
|
|
29
|
+
exports.hasUsableArguments = hasUsableArguments;
|
|
30
|
+
exports.hasRealUsage = hasRealUsage;
|
|
31
|
+
exports.parseHelpResponse = parseHelpResponse;
|
|
32
|
+
exports.isUnsupportedNamespaceError = isUnsupportedNamespaceError;
|
|
33
|
+
exports.buildParameterStructureFromVariants = buildParameterStructureFromVariants;
|
|
34
|
+
const ansi_1 = require("./ansi");
|
|
35
|
+
const commandTree_1 = require("./commandTree");
|
|
36
|
+
/**
|
|
37
|
+
* Tokenize parameter string handling nested structures
|
|
38
|
+
*/
|
|
39
|
+
function tokenizeParameterString(str) {
|
|
40
|
+
const tokens = [];
|
|
41
|
+
let current = '';
|
|
42
|
+
let depth = 0;
|
|
43
|
+
for (let i = 0; i < str.length; i++) {
|
|
44
|
+
const char = str[i];
|
|
45
|
+
if ((char === '<' || char === '[' || char === '(')) {
|
|
46
|
+
if (depth === 0 && current.trim()) {
|
|
47
|
+
// A bare literal abutting the start of a bracketed token.
|
|
48
|
+
tokens.push(current.trim());
|
|
49
|
+
current = '';
|
|
50
|
+
}
|
|
51
|
+
depth++;
|
|
52
|
+
current += char;
|
|
53
|
+
}
|
|
54
|
+
else if ((char === '>' || char === ']' || char === ')')) {
|
|
55
|
+
depth--;
|
|
56
|
+
current += char;
|
|
57
|
+
if (depth === 0) {
|
|
58
|
+
tokens.push(current.trim());
|
|
59
|
+
current = '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (char === ' ' && depth === 0) {
|
|
63
|
+
// Whitespace only separates tokens at the top level; inside a bracketed
|
|
64
|
+
// token (depth > 0) it's part of the token (e.g. "[plugin name]").
|
|
65
|
+
if (current.trim()) {
|
|
66
|
+
tokens.push(current.trim());
|
|
67
|
+
current = '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
current += char;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (current.trim()) {
|
|
75
|
+
tokens.push(current.trim());
|
|
76
|
+
}
|
|
77
|
+
return tokens;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parse a single parameter token. Every token maps to exactly one parameter
|
|
81
|
+
* (an unbracketed word becomes a `LITERAL`), so this never returns null.
|
|
82
|
+
*/
|
|
83
|
+
function parseParameter(token, position) {
|
|
84
|
+
// Check for choice list (option1|option2|...)
|
|
85
|
+
if (token.startsWith('(') && token.endsWith(')')) {
|
|
86
|
+
const choicesStr = token.slice(1, -1);
|
|
87
|
+
const choices = choicesStr.split('|').map((choice, idx) => ({
|
|
88
|
+
type: commandTree_1.ParameterType.LITERAL,
|
|
89
|
+
literal: choice.trim(),
|
|
90
|
+
optional: false,
|
|
91
|
+
position: idx
|
|
92
|
+
}));
|
|
93
|
+
return {
|
|
94
|
+
type: commandTree_1.ParameterType.CHOICE_LIST,
|
|
95
|
+
choices,
|
|
96
|
+
optional: false,
|
|
97
|
+
position
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Check for optional argument [name] or [<name>]
|
|
101
|
+
if (token.startsWith('[') && token.endsWith(']')) {
|
|
102
|
+
let name = token.slice(1, -1); // Remove [ and ]
|
|
103
|
+
// Also remove inner angle brackets if present
|
|
104
|
+
if (name.startsWith('<') && name.endsWith('>')) {
|
|
105
|
+
name = name.slice(1, -1); // Remove < and >
|
|
106
|
+
return {
|
|
107
|
+
type: commandTree_1.ParameterType.ARGUMENT,
|
|
108
|
+
name,
|
|
109
|
+
optional: true,
|
|
110
|
+
position
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// For [literal] without angle brackets, this could be an optional subcommand
|
|
114
|
+
// Return as LITERAL but we'll handle it specially in loadCommandDetails
|
|
115
|
+
return {
|
|
116
|
+
type: commandTree_1.ParameterType.LITERAL,
|
|
117
|
+
literal: name, // Store WITHOUT the brackets
|
|
118
|
+
optional: true,
|
|
119
|
+
position
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Check for required argument <name>
|
|
123
|
+
if (token.startsWith('<') && token.endsWith('>')) {
|
|
124
|
+
const name = token.slice(1, -1);
|
|
125
|
+
return {
|
|
126
|
+
type: commandTree_1.ParameterType.ARGUMENT,
|
|
127
|
+
name,
|
|
128
|
+
optional: false,
|
|
129
|
+
position
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Otherwise it's a literal (could be a subcommand name)
|
|
133
|
+
// We'll determine if it's actually a subcommand later when we see it has members
|
|
134
|
+
return {
|
|
135
|
+
type: commandTree_1.ParameterType.LITERAL,
|
|
136
|
+
literal: token,
|
|
137
|
+
optional: false,
|
|
138
|
+
position
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Classify a tokenized parameter string as either a named subcommand variant
|
|
143
|
+
* or a direct parameter list, parsing the relevant tokens along the way.
|
|
144
|
+
*/
|
|
145
|
+
function classifyParameterTokens(tokens) {
|
|
146
|
+
if (tokens.length === 0) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const firstToken = tokens[0];
|
|
150
|
+
// Determine if first token is a literal/subcommand or an argument
|
|
151
|
+
// FIXED: Better detection for optional subcommands vs optional arguments
|
|
152
|
+
let isArgument = false;
|
|
153
|
+
if (firstToken.startsWith('<')) {
|
|
154
|
+
// <arg> - required argument
|
|
155
|
+
isArgument = true;
|
|
156
|
+
}
|
|
157
|
+
else if (firstToken.startsWith('[') && firstToken.endsWith(']')) {
|
|
158
|
+
// Could be [<arg>] or [subcommand]
|
|
159
|
+
const inner = firstToken.slice(1, -1);
|
|
160
|
+
if (inner.startsWith('<') && inner.endsWith('>')) {
|
|
161
|
+
// [<arg>] - optional argument
|
|
162
|
+
isArgument = true;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// [subcommand] - optional subcommand, treat as subcommand
|
|
166
|
+
isArgument = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (firstToken.startsWith('(') && firstToken.endsWith(')')) {
|
|
170
|
+
// (choice1|choice2) - choice list, treat as argument
|
|
171
|
+
isArgument = true;
|
|
172
|
+
}
|
|
173
|
+
if (isArgument) {
|
|
174
|
+
// Every token is a direct parameter of the command/subcommand itself
|
|
175
|
+
const parameters = tokens.map((token, i) => parseParameter(token, i));
|
|
176
|
+
return { kind: 'direct', parameters };
|
|
177
|
+
}
|
|
178
|
+
// First token is a literal/subcommand name introducing a variant
|
|
179
|
+
let name = firstToken;
|
|
180
|
+
let optional = false;
|
|
181
|
+
// Strip optional brackets if present
|
|
182
|
+
if (name.startsWith('[') && name.endsWith(']')) {
|
|
183
|
+
name = name.slice(1, -1); // Remove [ and ]
|
|
184
|
+
optional = true;
|
|
185
|
+
}
|
|
186
|
+
const parameters = tokens.slice(1).map((token, i) => parseParameter(token, i));
|
|
187
|
+
return { kind: 'variant', name, optional, parameters };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Re-split a help response that packs multiple `/cmd ...` entries onto one
|
|
191
|
+
* line with no separators (e.g. a `minecraft:help` blob, or vanilla's `help
|
|
192
|
+
* <path>` for a multi-variant command like `gamerule`/`team`/`debug`) into
|
|
193
|
+
* one entry per line, ready for `parseHelpResponse`/`parseHelpLines`.
|
|
194
|
+
*
|
|
195
|
+
* Header/separator (`---...`) and blank lines are dropped first, so a
|
|
196
|
+
* `§e--------- §fHelp: /<cmd> §e----...` banner line - which itself contains
|
|
197
|
+
* a `/` - doesn't get re-split into a bogus `/<cmd> ----...` entry.
|
|
198
|
+
*/
|
|
199
|
+
function splitConcatenatedHelpLines(text) {
|
|
200
|
+
return text.split('\n')
|
|
201
|
+
.filter(line => {
|
|
202
|
+
const stripped = (0, ansi_1.stripColors)(line).trim();
|
|
203
|
+
return stripped && !stripped.startsWith('---');
|
|
204
|
+
})
|
|
205
|
+
.join('\n')
|
|
206
|
+
.replace(/\//g, '\n/');
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Parse a single (already `stripColors`'d and trimmed) help line as a
|
|
210
|
+
* vanilla `minecraft:help` alias redirect of the form `/<alias> -> <target>`
|
|
211
|
+
* (e.g. `/tp -> teleport`, `/minecraft:xp -> experience`). Either side may
|
|
212
|
+
* carry a namespace prefix (e.g. `minecraft:`); namespace prefixes are
|
|
213
|
+
* preserved verbatim on both sides - per "ingest everything", a namespaced
|
|
214
|
+
* alias like `minecraft:tp` is its own root command, not folded into `tp`.
|
|
215
|
+
* Returns `null` if `line` doesn't match this shape.
|
|
216
|
+
*/
|
|
217
|
+
function parseAliasRedirect(line) {
|
|
218
|
+
const match = line.match(/^\/([a-zA-Z0-9_:-]+)\s*->\s*([a-zA-Z0-9_:-]+)$/);
|
|
219
|
+
if (!match) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return { alias: match[1], target: match[2] };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Parse every line of `text` that describes `commandPath`'s syntax (an
|
|
226
|
+
* optional leading `/`, then `commandPath` with internal spaces matching
|
|
227
|
+
* runs of whitespace, then the argument tokens), collecting subcommand
|
|
228
|
+
* variants and/or a direct parameter list. Lines for other commands, alias
|
|
229
|
+
* redirects (`-> target`), and lines with no arguments at all are ignored.
|
|
230
|
+
*
|
|
231
|
+
* `text` is matched line-by-line (split on `\n`) — callers whose source may
|
|
232
|
+
* pack multiple commands' syntax onto one line without separators (e.g. a
|
|
233
|
+
* `minecraft:help` blob, where consecutive commands are simply concatenated)
|
|
234
|
+
* must first replace `/` with `\n/` to re-split it into one command per line.
|
|
235
|
+
*/
|
|
236
|
+
function parseHelpLines(text, commandPath) {
|
|
237
|
+
const escaped = commandPath
|
|
238
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
239
|
+
.replace(/ /g, '\\s+');
|
|
240
|
+
const pattern = new RegExp(`^/?${escaped}(?::)?\\s*(.*)$`, 'i');
|
|
241
|
+
const variants = new Map();
|
|
242
|
+
let direct = null;
|
|
243
|
+
for (const rawLine of text.split('\n')) {
|
|
244
|
+
const stripped = (0, ansi_1.stripColors)(rawLine).trim();
|
|
245
|
+
if (!stripped || stripped.startsWith('---')) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const match = stripped.match(pattern);
|
|
249
|
+
if (!match) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const afterCommand = match[1] || '';
|
|
253
|
+
if (!afterCommand || afterCommand.startsWith('->')) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const tokens = tokenizeParameterString(afterCommand);
|
|
257
|
+
const classified = classifyParameterTokens(tokens);
|
|
258
|
+
if (classified?.kind === 'variant') {
|
|
259
|
+
variants.set(classified.name, { optional: classified.optional, members: classified.parameters });
|
|
260
|
+
}
|
|
261
|
+
else if (classified?.kind === 'direct') {
|
|
262
|
+
direct = classified.parameters;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { variants, direct };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* True iff `parameters` is exactly a bare `<args>`/`[<args>]` placeholder -
|
|
269
|
+
* the generic stand-in Bukkit emits (e.g. for `minecraft:help <cmd>` on
|
|
270
|
+
* commands that aren't Brigadier-backed, like `version`/`reload`/`plugins`)
|
|
271
|
+
* when it has no real argument info, regardless of whether it's optional.
|
|
272
|
+
*/
|
|
273
|
+
function isArgsPlaceholder(parameters) {
|
|
274
|
+
return parameters.length === 1
|
|
275
|
+
&& parameters[0].type === commandTree_1.ParameterType.ARGUMENT
|
|
276
|
+
&& parameters[0].name === 'args';
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* True iff `parameters` is exactly the generic `[<args>]` placeholder Bukkit
|
|
280
|
+
* emits for `minecraft:help <cmd>` on commands that aren't Brigadier-backed
|
|
281
|
+
* (e.g. `version`, `reload`, `plugins`) — i.e. no real argument info.
|
|
282
|
+
*
|
|
283
|
+
* Narrower than `isArgsPlaceholder` on purpose: it requires the `[<args>]`
|
|
284
|
+
* form to be *optional*. Bukkit's stand-in is always optional, so a
|
|
285
|
+
* *required* `<args>` is treated as a genuine (if oddly-named) argument by
|
|
286
|
+
* `hasRealUsage`/`mergeHelpSources` — those guard against emitting a fake
|
|
287
|
+
* `[<args>]` token while still honoring a real one. (`hasUsableArguments`,
|
|
288
|
+
* used for the separate "should we re-fetch this subcommand?" decision,
|
|
289
|
+
* deliberately rejects both forms via `isArgsPlaceholder`.)
|
|
290
|
+
*/
|
|
291
|
+
function isGenericArgsPlaceholder(parameters) {
|
|
292
|
+
return isArgsPlaceholder(parameters) && parameters[0].optional === true;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* True iff `members` carries real, usable argument info for a subcommand
|
|
296
|
+
* variant - i.e. it's non-empty and not just an `<args>`/`[<args>]`
|
|
297
|
+
* placeholder (which means "no info available", whether or not it's
|
|
298
|
+
* optional). Used to decide whether the usage already parsed from a parent
|
|
299
|
+
* command's help output is trustworthy enough to skip a further per-subcommand
|
|
300
|
+
* help round trip.
|
|
301
|
+
*/
|
|
302
|
+
function hasUsableArguments(members) {
|
|
303
|
+
return members.length > 0 && !isArgsPlaceholder(members);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* True iff `result` carries real, usable syntax info - either a non-placeholder
|
|
307
|
+
* `direct` parameter list or at least one subcommand variant. Used to decide
|
|
308
|
+
* whether a help source already answers a command's usage, or whether the
|
|
309
|
+
* caller needs to try another source.
|
|
310
|
+
*/
|
|
311
|
+
function hasRealUsage(result) {
|
|
312
|
+
return (result.direct !== null && !isGenericArgsPlaceholder(result.direct)) || result.variants.size > 0;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Shapes a root-command line can take in a `/help`/`minecraft:help` response,
|
|
316
|
+
* tried in order (first match wins). Each captures the command name in group
|
|
317
|
+
* 1; namespace prefixes (`minecraft:`, ...) are part of the name, so `:` is in
|
|
318
|
+
* every character class - see `parseHelpResponse`.
|
|
319
|
+
*/
|
|
320
|
+
const ROOT_COMMAND_PATTERNS = [
|
|
321
|
+
/^\/([a-zA-Z0-9_:-]+)/, // /command or /namespace:command
|
|
322
|
+
/^([a-zA-Z0-9_:-]+):\s/, // command: or command-with-hyphens:
|
|
323
|
+
/^[-*]\s*([a-zA-Z0-9_:-]+)/, // - command or * command
|
|
324
|
+
/^([a-zA-Z0-9_:-]+)\s+[-<[(]/ // command followed by args
|
|
325
|
+
];
|
|
326
|
+
/** Words that look like commands in description text but never are. */
|
|
327
|
+
const NON_COMMAND_WORDS = new Set(['usage', 'example', 'description', 'syntax']);
|
|
328
|
+
/**
|
|
329
|
+
* Parse a `/help` or `minecraft:help` response into the root commands and
|
|
330
|
+
* alias redirects it describes.
|
|
331
|
+
*
|
|
332
|
+
* `response` is first run through `splitConcatenatedHelpLines` so that a
|
|
333
|
+
* `minecraft:help` blob (multiple `/cmd ...` entries packed onto one line)
|
|
334
|
+
* is split one-per-line. Each resulting line is then matched against one of:
|
|
335
|
+
*
|
|
336
|
+
* - an alias redirect (`/<alias> -> <target>`, via `parseAliasRedirect`)
|
|
337
|
+
* - `/command ...` or `/namespace:command ...`
|
|
338
|
+
* - `command: ...` or `command-with-hyphens: ...`
|
|
339
|
+
* - `- command ...` or `* command ...`
|
|
340
|
+
* - `command <args>` (command name followed by `-`/`<`/`[`/`(`)
|
|
341
|
+
*
|
|
342
|
+
* Namespace prefixes (`minecraft:`, `bukkit:`, ...) are part of the command
|
|
343
|
+
* name - per "ingest everything", `minecraft:advancement` is its own root
|
|
344
|
+
* command, not just `minecraft`, so `:` is included in every pattern's
|
|
345
|
+
* character class. Common non-command words that appear in descriptions
|
|
346
|
+
* (`usage`, `example`, `description`, `syntax`) are skipped.
|
|
347
|
+
*
|
|
348
|
+
* For each command found, `isPlaceholder` reflects whether its summary line
|
|
349
|
+
* already carries real syntax info (`hasRealUsage`), so callers can decide
|
|
350
|
+
* which help source to try first for that command's full details.
|
|
351
|
+
*/
|
|
352
|
+
function parseHelpResponse(response) {
|
|
353
|
+
const lines = splitConcatenatedHelpLines(response).split('\n');
|
|
354
|
+
const commands = [];
|
|
355
|
+
const aliases = [];
|
|
356
|
+
for (const line of lines) {
|
|
357
|
+
const stripped = (0, ansi_1.stripColors)(line).trim();
|
|
358
|
+
// Skip empty lines and headers
|
|
359
|
+
if (!stripped || stripped.startsWith('---') || stripped.startsWith('===')) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
// Alias redirect lines (`/tp -> teleport`) describe an alias, not a
|
|
363
|
+
// root command in their own right.
|
|
364
|
+
const redirect = parseAliasRedirect(stripped);
|
|
365
|
+
if (redirect) {
|
|
366
|
+
aliases.push(redirect);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
for (const pattern of ROOT_COMMAND_PATTERNS) {
|
|
370
|
+
const match = stripped.match(pattern);
|
|
371
|
+
if (!match) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const commandName = match[1];
|
|
375
|
+
// Skip common non-command words that appear in descriptions
|
|
376
|
+
if (NON_COMMAND_WORDS.has(commandName.toLowerCase())) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const summary = parseHelpLines(stripped, commandName);
|
|
380
|
+
commands.push({ name: commandName, isPlaceholder: !hasRealUsage(summary) });
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { commands, aliases };
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* True iff `response` is the Brigadier "unknown namespace" syntax error
|
|
388
|
+
* returned for `minecraft:help` on servers where the `minecraft:` command
|
|
389
|
+
* namespace prefix isn't registered (vanilla/fabric) — distinct from the
|
|
390
|
+
* normal "Unknown command or insufficient permissions" not-found message.
|
|
391
|
+
*/
|
|
392
|
+
function isUnsupportedNamespaceError(response) {
|
|
393
|
+
return /^Unknown or incomplete command/i.test((0, ansi_1.stripColors)(response).trim());
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Build the parameter structure representing a set of collected variants:
|
|
397
|
+
* a single SUBCOMMAND if there's only one, or a CHOICE_LIST wrapping a
|
|
398
|
+
* SUBCOMMAND for each variant (in encounter order) otherwise.
|
|
399
|
+
*/
|
|
400
|
+
function buildParameterStructureFromVariants(variants) {
|
|
401
|
+
if (variants.size === 0) {
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
const subcommandChoices = [];
|
|
405
|
+
for (const [name, { optional, members }] of variants) {
|
|
406
|
+
subcommandChoices.push({
|
|
407
|
+
type: commandTree_1.ParameterType.SUBCOMMAND,
|
|
408
|
+
name,
|
|
409
|
+
optional,
|
|
410
|
+
position: subcommandChoices.length,
|
|
411
|
+
members,
|
|
412
|
+
isComplete: false
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
// If there's only one variant, use it directly; otherwise wrap in a choice list
|
|
416
|
+
if (subcommandChoices.length === 1) {
|
|
417
|
+
return [subcommandChoices[0]];
|
|
418
|
+
}
|
|
419
|
+
return [{
|
|
420
|
+
type: commandTree_1.ParameterType.CHOICE_LIST,
|
|
421
|
+
optional: false,
|
|
422
|
+
position: 0,
|
|
423
|
+
choices: subcommandChoices
|
|
424
|
+
}];
|
|
425
|
+
}
|
|
426
|
+
//# sourceMappingURL=commandTreeParsingBrigadier.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/commandTreeParsingBukkit.ts
|
|
3
|
+
//
|
|
4
|
+
// Pure parsing of Bukkit's hand-written `/help <command>` pages. Two formats:
|
|
5
|
+
//
|
|
6
|
+
// 1. Description:/Usage:/Aliases: block — the standard format for commands with
|
|
7
|
+
// a single usage string (e.g. `/version`, `/reload`).
|
|
8
|
+
//
|
|
9
|
+
// 2. "Showing help for X" — a search-results page listing multiple subcommand
|
|
10
|
+
// usages as `<cmd> <subcmd> [args] - description` lines. Bukkit uses this
|
|
11
|
+
// for plugin commands that register each subcommand as its own help topic
|
|
12
|
+
// rather than a single `Usage:` string (e.g. multi-command plugin like mvp).
|
|
13
|
+
//
|
|
14
|
+
// Both formats are extracted by `extractBukkitUsageLines` and are a different
|
|
15
|
+
// grammar from the flat Brigadier `/cmd <args>` blobs that
|
|
16
|
+
// `commandTreeParsingBrigadier.ts` handles: on a server where the `minecraft:`
|
|
17
|
+
// namespace is supported (Paper/Spigot), `help <path>` is *always* one of
|
|
18
|
+
// these pages — so no shape-sniffing is needed to decide which parser applies,
|
|
19
|
+
// only `CommandTreeCrawler.supportsMinecraftNamespace`.
|
|
20
|
+
//
|
|
21
|
+
// No state, no IO — every export here is a deterministic function of its
|
|
22
|
+
// arguments.
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.extractBukkitUsageLines = extractBukkitUsageLines;
|
|
25
|
+
exports.extractBukkitAliases = extractBukkitAliases;
|
|
26
|
+
const ansi_1 = require("./ansi");
|
|
27
|
+
/**
|
|
28
|
+
* Find the index of the first ` - ` (space-dash-space) at bracket depth 0 in
|
|
29
|
+
* `s`. This is the separator between the usage string and the description in
|
|
30
|
+
* Bukkit's "Showing help for X" format. Returns -1 if not found.
|
|
31
|
+
*/
|
|
32
|
+
function descriptionSeparatorIndex(s) {
|
|
33
|
+
let depth = 0;
|
|
34
|
+
for (let i = 0; i < s.length - 2; i++) {
|
|
35
|
+
const c = s[i];
|
|
36
|
+
if (c === '<' || c === '[' || c === '(') {
|
|
37
|
+
depth++;
|
|
38
|
+
}
|
|
39
|
+
else if (c === '>' || c === ']' || c === ')') {
|
|
40
|
+
depth--;
|
|
41
|
+
}
|
|
42
|
+
else if (depth === 0 && c === ' ' && s[i + 1] === '-' && s[i + 2] === ' ') {
|
|
43
|
+
return i;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extract the `Usage: ...` line(s) from a Bukkit-style `/help <command>`
|
|
50
|
+
* response. Handles two formats:
|
|
51
|
+
*
|
|
52
|
+
* 1. Standard `Description:`/`Usage:`/`Aliases:` block (e.g. `/version`,
|
|
53
|
+
* `/reload`): returns the `Usage:` line(s), normalized for `parseHelpLines`.
|
|
54
|
+
* Returns `[]` if the Usage is just the bare command name (the generic
|
|
55
|
+
* response Bukkit gives for Brigadier-backed vanilla commands).
|
|
56
|
+
*
|
|
57
|
+
* 2. "Showing help for X" page (e.g. multi-command plugin help topics): lines
|
|
58
|
+
* of the form `<cmd> <subcmd> [args] - description`. Strips the ` -
|
|
59
|
+
* description` tail from each matching line and returns the usage portions,
|
|
60
|
+
* filtered to exclude the bare command itself.
|
|
61
|
+
*
|
|
62
|
+
* Returns `[]` if neither format yields usable argument info.
|
|
63
|
+
*/
|
|
64
|
+
function extractBukkitUsageLines(helpText, commandPath) {
|
|
65
|
+
const lines = (0, ansi_1.stripColors)(helpText).split('\n').map(line => line.trim());
|
|
66
|
+
const usageIndex = lines.findIndex(line => /^Usage:\s*/i.test(line));
|
|
67
|
+
if (usageIndex !== -1) {
|
|
68
|
+
const result = [lines[usageIndex].replace(/^Usage:\s*/i, '')];
|
|
69
|
+
for (let i = usageIndex + 1; i < lines.length; i++) {
|
|
70
|
+
const line = lines[i];
|
|
71
|
+
if (!line || /^[A-Za-z][A-Za-z ]*:/.test(line) || line.startsWith('---')) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
result.push(line);
|
|
75
|
+
}
|
|
76
|
+
const normalizedPath = commandPath.toLowerCase();
|
|
77
|
+
return result.filter(line => line.replace(/^\//, '').toLowerCase() !== normalizedPath);
|
|
78
|
+
}
|
|
79
|
+
// "Showing help for X" format: each line is `<cmd> <subcmd> [args] - description`.
|
|
80
|
+
// Match lines that start with commandPath followed by a space, strip descriptions.
|
|
81
|
+
const escaped = commandPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '\\s+');
|
|
82
|
+
const cmdPattern = new RegExp(`^/?${escaped}\\s`, 'i');
|
|
83
|
+
const normalizedPath = commandPath.toLowerCase();
|
|
84
|
+
const usageLines = [];
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (!line || line.startsWith('---') || line.startsWith('===')) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!cmdPattern.test(line)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const sepIdx = descriptionSeparatorIndex(line);
|
|
93
|
+
const usagePart = (sepIdx >= 0 ? line.slice(0, sepIdx) : line).trim();
|
|
94
|
+
if (usagePart.replace(/^\//, '').toLowerCase() !== normalizedPath) {
|
|
95
|
+
usageLines.push(usagePart);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return usageLines;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extract alias names from a Bukkit-style `/help <command>` response's
|
|
102
|
+
* `Aliases: a, b, c` line (e.g. "Description: ...\nUsage: ...\nAliases: ver,
|
|
103
|
+
* about"). Returns `[]` if there's no Aliases line.
|
|
104
|
+
*/
|
|
105
|
+
function extractBukkitAliases(helpText) {
|
|
106
|
+
const lines = (0, ansi_1.stripColors)(helpText).split('\n').map(line => line.trim());
|
|
107
|
+
const aliasesLine = lines.find(line => /^Aliases:\s*/i.test(line));
|
|
108
|
+
if (!aliasesLine) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return aliasesLine.replace(/^Aliases:\s*/i, '')
|
|
112
|
+
.split(',')
|
|
113
|
+
.map(alias => alias.trim())
|
|
114
|
+
.filter(alias => alias.length > 0);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=commandTreeParsingBukkit.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/commandTreeSuggestions.ts
|
|
3
|
+
//
|
|
4
|
+
// Pure suggestion generation: given the command tree `CommandTreeCrawler`
|
|
5
|
+
// builds and the user's current input line, work out what to suggest next
|
|
6
|
+
// and what argument-help text to show. No state, no IO — a deterministic
|
|
7
|
+
// function of the tree and the input.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getSuggestions = getSuggestions;
|
|
10
|
+
const commandTree_1 = require("./commandTree");
|
|
11
|
+
const commandLine_1 = require("./commandLine");
|
|
12
|
+
/**
|
|
13
|
+
* Get suggestions based on current input
|
|
14
|
+
*/
|
|
15
|
+
function getSuggestions(rootCommands, isReady, input) {
|
|
16
|
+
if (!isReady) {
|
|
17
|
+
return { suggestions: [], argumentHelp: undefined };
|
|
18
|
+
}
|
|
19
|
+
const trimmed = input.trim();
|
|
20
|
+
if (!trimmed.startsWith('/')) {
|
|
21
|
+
return { suggestions: [], argumentHelp: undefined };
|
|
22
|
+
}
|
|
23
|
+
const { parts, hasTrailingSpace } = (0, commandLine_1.splitCommandLine)(input);
|
|
24
|
+
const commandName = parts[0];
|
|
25
|
+
// Handle root command suggestions
|
|
26
|
+
if (parts.length === 0 || (parts.length === 1 && !hasTrailingSpace)) {
|
|
27
|
+
const suggestions = Array.from(rootCommands.keys())
|
|
28
|
+
.filter(cmd => cmd.startsWith(commandName || ''))
|
|
29
|
+
.sort();
|
|
30
|
+
return { suggestions, argumentHelp: undefined };
|
|
31
|
+
}
|
|
32
|
+
// Find the command node
|
|
33
|
+
const rootNode = rootCommands.get(commandName);
|
|
34
|
+
if (!rootNode) {
|
|
35
|
+
return { suggestions: [], argumentHelp: undefined };
|
|
36
|
+
}
|
|
37
|
+
// Navigate through the parameter tree
|
|
38
|
+
let currentParameters = rootNode.members;
|
|
39
|
+
let paramIndex = 1; // Start after the command name
|
|
40
|
+
// The command path consumed so far (root command + any literal/subcommand
|
|
41
|
+
// tokens navigated past) - see SuggestionResult.commandPath.
|
|
42
|
+
const pathParts = [commandName];
|
|
43
|
+
// Navigate through completed parts (not including what we're currently typing)
|
|
44
|
+
const partsToNavigate = hasTrailingSpace ? parts.length : parts.length - 1;
|
|
45
|
+
while (paramIndex < partsToNavigate && currentParameters.length > 0) {
|
|
46
|
+
const currentPart = parts[paramIndex];
|
|
47
|
+
let navigated = false;
|
|
48
|
+
// Get the first parameter at this position
|
|
49
|
+
const firstParam = currentParameters[0];
|
|
50
|
+
if (firstParam.type === commandTree_1.ParameterType.SUBCOMMAND) {
|
|
51
|
+
// Direct subcommand
|
|
52
|
+
if (firstParam.name === currentPart) {
|
|
53
|
+
currentParameters = firstParam.members;
|
|
54
|
+
navigated = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (firstParam.type === commandTree_1.ParameterType.CHOICE_LIST) {
|
|
58
|
+
// Choice list - find matching choice and navigate into it
|
|
59
|
+
for (const choice of firstParam.choices) {
|
|
60
|
+
if (choice.type === commandTree_1.ParameterType.SUBCOMMAND && choice.name === currentPart) {
|
|
61
|
+
// IMPORTANT: Navigate into the selected choice's members
|
|
62
|
+
currentParameters = choice.members;
|
|
63
|
+
navigated = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
else if (choice.type === commandTree_1.ParameterType.LITERAL && choice.literal === currentPart) {
|
|
67
|
+
// For literal choices, move to next parameter position
|
|
68
|
+
currentParameters = currentParameters.slice(1);
|
|
69
|
+
navigated = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (firstParam.type === commandTree_1.ParameterType.LITERAL && firstParam.literal === currentPart) {
|
|
75
|
+
// Literal parameter
|
|
76
|
+
currentParameters = currentParameters.slice(1);
|
|
77
|
+
navigated = true;
|
|
78
|
+
}
|
|
79
|
+
paramIndex++;
|
|
80
|
+
if (navigated) {
|
|
81
|
+
pathParts.push(currentPart);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// It's an argument value, skip to next position
|
|
85
|
+
currentParameters = currentParameters.slice(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Build argument help from current position
|
|
89
|
+
const argumentHelp = buildArgumentHelp(currentParameters);
|
|
90
|
+
const commandPath = pathParts.join(' ');
|
|
91
|
+
// Generate suggestions for the current position. A trailing space means the
|
|
92
|
+
// user is starting the next token, so match against an empty prefix (every
|
|
93
|
+
// candidate); otherwise match the partial token they're still typing.
|
|
94
|
+
const typedPrefix = hasTrailingSpace ? '' : (parts[parts.length - 1] || '');
|
|
95
|
+
const suggestions = suggestionsMatchingPrefix(currentParameters, typedPrefix);
|
|
96
|
+
return { suggestions, argumentHelp, commandPath };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Suggestions for the parameter position represented by `parameters`, limited
|
|
100
|
+
* to the candidates that start with `prefix` (pass `''` to get them all — e.g.
|
|
101
|
+
* after a trailing space, when the user is starting a fresh token). Only the
|
|
102
|
+
* first parameter at this position is suggestable; ARGUMENT positions have no
|
|
103
|
+
* fixed candidates and yield nothing.
|
|
104
|
+
*/
|
|
105
|
+
function suggestionsMatchingPrefix(parameters, prefix) {
|
|
106
|
+
if (parameters.length === 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const param = parameters[0];
|
|
110
|
+
// A CHOICE_LIST contributes each of its subcommand/literal choices; a bare
|
|
111
|
+
// SUBCOMMAND/LITERAL contributes itself; an ARGUMENT contributes nothing.
|
|
112
|
+
const candidates = param.type === commandTree_1.ParameterType.CHOICE_LIST
|
|
113
|
+
? param.choices.filter(c => c.type === commandTree_1.ParameterType.SUBCOMMAND || c.type === commandTree_1.ParameterType.LITERAL)
|
|
114
|
+
: (param.type === commandTree_1.ParameterType.SUBCOMMAND || param.type === commandTree_1.ParameterType.LITERAL)
|
|
115
|
+
? [param]
|
|
116
|
+
: [];
|
|
117
|
+
return candidates
|
|
118
|
+
.map(commandTree_1.parameterLabel)
|
|
119
|
+
.filter(label => label.startsWith(prefix))
|
|
120
|
+
.sort();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Build argument help string from parameters
|
|
124
|
+
*/
|
|
125
|
+
function buildArgumentHelp(parameters) {
|
|
126
|
+
if (parameters.length === 0) {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
return parameters.map(param => {
|
|
130
|
+
switch (param.type) {
|
|
131
|
+
case commandTree_1.ParameterType.ARGUMENT:
|
|
132
|
+
return param.optional ? `[<${param.name}>]` : `<${param.name}>`;
|
|
133
|
+
case commandTree_1.ParameterType.CHOICE_LIST:
|
|
134
|
+
return `(${param.choices.map(commandTree_1.parameterLabel).join('|')})`;
|
|
135
|
+
case commandTree_1.ParameterType.LITERAL:
|
|
136
|
+
return param.literal;
|
|
137
|
+
case commandTree_1.ParameterType.SUBCOMMAND:
|
|
138
|
+
return param.name; // Show subcommand name
|
|
139
|
+
}
|
|
140
|
+
}).join(' ');
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=commandTreeSuggestions.js.map
|