minercon 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +439 -0
  2. package/LICENSE +22 -0
  3. package/README.md +401 -0
  4. package/images/icon.png +0 -0
  5. package/out/ansi.js +123 -0
  6. package/out/argumentHint.js +43 -0
  7. package/out/bukkitHelpParsing.js +62 -0
  8. package/out/cli.js +253 -0
  9. package/out/cliConfig.js +141 -0
  10. package/out/commandLine.js +28 -0
  11. package/out/commandSuggestions.js +202 -0
  12. package/out/commandTree.js +46 -0
  13. package/out/commandTreeCache.js +171 -0
  14. package/out/commandTreeCrawler.js +583 -0
  15. package/out/commandTreeParsingBrigadier.js +426 -0
  16. package/out/commandTreeParsingBukkit.js +116 -0
  17. package/out/commandTreeSuggestions.js +142 -0
  18. package/out/completionBackend.js +94 -0
  19. package/out/completionEngine.js +376 -0
  20. package/out/completionQueries.js +86 -0
  21. package/out/completionsBackend.js +97 -0
  22. package/out/connectionManager.js +209 -0
  23. package/out/displayArgumentHint.js +43 -0
  24. package/out/displayCommandTree.js +115 -0
  25. package/out/displaySuggestion.js +282 -0
  26. package/out/extension.js +190 -0
  27. package/out/helpTextParsing.js +445 -0
  28. package/out/historySearch.js +46 -0
  29. package/out/historyStore.js +126 -0
  30. package/out/lineEditor.js +525 -0
  31. package/out/localCommandTree.js +541 -0
  32. package/out/logger.js +14 -0
  33. package/out/minercon +253 -0
  34. package/out/pager.js +168 -0
  35. package/out/pagination.js +142 -0
  36. package/out/rconClient.js +97 -0
  37. package/out/rconConnectionManager.js +238 -0
  38. package/out/rconProtocol.js +421 -0
  39. package/out/rconSession.js +920 -0
  40. package/out/rconTerminal.js +80 -0
  41. package/out/suggestionDisplay.js +286 -0
  42. package/out/terminalOutput.js +110 -0
  43. package/out/unpaginate.js +30 -0
  44. package/package.json +138 -0
@@ -0,0 +1,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