@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
253 lines (252 loc) • 10.4 kB
JavaScript
import { Args, Flags } from "@oclif/core";
import chalk from "chalk";
import { execSync } from "node:child_process";
import * as readline from "node:readline";
import { ControlBaseCommand } from "../../control-base-command.js";
import { ControlApi } from "../../services/control-api.js";
import { displayLogo } from "../../utils/logo.js";
// Moved function definition outside the class
function validateAndGetAlias(input, logFn) {
const trimmedAlias = input.trim();
if (!trimmedAlias) {
return null;
}
// Convert to lowercase for case-insensitive comparison
const lowercaseAlias = trimmedAlias.toLowerCase();
// First character must be a letter
if (!/^[a-z]/.test(lowercaseAlias)) {
logFn("Error: Alias must start with a letter");
return null;
}
// Only allow letters, numbers, dashes, and underscores after first character
if (!/^[a-z][\d_a-z-]*$/.test(lowercaseAlias)) {
logFn("Error: Alias can only contain letters, numbers, dashes, and underscores");
return null;
}
return lowercaseAlias;
}
export default class AccountsLogin extends ControlBaseCommand {
static args = {
token: Args.string({
description: "Access token (if not provided, will prompt for it)",
required: false,
}),
};
static description = "Log in to your Ably account";
static examples = [
"<%= config.bin %> <%= command.id %>",
"<%= config.bin %> <%= command.id %> --alias mycompany",
"<%= config.bin %> <%= command.id %> --json",
"<%= config.bin %> <%= command.id %> --pretty-json",
];
static flags = {
...ControlBaseCommand.globalFlags,
alias: Flags.string({
char: "a",
description: "Alias for this account (default account if not specified)",
}),
"no-browser": Flags.boolean({
default: false,
description: "Do not open a browser",
}),
};
async run() {
const { args, flags } = await this.parse(AccountsLogin);
// Display ASCII art logo if not in JSON mode
if (!this.shouldOutputJson(flags)) {
displayLogo(this.log.bind(this));
}
let accessToken;
if (args.token) {
accessToken = args.token;
}
else {
let obtainTokenPath = "https://ably.com/users/access_tokens";
if (flags["control-host"]) {
if (!this.shouldOutputJson(flags)) {
this.log("Using control host:", flags["control-host"]);
}
obtainTokenPath = flags["control-host"].includes("local")
? `http://${flags["control-host"]}/users/access_tokens`
: `https://${flags["control-host"]}/users/access_tokens`;
}
// Prompt the user to get a token
if (!flags["no-browser"]) {
if (!this.shouldOutputJson(flags)) {
this.log("Opening browser to get an access token...");
}
this.openBrowser(obtainTokenPath);
}
else if (!this.shouldOutputJson(flags)) {
this.log(`Please visit ${obtainTokenPath} to create an access token`);
}
accessToken = await this.promptForToken();
}
// If no alias flag provided, prompt the user if they want to provide one
let { alias } = flags;
if (!alias && !this.shouldOutputJson(flags)) {
// Check if the default account already exists
const accounts = this.configManager.listAccounts();
const hasDefaultAccount = accounts.some((account) => account.alias === "default");
if (hasDefaultAccount) {
// Explain to the user the implications of not providing an alias
this.log("\nYou have not specified an alias for this account.");
this.log("If you continue without an alias, your existing default account configuration will be overwritten.");
this.log("To maintain multiple account profiles, please provide an alias.");
// Ask if they want to provide an alias
const shouldProvideAlias = await this.promptYesNo("Would you like to provide an alias for this account?");
if (shouldProvideAlias) {
alias = await this.promptForAlias();
}
else {
alias = "default";
this.log("No alias provided. The default account configuration will be overwritten.");
}
}
else {
// No default account exists yet, but still offer to set an alias
this.log("\nYou have not specified an alias for this account.");
this.log("Using an alias allows you to maintain multiple account profiles that you can switch between.");
// Ask if they want to provide an alias
const shouldProvideAlias = await this.promptYesNo("Would you like to provide an alias for this account?");
if (shouldProvideAlias) {
alias = await this.promptForAlias();
}
else {
alias = "default";
this.log("No alias provided. This will be set as your default account.");
}
}
}
else if (!alias) {
alias = "default";
}
try {
// Fetch account information
const controlApi = new ControlApi({
accessToken,
controlHost: flags["control-host"],
});
const { account, user } = await controlApi.getMe();
// Store the account information
this.configManager.storeAccount(accessToken, alias, {
accountId: account.id,
accountName: account.name,
tokenId: "unknown", // Token ID is not returned by getMe(), would need additional API if needed
userEmail: user.email,
});
// Switch to this account
this.configManager.switchAccount(alias);
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({
account: {
alias,
id: account.id,
name: account.name,
user: {
email: user.email,
},
},
success: true,
}, flags));
}
else {
this.log(`Successfully logged in to ${chalk.cyan(account.name)} (account ID: ${chalk.greenBright(account.id)})`);
if (alias !== "default") {
this.log(`Account stored with alias: ${alias}`);
}
this.log(`Account ${chalk.cyan(alias)} is now the current account`);
}
}
catch (error) {
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({
error: error instanceof Error ? error.message : String(error),
success: false,
}, flags));
}
else {
this.error(`Failed to authenticate: ${error}`);
}
}
}
openBrowser(url) {
try {
const command = process.platform === "darwin"
? "open"
: process.platform === "win32"
? "start"
: "xdg-open";
execSync(`${command} ${url}`);
}
catch (error) {
this.warn(`Failed to open browser: ${error}`);
this.log(`Please visit ${url} manually to create an access token`);
}
}
promptForAlias() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Pass this.log as the logging function to the external validator
const logFn = this.log.bind(this);
return new Promise((resolve) => {
const askForAlias = () => {
rl.question('Enter an alias for this account (e.g. "dev", "production", "personal"): ', (alias) => {
// Use the external validator function, passing the log function
const validatedAlias = validateAndGetAlias(alias, logFn);
if (validatedAlias === null) {
if (!alias.trim()) {
logFn("Error: Alias cannot be empty"); // Use logFn here too
}
askForAlias();
}
else {
rl.close();
resolve(validatedAlias);
}
});
};
askForAlias();
});
}
promptForToken() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question("\nEnter your access token: ", (token) => {
rl.close();
resolve(token.trim());
});
});
}
promptYesNo(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
const askQuestion = () => {
rl.question(`${question} (y/n) `, (answer) => {
const lowercaseAnswer = answer.toLowerCase().trim();
if (lowercaseAnswer === "y" || lowercaseAnswer === "yes") {
rl.close();
resolve(true);
}
else if (lowercaseAnswer === "n" || lowercaseAnswer === "no") {
rl.close();
resolve(false);
}
else {
this.log("Please answer with yes/y or no/n");
askQuestion();
}
});
};
askQuestion();
});
}
}