@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
232 lines (231 loc) • 11.4 kB
JavaScript
import chalk from "chalk";
import inquirer from "inquirer";
import pkg from "fast-levenshtein";
import { InteractiveBaseCommand } from "./interactive-base-command.js";
import { runInquirerWithReadlineRestore } from "./utils/readline-helper.js";
import { WEB_CLI_RESTRICTED_COMMANDS, WEB_CLI_ANONYMOUS_RESTRICTED_COMMANDS, } from "./base-command.js";
import isWebCliMode from "./utils/web-mode.js";
const { get: levenshteinDistance } = pkg;
export class BaseTopicCommand extends InteractiveBaseCommand {
// Allow any arguments to enable did-you-mean functionality
static strict = false;
async run() {
// Check for --help flag first
if (this.argv.includes("--help") || this.argv.includes("-h")) {
// Show help for this topic command using CustomHelp
const { default: CustomHelp } = await import("./help.js");
const help = new CustomHelp(this.config);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await help.showCommandHelp(this.constructor);
return;
}
// Check for potential subcommands in raw argv
const rawArgv = this.argv.filter((arg) => !arg.startsWith("-")); // Filter out flags
if (rawArgv.length > 0) {
// User provided what might be a subcommand
const possibleSubcommand = rawArgv[0];
const fullCommandId = `${this.topicName}:${possibleSubcommand}`;
// Check if this exact command exists
const exactCommand = this.config.findCommand(fullCommandId);
if (exactCommand) {
// Exact command found - run it with remaining args and flags
const remainingArgs = [
...rawArgv.slice(1),
...this.argv.filter((arg) => arg.startsWith("-")),
];
// Special handling for help flags in interactive mode
if (process.env.ABLY_INTERACTIVE_MODE === "true" &&
(remainingArgs.includes("--help") || remainingArgs.includes("-h"))) {
const { default: CustomHelp } = await import("./help.js");
const help = new CustomHelp(this.config);
const cmd = this.config.findCommand(fullCommandId);
if (cmd) {
await help.showCommandHelp(cmd);
return;
}
}
return await this.config.runCommand(fullCommandId, remainingArgs);
}
else {
// Try to find the closest subcommand
const subcommands = await this.getTopicCommands();
const subcommandIds = subcommands.map((cmd) => `${this.topicName}:${cmd.id.split(" ").at(-1)}`);
let closestCommand = "";
let closestDistance = Infinity;
for (const cmdId of subcommandIds) {
const distance = levenshteinDistance(fullCommandId, cmdId, {
useCollator: true,
});
if (distance < closestDistance) {
closestDistance = distance;
closestCommand = cmdId;
}
}
// Check if we found a close match
const threshold = Math.max(1, Math.floor(possibleSubcommand.length / 2));
const maxDistance = 3;
if (closestCommand &&
closestDistance <= Math.min(threshold, maxDistance)) {
const isInteractiveMode = process.env.ABLY_INTERACTIVE_MODE === "true";
const displayOriginal = `${this.topicName} ${possibleSubcommand}`;
const displaySuggestion = closestCommand.replaceAll(":", " ");
// Warn about command not found
const warningMessage = `${chalk.cyan(displayOriginal)} is not an ably command.`;
// In interactive mode, we need to ensure the message is visible
// Write directly to stderr to avoid readline interference
if (isInteractiveMode) {
process.stderr.write(chalk.yellow(`Warning: ${warningMessage}\n`));
}
else {
this.warn(warningMessage);
}
// Handle confirmation
let confirmed = false;
const skipConfirmation = process.env.SKIP_CONFIRMATION === "true" ||
process.env.ABLY_CLI_NON_INTERACTIVE === "true";
if (skipConfirmation) {
confirmed = true;
}
else {
// In interactive mode, we need to handle readline carefully
const interactiveReadline = isInteractiveMode
? globalThis
.__ablyInteractiveReadline
: null;
const result = await runInquirerWithReadlineRestore(async () => inquirer.prompt([
{
name: "confirmed",
type: "confirm",
message: `Did you mean ${chalk.green(displaySuggestion)}?`,
default: true,
},
]), interactiveReadline);
confirmed = result.confirmed;
}
if (confirmed) {
// Run the suggested command with remaining args and original flags
const remainingArgs = [
...rawArgv.slice(1),
...this.argv.filter((arg) => arg.startsWith("-")),
];
try {
return await this.config.runCommand(closestCommand, remainingArgs);
}
catch (error) {
// Handle errors in interactive mode
if (isInteractiveMode) {
throw error;
}
else {
const err = error;
this.error(err.message || "Unknown error", {
exit: err.oclif?.exit || 1,
});
}
}
}
// If not confirmed, show available commands
// Fall through to the help display below
}
else {
// No close match found - show error first, then commands
const isInteractiveMode = process.env.ABLY_INTERACTIVE_MODE === "true";
const errorMessage = `Command ${this.topicName} ${possibleSubcommand} not found.`;
// Show the error
if (isInteractiveMode) {
console.error(chalk.red(errorMessage));
}
else {
this.warn(errorMessage);
}
// Fall through to show available commands
}
}
}
// No arguments provided or only unknown flags - show help for this topic
const commands = await this.getTopicCommands();
const isInteractiveMode = process.env.ABLY_INTERACTIVE_MODE === "true";
this.log(`Ably ${this.commandGroup} commands:`);
this.log("");
// If no commands found, show message and return
if (commands.length === 0) {
this.log(" No commands found.");
return;
}
const maxLength = Math.max(...commands.map((cmd) => cmd.id.length));
const prefix = isInteractiveMode ? "" : "ably ";
const prefixLength = prefix.length;
for (const cmd of commands) {
const paddedId = `${prefix}${cmd.id}`.padEnd(maxLength + prefixLength + 2); // +2 for spacing
const description = cmd.description || "";
this.log(` ${chalk.cyan(paddedId)} - ${description}`);
}
this.log("");
const helpCommand = isInteractiveMode
? `${this.topicName.replaceAll(":", " ")} COMMAND --help`
: `ably ${this.topicName.replaceAll(":", " ")} COMMAND --help`;
this.log(`Run \`${chalk.cyan(helpCommand)}\` for more information on a command.`);
}
/**
* Check if a command should be displayed based on web CLI and anonymous mode restrictions
*/
shouldDisplayCommand(commandId) {
// Not in web CLI mode - show all commands
if (!isWebCliMode()) {
return true;
}
// Check web CLI restrictions
const isWebRestricted = WEB_CLI_RESTRICTED_COMMANDS.some((restricted) => {
if (restricted.endsWith("*")) {
const prefix = restricted.slice(0, -1);
return (commandId === prefix ||
commandId.startsWith(prefix + ":") ||
commandId.startsWith(prefix));
}
return commandId === restricted || commandId.startsWith(restricted + ":");
});
if (isWebRestricted) {
return false;
}
// Check anonymous mode restrictions
if (process.env.ABLY_ANONYMOUS_USER_MODE === "true") {
const isAnonymousRestricted = WEB_CLI_ANONYMOUS_RESTRICTED_COMMANDS.some((restricted) => {
if (restricted.endsWith("*")) {
const prefix = restricted.slice(0, -1);
return (commandId === prefix ||
commandId.startsWith(prefix + ":") ||
commandId.startsWith(prefix));
}
return (commandId === restricted || commandId.startsWith(restricted + ":"));
});
return !isAnonymousRestricted;
}
return true;
}
async getTopicCommands() {
const commands = [];
const topicPrefix = `${this.topicName}:`;
for (const cmd of this.config.commands) {
if (cmd.id.startsWith(topicPrefix) && !cmd.hidden) {
// Check if this is a direct child (no additional colons after the topic prefix)
const remainingId = cmd.id.slice(topicPrefix.length);
const isDirectChild = !remainingId.includes(":");
if (isDirectChild && this.shouldDisplayCommand(cmd.id)) {
try {
const loadedCmd = await cmd.load();
if (!loadedCmd.hidden) {
commands.push({
id: cmd.id.replaceAll(":", " "),
description: loadedCmd.description || "",
});
}
}
catch {
// Skip commands that can't be loaded
}
}
}
}
return commands.sort((a, b) => a.id.localeCompare(b.id));
}
}