Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5ab5f61
Support app/queue/rule lookup by name or ID in commands
sacOO7 Apr 23, 2026
1abb28f
Support flexible identifiers for key and account commands
sacOO7 Apr 23, 2026
a82deba
Fixed copilot review comments
sacOO7 Apr 23, 2026
75df812
Fixed keys name or id arguments based implementation, added relevant …
sacOO7 Apr 24, 2026
d7d03e8
- Refactored auth keys commands and apps switch as per copilot review…
sacOO7 Apr 24, 2026
49ded59
Refactored controlApi.getKey to return fullKeyObject, fixed e2e tests
sacOO7 Apr 24, 2026
00b05d0
Addressed review feedback about multiple calls for apps/switch
sacOO7 Apr 24, 2026
4b5884c
Removed unncessary code for processing hints, replaced existing error…
sacOO7 Apr 24, 2026
10eb785
Refactored accounts/switch commands, removed unnecessary code, cleane…
sacOO7 Apr 26, 2026
238316a
Refactored keys command group to only accept key name or value
sacOO7 Apr 27, 2026
47de4f8
Merge remote-tracking branch 'origin/main' into fix/consistent-name-o…
sacOO7 Apr 27, 2026
0833b06
Fixed failing e2e tests
sacOO7 Apr 27, 2026
4219eff
Added missing e2e tests for keys switch, keys revoke and keys current
sacOO7 Apr 27, 2026
95c4690
Updated keys description and examples for `auth/keys`
sacOO7 Apr 27, 2026
5718edb
Merge remote-tracking branch 'origin/main' into fix/consistent-name-o…
sacOO7 Apr 27, 2026
2eb9d7d
Merge remote-tracking branch 'origin/main' into fix/consistent-name-o…
sacOO7 Apr 28, 2026
12391fa
Refactored apps update command to support no tls flag, updated descri…
sacOO7 Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
// Make sure we have authentication after potentially modifying options
if (!clientOptions.key && !clientOptions.token) {
this.fail(
"Authentication required. Please provide either an API key, a token, or log in first.",
'Authentication required. Please provide either an API key, a token, or log in first. Run "ably accounts login" to configure authentication.',
flags,
"auth",
);
Expand Down
46 changes: 21 additions & 25 deletions src/commands/accounts/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { promptForConfirmation } from "../../utils/prompt-confirmation.js";

export default class AccountsLogout extends ControlBaseCommand {
static override args = {
accountAlias: Args.string({
accountAliasOrId: Args.string({
description:
"Alias of the account to log out from (defaults to current account)",
"Alias or ID of the account to log out from (defaults to current account)",
required: false,
}),
};
Expand All @@ -19,6 +19,7 @@ export default class AccountsLogout extends ControlBaseCommand {
static override examples = [
"<%= config.bin %> <%= command.id %>",
"<%= config.bin %> <%= command.id %> mycompany",
"<%= config.bin %> <%= command.id %> VgQpOZ",
"<%= config.bin %> <%= command.id %> --json",
"<%= config.bin %> <%= command.id %> --pretty-json",
];
Expand All @@ -31,29 +32,24 @@ export default class AccountsLogout extends ControlBaseCommand {
public async run(): Promise<void> {
const { args, flags } = await this.parse(AccountsLogout);

// Determine which account to log out from
const targetAlias =
args.accountAlias || this.configManager.getCurrentAccountAlias();

if (!targetAlias) {
this.fail(
'No account is currently selected and no alias provided. Use "ably accounts list" to see available accounts.',
flags,
"accountLogout",
);
}

const accounts = this.configManager.listAccounts();
const accountExists = accounts.some(
(account) => account.alias === targetAlias,
);

if (!accountExists) {
this.fail(
`Account with alias "${targetAlias}" not found. Use "ably accounts list" to see available accounts.`,
flags,
"accountLogout",
);
// The accountAliasOrId arg accepts two formats:
// 1. Account alias — e.g. "mycompany" (the label set during login)
// 2. Account ID — e.g. "VgQpOZ" (the Ably-assigned account ID)
let targetAlias: string;
if (args.accountAliasOrId) {
targetAlias = this.resolveAccountAlias(args.accountAliasOrId, flags);
} else {
const currentAlias = this.configManager.getCurrentAccountAlias();
if (!currentAlias) {
this.fail(
'No account is currently selected and no alias or ID provided. Run "ably accounts list" to see available accounts.',
flags,
"accountLogout",
);
}
// Validate that the current alias still exists in the accounts list.
// This catches stale config where current.account points to a removed alias.
targetAlias = this.resolveAccountAlias(currentAlias, flags);
}

// In JSON mode, require --force to prevent accidental destructive actions
Expand Down
56 changes: 18 additions & 38 deletions src/commands/accounts/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { pickUniqueAlias, slugifyAccountName } from "../../utils/slugify.js";

export default class AccountsSwitch extends ControlBaseCommand {
static override args = {
accountAlias: Args.string({
description: "Alias of the account to switch to",
accountAliasOrId: Args.string({
description: "Alias or ID of the account to switch to",
required: false,
}),
};
Expand All @@ -21,6 +21,7 @@ export default class AccountsSwitch extends ControlBaseCommand {
static override examples = [
"<%= config.bin %> <%= command.id %>",
"<%= config.bin %> <%= command.id %> mycompany",
"<%= config.bin %> <%= command.id %> VgQpOZ",
"<%= config.bin %> <%= command.id %> --json",
"<%= config.bin %> <%= command.id %> --pretty-json",
];
Expand All @@ -38,7 +39,7 @@ export default class AccountsSwitch extends ControlBaseCommand {
if (localAccounts.length === 0) {
if (this.shouldOutputJson(flags)) {
this.fail(
'No accounts configured. Use "ably accounts login" to add an account.',
'No accounts configured. Run "ably accounts login" to add an account.',
flags,
"accountSwitch",
);
Expand All @@ -50,16 +51,23 @@ export default class AccountsSwitch extends ControlBaseCommand {
return;
}

// If alias is provided, switch directly
if (args.accountAlias) {
await this.switchToLocalAccount(args.accountAlias, localAccounts, flags);
// If alias or ID is provided, resolve and switch directly.
// The accountAliasOrId arg accepts two formats:
// 1. Account alias — e.g. "mycompany" (the label set during login)
// 2. Account ID — e.g. "VgQpOZ" (the Ably-assigned account ID)
if (args.accountAliasOrId) {
const resolvedAlias = this.resolveAccountAlias(
args.accountAliasOrId,
flags,
);
await this.switchToLocalAccount(resolvedAlias, flags);
return;
}

// JSON mode requires an explicit alias
if (this.shouldOutputJson(flags)) {
this.fail(
"No account alias provided. Please specify an account alias to switch to.",
'No account alias or ID provided. Run "ably accounts list" to see available accounts.',
flags,
"accountSwitch",
{
Expand Down Expand Up @@ -162,16 +170,7 @@ export default class AccountsSwitch extends ControlBaseCommand {
};

if (selected.type === "local") {
const validAccounts = localAccounts
.filter((a) => a.account.accountId && a.account.accountName)
.map((a) => ({
account: {
accountId: a.account.accountId!,
accountName: a.account.accountName!,
},
alias: a.alias,
}));
await this.switchToLocalAccount(selected.alias, validAccounts, flags);
await this.switchToLocalAccount(selected.alias, flags);
return true;
}

Expand Down Expand Up @@ -253,29 +252,10 @@ export default class AccountsSwitch extends ControlBaseCommand {

private async switchToLocalAccount(
alias: string,
accounts: Array<{
account: { accountId: string; accountName: string };
alias: string;
}>,
flags: Record<string, unknown>,
): Promise<void> {
const accountExists = accounts.some((account) => account.alias === alias);

if (!accountExists) {
this.fail(
`Account with alias "${alias}" not found. Use "ably accounts list" to see available accounts.`,
flags,
"accountSwitch",
{
availableAccounts: accounts.map(({ account, alias }) => ({
alias,
id: account.accountId,
name: account.accountName,
})),
},
);
}

// Alias is already validated by resolveAccountAlias() or interactive
// selection before reaching this method.
this.configManager.switchAccount(alias);

// Store custom endpoint if provided
Expand Down
17 changes: 11 additions & 6 deletions src/commands/apps/rules/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { promptForConfirmation } from "../../../utils/prompt-confirmation.js";

export default class RulesDeleteCommand extends ControlBaseCommand {
static args = {
nameOrId: Args.string({
ruleNameOrId: Args.string({
description: "Name or ID of the rule to delete",
required: true,
}),
Expand Down Expand Up @@ -40,14 +40,19 @@ export default class RulesDeleteCommand extends ControlBaseCommand {

try {
const controlApi = this.createControlApi(flags);
// Find the namespace by name or ID
// Resolve the rule. The ruleNameOrId arg accepts the rule identifier which serves as both the name and ID
// (e.g. "chat", "events"). For channel rules, the namespace ID is the rule name — they are the
// same value — so a single match on n.id covers both cases.
const namespaces = await controlApi.listNamespaces(appId);
const namespace = namespaces.find((n) => n.id === args.nameOrId);
const namespace = namespaces.find((n) => n.id === args.ruleNameOrId);

if (!namespace) {
this.fail(`Rule "${args.nameOrId}" not found`, flags, "ruleDelete", {
appId,
});
this.fail(
`Rule "${args.ruleNameOrId}" not found. Run "ably apps rules list" to see available rules.`,
flags,
"ruleDelete",
{ appId },
);
}

// In JSON mode, require --force to prevent accidental destructive actions
Expand Down
17 changes: 11 additions & 6 deletions src/commands/apps/rules/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { formatLabel, formatResource } from "../../../utils/output.js";

export default class RulesUpdateCommand extends ControlBaseCommand {
static args = {
nameOrId: Args.string({
ruleNameOrId: Args.string({
description: "Name or ID of the rule to update",
required: true,
}),
Expand Down Expand Up @@ -120,14 +120,19 @@ export default class RulesUpdateCommand extends ControlBaseCommand {

try {
const controlApi = this.createControlApi(flags);
// Find the namespace by name or ID
// Resolve the rule. The ruleNameOrId arg accepts the rule identifier which serves as both the name and ID
// (e.g. "chat", "events"). For channel rules, the namespace ID is the rule name — they are the
// same value — so a single match on n.id covers both cases.
const namespaces = await controlApi.listNamespaces(appId);
const namespace = namespaces.find((n) => n.id === args.nameOrId);
const namespace = namespaces.find((n) => n.id === args.ruleNameOrId);

if (!namespace) {
this.fail(`Rule "${args.nameOrId}" not found`, flags, "ruleUpdate", {
appId,
});
this.fail(
`Rule "${args.ruleNameOrId}" not found. Run "ably apps rules list" to see available rules.`,
flags,
"ruleUpdate",
{ appId },
);
}

// Prepare update data
Expand Down
85 changes: 37 additions & 48 deletions src/commands/apps/switch.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { Args } from "@oclif/core";

import { ControlBaseCommand } from "../../control-base-command.js";
import { ControlApi } from "../../services/control-api.js";
import { formatResource } from "../../utils/output.js";

export default class AppsSwitch extends ControlBaseCommand {
static override args = {
appId: Args.string({
description: "ID of the app to switch to",
appNameOrId: Args.string({
description: "App name or ID to switch to",
required: false,
}),
};

static override description = "Switch to a different Ably app";

static override examples = [
"<%= config.bin %> <%= command.id %> APP_ID",
'<%= config.bin %> <%= command.id %> "My App"',
"<%= config.bin %> <%= command.id %> app-id",
"<%= config.bin %> <%= command.id %>",
"<%= config.bin %> <%= command.id %> APP_ID --json",
];

static override flags = {
Expand All @@ -28,11 +27,27 @@ export default class AppsSwitch extends ControlBaseCommand {
const { args, flags } = await this.parse(AppsSwitch);

try {
const controlApi = this.createControlApi({});
const controlApi = this.createControlApi(flags);

// If app ID is provided, switch directly
if (args.appId) {
await this.switchToApp(args.appId, controlApi, flags);
// If app name or ID is provided, resolve and switch directly.
// The appNameOrId arg accepts two formats:
// 1. App name — e.g. "My App" (human-readable, may contain spaces)
// 2. App ID — e.g. "s57drg" (the Ably-assigned app ID)
if (args.appNameOrId) {
const apps = await controlApi.listApps();
const matchedApp = apps.find(
(a) => a.name === args.appNameOrId || a.id === args.appNameOrId,
);

if (!matchedApp) {
this.fail(
`App "${args.appNameOrId}" not found. Run "ably apps list" to see available apps.`,
flags,
"appSwitch",
);
}

this.saveAndReportSwitch(matchedApp, flags);
return;
}

Expand All @@ -43,22 +58,7 @@ export default class AppsSwitch extends ControlBaseCommand {
const selectedApp = await this.interactiveHelper.selectApp(controlApi);

if (selectedApp) {
// Save the app info and set as current
this.configManager.setCurrentApp(selectedApp.id);
this.configManager.storeAppInfo(selectedApp.id, {
appName: selectedApp.name,
});
if (this.shouldOutputJson(flags)) {
this.logJsonResult(
{ app: { id: selectedApp.id, name: selectedApp.name } },
flags,
);
} else {
this.logSuccessMessage(
`Switched to app ${formatResource(selectedApp.name)} (${selectedApp.id}).`,
flags,
);
}
this.saveAndReportSwitch(selectedApp, flags);
} else {
this.logWarning("App switch cancelled.", flags);
}
Expand All @@ -67,31 +67,20 @@ export default class AppsSwitch extends ControlBaseCommand {
}
}

private async switchToApp(
appId: string,
controlApi: ControlApi,
private saveAndReportSwitch(
app: { id: string; name: string },
flags: Record<string, unknown>,
): Promise<void> {
try {
// Verify the app exists
const app = await controlApi.getApp(appId);

// Save app info and set as current
this.configManager.setCurrentApp(appId);
this.configManager.storeAppInfo(appId, { appName: app.name });
): void {
this.configManager.setCurrentApp(app.id);
this.configManager.storeAppInfo(app.id, { appName: app.name });

if (this.shouldOutputJson(flags)) {
this.logJsonResult({ app: { id: app.id, name: app.name } }, flags);
} else {
this.logSuccessMessage(
`Switched to app ${formatResource(app.name)} (${app.id}).`,
flags,
);
}
} catch (error) {
this.fail(error, flags, "appSwitch", {
context: `switching to app "${appId}"`,
});
if (this.shouldOutputJson(flags)) {
this.logJsonResult({ app: { id: app.id, name: app.name } }, flags);
} else {
this.logSuccessMessage(
`Switched to app ${formatResource(app.name)} (${app.id}).`,
flags,
);
}
}
}
Loading
Loading