diff --git a/docs/Product-Requirements.md b/docs/Product-Requirements.md index 63bc0ac8..98130793 100644 --- a/docs/Product-Requirements.md +++ b/docs/Product-Requirements.md @@ -48,8 +48,6 @@ The CLI uses topics (space-separated) to group related commands logically. - `$ ably apps stats [ID]`: Views app stats for the specified (or current) app. Supports `--live` polling, `--unit`, `--start`, `--end`, `--limit`. - `$ ably apps switch [APPID]`: Switches the active app configuration to the specified App ID or prompts for selection. - `$ ably apps current`: Shows the currently selected app configuration. -- `$ ably apps logs subscribe`: Alias for `ably logs app subscribe`. -- `$ ably apps logs history`: Alias for `ably logs app history`. **Channel Rules (`ably apps channel-rules`)** *(Manage Ably channel rules/namespaces via Control API)* @@ -134,8 +132,8 @@ The CLI uses topics (space-separated) to group related commands logically. **Logging (`ably logs`)** *(Stream and retrieve logs from meta channels)* -- `$ ably logs app subscribe`: Streams logs from `[meta]log`. Supports `--rewind`. -- `$ ably logs app history`: Retrieves historical logs from `[meta]log`. Supports `--limit`, `--direction`. +- `$ ably logs subscribe`: Streams logs from `[meta]log`. Supports `--rewind`. +- `$ ably logs history`: Retrieves historical logs from `[meta]log`. Supports `--limit`, `--direction`. - `$ ably logs channel-lifecycle subscribe`: Streams logs from `[meta]channel.lifecycle`. - `$ ably logs connection-lifecycle subscribe`: Streams logs from `[meta]connection.lifecycle`. - `$ ably logs connection-lifecycle history`: Retrieves historical connection logs. Supports `--limit`, `--direction`. diff --git a/src/commands/apps/logs/history.ts b/src/commands/apps/logs/history.ts deleted file mode 100644 index 242c0520..00000000 --- a/src/commands/apps/logs/history.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Flags as _Flags } from "@oclif/core"; -import * as Ably from "ably"; -import chalk from "chalk"; - -import { AblyBaseCommand } from "../../../base-command.js"; - -export default class AppsLogsHistory extends AblyBaseCommand { - static override description = "Alias for `ably logs app history`"; - - static override examples = [ - "$ ably apps logs history", - "$ ably apps logs history --limit 20", - "$ ably apps logs history --direction forwards", - "$ ably apps logs history --json", - "$ ably apps logs history --pretty-json", - ]; - - static override flags = { - ...AblyBaseCommand.globalFlags, - direction: _Flags.string({ - default: "backwards", - description: "Direction of message retrieval", - options: ["backwards", "forwards"], - }), - json: _Flags.boolean({ - default: false, - description: "Output results in JSON format", - }), - limit: _Flags.integer({ - default: 100, - description: "Maximum number of messages to retrieve", - }), - }; - - async run(): Promise { - const { flags } = await this.parse(AppsLogsHistory); - - try { - // Create a REST client - const client = await this.createAblyRestClient(flags); - if (!client) { - return; - } - - // Get the channel - const channel = client.channels.get("[meta]log"); - - // Build history query parameters - const historyParams: Ably.RealtimeHistoryParams = { - direction: flags.direction as "backwards" | "forwards", - limit: flags.limit, - }; - - // Get history - const history = await channel.history(historyParams); - const messages = history.items; - - // Display results based on format - if (this.shouldOutputJson(flags)) { - this.log(this.formatJsonOutput({ messages }, flags)); - } else { - if (messages.length === 0) { - this.log("No application log messages found."); - return; - } - - this.log( - `Found ${chalk.cyan(messages.length.toString())} application log messages:`, - ); - this.log(""); - - for (const message of messages) { - // Format timestamp - const timestamp = new Date(message.timestamp).toISOString(); - this.log( - `${chalk.gray(timestamp)} [${chalk.yellow(message.name || "message")}]`, - ); - - // Format data based on type - if (typeof message.data === "object") { - try { - this.log(this.formatJsonOutput(message.data, flags)); - } catch { - this.log(String(message.data)); - } - } else { - this.log(String(message.data)); - } - - this.log(""); // Add a blank line between messages - } - - if (messages.length === flags.limit) { - this.log( - chalk.yellow( - `Showing maximum of ${flags.limit} messages. Use --limit to show more.`, - ), - ); - } - } - } catch (error) { - this.error( - `Error retrieving application logs: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } -} diff --git a/src/commands/apps/logs/index.ts b/src/commands/apps/logs/index.ts deleted file mode 100644 index 050d595d..00000000 --- a/src/commands/apps/logs/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Command } from "@oclif/core"; - -export default class AppsLogs extends Command { - static override description = "Stream or retrieve app logs"; - - static override examples = [ - "$ ably apps logs subscribe", - "$ ably apps logs subscribe --rewind 10", - "$ ably apps logs history", - ]; - - async run() { - this.log("App logs commands:"); - this.log(""); - this.log( - " ably apps logs subscribe - Stream logs from the app-wide meta channel", - ); - this.log(" ably apps logs history - View historical app logs"); - this.log(""); - this.log( - "Run `ably apps logs COMMAND --help` for more information on a command.", - ); - } -} diff --git a/src/commands/apps/logs/subscribe.ts b/src/commands/apps/logs/subscribe.ts deleted file mode 100644 index 047ed34c..00000000 --- a/src/commands/apps/logs/subscribe.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Flags } from "@oclif/core"; - -import { AblyBaseCommand } from "../../../base-command.js"; - -export default class AppsLogsSubscribe extends AblyBaseCommand { - static override description = "Alias for ably logs app subscribe"; - - static override examples = [ - "$ ably apps logs subscribe", - "$ ably apps logs subscribe --rewind 10", - ]; - - static override flags = { - ...AblyBaseCommand.globalFlags, - json: Flags.boolean({ - default: false, - description: "Output results as JSON", - }), - rewind: Flags.integer({ - default: 0, - description: "Number of messages to rewind when subscribing", - }), - }; - - async run(): Promise { - const { flags } = await this.parse(AppsLogsSubscribe); - - // Delegate to the original command - await this.config.runCommand("logs:app:subscribe", [ - ...(flags.rewind ? ["--rewind", flags.rewind.toString()] : []), - ...(flags.json ? ["--json"] : []), - // Forward all global flags - ...(flags.host ? ["--host", flags.host] : []), - ...(flags.env ? ["--env", flags.env] : []), - ...(flags["control-host"] - ? ["--control-host", flags["control-host"]] - : []), - ...(flags["access-token"] - ? ["--access-token", flags["access-token"]] - : []), - ...(flags["api-key"] ? ["--api-key", flags["api-key"]] : []), - ...(flags["client-id"] ? ["--client-id", flags["client-id"]] : []), - ...(flags.app ? ["--app", flags.app] : []), - ...(flags.endpoint ? ["--endpoint", flags.endpoint] : []), - ]); - } -} diff --git a/src/commands/channels/logs.ts b/src/commands/channels/logs.ts deleted file mode 100644 index 68aef86c..00000000 --- a/src/commands/channels/logs.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Args, Flags } from "@oclif/core"; - -import { AblyBaseCommand } from "../../base-command.js"; - -export default class ChannelsLogs extends AblyBaseCommand { - static override args = { - topic: Args.string({ - default: "channel-lifecycle", - description: - "Log topic to subscribe to (currently only channel-lifecycle is supported)", - }), - }; - - static override description = - "Alias for ably logs channel-lifecycle subscribe"; - - static override examples = [ - "$ ably channels logs channel-lifecycle", - "$ ably channels logs channel-lifecycle --rewind 10", - ]; - - static override flags = { - ...AblyBaseCommand.globalFlags, - json: Flags.boolean({ - default: false, - description: "Output results as JSON", - }), - rewind: Flags.integer({ - default: 0, - description: "Number of messages to rewind when subscribing", - }), - }; - - async run(): Promise { - const { args, flags } = await this.parse(ChannelsLogs); - - // Currently only support channel-lifecycle - if (args.topic !== "channel-lifecycle") { - this.error( - `Unsupported log topic: ${args.topic}. Currently only 'channel-lifecycle' is supported.`, - ); - return; - } - - // Delegate to the original command - await this.config.runCommand("logs:channel-lifecycle:subscribe", [ - "--rewind", - flags.rewind.toString(), - ...(flags.json ? ["--json"] : []), - ...(flags.json ? ["--pretty-json"] : []), - // Forward all global flags - ...(flags.host ? ["--host", flags.host] : []), - ...(flags.env ? ["--env", flags.env] : []), - ...(flags["control-host"] - ? ["--control-host", flags["control-host"]] - : []), - ...(flags["access-token"] - ? ["--access-token", flags["access-token"]] - : []), - ...(flags["api-key"] ? ["--api-key", flags["api-key"]] : []), - ...(flags["client-id"] ? ["--client-id", flags["client-id"]] : []), - ...(flags.app ? ["--app", flags.app] : []), - ...(flags.endpoint ? ["--endpoint", flags.endpoint] : []), - ]); - } -} diff --git a/src/commands/connections/logs.ts b/src/commands/connections/logs.ts deleted file mode 100644 index c81360f6..00000000 --- a/src/commands/connections/logs.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Args, Flags } from "@oclif/core"; - -import { AblyBaseCommand } from "../../base-command.js"; - -export default class ConnectionsLogs extends AblyBaseCommand { - static override args = { - topic: Args.string({ - default: "connections-lifecycle", - description: - "Log topic to subscribe to (currently only connections-lifecycle is supported)", - }), - }; - - static override description = - "Alias for ably logs connection-lifecycle subscribe"; - - static override examples = [ - "$ ably connections logs connections-lifecycle", - "$ ably connections logs connections-lifecycle --rewind 10", - ]; - - static override flags = { - ...AblyBaseCommand.globalFlags, - json: Flags.boolean({ - default: false, - description: "Output results as JSON", - }), - rewind: Flags.integer({ - default: 0, - description: "Number of messages to rewind when subscribing", - }), - }; - - async run(): Promise { - const { args, flags } = await this.parse(ConnectionsLogs); - - // Currently only support connections-lifecycle - if (args.topic !== "connections-lifecycle") { - this.error( - `Unsupported log topic: ${args.topic}. Currently only 'connections-lifecycle' is supported.`, - ); - return; - } - - // Delegate to the original command - await this.config.runCommand("logs:connection-lifecycle:subscribe", [ - "--rewind", - flags.rewind.toString(), - ...(flags.json ? ["--json"] : []), - ...(flags.json ? ["--pretty-json"] : []), - // Forward all global flags - ...(flags.host ? ["--host", flags.host] : []), - ...(flags.env ? ["--env", flags.env] : []), - ...(flags["control-host"] - ? ["--control-host", flags["control-host"]] - : []), - ...(flags["access-token"] - ? ["--access-token", flags["access-token"]] - : []), - ...(flags["api-key"] ? ["--api-key", flags["api-key"]] : []), - ...(flags["client-id"] ? ["--client-id", flags["client-id"]] : []), - ...(flags.app ? ["--app", flags.app] : []), - ...(flags.endpoint ? ["--endpoint", flags.endpoint] : []), - ]); - } -} diff --git a/src/commands/interactive.ts b/src/commands/interactive.ts index 347fa3dd..0bad5285 100644 --- a/src/commands/interactive.ts +++ b/src/commands/interactive.ts @@ -184,10 +184,7 @@ export default class Interactive extends Command { // Commands available only for authenticated users if (!isAnonymousMode) { - commands.push( - ["channels logs", "View live channel events"], - ["channels list", "List active channels"], - ); + commands.push(["channels list", "List active channels"]); } commands.push( diff --git a/src/commands/logs/app/index.ts b/src/commands/logs/app/index.ts deleted file mode 100644 index 5c77d26d..00000000 --- a/src/commands/logs/app/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Command } from "@oclif/core"; - -export default class LogsApp extends Command { - static override description = - "Stream or retrieve logs from the app-wide meta channel [meta]log"; - - static override examples = [ - "$ ably logs app subscribe", - "$ ably logs app subscribe --rewind 10", - "$ ably logs app history", - ]; - - async run() { - this.log("App logs commands:"); - this.log(""); - this.log( - " ably logs app subscribe - Stream logs from the app-wide meta channel", - ); - this.log(" ably logs app history - View historical app logs"); - this.log(""); - this.log( - "Run `ably logs app COMMAND --help` for more information on a command.", - ); - } -} diff --git a/src/commands/logs/channel-lifecycle.ts b/src/commands/logs/channel-lifecycle.ts deleted file mode 100644 index 66742f3b..00000000 --- a/src/commands/logs/channel-lifecycle.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Flags } from "@oclif/core"; -import * as Ably from "ably"; -import chalk from "chalk"; - -import { AblyBaseCommand } from "../../base-command.js"; -import { waitUntilInterruptedOrTimeout } from "../../utils/long-running.js"; - -export default class LogsChannelLifecycle extends AblyBaseCommand { - static override description = - "Stream logs from [meta]channel.lifecycle meta channel"; - - static override examples = [ - "$ ably logs channel-lifecycle subscribe", - "$ ably logs channel-lifecycle subscribe --rewind 10", - ]; - - static override flags = { - ...AblyBaseCommand.globalFlags, - json: Flags.boolean({ - default: false, - description: "Output results as JSON", - }), - rewind: Flags.integer({ - default: 0, - description: "Number of messages to rewind when subscribing", - }), - }; - - async run(): Promise { - const { flags } = await this.parse(LogsChannelLifecycle); - - let client: Ably.Realtime | null = null; - - try { - // Create the Ably client - client = await this.createAblyRealtimeClient(flags); - if (!client) return; - - const channelName = "[meta]channel.lifecycle"; - const channelOptions: Ably.ChannelOptions = {}; - - // Configure rewind if specified - if (flags.rewind > 0) { - channelOptions.params = { - ...channelOptions.params, - rewind: flags.rewind.toString(), - }; - } - - const channel = client.channels.get(channelName, channelOptions); - - this.log(`Subscribing to ${chalk.cyan(channelName)}...`); - this.log("Press Ctrl+C to exit"); - this.log(""); - - // Subscribe to the channel - channel.subscribe((message) => { - const timestamp = message.timestamp - ? new Date(message.timestamp).toISOString() - : new Date().toISOString(); - const event = message.name || "unknown"; - - if (this.shouldOutputJson(flags)) { - const jsonOutput = { - channel: channelName, - data: message.data, - event: message.name || "unknown", - timestamp, - }; - this.log(this.formatJsonOutput(jsonOutput, flags)); - return; - } - - // Color-code different event types - let eventColor = chalk.blue; - - // For channel lifecycle events - if (event.includes("attached")) { - eventColor = chalk.green; - } else if (event.includes("detached")) { - eventColor = chalk.yellow; - } else if (event.includes("failed")) { - eventColor = chalk.red; - } else if (event.includes("suspended")) { - eventColor = chalk.magenta; - } - - // Format the log output - this.log( - `${chalk.dim(`[${timestamp}]`)} Channel: ${chalk.cyan(channelName)} | Event: ${eventColor(event)}`, - ); - if (this.shouldOutputJson(flags)) { - this.log(this.formatJsonOutput(message.data, flags)); - } else { - this.log(`Data: ${this.formatJsonOutput(message.data, flags)}`); - } - - this.log(""); - }); - - // Wait until interrupted - await waitUntilInterruptedOrTimeout(); - } catch (error: unknown) { - const err = error as Error; - this.error(err.message); - } - // Client cleanup is handled by command finally() method - } -} diff --git a/src/commands/logs/channel-lifecycle/index.ts b/src/commands/logs/channel-lifecycle/index.ts index 01907b74..80482da1 100644 --- a/src/commands/logs/channel-lifecycle/index.ts +++ b/src/commands/logs/channel-lifecycle/index.ts @@ -1,15 +1,14 @@ -import { Command } from "@oclif/core"; +import { BaseTopicCommand } from "../../../base-topic-command.js"; + +export default class LogsChannelLifecycleIndexCommand extends BaseTopicCommand { + protected topicName = "logs:channel-lifecycle"; + protected commandGroup = "logging"; -export default class LogsChannelLifecycle extends Command { static override description = "Stream logs from [meta]channel.lifecycle meta channel"; static override examples = [ - "$ ably logs channel-lifecycle subscribe", - "$ ably logs channel-lifecycle subscribe --rewind 10", + "ably logs channel-lifecycle subscribe", + "ably logs channel-lifecycle subscribe --rewind 10", ]; - - async run() { - this.log("Use the subscribe subcommand. See --help for more information."); - } } diff --git a/src/commands/logs/connection-lifecycle/index.ts b/src/commands/logs/connection-lifecycle/index.ts index d3ca8798..fe822747 100644 --- a/src/commands/logs/connection-lifecycle/index.ts +++ b/src/commands/logs/connection-lifecycle/index.ts @@ -1,15 +1,14 @@ -import { Command } from "@oclif/core"; +import { BaseTopicCommand } from "../../../base-topic-command.js"; + +export default class LogsConnectionLifecycleIndexCommand extends BaseTopicCommand { + protected topicName = "logs:connection-lifecycle"; + protected commandGroup = "logging"; -export default class LogsConnectionLifecycle extends Command { static override description = "Stream logs from [meta]connection.lifecycle meta channel"; static override examples = [ - "$ ably logs connection-lifecycle subscribe", - "$ ably logs connection-lifecycle subscribe --rewind 10", + "ably logs connection-lifecycle subscribe", + "ably logs connection-lifecycle subscribe --rewind 10", ]; - - async run() { - this.log("Use the subscribe subcommand. See --help for more information."); - } } diff --git a/src/commands/logs/connection/subscribe.ts b/src/commands/logs/connection/subscribe.ts deleted file mode 100644 index 16224ce0..00000000 --- a/src/commands/logs/connection/subscribe.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Flags } from "@oclif/core"; -import * as Ably from "ably"; -import chalk from "chalk"; - -import { AblyBaseCommand } from "../../../base-command.js"; -import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; - -export default class LogsConnectionSubscribe extends AblyBaseCommand { - static override description = "Subscribe to live connection logs"; - - static override examples = [ - "$ ably logs connection subscribe", - "$ ably logs connection subscribe --json", - "$ ably logs connection subscribe --pretty-json", - "$ ably logs connection subscribe --duration 30", - ]; - - static override flags = { - ...AblyBaseCommand.globalFlags, - duration: Flags.integer({ - description: - "Automatically exit after the given number of seconds (0 = run indefinitely)", - char: "D", - required: false, - }), - }; - - private cleanupInProgress = false; - private client: Ably.Realtime | null = null; - - async run(): Promise { - const { flags } = await this.parse(LogsConnectionSubscribe); - let channel: Ably.RealtimeChannel | null = null; - - try { - this.client = await this.createAblyRealtimeClient(flags); - if (!this.client) return; - - const client = this.client; - - // Set up connection state logging - this.setupConnectionStateLogging(client, flags, { - includeUserFriendlyMessages: true, - }); - - // Get the logs channel - const appConfig = await this.ensureAppAndKey(flags); - if (!appConfig) { - this.error("Unable to determine app configuration"); - return; - } - const logsChannelName = `[meta]connection.lifecycle`; - channel = client.channels.get(logsChannelName); - - // Set up channel state logging - this.setupChannelStateLogging(channel, flags, { - includeUserFriendlyMessages: true, - }); - - this.logCliEvent( - flags, - "logs", - "subscribing", - `Subscribing to connection logs`, - { channel: logsChannelName }, - ); - - if (!this.shouldOutputJson(flags)) { - this.log(`${chalk.green("Subscribing to connection logs")}`); - } - - // Subscribe to connection logs - channel.subscribe((message: Ably.Message) => { - const timestamp = message.timestamp - ? new Date(message.timestamp).toISOString() - : new Date().toISOString(); - const event = { - timestamp, - event: message.name || "connection.log", - data: message.data, - id: message.id, - }; - this.logCliEvent( - flags, - "logs", - "logReceived", - `Connection log received`, - event, - ); - - if (this.shouldOutputJson(flags)) { - this.log(this.formatJsonOutput(event, flags)); - } else { - this.log( - `${chalk.gray(`[${timestamp}]`)} ${chalk.cyan(`Event: ${event.event}`)}`, - ); - - if (message.data !== null && message.data !== undefined) { - this.log( - `${chalk.green("Data:")} ${JSON.stringify(message.data, null, 2)}`, - ); - } - - this.log(""); // Empty line for better readability - } - }); - - this.logCliEvent( - flags, - "logs", - "listening", - "Listening for connection log events. Press Ctrl+C to exit.", - ); - if (!this.shouldOutputJson(flags)) { - this.log("Listening for connection log events. Press Ctrl+C to exit."); - } - - // Wait until the user interrupts or the optional duration elapses - const exitReason = await waitUntilInterruptedOrTimeout(flags.duration); - this.logCliEvent(flags, "logs", "runComplete", "Exiting wait loop", { - exitReason, - }); - this.cleanupInProgress = exitReason === "signal"; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - this.logCliEvent( - flags, - "logs", - "fatalError", - `Error during connection logs subscription: ${errorMsg}`, - { error: errorMsg }, - ); - if (this.shouldOutputJson(flags)) { - this.jsonError({ error: errorMsg, success: false }, flags); - } else { - this.error(`Error: ${errorMsg}`); - } - } - } -} diff --git a/src/commands/logs/app/history.ts b/src/commands/logs/history.ts similarity index 89% rename from src/commands/logs/app/history.ts rename to src/commands/logs/history.ts index b702d037..033b3964 100644 --- a/src/commands/logs/app/history.ts +++ b/src/commands/logs/history.ts @@ -2,18 +2,18 @@ import { Flags } from "@oclif/core"; import * as Ably from "ably"; import chalk from "chalk"; -import { AblyBaseCommand } from "../../../base-command.js"; -import { formatJson, isJsonData } from "../../../utils/json-formatter.js"; +import { AblyBaseCommand } from "../../base-command.js"; +import { formatJson, isJsonData } from "../../utils/json-formatter.js"; -export default class LogsAppHistory extends AblyBaseCommand { +export default class LogsHistory extends AblyBaseCommand { static override description = "Retrieve application log history"; static override examples = [ - "$ ably logs app history", - "$ ably logs app history --limit 20", - "$ ably logs app history --direction forwards", - "$ ably logs app history --json", - "$ ably logs app history --pretty-json", + "$ ably logs history", + "$ ably logs history --limit 20", + "$ ably logs history --direction forwards", + "$ ably logs history --json", + "$ ably logs history --pretty-json", ]; static override flags = { @@ -30,7 +30,7 @@ export default class LogsAppHistory extends AblyBaseCommand { }; async run(): Promise { - const { flags } = await this.parse(LogsAppHistory); + const { flags } = await this.parse(LogsHistory); try { // Create a REST client diff --git a/src/commands/logs/index.ts b/src/commands/logs/index.ts index 9a5cc467..a6f44750 100644 --- a/src/commands/logs/index.ts +++ b/src/commands/logs/index.ts @@ -7,8 +7,8 @@ export default class Logs extends BaseTopicCommand { static override description = "Streaming and retrieving logs from Ably"; static override examples = [ - "<%= config.bin %> <%= command.id %> app subscribe", - "<%= config.bin %> <%= command.id %> app history", + "<%= config.bin %> <%= command.id %> subscribe", + "<%= config.bin %> <%= command.id %> history", "<%= config.bin %> <%= command.id %> channel-lifecycle subscribe", ]; } diff --git a/src/commands/logs/push/index.ts b/src/commands/logs/push/index.ts index 1981f285..0f73ad93 100644 --- a/src/commands/logs/push/index.ts +++ b/src/commands/logs/push/index.ts @@ -1,6 +1,9 @@ -import { Command } from "@oclif/core"; +import { BaseTopicCommand } from "../../../base-topic-command.js"; + +export default class LogsPushIndexCommand extends BaseTopicCommand { + protected topicName = "logs:push"; + protected commandGroup = "logging"; -export default class LogsPush extends Command { static override description = "Stream or retrieve push notification logs from [meta]log:push"; @@ -9,17 +12,4 @@ export default class LogsPush extends Command { "$ ably logs push subscribe --rewind 10", "$ ably logs push history", ]; - - async run() { - this.log("Push notification logs commands:"); - this.log(""); - this.log( - " ably logs push subscribe - Stream logs from the app push notifications", - ); - this.log(" ably logs push history - View historical push logs"); - this.log(""); - this.log( - "Run `ably logs push COMMAND --help` for more information on a command.", - ); - } } diff --git a/src/commands/logs/app/subscribe.ts b/src/commands/logs/subscribe.ts similarity index 90% rename from src/commands/logs/app/subscribe.ts rename to src/commands/logs/subscribe.ts index 62a55e41..27024d21 100644 --- a/src/commands/logs/app/subscribe.ts +++ b/src/commands/logs/subscribe.ts @@ -2,19 +2,19 @@ import { Flags } from "@oclif/core"; import * as Ably from "ably"; import chalk from "chalk"; -import { AblyBaseCommand } from "../../../base-command.js"; -import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; +import { AblyBaseCommand } from "../../base-command.js"; +import { waitUntilInterruptedOrTimeout } from "../../utils/long-running.js"; -export default class LogsAppSubscribe extends AblyBaseCommand { +export default class LogsSubscribe extends AblyBaseCommand { static override description = "Subscribe to live app logs"; static override examples = [ - "$ ably logs app subscribe", - "$ ably logs app subscribe --rewind 10", - "$ ably logs app subscribe --type channel.lifecycle", - "$ ably logs app subscribe --json", - "$ ably logs app subscribe --pretty-json", - "$ ably logs app subscribe --duration 30", + "$ ably logs subscribe", + "$ ably logs subscribe --rewind 10", + "$ ably logs subscribe --type channel.lifecycle", + "$ ably logs subscribe --json", + "$ ably logs subscribe --pretty-json", + "$ ably logs subscribe --duration 30", ]; static override flags = { @@ -45,7 +45,7 @@ export default class LogsAppSubscribe extends AblyBaseCommand { private client: Ably.Realtime | null = null; async run(): Promise { - const { flags } = await this.parse(LogsAppSubscribe); + const { flags } = await this.parse(LogsSubscribe); let channel: Ably.RealtimeChannel | null = null; let subscribedEvents: string[] = []; diff --git a/src/help.ts b/src/help.ts index c3e70d40..ca359d65 100644 --- a/src/help.ts +++ b/src/help.ts @@ -374,7 +374,22 @@ export default class CustomHelp extends Help { // Commands available only for authenticated users if (!isAnonymousMode) { - commands.push([`${cmdPrefix}channels logs`, "View live channel events"]); + commands.push( + [`${cmdPrefix}logs history`, "Retrieve application log history"], + [`${cmdPrefix}logs subscribe`, "Subscribe to live app logs"], + [ + `${cmdPrefix}logs channel-lifecycle subscribe`, + "Stream logs from [meta]channel.lifecycle meta channel", + ], + [ + `${cmdPrefix}logs connection-lifecycle subscribe`, + "Stream logs from [meta]connection.lifecycle meta channel", + ], + [ + `${cmdPrefix}logs push subscribe`, + "Stream logs from the push notifications meta channel [meta]log:push", + ], + ); } commands.push( diff --git a/test/unit/commands/apps/logs/history.test.ts b/test/unit/commands/apps/logs/history.test.ts deleted file mode 100644 index 4a1a9301..00000000 --- a/test/unit/commands/apps/logs/history.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { getMockAblyRest } from "../../../../helpers/mock-ably-rest.js"; - -describe("apps:logs:history command", () => { - beforeEach(() => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - channel.history.mockResolvedValue({ items: [] }); - }); - - describe("successful log history retrieval", () => { - it("should retrieve application log history", async () => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - const mockTimestamp = 1234567890000; - const mockLogMessage = "User login successful"; - const mockLogLevel = "info"; - - channel.history.mockResolvedValue({ - items: [ - { - name: "log.info", - data: { - message: mockLogMessage, - level: mockLogLevel, - userId: "user123", - }, - timestamp: mockTimestamp, - }, - ], - }); - - const { stdout } = await runCommand( - ["apps:logs:history"], - import.meta.url, - ); - - // Verify the correct channel was requested - expect(mock.channels.get).toHaveBeenCalledWith("[meta]log"); - - // Verify history was called with default parameters - expect(channel.history).toHaveBeenCalledWith({ - direction: "backwards", - limit: 100, - }); - - // Verify output contains the log count - expect(stdout).toContain("Found 1 application log messages"); - - // Verify output contains the log event name - expect(stdout).toContain("log.info"); - - // Verify output contains the actual log message content - expect(stdout).toContain(mockLogMessage); - expect(stdout).toContain(mockLogLevel); - expect(stdout).toContain("user123"); - }); - - it("should handle empty log history", async () => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - channel.history.mockResolvedValue({ items: [] }); - - const { stdout } = await runCommand( - ["apps:logs:history"], - import.meta.url, - ); - - expect(stdout).toContain("No application log messages found"); - }); - - it("should accept limit flag", async () => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - channel.history.mockResolvedValue({ items: [] }); - - await runCommand(["apps:logs:history", "--limit", "50"], import.meta.url); - - // Verify history was called with custom limit - expect(channel.history).toHaveBeenCalledWith({ - direction: "backwards", - limit: 50, - }); - }); - - it("should accept direction flag", async () => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - channel.history.mockResolvedValue({ items: [] }); - - await runCommand( - ["apps:logs:history", "--direction", "forwards"], - import.meta.url, - ); - - // Verify history was called with forwards direction - expect(channel.history).toHaveBeenCalledWith({ - direction: "forwards", - limit: 100, - }); - }); - - it("should display multiple log messages with their content", async () => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - const timestamp1 = 1234567890000; - const timestamp2 = 1234567891000; - - channel.history.mockResolvedValue({ - items: [ - { - name: "log.info", - data: { message: "First log entry", operation: "login" }, - timestamp: timestamp1, - }, - { - name: "log.error", - data: { message: "Error occurred", error: "Database timeout" }, - timestamp: timestamp2, - }, - ], - }); - - const { stdout } = await runCommand( - ["apps:logs:history"], - import.meta.url, - ); - - expect(stdout).toContain("Found 2 application log messages"); - expect(stdout).toContain("log.info"); - expect(stdout).toContain("log.error"); - - // Verify actual log content is displayed - expect(stdout).toContain("First log entry"); - expect(stdout).toContain("login"); - expect(stdout).toContain("Error occurred"); - expect(stdout).toContain("Database timeout"); - }); - - it("should handle string data in messages", async () => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - channel.history.mockResolvedValue({ - items: [ - { - name: "log.warning", - data: "Simple string log message", - timestamp: Date.now(), - }, - ], - }); - - const { stdout } = await runCommand( - ["apps:logs:history"], - import.meta.url, - ); - - expect(stdout).toContain("Simple string log message"); - }); - - it("should show limit warning when max messages reached", async () => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - const messages = Array.from({ length: 50 }, (_, i) => ({ - name: "log.info", - data: `Message ${i}`, - timestamp: Date.now() + i, - })); - - channel.history.mockResolvedValue({ - items: messages, - }); - - const { stdout } = await runCommand( - ["apps:logs:history", "--limit", "50"], - import.meta.url, - ); - - expect(stdout).toContain("Showing maximum of 50 messages"); - }); - - it("should output JSON format when --json flag is used", async () => { - const mock = getMockAblyRest(); - const channel = mock.channels._getChannel("[meta]log"); - const mockMessage = { - name: "log.info", - data: { message: "Test message", severity: "info" }, - timestamp: Date.now(), - }; - - channel.history.mockResolvedValue({ - items: [mockMessage], - }); - - const { stdout } = await runCommand( - ["apps:logs:history", "--json"], - import.meta.url, - ); - - const parsed = JSON.parse(stdout); - expect(parsed).toHaveProperty("messages"); - expect(parsed.messages).toHaveLength(1); - expect(parsed.messages[0]).toHaveProperty("name", "log.info"); - expect(parsed.messages[0].data).toHaveProperty("message", "Test message"); - }); - }); -}); diff --git a/test/unit/commands/apps/logs/subscribe.test.ts b/test/unit/commands/apps/logs/subscribe.test.ts deleted file mode 100644 index 0f65fde1..00000000 --- a/test/unit/commands/apps/logs/subscribe.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { getMockAblyRealtime } from "../../../../helpers/mock-ably-realtime.js"; - -describe("apps:logs:subscribe command", () => { - beforeEach(() => { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]log"); - - // Configure connection.once to immediately call callback for 'connected' - mock.connection.once.mockImplementation( - (event: string, callback: () => void) => { - if (event === "connected") { - callback(); - } - }, - ); - - // Configure channel.once to immediately call callback for 'attached' - channel.once.mockImplementation((event: string, callback: () => void) => { - if (event === "attached") { - channel.state = "attached"; - callback(); - } - }); - }); - - describe("command flags", () => { - it("should reject unknown flags", async () => { - const { error } = await runCommand( - ["apps:logs:subscribe", "--unknown-flag-xyz"], - import.meta.url, - ); - - // Unknown flag should cause an error - expect(error).toBeDefined(); - expect(error!.message).toMatch(/unknown|Nonexistent flag/i); - }); - }); - - describe("alias behavior", () => { - it("should delegate to logs:app:subscribe with --rewind flag", async () => { - const mock = getMockAblyRealtime(); - - const { stdout } = await runCommand( - ["apps:logs:subscribe", "--rewind", "5"], - import.meta.url, - ); - - // Should delegate to logs:app:subscribe and show subscription message - expect(stdout).toContain("Subscribing to app logs"); - // Verify rewind was passed through - expect(mock.channels.get).toHaveBeenCalledWith("[meta]log", { - params: { rewind: "5" }, - }); - }); - - it("should accept --json flag", async () => { - const { error } = await runCommand( - ["apps:logs:subscribe", "--json"], - import.meta.url, - ); - - // Should not have unknown flag error - expect(error?.message || "").not.toMatch(/unknown|Nonexistent flag/i); - }); - }); -}); diff --git a/test/unit/commands/interactive-anonymous-mode.test.ts b/test/unit/commands/interactive-anonymous-mode.test.ts index d0173794..c3deed70 100644 --- a/test/unit/commands/interactive-anonymous-mode.test.ts +++ b/test/unit/commands/interactive-anonymous-mode.test.ts @@ -82,7 +82,7 @@ describe("Interactive Mode - Anonymous Restrictions", () => { }); it("should handle logs wildcard pattern", () => { - expect(matchesPattern("logs:app:history", "logs*")).toBe(true); + expect(matchesPattern("logs:history", "logs*")).toBe(true); expect(matchesPattern("logs:push:subscribe", "logs*")).toBe(true); expect(matchesPattern("logs", "logs*")).toBe(true); }); diff --git a/test/unit/commands/logs/channel-lifecycle.test.ts b/test/unit/commands/logs/channel-lifecycle.test.ts deleted file mode 100644 index fb883289..00000000 --- a/test/unit/commands/logs/channel-lifecycle.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { getMockAblyRealtime } from "../../../helpers/mock-ably-realtime.js"; - -describe("logs:channel-lifecycle command", () => { - beforeEach(() => { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]channel.lifecycle"); - - // Configure connection.once to immediately call callback for 'connected' - mock.connection.once.mockImplementation( - (event: string, callback: () => void) => { - if (event === "connected") { - callback(); - } - }, - ); - - // Configure channel.once to immediately call callback for 'attached' - channel.once.mockImplementation((event: string, callback: () => void) => { - if (event === "attached") { - channel.state = "attached"; - callback(); - } - }); - }); - - describe("command flags", () => { - it("should reject unknown flags", async () => { - const { error } = await runCommand( - ["logs:channel-lifecycle", "--unknown-flag-xyz"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error!.message).toMatch(/unknown|Nonexistent flag/i); - }); - }); - - describe("subscription behavior", () => { - it("should subscribe to [meta]channel.lifecycle and show initial message", async () => { - // Emit SIGINT after a short delay to exit the command - - const { stdout } = await runCommand( - ["logs:channel-lifecycle"], - import.meta.url, - ); - - expect(stdout).toContain("Subscribing to"); - expect(stdout).toContain("[meta]channel.lifecycle"); - expect(stdout).toContain("Press Ctrl+C to exit"); - }); - - it("should get channel without rewind params when --rewind is not specified", async () => { - const mock = getMockAblyRealtime(); - - await runCommand(["logs:channel-lifecycle"], import.meta.url); - - // Verify channel was gotten with empty options (no rewind) - expect(mock.channels.get).toHaveBeenCalledWith( - "[meta]channel.lifecycle", - {}, - ); - }); - - it("should configure rewind channel option when --rewind is specified", async () => { - const mock = getMockAblyRealtime(); - - await runCommand( - ["logs:channel-lifecycle", "--rewind", "5"], - import.meta.url, - ); - - // Verify channel was gotten with rewind params - expect(mock.channels.get).toHaveBeenCalledWith( - "[meta]channel.lifecycle", - { - params: { - rewind: "5", - }, - }, - ); - }); - - it("should subscribe to channel messages", async () => { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]channel.lifecycle"); - - await runCommand(["logs:channel-lifecycle"], import.meta.url); - - // Verify subscribe was called with a callback function - expect(channel.subscribe).toHaveBeenCalledWith(expect.any(Function)); - }); - }); - - describe("error handling", () => { - it("should handle subscription errors gracefully", async () => { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]channel.lifecycle"); - channel.subscribe.mockImplementation(() => { - throw new Error("Subscription failed"); - }); - - const { error } = await runCommand( - ["logs:channel-lifecycle"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error?.message).toMatch(/Subscription failed/i); - }); - - it("should handle missing mock client in test mode", async () => { - // Clear the realtime mock - if (globalThis.__TEST_MOCKS__) { - delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; - } - - const { error } = await runCommand( - ["logs:channel-lifecycle"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error?.message).toMatch(/No mock|client/i); - }); - }); - - describe("cleanup behavior", () => { - it("should call client.close on cleanup", async () => { - const mock = getMockAblyRealtime(); - - await runCommand(["logs:channel-lifecycle"], import.meta.url); - - // Verify close was called during cleanup - expect(mock.close).toHaveBeenCalled(); - }); - }); - - describe("output formats", () => { - it("should accept --json flag", async () => { - const { error } = await runCommand( - ["logs:channel-lifecycle", "--json"], - import.meta.url, - ); - - // No flag-related error should occur - expect(error?.message || "").not.toMatch(/unknown|Nonexistent flag/i); - }); - }); -}); diff --git a/test/unit/commands/logs/connection/subscribe.test.ts b/test/unit/commands/logs/connection/subscribe.test.ts deleted file mode 100644 index d0fd92df..00000000 --- a/test/unit/commands/logs/connection/subscribe.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { getMockAblyRealtime } from "../../../../helpers/mock-ably-realtime.js"; - -describe("LogsConnectionSubscribe", function () { - beforeEach(function () { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]connection.lifecycle"); - - // Configure connection.on to simulate connection state changes - mock.connection.on.mockImplementation( - (event: string, callback: () => void) => { - if (event === "connected") { - setTimeout(() => { - mock.connection.state = "connected"; - callback(); - }, 10); - } - }, - ); - - // Configure channel.once to immediately call callback for 'attached' - channel.once.mockImplementation((event: string, callback: () => void) => { - if (event === "attached") { - channel.state = "attached"; - callback(); - } - }); - }); - - it("should subscribe to [meta]connection.lifecycle channel", async function () { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]connection.lifecycle"); - - // Emit SIGINT to stop the command - - const { stdout } = await runCommand( - ["logs:connection:subscribe"], - import.meta.url, - ); - - expect(mock.channels.get).toHaveBeenCalledWith( - "[meta]connection.lifecycle", - ); - expect(channel.subscribe).toHaveBeenCalled(); - expect(stdout).toContain("Subscribing"); - }); - - it("should handle log message reception", async function () { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]connection.lifecycle"); - - // Capture the subscription callback - let messageCallback: ((message: unknown) => void) | null = null; - channel.subscribe.mockImplementation( - (callback: (message: unknown) => void) => { - messageCallback = callback; - }, - ); - - // Simulate receiving a message - setTimeout(() => { - if (messageCallback) { - messageCallback({ - name: "connection.opened", - data: { connectionId: "test-connection-123" }, - timestamp: Date.now(), - clientId: "test-client", - connectionId: "test-connection-123", - id: "msg-123", - }); - } - }, 50); - - // Stop the command after message is received - - const { stdout } = await runCommand( - ["logs:connection:subscribe"], - import.meta.url, - ); - - expect(stdout).toContain("connection.opened"); - }); - - it("should output JSON when requested", async function () { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]connection.lifecycle"); - - // Capture the subscription callback - let messageCallback: ((message: unknown) => void) | null = null; - channel.subscribe.mockImplementation( - (callback: (message: unknown) => void) => { - messageCallback = callback; - }, - ); - - // Simulate receiving a message - setTimeout(() => { - if (messageCallback) { - messageCallback({ - name: "connection.opened", - data: { connectionId: "test-connection-123" }, - timestamp: Date.now(), - clientId: "test-client", - connectionId: "test-connection-123", - id: "msg-123", - }); - } - }, 50); - - const { stdout } = await runCommand( - ["logs:connection:subscribe", "--json"], - import.meta.url, - ); - - // Verify JSON output - the output contains the event name in JSON format - expect(stdout).toContain("connection.opened"); - }); - - it("should handle connection state changes", async function () { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]connection.lifecycle"); - - // Capture the subscription callback - let messageCallback: ((message: unknown) => void) | null = null; - channel.subscribe.mockImplementation( - (callback: (message: unknown) => void) => { - messageCallback = callback; - }, - ); - - // Simulate receiving a connection state change event - setTimeout(() => { - if (messageCallback) { - messageCallback({ - name: "connection.connected", - data: { - connectionId: "test-connection-456", - transport: "websocket", - }, - timestamp: Date.now(), - clientId: "test-client", - connectionId: "test-connection-456", - id: "msg-state-change", - }); - } - }, 50); - - const { stdout } = await runCommand( - ["logs:connection:subscribe"], - import.meta.url, - ); - - expect(channel.subscribe).toHaveBeenCalled(); - expect(stdout).toContain("connection.connected"); - }); - - it("should handle connection failures", async function () { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("[meta]connection.lifecycle"); - - // Capture the subscription callback - let messageCallback: ((message: unknown) => void) | null = null; - channel.subscribe.mockImplementation( - (callback: (message: unknown) => void) => { - messageCallback = callback; - }, - ); - - // Simulate receiving a connection failure event - setTimeout(() => { - if (messageCallback) { - messageCallback({ - name: "connection.failed", - data: { - connectionId: "test-connection-789", - reason: "Network error", - }, - timestamp: Date.now(), - clientId: "test-client", - connectionId: "test-connection-789", - id: "msg-failed", - }); - } - }, 50); - - const { stdout } = await runCommand( - ["logs:connection:subscribe"], - import.meta.url, - ); - - expect(channel.subscribe).toHaveBeenCalled(); - expect(stdout).toContain("connection.failed"); - }); - - it("should handle missing mock client in test mode", async function () { - // Clear the realtime mock - if (globalThis.__TEST_MOCKS__) { - delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; - } - - const { error } = await runCommand( - ["logs:connection:subscribe"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error?.message).toMatch(/No mock|client/i); - }); -}); diff --git a/test/unit/commands/logs/app/subscribe.test.ts b/test/unit/commands/logs/subscribe.test.ts similarity index 78% rename from test/unit/commands/logs/app/subscribe.test.ts rename to test/unit/commands/logs/subscribe.test.ts index e8592b48..ec535fae 100644 --- a/test/unit/commands/logs/app/subscribe.test.ts +++ b/test/unit/commands/logs/subscribe.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach } from "vitest"; import { runCommand } from "@oclif/test"; -import { getMockAblyRealtime } from "../../../../helpers/mock-ably-realtime.js"; +import { getMockAblyRealtime } from "../../../helpers/mock-ably-realtime.js"; -describe("logs:app:subscribe command", () => { +describe("logs:subscribe command", () => { beforeEach(() => { const mock = getMockAblyRealtime(); const channel = mock.channels._getChannel("[meta]log"); @@ -28,7 +28,7 @@ describe("logs:app:subscribe command", () => { describe("command flags", () => { it("should reject unknown flags", async () => { const { error } = await runCommand( - ["logs:app:subscribe", "--unknown-flag-xyz"], + ["logs:subscribe", "--unknown-flag-xyz"], import.meta.url, ); @@ -39,7 +39,7 @@ describe("logs:app:subscribe command", () => { it("should accept --rewind flag", async () => { // Run with --duration 0 to exit immediately const { error } = await runCommand( - ["logs:app:subscribe", "--rewind", "10", "--duration", "0"], + ["logs:subscribe", "--rewind", "10", "--duration", "0"], import.meta.url, ); @@ -49,13 +49,7 @@ describe("logs:app:subscribe command", () => { it("should accept --type flag with valid option", async () => { const { error } = await runCommand( - [ - "logs:app:subscribe", - "--type", - "channel.lifecycle", - "--duration", - "0", - ], + ["logs:subscribe", "--type", "channel.lifecycle", "--duration", "0"], import.meta.url, ); @@ -65,10 +59,7 @@ describe("logs:app:subscribe command", () => { describe("subscription behavior", () => { it("should subscribe to log channel and show initial message", async () => { - const { stdout } = await runCommand( - ["logs:app:subscribe"], - import.meta.url, - ); + const { stdout } = await runCommand(["logs:subscribe"], import.meta.url); expect(stdout).toContain("Subscribing to app logs"); expect(stdout).toContain("Press Ctrl+C to exit"); @@ -79,7 +70,7 @@ describe("logs:app:subscribe command", () => { const channel = mock.channels._getChannel("[meta]log"); await runCommand( - ["logs:app:subscribe", "--type", "channel.lifecycle"], + ["logs:subscribe", "--type", "channel.lifecycle"], import.meta.url, ); @@ -93,10 +84,7 @@ describe("logs:app:subscribe command", () => { it("should configure rewind when --rewind is specified", async () => { const mock = getMockAblyRealtime(); - await runCommand( - ["logs:app:subscribe", "--rewind", "10"], - import.meta.url, - ); + await runCommand(["logs:subscribe", "--rewind", "10"], import.meta.url); // Verify channel was gotten with rewind params expect(mock.channels.get).toHaveBeenCalledWith("[meta]log", { @@ -112,10 +100,7 @@ describe("logs:app:subscribe command", () => { delete globalThis.__TEST_MOCKS__.ablyRealtimeMock; } - const { error } = await runCommand( - ["logs:app:subscribe"], - import.meta.url, - ); + const { error } = await runCommand(["logs:subscribe"], import.meta.url); expect(error).toBeDefined(); expect(error?.message).toMatch(/No mock|client/i); diff --git a/test/unit/help/interactive-help.test.ts b/test/unit/help/interactive-help.test.ts index d8771315..4fb9b194 100644 --- a/test/unit/help/interactive-help.test.ts +++ b/test/unit/help/interactive-help.test.ts @@ -145,7 +145,7 @@ ably> channels publish test "msg2" hidden: false, }, { - id: "logs:app:history", + id: "logs:history", description: "View app logs", hidden: false, }, @@ -200,7 +200,7 @@ ably> channels publish test "msg2" expect(help.shouldDisplay({ id: "apps:list" } as any)).toBe(false); expect(help.shouldDisplay({ id: "bench:publisher" } as any)).toBe(false); expect(help.shouldDisplay({ id: "auth:keys:list" } as any)).toBe(false); - expect(help.shouldDisplay({ id: "logs:app:history" } as any)).toBe(false); + expect(help.shouldDisplay({ id: "logs:history" } as any)).toBe(false); expect(help.shouldDisplay({ id: "spaces:list" } as any)).toBe(false); expect(help.shouldDisplay({ id: "rooms:list" } as any)).toBe(false); expect(help.shouldDisplay({ id: "integrations:create" } as any)).toBe( diff --git a/test/unit/help/web-cli-help.test.ts b/test/unit/help/web-cli-help.test.ts index f1fb49f3..95b091d2 100644 --- a/test/unit/help/web-cli-help.test.ts +++ b/test/unit/help/web-cli-help.test.ts @@ -103,10 +103,6 @@ describe("CLI Help", function () { expect(output).toContain("channels subscribe [channel]"); expect(output).toContain("Subscribe to a channel"); - // Should show channels:logs command for authenticated users - expect(output).toContain("channels logs"); - expect(output).toContain("View live channel events"); - // Check for help instructions (less brittle - just check key parts) expect(output).toContain("Type"); expect(output).toContain("help"); @@ -237,7 +233,7 @@ describe("CLI Help", function () { expect(output).not.toContain("mcp"); }); - it("should hide channels:logs in anonymous mode", async function () { + it("should show common commands in anonymous mode", async function () { const mockConfig = createMockConfig(); const help = new CustomHelp(mockConfig); @@ -263,10 +259,6 @@ describe("CLI Help", function () { expect(output).toContain("channels subscribe [channel]"); expect(output).toContain("Subscribe to a channel"); - // Should NOT show channels:logs command for anonymous users - expect(output).not.toContain("channels logs"); - expect(output).not.toContain("View live channel events"); - // Clean up delete process.env.ABLY_ANONYMOUS_USER_MODE; }); @@ -352,7 +344,9 @@ describe("CLI Help", function () { expect(help.shouldDisplay({ id: "channels:subscribe" } as any)).toBe( true, ); - expect(help.shouldDisplay({ id: "channels:logs" } as any)).toBe(true); // Now allowed for authenticated users + expect(help.shouldDisplay({ id: "channels:history" } as any)).toBe( + true, + ); // Allowed for authenticated users expect(help.shouldDisplay({ id: "rooms:get" } as any)).toBe(true); expect(help.shouldDisplay({ id: "help" } as any)).toBe(true); });