Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
54 changes: 54 additions & 0 deletions src/commands/channels/inspect.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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")}.`,
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Web CLI mode may lack account config

Medium Severity

In web CLI mode, channels:inspect still requires this.configManager.getCurrentAccount() and getCurrentAppId() to be configured, and errors out if they aren’t. Since web CLI mode typically relies on environment-provided context rather than local config, this can make the new command unusable in the intended environment even though openUrl supports web CLI output.

Fix in Cursor Fix in Web


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);
}
}
1 change: 1 addition & 0 deletions src/types/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions src/utils/open-url.ts
Original file line number Diff line number Diff line change
@@ -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 <url>" 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<void> => {
if (isWebCliMode()) {
logger.log(`${chalk.cyan("Visit")} ${url}`);
return;
}
logger.log(
`${chalk.cyan("Opening")} ${url} ${chalk.cyan("in your browser")}...`,
);
Expand Down
191 changes: 191 additions & 0 deletions test/unit/commands/channels/inspect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
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`,
);
});

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");
});

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", () => {
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");
});
});
});
Loading