mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-21 14:37:47 +00:00
72a924f00e
Change-type: patch
241 lines
6.3 KiB
TypeScript
241 lines
6.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2017-2020 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 { Help } from '@oclif/core';
|
|
import * as indent from 'indent-string';
|
|
import { getChalk } from './utils/lazy';
|
|
|
|
// Partially overrides standard implementation of help plugin
|
|
// https://github.com/oclif/plugin-help/blob/master/src/index.ts
|
|
|
|
function getHelpSubject(args: string[]): string | undefined {
|
|
for (const arg of args) {
|
|
if (arg === '--') {
|
|
return;
|
|
}
|
|
if (arg === 'help' || arg === '--help' || arg === '-h') {
|
|
continue;
|
|
}
|
|
if (arg.startsWith('-')) {
|
|
return;
|
|
}
|
|
return arg;
|
|
}
|
|
}
|
|
|
|
export default class BalenaHelp extends Help {
|
|
public static usage: 'help [command]';
|
|
|
|
public async showHelp(argv: string[]) {
|
|
const chalk = getChalk();
|
|
const subject = getHelpSubject(argv);
|
|
if (!subject) {
|
|
const verbose = argv.includes('-v') || argv.includes('--verbose');
|
|
console.log(this.getCustomRootHelp(verbose));
|
|
return;
|
|
}
|
|
|
|
const command = this.config.findCommand(subject);
|
|
if (command) {
|
|
await this.showCommandHelp(command);
|
|
return;
|
|
}
|
|
|
|
// If they've typed a topic (e.g. `balena os`) that isn't also a command (e.g. `balena device`)
|
|
// then list the associated commands.
|
|
const topicCommands = await Promise.all(
|
|
this.config.commands
|
|
.filter((c) => {
|
|
return c.id.startsWith(`${subject}:`);
|
|
})
|
|
.map((topic) => topic.load()),
|
|
);
|
|
|
|
if (topicCommands.length > 0) {
|
|
console.log(`${chalk.yellow(subject)} commands include:`);
|
|
console.log(this.formatCommands(topicCommands));
|
|
console.log(
|
|
`\nRun ${chalk.cyan.bold(
|
|
'balena help -v',
|
|
)} for a list of all available commands,`,
|
|
);
|
|
console.log(
|
|
` or ${chalk.cyan.bold(
|
|
'balena help <command>',
|
|
)} for detailed help on a specific command.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log(`command ${chalk.cyan.bold(subject)} not found`);
|
|
}
|
|
|
|
getCustomRootHelp(showAllCommands: boolean): string {
|
|
const { bold, cyan } = getChalk();
|
|
|
|
let commands = this.config.commands;
|
|
commands = commands.filter((c) => this.opts.all || !c.hidden);
|
|
|
|
// Get Primary Commands, sorted as in manual list
|
|
const primaryCommands = this.manuallySortedPrimaryCommands
|
|
.map((pc) => {
|
|
return commands.find((c) => c.id === pc.replace(' ', ':'));
|
|
})
|
|
.filter((c): c is (typeof commands)[0] => !!c);
|
|
|
|
let usageLength = 0;
|
|
for (const cmd of primaryCommands) {
|
|
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
|
|
}
|
|
|
|
let additionalCmdSection: string[];
|
|
if (showAllCommands) {
|
|
// Get the rest as Additional Commands
|
|
const additionalCommands = commands.filter(
|
|
(c) =>
|
|
!this.manuallySortedPrimaryCommands.includes(c.id.replace(':', ' ')),
|
|
);
|
|
|
|
// Find longest usage, and pad usage of first command in each category
|
|
// This is to ensure that both categories align visually
|
|
for (const cmd of additionalCommands) {
|
|
usageLength = Math.max(usageLength, cmd.usage?.length || 0);
|
|
}
|
|
|
|
if (
|
|
typeof primaryCommands[0].usage === 'string' &&
|
|
typeof additionalCommands[0].usage === 'string'
|
|
) {
|
|
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
|
|
additionalCommands[0].usage =
|
|
additionalCommands[0].usage.padEnd(usageLength);
|
|
}
|
|
|
|
additionalCmdSection = [
|
|
bold('\nADDITIONAL COMMANDS'),
|
|
this.formatCommands(additionalCommands),
|
|
];
|
|
} else {
|
|
const cmd = cyan.bold('balena help --verbose');
|
|
additionalCmdSection = [
|
|
`\n${bold('...MORE')} run ${cmd} to list additional commands.`,
|
|
];
|
|
}
|
|
|
|
const globalOps = [
|
|
['--help, -h', 'display command help'],
|
|
['--debug', 'enable debug output'],
|
|
[
|
|
'--unsupported',
|
|
`\
|
|
prevent exit with an error as per Deprecation Policy
|
|
See: https://git.io/JRHUW#deprecation-policy`,
|
|
],
|
|
];
|
|
globalOps[0][0] = globalOps[0][0].padEnd(usageLength);
|
|
|
|
const { deprecationPolicyNote, reachingOut } =
|
|
require('./utils/messages') as typeof import('./utils/messages');
|
|
|
|
return [
|
|
bold('USAGE'),
|
|
'$ balena [COMMAND] [OPTIONS]',
|
|
bold('\nPRIMARY COMMANDS'),
|
|
this.formatCommands(primaryCommands),
|
|
...additionalCmdSection,
|
|
bold('\nGLOBAL OPTIONS'),
|
|
this.formatGlobalOpts(globalOps),
|
|
bold('\nDeprecation Policy Reminder'),
|
|
deprecationPolicyNote,
|
|
reachingOut,
|
|
].join('\n');
|
|
}
|
|
|
|
protected formatGlobalOpts(opts: string[][]) {
|
|
const { dim } = getChalk();
|
|
const outLines: string[] = [];
|
|
let flagWidth = 0;
|
|
for (const opt of opts) {
|
|
flagWidth = Math.max(flagWidth, opt[0].length);
|
|
}
|
|
for (const opt of opts) {
|
|
const descriptionLines = opt[1].split('\n');
|
|
outLines.push(
|
|
` ${opt[0].padEnd(flagWidth + 2)}${dim(descriptionLines[0])}`,
|
|
);
|
|
outLines.push(
|
|
...descriptionLines
|
|
.slice(1)
|
|
.map((line) => ` ${' '.repeat(flagWidth + 2)}${dim(line)}`),
|
|
);
|
|
}
|
|
return outLines.join('\n');
|
|
}
|
|
|
|
protected formatCommands(commands: any[]): string {
|
|
if (commands.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
const body = this.renderList(
|
|
commands
|
|
.filter((c) => c.usage != null && c.usage !== '')
|
|
.map((c) => [c.usage, this.formatDescription(c.description)]),
|
|
{
|
|
spacer: '\n',
|
|
stripAnsi: this.opts.stripAnsi,
|
|
indentation: 2,
|
|
multiline: false,
|
|
},
|
|
);
|
|
|
|
return indent(body, 2);
|
|
}
|
|
|
|
protected formatDescription(desc: string = '') {
|
|
const chalk = getChalk();
|
|
|
|
desc = desc.split('\n')[0];
|
|
// Remove any ending .
|
|
if (desc[desc.length - 1] === '.') {
|
|
desc = desc.substring(0, desc.length - 1);
|
|
}
|
|
// Lowercase first letter if second char is lowercase, to preserve e.g. 'SSH ...')
|
|
if (desc[1] === desc[1]?.toLowerCase()) {
|
|
desc = `${desc[0].toLowerCase()}${desc.substring(1)}`;
|
|
}
|
|
return chalk.grey(desc);
|
|
}
|
|
|
|
readonly manuallySortedPrimaryCommands = [
|
|
'login',
|
|
'push',
|
|
'logs',
|
|
'ssh',
|
|
'fleets',
|
|
'fleet',
|
|
'devices',
|
|
'device',
|
|
'tunnel',
|
|
'preload',
|
|
'build',
|
|
'deploy',
|
|
'join',
|
|
'leave',
|
|
'scan',
|
|
];
|
|
}
|