/** * @license * Copyright 2019 Balena Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as path from 'path'; import { MarkdownFileParser } from './utils'; import { GlobSync } from 'glob'; /** * This is the skeleton of CLI documentation/reference web page at: * https://www.balena.io/docs/reference/cli/ * * The `getCapitanoDoc` function in this module parses README.md and adds * some content to this object. * * IMPORTANT * * All commands need to be stored under a folder in lib/commands to maintain uniformity * Generating docs will error out if directive not followed * To add a custom heading for command docs, add the heading next to the folder name * in the `commandHeadings` dictionary. * * This dictionary is the source of truth that creates the docs config which is used * to generate the CLI documentation. By default, the folder name will be used. * * Resources with plural names needs to have 2 sections if they have commands like: * "fleet, fleets" or "device, devices" or "tag, tags" * */ interface Category { title: string; files: string[]; } interface Documentation { title: string; introduction: string; categories: Category[]; } // Mapping folders names to custom headings in the docs const commandHeadings: { [key: string]: string } = { 'api-key': 'API Key', 'api-keys': 'API Keys', auth: 'Authentication', env: 'Environment Variable', envs: 'Environment Variables', help: 'Help and Version', key: 'SSH Key', keys: 'SSH Keys', orgs: 'Organizations', os: 'OS', util: 'Utilities', ssh: 'Network', scan: 'Network', tunnel: 'Network', build: 'Deploy', join: 'Platform', leave: 'Platform', }; // Fetch all available commands const allCommandsPaths = new GlobSync('build/commands/**/*.js', { ignore: 'build/commands/internal/**', }).found; // Throw error if any commands found outside of command directories const illegalCommandPaths = allCommandsPaths.filter((commandPath: string) => /^build\/commands\/[^/]+\.js$/.test(commandPath), ); if (illegalCommandPaths.length !== 0) { throw new Error( `Found the following commands without a command directory: ${illegalCommandPaths}\n To resolve this error, move the respective commands to their resource directories or create new ones.\n Refer to the automation/capitanodoc/capitanodoc.ts file for more information.`, ); } // Docs config template const capitanoDoc: Documentation = { title: 'balena CLI Documentation', introduction: '', categories: [], }; // Helper function to capitalize each word of directory name function formatTitle(dir: string): string { return dir.replace(/(^\w|\s\w)/g, (word) => word.toUpperCase()); } // Create a map to track the categories for faster lookup const categoriesMap: { [key: string]: Category } = {}; for (const commandPath of allCommandsPaths) { const commandDir = path.basename(path.dirname(commandPath)); const heading = commandHeadings[commandDir] || formatTitle(commandDir); if (!categoriesMap[heading]) { categoriesMap[heading] = { title: heading, files: [] }; capitanoDoc.categories.push(categoriesMap[heading]); } categoriesMap[heading].files.push(commandPath); } // Sort Category titles alphabetically capitanoDoc.categories = capitanoDoc.categories.sort((a, b) => a.title.localeCompare(b.title), ); // Sort Category file paths alphabetically capitanoDoc.categories.forEach((category) => { category.files.sort((a, b) => a.localeCompare(b)); }); /** * Modify and return the `capitanoDoc` object above in order to generate the * CLI documentation at docs/balena-cli.md * * This function parses the README.md file to extract relevant sections * for the documentation web page. */ export async function getCapitanoDoc(): Promise { const readmePath = path.join(__dirname, '..', '..', 'README.md'); const mdParser = new MarkdownFileParser(readmePath); const sections: string[] = await Promise.all([ mdParser.getSectionOfTitle('About').then((sectionLines: string) => { // delete the title of the 'About' section for the web page const match = /^(#+)\s+.+?\n\s*([^]*)/.exec(sectionLines); if (!match || match.length < 3) { throw new Error(`Error parsing section title`); } // match[1] has the title, match[2] has the rest return match && match[2]; }), mdParser.getSectionOfTitle('Installation'), mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'), mdParser.getSectionOfTitle('Logging in'), mdParser.getSectionOfTitle('Proxy support'), mdParser.getSectionOfTitle('Support, FAQ and troubleshooting'), mdParser.getSectionOfTitle('Deprecation policy'), ]); capitanoDoc.introduction = sections.join('\n'); return capitanoDoc; }