From a050e010585e2ea36e99aebd3f9e2736dad40963 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 17 Sep 2026 15:52:11 +0300 Subject: [PATCH 1/3] fix(documentation): Correctly generate subcommands' documentation --- internal/documentation/scripts/generateCliDoc.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/documentation/scripts/generateCliDoc.js b/internal/documentation/scripts/generateCliDoc.js index e833dff57b7..a1857877d23 100644 --- a/internal/documentation/scripts/generateCliDoc.js +++ b/internal/documentation/scripts/generateCliDoc.js @@ -108,7 +108,7 @@ function generateDoc() { const commandsObj = []; obj.commands.shift(); - if (!(obj.commands.length <= 1)) { + if (!(obj.commands.length < 1)) { for (const all of obj.commands) { const temp = checkChars(all); const {command, description} = splitString(temp); @@ -143,6 +143,10 @@ function generateDoc() { if (!(obj.addOptions.length <= 1)) { for (const all of obj.addOptions) { const temp = checkChars(all); + // yargs appends a trailing empty line to some sections; skip it to avoid ghost table rows + if (temp == "") { + continue; + } const {command, description, details} = splitString(temp); optionObj.push({option: command, optionDescription: description, optionDetails: details}); } From ea17cb232832ca18e8562d4bb842cf2980e43f70 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Fri, 18 Sep 2026 13:11:22 +0300 Subject: [PATCH 2/3] feat: Discover subcommands and aliases --- .../documentation/scripts/generateCliDoc.js | 194 +++++++++++------- .../scripts/resources/CLI.template.md | 4 + 2 files changed, 128 insertions(+), 70 deletions(-) diff --git a/internal/documentation/scripts/generateCliDoc.js b/internal/documentation/scripts/generateCliDoc.js index a1857877d23..eeb8b9f49a9 100644 --- a/internal/documentation/scripts/generateCliDoc.js +++ b/internal/documentation/scripts/generateCliDoc.js @@ -77,6 +77,122 @@ function parseOutput(stdout) { } } + +function parseAliases(details) { + if (!details || !details.startsWith("[aliases:")) return []; + return details.slice("[aliases:".length, -1).trim().split(",").map((s) => s.trim()).filter(Boolean); +} + +// Calls --help for commandPath and builds a full command entry from the current obj state. +/** + * @param {string} commandPath + * @param {string[]} aliases + */ +function buildCommandEntry(commandPath, aliases = []) { + execute(commandPath + " --help"); + + const commandsObj = []; + obj.commands.shift(); + // `< 1` (not `<= 1`) so commands with exactly one subcommand are included too + if (!(obj.commands.length < 1)) { + for (const all of obj.commands) { + const temp = checkChars(all); + const {command, description} = splitString(temp); + commandsObj.push({childCommand: command, commandDescription: description}); + } + } + + const positionalObj = []; + obj.positionals.shift(); + if (!(obj.positionals.length < 1)) { + let index = 0; + for (const all of obj.positionals) { + const temp = checkChars(all); + const {command, description, details} = splitString(temp); + if (!(/\S/.test(command))) { + if (index > 0) { + positionalObj[index - 1].positionalDescription = + positionalObj[index - 1].positionalDescription.concat("
", description); + positionalObj[index - 1].positionalDetails = details; + } + continue; + } + positionalObj.push({ + positional: command, + positionalDescription: description, + positionalDetails: details + }); + index++; + } + } + + const optionObj = []; + obj.addOptions.shift(); + if (!(obj.addOptions.length <= 1)) { + for (const all of obj.addOptions) { + const temp = checkChars(all); + // yargs appends a trailing empty line to some sections; skip it to avoid ghost table rows + if (temp == "") { + continue; + } + const {command, description, details} = splitString(temp); + optionObj.push({option: command, optionDescription: description, optionDetails: details}); + } + } + + const exampleObj = []; + obj.examples.shift(); + if (!(obj.examples.length <= 1)) { + for (const all of obj.examples) { + const temp = checkChars(all); + if (temp == "") { + continue; + } + const {command, description} = splitString(temp); + exampleObj.push({example: command, exampleDescription: description}); + } + } + + return { + command: commandPath, + description: obj.desc, + usage: obj.usage, + aliases, + childCommands: commandsObj, + positionals: positionalObj, + options: optionObj, + examples: exampleObj + }; +} + +/** + * Iterates commandsArray by index so newly appended subcommand entries are + * automatically picked up on subsequent iterations — depth-first without explicit recursion. + * + * @param {Record[]} commandsArray + */ +function discoverSubCommands(commandsArray) { + const seen = new Set(commandsArray.map((e) => e.command)); + + for (let i = 0; i < commandsArray.length; i++) { + for (const child of commandsArray[i].childCommands || []) { + const subPath = child.childCommand && child.childCommand.trim(); + if (!subPath || !/\S/.test(subPath)) { + continue; + } + // A command with positionals ( or [arg]) is a leaf — skip recursion. + if (subPath.includes("<") || subPath.includes("[")) { + continue; + } + if (seen.has(subPath)) { + continue; + } + seen.add(subPath); + commandsArray.push(buildCommandEntry(subPath)); + } + } +} + function generateDoc() { execute("ui5 --help"); @@ -104,78 +220,16 @@ function generateDoc() { const commands = obj.commands; for (const all of commands) { const command = all.trim().split(" ").slice(0, 2).join(" "); - execute(command + " --help"); - - const commandsObj = []; - obj.commands.shift(); - if (!(obj.commands.length < 1)) { - for (const all of obj.commands) { - const temp = checkChars(all); - const {command, description} = splitString(temp); - commandsObj.push({childCommand: command, commandDescription: description}); - } - } - - const positionalObj = []; - obj.positionals.shift(); - - if (!(obj.positionals.length < 1)) { - let index = 0; - for (const all of obj.positionals) { - const temp = checkChars(all); - const {command, description, details} = splitString(temp); - if (!(/\S/.test(command))) { - positionalObj[index - 1].positionalDescription = - positionalObj[index - 1].positionalDescription.concat("
", description); - positionalObj[index - 1].positionalDetails =details; - continue; - } - positionalObj.push({ - positional: command, - positionalDescription: description, - positionalDetails: details - }); - index++; - } - } - const optionObj = []; - obj.addOptions.shift(); - if (!(obj.addOptions.length <= 1)) { - for (const all of obj.addOptions) { - const temp = checkChars(all); - // yargs appends a trailing empty line to some sections; skip it to avoid ghost table rows - if (temp == "") { - continue; - } - const {command, description, details} = splitString(temp); - optionObj.push({option: command, optionDescription: description, optionDetails: details}); - } - } - - const exampleObj = []; - obj.examples.shift(); - if (!(obj.examples.length <= 1)) { - for (const all of obj.examples) { - const temp = checkChars(all); - if (temp == "") { - continue; - } - const {command, description} = splitString(temp); - exampleObj.push({example: command, exampleDescription: description}); - } - } - - const commandObj = { - command: command, - description: obj.desc, - usage: obj.usage, - childCommands: commandsObj, - positionals: positionalObj, - options: optionObj, - examples: exampleObj - }; + // Parse [aliases: ...] from the root commands-list line before execute() overwrites obj + const {details: lineDetails} = splitString(checkChars(all)); + const commandObj = buildCommandEntry(command, parseAliases(lineDetails)); commandsArray.push(commandObj); } + + discoverSubCommands(commandsArray); + // Order subcommands right after their parent commands + commandsArray.sort((a, b) => a.command.localeCompare(b.command)); + let content = template({ common: obj.common.split("Usage:").join(""), commonOptions: optionObj, diff --git a/internal/documentation/scripts/resources/CLI.template.md b/internal/documentation/scripts/resources/CLI.template.md index 19a4f06ff9c..2ed7fdf378c 100644 --- a/internal/documentation/scripts/resources/CLI.template.md +++ b/internal/documentation/scripts/resources/CLI.template.md @@ -50,6 +50,10 @@ These options you can use with each command. {{description}} +{{#if aliases}} +**Aliases**: {{#each aliases}}`ui5 {{this}}`{{#unless @last}}, {{/unless}}{{/each}} + +{{/if}} **Usage** ` From c4233b4be1a01e50936cde58f22716ad6da18873 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Fri, 18 Sep 2026 17:41:14 +0300 Subject: [PATCH 3/3] fix(documentation): Show child command options in generated CLI doc --- internal/documentation/scripts/generateCliDoc.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/documentation/scripts/generateCliDoc.js b/internal/documentation/scripts/generateCliDoc.js index eeb8b9f49a9..f5e142bdbd8 100644 --- a/internal/documentation/scripts/generateCliDoc.js +++ b/internal/documentation/scripts/generateCliDoc.js @@ -60,6 +60,8 @@ function parseOutput(stdout) { } if (section.includes("Options:")) { obj.addOptions = section.split("\n").filter(function(el) { + // Skip section headers (non-indented lines ending with ":", e.g. "Options:") + if (!el.startsWith(" ") && el.trim().endsWith(":")) return false; const array = obj.commonOptions; array.forEach(function(item, index, array) { array[index] = item.replace(/\s+/g, ""); @@ -127,8 +129,7 @@ function buildCommandEntry(commandPath, aliases = []) { } const optionObj = []; - obj.addOptions.shift(); - if (!(obj.addOptions.length <= 1)) { + if (!(obj.addOptions.length < 1)) { for (const all of obj.addOptions) { const temp = checkChars(all); // yargs appends a trailing empty line to some sections; skip it to avoid ghost table rows