@kui-shell/core
Version:
An Electron-based shell for cloud-native development
612 lines (608 loc) • 20.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ImplForPlugins = void 0;
exports.proxy = proxy;
exports.setPluginResolver = exports.read = void 0;
var _debug = _interopRequireDefault(require("debug"));
var _command = require("../models/command");
var _events = _interopRequireDefault(require("./events"));
var _usageError = require("./usage-error");
var _oops = require("./oops");
var _plugins = require("../plugins/plugins");
var _tree = require("../commands/tree");
var _typeahead = require("../commands/typeahead");
var _context = require("../commands/context");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*
* Copyright 2017 The Kubernetes Authors
*
* 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.
*/
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const debug = (0, _debug.default)('core/command-tree');
debug('loading');
debug('finished loading modules');
/**
* Plugin registry
*
*/
let resolver;
const setPluginResolver = _ => {
resolver = _;
};
/**
* Is route (/a/b/c) exactly the same as path (['a', 'b', 'c'])
*
* @param route is a registered command handler
* @param path is what the user typed
*
*/
exports.setPluginResolver = setPluginResolver;
const exactlyTheSameRoute = (route, path) => {
const routeAsPath = route.split('/').slice(1);
for (let idx = 0; idx < routeAsPath.length; idx++) {
if (routeAsPath[idx] !== path[idx]) {
return false;
}
}
// if we got to this point, then route is a prefix of path
if (routeAsPath.length !== path.length) {
return false;
} else {
return true;
}
};
/**
* Navigate the given tree `model`, following the given `path` as [n1,n2,n3]
*
*/
const treeMatch = (model, path, readonly = false, hide = false, idxStart = 0, noWildcard = false) => {
let parent = model;
let cur;
for (let idx = idxStart; idx < path.length; idx++) {
cur = parent.children && parent.children[path[idx]];
if (!cur) {
// then we've reached the bottom of the tree
if (readonly) {
// we were instructed to give up
break;
} else {
// otherwise, we were instructed to create a new leaf node
if (!parent.children) {
parent.children = {};
}
cur = parent.children[path[idx]] = {
$: undefined,
parent,
key: path[idx],
options: {
hide
},
route: `${parent.route === '/' ? '' : parent.route}/${path[idx]}`
};
}
} else {
// console.log('found', path[idx])
}
parent = cur;
cur = cur.children && cur.children[path[idx]];
// console.log('match', idx, path[idx], cur)
}
if (!cur && !noWildcard) {
// prefix match, e.g. "cleanAll !!!" should match a /cleanAll listener, as we have an implicit suffix wildcard
// console.log('end of the line', parent)
cur = parent;
}
if (cur.options && cur.options.noArgs && !exactlyTheSameRoute(cur.route, path)) {
// if cur represents a command registration that has asserted
// it takes no extra arguments, we can fast-path this as a
// non-match, if cur's route doesn't contain the requested
// command path
} else {
return cur;
}
};
const match = (path, readonly) => {
return treeMatch((0, _tree.getModelInternal)().root, path, readonly);
};
class DefaultCommandOptions {}
/**
* Register a command handler on the given route
*
*/
const _listen = (model, route, handler, options = new DefaultCommandOptions()) => {
if (resolver && resolver.isOverridden(route)) {
// then the overridden function was prematurely loaded; force a
// load of the overriding plugin
resolver.resolve(route, {
tryCatchalls: false
});
}
const path = route.split('/').splice(1);
const leaf = treeMatch((0, _tree.getModelInternal)().root, path, false, options.hide);
if (leaf) {
const prevOptions = leaf.options;
if (options) {
leaf.options = options;
}
if (leaf.$) {
// then we're overriding an existing command
if (!leaf.options) leaf.options = {};
if (prevOptions) {
Object.assign(leaf.options, prevOptions);
}
leaf.options.override = leaf.$;
}
leaf.$ = handler;
leaf.route = route;
// populate the typeahead trie
(0, _typeahead.registerTypeahead)(route);
return leaf;
}
};
const listen = (route, handler, options) => _listen((0, _tree.getModelInternal)().root, route, handler, options);
/**
* Register a subtree in the command tree
*
*/
const _subtree = (route, options) => {
const myListen = options.listen || listen;
const path = route.split('/').splice(1);
const leaf = match(path, false /*, options */);
if (leaf) {
leaf.route = route;
if (options) {
leaf.options = options;
} else {
leaf.options = {};
}
leaf.options.isDirectory = true;
const help = () => {
// the usage message
const usage = options.usage || options.synonymFor && options.synonymFor.options && options.synonymFor.options.usage;
/* if (options.synonymFor) {
usageMessage.synonymFor = options.synonymFor
} */
throw new _usageError.UsageError({
usage
});
};
//
// listen on /kubectl and /kubectl/help to present usage information
//
const opts = {
noArgs: true,
subtreeHandler: true,
noAuthOk: true,
requiresFullyQualifiedRoute: true
};
myListen(route, help, Object.assign({}, options, opts));
myListen(`${route}/help`, help, Object.assign({}, options, opts));
return leaf;
}
};
/**
* Register a synonym of a subtree
*
*/
const _subtreeSynonym = (route, master, options = master.options) => {
if (route !== master.route) {
// <-- don't alias to yourself!
const mySubtree = _subtree(route, Object.assign({}, options, {
synonymFor: master
}));
// reverse mapping from master to synonym
if (!master.synonyms) master.synonyms = {};
master.synonyms[mySubtree.route] = mySubtree;
}
};
/**
* Register a command handler on the given route, as a synonym of the given master handler
* master is the return value of `listen`
*
*/
const _synonym = (route, handler, master, options = master.options) => {
if (route !== master.route) {
// don't alias to yourself!
const node = listen(route, handler, Object.assign({}, options, {
synonymFor: master
}));
// reverse mapping from master to synonym
if (!master.synonyms) master.synonyms = {};
master.synonyms[node.route] = node;
}
};
/**
* Oops, we couldn't resolve the given command. But maybe we found
* some partial matches that might be helpful to the user.
*
*/
const commandNotFoundMessage = 'Command not found';
const commandNotFoundMessageWithPartialMatches = 'The following commands are partial matches for your request.';
/**
* Help the user with some partial matches for a command not found
* condition. Here, we reuse the usage-error formatter, to present the
* user with a list of possible completions to their (mistyped or
* otherwise) command.
*
* We use the `available` list to present the list of available
* command completions to what they typed.
*
*/
const formatPartialMatches = (partialMatches, hide) => {
return new _usageError.UsageError({
message: hide ? '' : commandNotFoundMessage,
usage: {
header: commandNotFoundMessageWithPartialMatches,
available: partialMatches.map(_ => _.usage)
}
}, {
noBreadcrumb: true,
noHide: true
});
};
const suggestPartialMatches = (command, partialMatches, noThrow = false, hide = false) => {
const availablePartials = partialMatches && partialMatches.filter(_ => _.usage);
const anyPartials = availablePartials && availablePartials.length > 0;
const error = anyPartials ? formatPartialMatches(availablePartials, hide) : new Error(`${commandNotFoundMessage}: ${command}`);
error.code = 404;
// to allow for programmatic use of the partial matches, e.g. for tab completion
if (anyPartials) {
error.partialMatches = availablePartials.map(_ => ({
command: _.route.split('/').slice(1).join(' '),
usage: _.usage
}));
} else {
error.hide = hide;
}
if (noThrow) {
return error;
} else {
throw error;
}
};
/**
*
* @return a command handler with success and failure event handlers
*
*/
const withEvents = (evaluator, leaf, partialMatches) => {
// let the world know we have resolved a command, and are about to evaluate it
const event = {
// context: currentContext()
// ANONYMIZE: namespace: namespace.current()
};
// if we have a command tree node, add some extra fields to the event
if (leaf) {
event.route = leaf.route; // e.g. "/git/status" from the bash-like plugin
event.plugin = leaf.options.plugin || 'builtin'; // e.g. "bash-like"
}
const handler = {
subtree: leaf,
route: leaf.route,
eval: evaluator,
options: leaf && leaf.options,
success: ({
tab,
type,
command,
isDrilldown = false,
parsedOptions
}) => {
event.tab = tab;
event.execType = type;
event.command = command;
event.isDrilldown = isDrilldown;
// any command line options that the command has blessed to pass through to the event bus
if (parsedOptions && leaf.options && leaf.options.okOptions) {
const opts = leaf.options.okOptions.filter(_ => parsedOptions[_]);
if (opts) {
event.options = opts;
}
}
/* if (leaf && eventChannelUnsafe) {
eventChannelUnsafe.emit('/command/complete', event)
if (event.execType !== ExecType.Nested) {
eventChannelUnsafe.emit('/command/complete/fromuser', tab)
}
} */
},
error: (command, tab, execType, err) => {
event.tab = tab;
event.execType = execType;
event.command = command;
event.error = (0, _oops.oopsMessage)(err);
if (leaf && _events.default) {
_events.default.emit('/command/complete', event);
if (event.execType !== _command.ExecType.Nested) {
_events.default.emit('/command/complete/fromuser', tab);
}
}
if (err && err.code === 127 && partialMatches) {
// command not found
const suggestions = suggestPartialMatches(command, partialMatches, true, err.hide); // true: don't throw an exception
return suggestions;
}
return err;
}
};
return handler;
};
/**
* Parse the given argv, and return an evaluator or throw an Error
*
*/
const _read = (model, argv, contextRetry, originalArgv, tryCatchalls) => __awaiter(void 0, void 0, void 0, function* () {
let leaf = treeMatch((0, _tree.getModelInternal)().root, argv, true); // true means read-only, don't modify the context model please
let evaluator = leaf && leaf.$;
if (!evaluator) {
//
// maybe the plugin that supports this route hasn't been
// loaded, yet; so: invoke the plugin resolver and retry
//
const route = `/${argv.join('/')}`;
yield resolver.resolve(route, {
tryCatchalls: tryCatchalls && (!contextRetry || contextRetry.length === 0)
});
leaf = treeMatch((0, _tree.getModelInternal)().root, argv, true); // true means read-only, don't modify the context model please
evaluator = leaf && leaf.$;
}
if (!evaluator) {
if (!contextRetry) {
return false;
} else if (contextRetry.length === 0) {
return _read((0, _tree.getModelInternal)().root, originalArgv, undefined, originalArgv, tryCatchalls);
} else if (contextRetry.length > 0 && contextRetry[contextRetry.length - 1] !== originalArgv[originalArgv.length - 1]) {
// command not found so far, look further afield.
const maybeInContextRetry = _read((0, _tree.getModelInternal)().root, contextRetry.concat(originalArgv), contextRetry.slice(0, contextRetry.length - 1), originalArgv, tryCatchalls);
if (maybeInContextRetry) {
return maybeInContextRetry;
}
// oof, fallback plan: look in the default context
const newContext = (0, _context.getCurrentContext)().concat(originalArgv).filter((elt, idx, A) => elt !== A[idx - 1]);
const maybeInDefaultContext = _read((0, _tree.getModelInternal)().root, newContext, contextRetry.slice(0, contextRetry.length - 1), originalArgv, tryCatchalls);
return maybeInDefaultContext;
} else {
// if we get here, we can't find a matching command
return false;
}
} else {
return withEvents(evaluator, leaf);
}
});
/** read, with retries based on the current context */
const internalRead = (model, argv, tryCatchalls) => {
if (argv[0] === 'kui') argv.shift();
return _read((0, _tree.getModelInternal)().root, argv, _context.Context.current, argv, tryCatchalls);
};
/**
* We could not find a registered command handler for the given `argv`.
*
*/
const commandNotFound = (argv, partialMatches, execOptions, tryCatchalls = true) => {
// first, see if we have any catchall handlers; offer the argv, and
// choose the highest priority handler that accepts the argv
if (!execOptions || !execOptions.failWithUsage) {
const catchallHandler = tryCatchalls && (0, _tree.getModelInternal)().catchalls.filter(({
offer
}) => offer(argv)).sort(({
prio: prio1
}, {
prio: prio2
}) => prio2 - prio1)[0];
if (catchallHandler) {
return withEvents(catchallHandler.eval, catchallHandler, partialMatches);
}
// otherwise, give up trying to find a registered command handler,
// and look for partial matches
_events.default.emit('/command/resolved', {
// ANONYMIZE: namespace: namespace.current(),
error: `${commandNotFoundMessage}: ${argv.join(' ')}`,
command: argv[0]
});
}
return suggestPartialMatches(argv.join(' '), partialMatches);
};
/**
* Find partial matches at head of the given subtree. We hope that the
* last part of the argv (represented here in `partial`) is a partial
* match for some command at this root.
*
* @return all such prefix matches
*
*/
const findPartialMatchesAt = (usage, partial) => {
const matches = [];
function maybeAdd(match) {
if (match.route.indexOf(partial) >= 0 && (!match.usage || !match.usage.synonymFor && !match.usage.hide)) {
matches.push(match);
}
}
for (const route in usage) {
maybeAdd(usage[route]);
for (const cmd in usage[route].children) {
maybeAdd(usage[route].children[cmd]);
}
}
return matches;
};
/**
* Look up a command handler for the given `argv`. This is the main
* Read part of a REPL.
*
*/
const read = (root, argv, noRetry = false, execOptions, tryCatchalls = true) => __awaiter(void 0, void 0, void 0, function* () {
let cmd;
if (!noRetry) {
yield resolver.resolve(`/${argv.join('/')}`, {
tryCatchalls: false
});
cmd = yield internalRead(root, argv, tryCatchalls);
}
if (!cmd && /\.?\//.test(argv[0])) {
// Look for slashes instead of spaces. e.g. if the user types
// /a/b/c, and there is a registered command route /a/b/c,
// ... then we should resolve that command as a kui command
const {
cwd
} = yield Promise.resolve().then(() => require('../util/home'));
const exe = argv[0].replace(/^\.\//, cwd() + '/');
const argv2 = exe.split(/\//).slice(1);
yield resolver.resolve(exe, {
tryCatchalls: false
});
cmd = yield internalRead(root, argv2, tryCatchalls);
}
if (!cmd) {
// command not found, but maybe we can find partial matches
// that might be helpful?
let matches;
if (argv.length === 1) {
matches = yield findPartialMatchesAt((0, _plugins.prescanModel)().usage, argv[0]);
} else {
const allButLast = argv.slice(0, argv.length - 1);
const last = argv[argv.length - 1];
const parent = yield internalRead(root, allButLast, tryCatchalls);
if (parent) {
matches = yield findPartialMatchesAt((0, _plugins.prescanModel)().usage, last);
}
}
// found some partial matches?
if (matches && matches.length > 0) {
// debug('found partial matches', matches)
} else {
matches = undefined;
}
return commandNotFound(argv, matches, execOptions, tryCatchalls);
} else {
return cmd;
}
});
/**
* The model we present to plugins, a `CommandRegistrar`
*
*/
exports.read = read;
class ImplForPlugins {
// eslint-disable-next-line no-useless-constructor
constructor(plugin) {
this.plugin = plugin;
}
catchall(offer, handler, prio = 0, options = new DefaultCommandOptions()) {
return (0, _tree.getModelInternal)().catchalls.push({
route: '*',
offer,
eval: handler,
prio,
plugin: this.plugin,
options,
$: undefined,
key: undefined,
parent: undefined
});
}
listen(route, handler, options) {
return listen(route, handler, Object.assign({}, options, {
plugin: this.plugin
}));
}
synonym(route, handler, master, options) {
return _synonym(route, handler, master, options && Object.assign({}, options, {
plugin: this.plugin
}));
}
subtree(route, options) {
return _subtree(route, options);
}
subtreeSynonym(route, master, options = master.options) {
return _subtreeSynonym(route, master, options);
}
override(route, fromPlugin, overrideHandler, options) {
return __awaiter(this, void 0, void 0, function* () {
let currentHandler = (yield this.find(route, fromPlugin)).$;
if (!currentHandler) {
if (resolver) {
yield resolver.resolveOne(fromPlugin);
}
currentHandler = (yield this.find(route, fromPlugin)).$;
if (!currentHandler) {
throw new Error(`Cannot find desired command handler for ${route} from plugin ${fromPlugin}`);
}
}
const handler = args => overrideHandler(args, currentHandler);
return this.listen(route, handler, options);
});
}
find(route, fromPlugin, noOverride = true) {
return __awaiter(this, void 0, void 0, function* () {
const cmd = match(route.split('/').slice(1), true);
if (!cmd || cmd.route !== route || !noOverride && resolver && resolver.isOverridden(cmd.route)) {
if (resolver) {
if (fromPlugin) {
yield resolver.resolveOne(fromPlugin);
} else {
yield resolver.resolve(route, {
tryCatchalls: false
});
}
}
return match(route.split('/').slice(1), true);
} else {
return cmd;
}
});
}
}
/**
* Create a `CommandRegistrar` facade, for use by plugins in
* registering commands.
*
* This method is named "proxy" because it mostly delegates to the
* underlying implementation, with extra help in:
*
* - remembering from which plugin calls to listen emanate.
* - consolidating the tree model across separately-installed @kui-shell/core
*
*/
exports.ImplForPlugins = ImplForPlugins;
function proxy(plugin) {
return new ImplForPlugins(plugin);
}