diff --git a/.vscode/launch.json b/.vscode/launch.json index a024609c..46edf0c7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,18 @@ "" ] }, + { + "type": "node", + "request": "launch", + "name": "Launch docs generator", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\bin\\run", + "args": [ + "docs-generator" + ] + }, { "name": "Debug All Tests", "skipFiles": [ diff --git a/integration-tests/formating/formatting.test.ts b/integration-tests/formating/formatting.test.ts index 09128270..d683088c 100644 --- a/integration-tests/formating/formatting.test.ts +++ b/integration-tests/formating/formatting.test.ts @@ -11,7 +11,7 @@ describe('Command formatting tests', async () => { userPlugins: false, }); - allCommands = config.commands.filter(command => !command.id.startsWith("plugins") && !command.id.startsWith("help")) + allCommands = config.commands.filter(command => !command.id.startsWith("plugins") && !command.id.startsWith("help") && !command.hidden) .map((command) => ({ cmd: command, diff --git a/package.json b/package.json index f66f199d..08bdf279 100644 --- a/package.json +++ b/package.json @@ -71,15 +71,87 @@ }, "topicSeparator": " ", "topics": { - "hello": { - "description": "Say hello to the world and others" + "auth": { + "description": "Authenticate itp with Bentley." + }, + "itwin": { + "description": "Work with iTwins." + }, + "itwin repository": { + "description": "Work with iTwin repositories." + }, + "imodel": { + "description": "Work with iModels of an iTwin." + }, + "imodel view": { + "description": "Work with views for an iModel." + }, + "imodel changeset": { + "description": "Work with changesets of an iModel." + }, + "imodel namedversion": { + "description": "Work with named versions of an iModel." + }, + "imodel connection": { + "description": "Work with iModel connections." + }, + "imodel connection run": { + "description": "Work with iModel connection runs." + }, + "imodel connection sourcefile": { + "description": "Work with iModel connection source files." + }, + "access-control": { + "description": "Manage iTwin access control." + }, + "access-control permissions": { + "description": "Get permission information." + }, + "access-control role": { + "description": "Manage iTwin access control roles." + }, + "access-control group": { + "description": "Manage iTwin access control groups." + }, + "access-control member": { + "description": "Manage members that can access an iTwin." + }, + "access-control member owner": { + "description": "Manage access control for iTwin owners." + }, + "access-control member group": { + "description": "Manage groups that can access an iTwin." + }, + "access-control member user": { + "description": "Manage users that can access an iTwin." + }, + "storage": { + "description": "Interact with the storage repository of an iTwin." + }, + "storage folder": { + "description": "Interact with folders inside a storage repository." + }, + "storage file": { + "description": "Interact with files inside a storage repository." + }, + "user": { + "description": "Access user information." + }, + "changed-elements": { + "description": "Compare two changesets of an iModel." + }, + "workflows": { + "description": "Workflow reference for combined commands." + }, + "combined-commands": { + "description": "Workflow reference for combined commands." } }, "additionalHelpFlags": ["-h"] }, "repository": "iTwin/itwin-cli", "scripts": { - "clean": "shx rm -rf dist", + "clean": "shx rm -rf dist tsconfig.tsbuildinfo", "build": "npm run clean && tsc -b", "lint": "eslint . --ext .ts", "postpack": "shx rm -f oclif.manifest.json", diff --git a/pipelines/scripts/install-and-test-installer.ps1 b/pipelines/scripts/install-and-test-installer.ps1 index cdbfa65e..de8c282e 100644 --- a/pipelines/scripts/install-and-test-installer.ps1 +++ b/pipelines/scripts/install-and-test-installer.ps1 @@ -18,7 +18,7 @@ if ($version -like "itp/* win32-x64 node-v20.16.0") { } $help = & itp itwin --help -if ($help -like "Create an iTwin*") { +if ($help -like "Work with iTwins.*") { Write-Output "Help output check passed" } else { Write-Error "Unexpected help output: $help" diff --git a/pipelines/test_installers.yml b/pipelines/test_installers.yml index 9eb02289..ad061549 100644 --- a/pipelines/test_installers.yml +++ b/pipelines/test_installers.yml @@ -50,7 +50,7 @@ jobs: displayName: 'Check itp version' - script: | itwin_help=$(itp itwin --help) - if [[ $itwin_help == "Create an iTwin"* ]]; then + if [[ $itwin_help == "Work with iTwins."* ]]; then echo "Help output is correct: $itwin_help" else echo "Unexpected help output: $itwin_help" @@ -85,7 +85,7 @@ jobs: displayName: 'Check itp version' - script: | itwin_help=$(itp itwin --help) - if [[ $itwin_help == "Create an iTwin"* ]]; then + if [[ $itwin_help == "Work with iTwins."* ]]; then echo "Help output is correct: $itwin_help" else echo "Unexpected help output: $itwin_help" diff --git a/src/commands/access-control/group/create.ts b/src/commands/access-control/group/create.ts index 5f8a566a..52c9a475 100644 --- a/src/commands/access-control/group/create.ts +++ b/src/commands/access-control/group/create.ts @@ -5,9 +5,15 @@ import { Flags } from "@oclif/core"; +import { apiReference } from "../../../extensions/api-reference.js"; import BaseCommand from "../../../extensions/base-command.js"; export default class CreateAccessControlGroup extends BaseCommand { + static apiReference : apiReference = { + link: "https://developer.bentley.com/apis/access-control-v2/operations/create-itwin-group/", + name: "Create iTwin Group", + } + static description = 'Create a new group for an iTwin.'; static examples = [ diff --git a/src/commands/docs-generator.ts b/src/commands/docs-generator.ts new file mode 100644 index 00000000..ebdaf45a --- /dev/null +++ b/src/commands/docs-generator.ts @@ -0,0 +1,112 @@ +import { Command, Config, Flags } from "@oclif/core"; +import fs from "node:fs"; +import path from "node:path"; + +import BaseCommand from "../extensions/base-command.js"; + +export default class DocsGenerator extends BaseCommand { + static description = "Generate command and overview markdown files for the CLI."; + + static flags = { + "output-dir": Flags.string({ + char: "o", + description: "The output directory for the documentation files.", + + }), + } + + static hidden = true; + + generateCommandMarkdown(command: Command.Loadable, flags: [string, Command.Flag.Cached][]): string { + const options = flags.length > 0 + ? + flags.filter(([_, flag]) => !flag.hidden && flag.helpGroup !== "GLOBAL") + .sort(([_, flag]) => (flag.required ? -1 : 1)) + .map(([name, flag]) => { + const required = flag.required ? "**Required:** Yes" : "**Required:** No"; + const typeValue = flag.type === "option" ? flag.helpValue ?? "" : ""; + const type = `**Type:** \`${typeValue}\``; + const description = flag.description ?? ""; + const flagName = flag.char ? `-${flag.char}, --${name}` : `--${name}`; + return `- **\`${flagName}\`** \n ${description} \n ${type} ${required}`; + }) + .join("\n\n") + : ""; + + + + let examplesText = ""; + + if (command.examples){ + for (const example of command.examples) { + if (typeof example === "string") { + examplesText += `\n${example}`; + } + + if(typeof example === "object") { + examplesText += `\n# ${example.description}\n${example.command}\n`; + } + } + } + + const commandName = command.id.split(":").join(" "); + + examplesText = examplesText.replaceAll("<%= config.bin %>", "itp").replaceAll("<%= command.id %>", commandName).trimEnd(); + + const apiReference = command.apiReference as string; + const apiReferenceName = command.apiReferenceName as string; + + return `# itp ${commandName}\n\n${command.description || ""}\n\n## Options\n\n${options}\n\n## Examples\n\n\`\`\`bash${examplesText}\n\`\`\`\n\n## API Reference\n\n[${apiReferenceName}](${apiReference})`; + } + + async generateDocs(config: Config, basePath: string) { + const filteredCommands = config.commands.filter(c => !c.id.includes("help") && !c.id.includes("plugins") && !c.hidden); + if(!filteredCommands) { + return; + } + + for (const command of filteredCommands) { + const markdown: string = this.generateCommandMarkdown(command, Object.entries(command.flags)); + + const commandDepth = command.id.split(":"); + let filePath = basePath; + for (const depth of commandDepth) { + filePath = `${filePath}/${depth}`; + } + + this.writeToFile(filePath, markdown); + } + } + + async run() { + const {flags} = await this.parse(DocsGenerator); + + const config = await Config.load( + { + devPlugins: false, + root: process.cwd(), + userPlugins: false, + } + ); + + this.generateDocs(config, flags["output-dir"] ?? `${config.root}/docs`); + } + + writeToFile(filePath: string, markdown: string) { + const finalPath = `${filePath}.md`; + console.log(`Writing to directory: ${finalPath}`); + console.log(markdown); + + const dirName = path.dirname(finalPath); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { + recursive: true, + }); + } + + fs.writeFileSync(finalPath, markdown); + } +} + + + diff --git a/src/extensions/api-reference.ts b/src/extensions/api-reference.ts new file mode 100644 index 00000000..0276b8bb --- /dev/null +++ b/src/extensions/api-reference.ts @@ -0,0 +1,4 @@ +export type apiReference = { + link: string; + name: string; +}; \ No newline at end of file