From 0b69485d099b23de580b7a638ba943e503a59dd5 Mon Sep 17 00:00:00 2001 From: zak Date: Fri, 20 Feb 2026 15:04:43 +0000 Subject: [PATCH 1/3] Add channels:inspect command to open dashboard Opens the Ably dashboard channel page in the browser for a given channel name. Constructs the URL from the configured account ID and app ID, with URL-encoding for special characters in channel names. Supports --app flag to override the current app, web CLI mode (prints URL instead of opening browser), and validates that both account and app are configured before proceeding. This is change obviously falls short of a full channel inspector experience in the CLI, but at least the functionality is exposed to cli users, and provides a quick jump to the dashboard inspectors. --- src/commands/channels/inspect.ts | 54 +++++++ test/unit/commands/channels/inspect.test.ts | 150 ++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 src/commands/channels/inspect.ts create mode 100644 test/unit/commands/channels/inspect.test.ts diff --git a/src/commands/channels/inspect.ts b/src/commands/channels/inspect.ts new file mode 100644 index 00000000..28de09ff --- /dev/null +++ b/src/commands/channels/inspect.ts @@ -0,0 +1,54 @@ +import { Args, Flags } from "@oclif/core"; +import chalk from "chalk"; + +import { AblyBaseCommand } from "../../base-command.js"; +import openUrl from "../../utils/open-url.js"; + +export default class ChannelsInspect extends AblyBaseCommand { + static override args = { + channel: Args.string({ + description: "The name of the channel to inspect in the Ably dashboard", + required: true, + }), + }; + + static override description = + "Open the Ably dashboard to inspect a specific channel"; + + static override examples = ["<%= config.bin %> <%= command.id %> my-channel"]; + + static override flags = { + ...AblyBaseCommand.globalFlags, + app: Flags.string({ + description: "App ID to use (uses current app if not specified)", + env: "ABLY_APP_ID", + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(ChannelsInspect); + + const currentAccount = this.configManager.getCurrentAccount(); + const accountId = currentAccount?.accountId; + if (!accountId) { + this.error( + `No account configured. Please log in first with ${chalk.cyan('"ably accounts login"')}.`, + ); + } + + const appId = flags.app ?? this.configManager.getCurrentAppId(); + if (!appId) { + this.error( + `No app selected. Please select an app first with ${chalk.cyan('"ably apps switch"')} or specify one with ${chalk.cyan("--app")}.`, + ); + } + + const url = `https://ably.com/accounts/${accountId}/apps/${appId}/channels/${encodeURIComponent(args.channel)}`; + + if (this.isWebCliMode) { + this.log(`${chalk.cyan("Visit")} ${url}`); + } else { + await openUrl(url, this); + } + } +} diff --git a/test/unit/commands/channels/inspect.test.ts b/test/unit/commands/channels/inspect.test.ts new file mode 100644 index 00000000..86b9aafc --- /dev/null +++ b/test/unit/commands/channels/inspect.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { runCommand } from "@oclif/test"; +import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; + +describe("channels:inspect command", () => { + const originalEnv = process.env.ABLY_WEB_CLI_MODE; + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.ABLY_WEB_CLI_MODE; + } else { + process.env.ABLY_WEB_CLI_MODE = originalEnv; + } + + vi.clearAllMocks(); + }); + + describe("normal CLI mode", () => { + beforeEach(() => { + delete process.env.ABLY_WEB_CLI_MODE; + }); + + it("should open browser with correct dashboard URL", async () => { + const mockConfig = getMockConfigManager(); + const accountId = mockConfig.getCurrentAccount()!.accountId!; + const appId = mockConfig.getCurrentAppId()!; + + const { stdout } = await runCommand( + ["channels:inspect", "my-channel"], + import.meta.url, + ); + + expect(stdout).toContain("Opening"); + expect(stdout).toContain("in your browser"); + expect(stdout).toContain( + `https://ably.com/accounts/${accountId}/apps/${appId}/channels/my-channel`, + ); + }); + + it("should URL-encode special characters in channel name", async () => { + const mockConfig = getMockConfigManager(); + const accountId = mockConfig.getCurrentAccount()!.accountId!; + const appId = mockConfig.getCurrentAppId()!; + + const { stdout } = await runCommand( + ["channels:inspect", "my-channel/foo#bar"], + import.meta.url, + ); + + expect(stdout).toContain( + `https://ably.com/accounts/${accountId}/apps/${appId}/channels/my-channel%2Ffoo%23bar`, + ); + }); + + it("should error when no account is configured", async () => { + const mockConfig = getMockConfigManager(); + mockConfig.clearAccounts(); + + const { error } = await runCommand( + ["channels:inspect", "my-channel"], + import.meta.url, + ); + + expect(error).toBeDefined(); + expect(error?.message).toContain("No account configured"); + expect(error?.message).toContain("ably accounts login"); + }); + + it("should error when no app is selected", async () => { + const mockConfig = getMockConfigManager(); + mockConfig.setCurrentAppIdForAccount(undefined); + + const { error } = await runCommand( + ["channels:inspect", "my-channel"], + import.meta.url, + ); + + expect(error).toBeDefined(); + expect(error?.message).toContain("No app selected"); + expect(error?.message).toContain("ably apps switch"); + expect(error?.message).toContain("--app"); + }); + + it("should use --app flag over current app", async () => { + const mockConfig = getMockConfigManager(); + const accountId = mockConfig.getCurrentAccount()!.accountId!; + + const { stdout } = await runCommand( + ["channels:inspect", "my-channel", "--app", "custom-app-id"], + import.meta.url, + ); + + expect(stdout).toContain( + `https://ably.com/accounts/${accountId}/apps/custom-app-id/channels/my-channel`, + ); + }); + + it("should use --app flag when no current app is set", async () => { + const mockConfig = getMockConfigManager(); + const accountId = mockConfig.getCurrentAccount()!.accountId!; + mockConfig.setCurrentAppIdForAccount(undefined); + + const { stdout } = await runCommand( + ["channels:inspect", "my-channel", "--app", "override-app"], + import.meta.url, + ); + + expect(stdout).toContain( + `https://ably.com/accounts/${accountId}/apps/override-app/channels/my-channel`, + ); + }); + }); + + describe("web CLI mode", () => { + beforeEach(() => { + process.env.ABLY_WEB_CLI_MODE = "true"; + }); + + it("should display URL without opening browser", async () => { + const mockConfig = getMockConfigManager(); + const accountId = mockConfig.getCurrentAccount()!.accountId!; + const appId = mockConfig.getCurrentAppId()!; + + const { stdout } = await runCommand( + ["channels:inspect", "my-channel"], + import.meta.url, + ); + + expect(stdout).toContain("Visit"); + expect(stdout).toContain( + `https://ably.com/accounts/${accountId}/apps/${appId}/channels/my-channel`, + ); + expect(stdout).not.toContain("Opening"); + expect(stdout).not.toContain("in your browser"); + }); + }); + + describe("help", () => { + it("should display help with --help flag", async () => { + const { stdout } = await runCommand( + ["channels:inspect", "--help"], + import.meta.url, + ); + + expect(stdout).toContain("Open the Ably dashboard to inspect"); + expect(stdout).toContain("USAGE"); + expect(stdout).toContain("ARGUMENTS"); + }); + }); +}); From f932b47ef3d2ad626d4f2eeb22c7a95f265bccd5 Mon Sep 17 00:00:00 2001 From: zak Date: Mon, 23 Feb 2026 15:31:39 +0000 Subject: [PATCH 2/3] Make dashboard base URL configurable and centralize web CLI mode in openUrl Add a --dashboard-host global flag (env: ABLY_DASHBOARD_HOST) so the channels:inspect command can target staging or enterprise dashboard environments instead of the hardcoded https://ably.com. Move the web CLI mode check ("Visit " instead of opening a browser) from individual commands into the openUrl utility so all callers get consistent behavior without duplicating the logic. --- src/base-command.ts | 6 ++++++ src/commands/channels/inspect.ts | 9 +++------ src/types/cli.ts | 1 + src/utils/open-url.ts | 10 ++++++++-- test/unit/commands/channels/inspect.test.ts | 21 +++++++++++++++++++++ 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/base-command.ts b/src/base-command.ts index 8f18aa56..74ec2c65 100644 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -118,6 +118,12 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { hidden: process.env.ABLY_SHOW_DEV_FLAGS !== "true", env: "ABLY_CONTROL_HOST", }), + "dashboard-host": Flags.string({ + description: + "Override the host for the Ably dashboard, which defaults to https://ably.com", + hidden: process.env.ABLY_SHOW_DEV_FLAGS !== "true", + env: "ABLY_DASHBOARD_HOST", + }), env: Flags.string({ description: "Override the environment for all product API calls", env: "ABLY_CLI_ENV", diff --git a/src/commands/channels/inspect.ts b/src/commands/channels/inspect.ts index 28de09ff..97fde4d2 100644 --- a/src/commands/channels/inspect.ts +++ b/src/commands/channels/inspect.ts @@ -43,12 +43,9 @@ export default class ChannelsInspect extends AblyBaseCommand { ); } - const url = `https://ably.com/accounts/${accountId}/apps/${appId}/channels/${encodeURIComponent(args.channel)}`; + const dashboardHost = flags["dashboard-host"] ?? "https://ably.com"; + const url = `${dashboardHost}/accounts/${accountId}/apps/${appId}/channels/${encodeURIComponent(args.channel)}`; - if (this.isWebCliMode) { - this.log(`${chalk.cyan("Visit")} ${url}`); - } else { - await openUrl(url, this); - } + await openUrl(url, this); } } diff --git a/src/types/cli.ts b/src/types/cli.ts index cde9e952..7b075aae 100644 --- a/src/types/cli.ts +++ b/src/types/cli.ts @@ -9,6 +9,7 @@ export interface BaseFlags { "api-key"?: string; "client-id"?: string; "control-host"?: string; + "dashboard-host"?: string; env?: string; endpoint?: string; host?: string; diff --git a/src/utils/open-url.ts b/src/utils/open-url.ts index c31cb3b9..93108510 100644 --- a/src/utils/open-url.ts +++ b/src/utils/open-url.ts @@ -1,15 +1,21 @@ import open from "open"; import isTestMode from "./test-mode.js"; +import isWebCliMode from "./web-mode.js"; import chalk from "chalk"; interface Logger { log: (msg: string) => void; } -// openUrl opens a browser window if we're running normally, but just prints that it will if we're testing -// we don't want to open browsers in unit tests, and we can't use mocking to catch the calls because of how +// openUrl opens a browser window if we're running normally, but just prints that it will if we're testing. +// In web CLI mode it prints "Visit " instead of trying to open a browser. +// We don't want to open browsers in unit tests, and we can't use mocking to catch the calls because of how // oclif loads the commands. const openUrl = async (url: string, logger: Logger): Promise => { + if (isWebCliMode()) { + logger.log(`${chalk.cyan("Visit")} ${url}`); + return; + } logger.log( `${chalk.cyan("Opening")} ${url} ${chalk.cyan("in your browser")}...`, ); diff --git a/test/unit/commands/channels/inspect.test.ts b/test/unit/commands/channels/inspect.test.ts index 86b9aafc..142a29b6 100644 --- a/test/unit/commands/channels/inspect.test.ts +++ b/test/unit/commands/channels/inspect.test.ts @@ -109,6 +109,27 @@ describe("channels:inspect command", () => { `https://ably.com/accounts/${accountId}/apps/override-app/channels/my-channel`, ); }); + + it("should use --dashboard-host flag to override base URL", async () => { + const mockConfig = getMockConfigManager(); + const accountId = mockConfig.getCurrentAccount()!.accountId!; + const appId = mockConfig.getCurrentAppId()!; + + const { stdout } = await runCommand( + [ + "channels:inspect", + "my-channel", + "--dashboard-host", + "https://staging.ably.com", + ], + import.meta.url, + ); + + expect(stdout).toContain( + `https://staging.ably.com/accounts/${accountId}/apps/${appId}/channels/my-channel`, + ); + expect(stdout).not.toContain("https://ably.com/accounts"); + }); }); describe("web CLI mode", () => { From d4e37c639aafd981d3e36cb8fcbc78c3d7324591 Mon Sep 17 00:00:00 2001 From: zak Date: Mon, 23 Feb 2026 16:36:02 +0000 Subject: [PATCH 3/3] Default dashboard-host to https:// when no scheme given When the --dashboard-host flag or ABLY_DASHBOARD_HOST env var is provided without a URL scheme, automatically prepend https:// so that the constructed dashboard URL is valid. --- src/commands/channels/inspect.ts | 5 ++++- test/unit/commands/channels/inspect.test.ts | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/commands/channels/inspect.ts b/src/commands/channels/inspect.ts index 97fde4d2..c83935b9 100644 --- a/src/commands/channels/inspect.ts +++ b/src/commands/channels/inspect.ts @@ -43,7 +43,10 @@ export default class ChannelsInspect extends AblyBaseCommand { ); } - const dashboardHost = flags["dashboard-host"] ?? "https://ably.com"; + let dashboardHost = flags["dashboard-host"] ?? "https://ably.com"; + if (dashboardHost && !/^https?:\/\//i.test(dashboardHost)) { + dashboardHost = `https://${dashboardHost}`; + } const url = `${dashboardHost}/accounts/${accountId}/apps/${appId}/channels/${encodeURIComponent(args.channel)}`; await openUrl(url, this); diff --git a/test/unit/commands/channels/inspect.test.ts b/test/unit/commands/channels/inspect.test.ts index 142a29b6..947d5524 100644 --- a/test/unit/commands/channels/inspect.test.ts +++ b/test/unit/commands/channels/inspect.test.ts @@ -130,6 +130,26 @@ describe("channels:inspect command", () => { ); expect(stdout).not.toContain("https://ably.com/accounts"); }); + + it("should prepend https:// when --dashboard-host has no scheme", async () => { + const mockConfig = getMockConfigManager(); + const accountId = mockConfig.getCurrentAccount()!.accountId!; + const appId = mockConfig.getCurrentAppId()!; + + const { stdout } = await runCommand( + [ + "channels:inspect", + "my-channel", + "--dashboard-host", + "staging.ably.com", + ], + import.meta.url, + ); + + expect(stdout).toContain( + `https://staging.ably.com/accounts/${accountId}/apps/${appId}/channels/my-channel`, + ); + }); }); describe("web CLI mode", () => {