diff --git a/.gitignore b/.gitignore index 7aa1a2e9..8f146b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,3 @@ /package-lock.json /resinrc.yml /tmp/ -/completion/bash/ -/completion/zsh/ diff --git a/completion/bash/balena_comp b/completion/bash/balena_comp new file mode 100644 index 00000000..a124dd34 --- /dev/null +++ b/completion/bash/balena_comp @@ -0,0 +1,74 @@ +#!/bin/bash + +_balena_complete() +{ + local cur prev + + # Valid top-level completions + main_commands="apps build deploy envs join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env internal key key local os tag util" + # Sub-completions + api_key_cmds="generate" + app_cmds="create purge rename restart rm" + config_cmds="generate inject read reconfigure write" + device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown" + devices_cmds="supported" + env_cmds="add rename rm" + internal_cmds="osinit" + key_cmds="add rm" + local_cmds="configure flash" + os_cmds="build-config configure download initialize versions" + tag_cmds="rm set" + + + + COMPREPLY=() + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + if [ $COMP_CWORD -eq 1 ] + then + COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) ) + elif [ $COMP_CWORD -eq 2 ] + then + case "$prev" in + api-key) + COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) ) + ;; + app) + COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) ) + ;; + config) + COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) ) + ;; + device) + COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) ) + ;; + devices) + COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) ) + ;; + env) + COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) ) + ;; + internal) + COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) ) + ;; + key) + COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) ) + ;; + local) + COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) ) + ;; + os) + COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) ) + ;; + tag) + COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) ) + ;; + + "*") + ;; + esac + fi + +} +complete -F _balena_complete balena diff --git a/completion/generate-completion.js b/completion/generate-completion.js index 0123c7fb..31cf8d93 100644 --- a/completion/generate-completion.js +++ b/completion/generate-completion.js @@ -1,95 +1,147 @@ +/** + * @license + * Copyright 2021 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. + */ + const path = require('path'); const rootDir = path.join(__dirname, '..'); const fs = require('fs'); +const manifestFile = 'oclif.manifest.json'; -commandsFilePath = path.join(rootDir, 'oclif.manifest.json') +commandsFilePath = path.join(rootDir, manifestFile); if (fs.existsSync(commandsFilePath)) { - console.log('Found file'); + console.log('Generating shell auto completion files...'); } else { - console.error('Not Found file'); - return; + console.error(`generate-completion.js: Could not find "${manifestFile}"`); + process.exitCode = 1; + return; } -const commands_json = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8')); +const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8')); var mainCommands = []; var additionalCommands = []; -for (const key in commands_json.commands) { - const cmd = key.split(":"); - if (cmd.length > 1) { - additionalCommands.push(cmd); - if (!mainCommands.includes(cmd[0])) { - mainCommands.push(cmd[0]); - } - } else { - mainCommands.push(cmd[0]); - } +for (const key of Object.keys(commandsJson.commands)) { + const cmd = key.split(':'); + if (cmd.length > 1) { + additionalCommands.push(cmd); + if (!mainCommands.includes(cmd[0])) { + mainCommands.push(cmd[0]); + } + } else { + mainCommands.push(cmd[0]); + } } -const mainCommandsStr = mainCommands.join(" "); +const mainCommandsStr = mainCommands.join(' '); // GENERATE BASH COMPLETION FILE -bashFilePathIn = path.join(__dirname, "balena_bash_template"); -bashFilePathOut = path.join(__dirname, "bash/balena_comp"); -fs.readFile(bashFilePathIn, 'utf8', function (err,data) { - if (err) { - return console.error(err); - } - data = data.replace(/\$main_commands\$/g, 'main_commands=\"' + mainCommandsStr + '\"'); - var subCommands = []; - var prevElement = additionalCommands[0][0]; - additionalCommands.forEach(function(element) { - if (element[0] == prevElement) { - subCommands.push(element[1]); - } else { - const prevElement2 = prevElement.replace(/-/g, "_") + "_cmds"; - data = data.replace(/\$sub_cmds\$/g, " " + prevElement2 + '=\"' + subCommands.join(" ") + '\"\n\$sub_cmds\$'); - data = data.replace(/\$sub_cmds_prev\$/g, " " + prevElement + ")\n COMPREPLY=( $(compgen -W \"$" + prevElement2 + "\" -- $cur) )\n ;;\n\$sub_cmds_prev\$"); - prevElement = element[0]; - subCommands = []; - subCommands.push(element[1]); - } - }); - // cleanup placeholders - data = data.replace(/\$sub_cmds\$/g, ""); - data = data.replace(/\$sub_cmds_prev\$/g, ""); +bashFilePathIn = path.join(__dirname, 'balena_bash_template'); +bashFilePathOut = path.join(__dirname, 'bash/balena_comp'); +fs.readFile(bashFilePathIn, 'utf8', function (err, data) { + if (err) { + process.exitCode = 1; + return console.error(err); + } + data = data.replace( + /\$main_commands\$/g, + 'main_commands="' + mainCommandsStr + '"', + ); + var subCommands = []; + var prevElement = additionalCommands[0][0]; + additionalCommands.forEach(function (element) { + if (element[0] === prevElement) { + subCommands.push(element[1]); + } else { + const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds'; + data = data.replace( + /\$sub_cmds\$/g, + ' ' + prevElement2 + '="' + subCommands.join(' ') + '"\n$sub_cmds$', + ); + data = data.replace( + /\$sub_cmds_prev\$/g, + ' ' + + prevElement + + ')\n COMPREPLY=( $(compgen -W "$' + + prevElement2 + + '" -- $cur) )\n ;;\n$sub_cmds_prev$', + ); + prevElement = element[0]; + subCommands = []; + subCommands.push(element[1]); + } + }); + // cleanup placeholders + data = data.replace(/\$sub_cmds\$/g, ''); + data = data.replace(/\$sub_cmds_prev\$/g, ''); - fs.writeFile(bashFilePathOut, data, 'utf8', function (err) { - if (err) { - return console.error(err); - } - }); + fs.writeFile(bashFilePathOut, data, 'utf8', function (error) { + if (error) { + process.exitCode = 1; + return console.error(error); + } + }); }); // GENERATE ZSH COMPLETION FILE -zshFilePathIn = path.join(__dirname, "balena_zsh_template"); -zshFilePathOut = path.join(__dirname, "zsh/_balena"); -fs.readFile(zshFilePathIn, 'utf8', function (err,data) { - if (err) { - return console.error(err); - } - data = data.replace(/\$main_commands\$/g, 'main_commands=( ' + mainCommandsStr + ' )'); - var subCommands = []; - var prevElement = additionalCommands[0][0]; - additionalCommands.forEach(function(element) { - if (element[0] == prevElement) { - subCommands.push(element[1]); - } else { - const prevElement2 = prevElement.replace(/-/g, "_") + "_cmds"; - data = data.replace(/\$sub_cmds\$/g, " " + prevElement2 + '=( ' + subCommands.join(" ") + ' )\n\$sub_cmds\$'); - data = data.replace(/\$sub_cmds_prev\$/g, " \"" + prevElement + "\")\n _describe -t " + prevElement2 + " \'" + prevElement + '_cmd\' ' + prevElement2 + " \"$@\" && ret=0\n ;;\n\$sub_cmds_prev\$"); - prevElement = element[0]; - subCommands = []; - subCommands.push(element[1]); - } - }); - // cleanup placeholders - data = data.replace(/\$sub_cmds\$/g, ""); - data = data.replace(/\$sub_cmds_prev\$/g, ""); +zshFilePathIn = path.join(__dirname, 'balena_zsh_template'); +zshFilePathOut = path.join(__dirname, 'zsh/_balena'); +fs.readFile(zshFilePathIn, 'utf8', function (err, data) { + if (err) { + process.exitCode = 1; + return console.error(err); + } + data = data.replace( + /\$main_commands\$/g, + 'main_commands=( ' + mainCommandsStr + ' )', + ); + var subCommands = []; + var prevElement = additionalCommands[0][0]; + additionalCommands.forEach(function (element) { + if (element[0] === prevElement) { + subCommands.push(element[1]); + } else { + const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds'; + data = data.replace( + /\$sub_cmds\$/g, + ' ' + prevElement2 + '=( ' + subCommands.join(' ') + ' )\n$sub_cmds$', + ); + data = data.replace( + /\$sub_cmds_prev\$/g, + ' "' + + prevElement + + '")\n _describe -t ' + + prevElement2 + + " '" + + prevElement + + "_cmd' " + + prevElement2 + + ' "$@" && ret=0\n ;;\n$sub_cmds_prev$', + ); + prevElement = element[0]; + subCommands = []; + subCommands.push(element[1]); + } + }); + // cleanup placeholders + data = data.replace(/\$sub_cmds\$/g, ''); + data = data.replace(/\$sub_cmds_prev\$/g, ''); - fs.writeFile(zshFilePathOut, data, 'utf8', function (err) { - if (err) { - return console.error(err); - } - }); + fs.writeFile(zshFilePathOut, data, 'utf8', function (error) { + if (error) { + process.exitCode = 1; + return console.error(error); + } + }); }); - diff --git a/completion/zsh/_balena b/completion/zsh/_balena new file mode 100644 index 00000000..1d395eae --- /dev/null +++ b/completion/zsh/_balena @@ -0,0 +1,77 @@ +#compdef balena +#autoload + +_balena() { + typeset -A opt_args + local context state line curcontext="$curcontext" + + # Valid top-level completions + main_commands=( apps build deploy envs join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env internal key key local os tag util ) + # Sub-completions + api_key_cmds=( generate ) + app_cmds=( create purge rename restart rm ) + config_cmds=( generate inject read reconfigure write ) + device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown ) + devices_cmds=( supported ) + env_cmds=( add rename rm ) + internal_cmds=( osinit ) + key_cmds=( add rm ) + local_cmds=( configure flash ) + os_cmds=( build-config configure download initialize versions ) + tag_cmds=( rm set ) + + + _arguments -C \ + '(- 1 *)--version[show version and exit]' \ + '(- 1 *)'{-h,--help}'[show help options and exit]' \ + '1:first command:_balena_main_cmds' \ + '2:second command:_balena_sec_cmds' \ + && ret=0 +} + +(( $+functions[_balena_main_cmds] )) || +_balena_main_cmds() { + _describe -t main_commands 'command' main_commands "$@" && ret=0 +} + +(( $+functions[_balena_sec_cmds] )) || +_balena_sec_cmds() { + case $line[1] in + "api-key") + _describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0 + ;; + "app") + _describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0 + ;; + "config") + _describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0 + ;; + "device") + _describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0 + ;; + "devices") + _describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0 + ;; + "env") + _describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0 + ;; + "internal") + _describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0 + ;; + "key") + _describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0 + ;; + "local") + _describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0 + ;; + "os") + _describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0 + ;; + "tag") + _describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0 + ;; + + esac +} + +_balena "$@" diff --git a/package.json b/package.json index b8f3bd18..a91d648c 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,11 @@ "postinstall": "node patches/apply-patches.js", "prebuild": "rimraf build/ build-bin/", "build": "npm run build:src && npm run catch-uncommitted", - "build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc", + "build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion", "build:fast": "gulp pages && tsc && npx oclif-dev manifest", "build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit", "build:doc": "mkdirp doc/ && ts-node --transpile-only automation/capitanodoc/index.ts > doc/cli.markdown", + "build:completion": "node completion/generate-completion.js", "build:standalone": "ts-node --transpile-only automation/run.ts build:standalone", "build:installer": "ts-node --transpile-only automation/run.ts build:installer", "package": "npm run build:fast && npm run build:standalone && npm run build:installer", @@ -67,7 +68,7 @@ "catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted", "ci": "npm run test && npm run catch-uncommitted", "watch": "gulp watch", - "lint": "balena-lint -e ts -e js --typescript --fix automation/ lib/ typings/ tests/ bin/balena bin/balena-dev gulpfile.js .mocharc.js .mocharc-standalone.js", + "lint": "balena-lint -e ts -e js --typescript --fix automation/ completion/ lib/ typings/ tests/ bin/balena bin/balena-dev gulpfile.js .mocharc.js .mocharc-standalone.js", "update": "ts-node --transpile-only ./automation/update-module.ts", "prepare": "echo {} > bin/.fast-boot.json", "prepublishOnly": "npm run build"