From 0b69485d099b23de580b7a638ba943e503a59dd5 Mon Sep 17 00:00:00 2001 From: zak Date: Fri, 20 Feb 2026 15:04:43 +0000 Subject: [PATCH] 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"); + }); + }); +});