From 99b9745fecce63b03cf64779d5b833d4cb772924 Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 3 Mar 2026 23:20:43 +0000 Subject: [PATCH 1/7] Streamline global flags so commands only expose what they need MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DX-786: Replaced monolithic globalFlags (16 flags on every command) with composable flag groups. Utility commands like `ably version` now only show --verbose/--json. Auth flags (--api-key, --access-token, --token) removed entirely — auth is via `ably login` or env vars (ABLY_API_KEY, ABLY_TOKEN, ABLY_ACCESS_TOKEN). Moved --endpoint to login/switch only, stored per-account in config. Removed --env and --host. Kept --client-id on presence commands. --- src/base-command.ts | 230 +++++------------- src/chat-base-command.ts | 2 + src/commands/accounts/login.ts | 7 + src/commands/accounts/switch.ts | 7 + src/commands/apps/create.ts | 2 +- src/commands/apps/delete.ts | 2 +- src/commands/apps/update.ts | 2 +- src/commands/auth/issue-ably-token.ts | 5 +- src/commands/auth/issue-jwt-token.ts | 5 +- src/commands/auth/revoke-token.ts | 3 +- src/commands/bench/publisher.ts | 3 +- src/commands/bench/subscriber.ts | 3 +- src/commands/channels/batch-publish.ts | 3 +- src/commands/channels/history.ts | 3 +- src/commands/channels/inspect.ts | 4 +- src/commands/channels/list.ts | 3 +- src/commands/channels/occupancy/get.ts | 5 +- src/commands/channels/occupancy/subscribe.ts | 6 +- src/commands/channels/presence/enter.ts | 7 +- src/commands/channels/presence/subscribe.ts | 7 +- src/commands/channels/publish.ts | 6 +- src/commands/channels/subscribe.ts | 6 +- src/commands/config/index.ts | 3 +- src/commands/config/path.ts | 3 +- src/commands/config/show.ts | 3 +- src/commands/connections/test.ts | 3 +- .../logs/channel-lifecycle/subscribe.ts | 3 +- .../logs/connection-lifecycle/history.ts | 3 +- .../logs/connection-lifecycle/subscribe.ts | 3 +- src/commands/logs/history.ts | 3 +- src/commands/logs/push/history.ts | 3 +- src/commands/logs/push/subscribe.ts | 3 +- src/commands/logs/subscribe.ts | 3 +- src/commands/rooms/messages/history.ts | 2 +- .../rooms/messages/reactions/remove.ts | 2 +- src/commands/rooms/messages/reactions/send.ts | 2 +- src/commands/rooms/messages/send.ts | 2 +- src/commands/rooms/messages/subscribe.ts | 2 +- src/commands/rooms/occupancy/get.ts | 2 +- src/commands/rooms/reactions/send.ts | 2 +- src/commands/rooms/typing/keystroke.ts | 2 +- src/commands/rooms/typing/subscribe.ts | 2 +- src/commands/spaces/cursors/set.ts | 2 +- src/commands/version.ts | 3 +- src/control-base-command.ts | 14 +- src/flags.ts | 99 ++++++++ src/services/config-manager.ts | 27 ++ src/spaces-base-command.ts | 2 + src/types/cli.ts | 6 +- test/helpers/mock-config-manager.ts | 19 ++ test/unit/base/auth-info-display.test.ts | 40 ++- test/unit/base/base-command.test.ts | 54 ++-- test/unit/commands/apps/create.test.ts | 13 +- test/unit/commands/apps/delete.test.ts | 13 +- test/unit/commands/apps/list.test.ts | 9 +- test/unit/commands/apps/update.test.ts | 14 +- test/unit/commands/auth/keys/create.test.ts | 15 +- test/unit/commands/auth/revoke-token.test.ts | 59 +---- test/unit/commands/bench/bench.test.ts | 2 - test/unit/commands/bench/benchmarking.test.ts | 14 +- .../commands/channels/batch-publish.test.ts | 46 +--- test/unit/commands/channels/history.test.ts | 54 +--- test/unit/commands/channels/list.test.ts | 36 +-- .../commands/channels/presence/enter.test.ts | 41 +--- .../channels/presence/subscribe.test.ts | 29 +-- test/unit/commands/channels/subscribe.test.ts | 29 +-- .../commands/interactive-autocomplete.test.ts | 40 ++- test/unit/commands/queues/create.test.ts | 13 +- test/unit/commands/queues/delete.test.ts | 13 +- test/unit/commands/queues/list.test.ts | 10 +- test/unit/commands/spaces/spaces.test.ts | 26 +- 71 files changed, 475 insertions(+), 639 deletions(-) create mode 100644 src/flags.ts diff --git a/src/base-command.ts b/src/base-command.ts index de970dd1..a57bb9e4 100644 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -1,4 +1,3 @@ -import { Flags } from "@oclif/core"; import { InteractiveBaseCommand } from "./interactive-base-command.js"; import * as Ably from "ably"; import chalk from "chalk"; @@ -10,6 +9,7 @@ import { createConfigManager, } from "./services/config-manager.js"; import { ControlApi } from "./services/control-api.js"; +import { coreGlobalFlags } from "./flags.js"; import { InteractiveHelper } from "./services/interactive-helper.js"; import { BaseFlags, CommandConfig, ErrorDetails } from "./types/cli.js"; import { getCliVersion } from "./utils/version.js"; @@ -96,78 +96,8 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { private _cachedRestClient: Ably.Rest | null = null; private _cachedRealtimeClient: Ably.Realtime | null = null; - // Add static flags that will be available to all commands - static globalFlags = { - "access-token": Flags.string({ - description: - "Overrides any configured access token used for the Control API", - }), - "api-key": Flags.string({ - description: "Overrides any configured API key used for the product APIs", - }), - "client-id": Flags.string({ - description: - 'Overrides any default client ID when using API authentication. Use "none" to explicitly set no client ID. Not applicable when using token authentication.', - }), - "control-host": Flags.string({ - description: - "Override the host endpoint for the control API, which defaults to control.ably.net", - 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", - }), - endpoint: Flags.string({ - description: "Override the endpoint for all product API calls", - env: "ABLY_ENDPOINT", - }), - host: Flags.string({ - description: "Override the host endpoint for all product API calls", - }), - port: Flags.integer({ - description: "Override the port for product API calls", - hidden: process.env.ABLY_SHOW_DEV_FLAGS !== "true", - }), - tlsPort: Flags.integer({ - description: "Override the TLS port for product API calls", - hidden: process.env.ABLY_SHOW_DEV_FLAGS !== "true", - }), - tls: Flags.string({ - description: "Use TLS for product API calls (default is true)", - hidden: process.env.ABLY_SHOW_DEV_FLAGS !== "true", - }), - json: Flags.boolean({ - description: "Output in JSON format", - exclusive: ["pretty-json"], // Cannot use with pretty-json - }), - "pretty-json": Flags.boolean({ - description: "Output in colorized JSON format", - exclusive: ["json"], // Cannot use with json - }), - token: Flags.string({ - description: - "Authenticate using an Ably Token or JWT Token instead of an API key", - }), - verbose: Flags.boolean({ - char: "v", - default: false, - description: "Output verbose logs", - required: false, - }), - // Web CLI specific flag, hidden from regular help - "web-cli-help": Flags.boolean({ - description: "Show help formatted for the web CLI", - hidden: true, // Hide from regular help output - }), - }; + // Core global flags available to all commands (verbose, json, pretty-json, web-cli-help) + static globalFlags = { ...coreGlobalFlags }; protected configManager: ConfigManager; protected interactiveHelper: InteractiveHelper; @@ -443,19 +373,21 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { return null; } - // Track whether the user explicitly provided authentication + // Track whether the user explicitly provided authentication via env vars const hasExplicitAuth = !!( - flags.token || - flags["api-key"] || - process.env.ABLY_API_KEY + process.env.ABLY_TOKEN || process.env.ABLY_API_KEY ); - // If token is provided or API key is in environment, we can skip the ensureAppAndKey step - if (!flags.token && !flags["api-key"] && !process.env.ABLY_API_KEY) { + // Auth resolution: env vars first, then config via ensureAppAndKey + if (process.env.ABLY_TOKEN) { + // ABLY_TOKEN env var — skip ensureAppAndKey, use token auth + } else if (process.env.ABLY_API_KEY) { + // ABLY_API_KEY env var — skip ensureAppAndKey, use key auth + } else { const appAndKey = await this.ensureAppAndKey(flags); if (!appAndKey) { this.error( - `${chalk.yellow("No app or API key configured for this command")}.\nPlease log in first with "${chalk.cyan("ably accounts login")}" (recommended approach).\nAlternatively you can provide an API key with the ${chalk.cyan("--api-key")} argument or set the ${chalk.cyan("ABLY_API_KEY")} environment variable.`, + `${chalk.yellow("No app or API key configured for this command")}.\nPlease log in first with "${chalk.cyan("ably accounts login")}" (recommended approach).\nAlternatively you can set the ${chalk.cyan("ABLY_API_KEY")} environment variable.`, ); return null; } @@ -615,14 +547,13 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { // Get access token for control API const currentAccount = this.configManager.getCurrentAccount(); const accessToken = - flags["access-token"] || - process.env.ABLY_ACCESS_TOKEN || - currentAccount?.accessToken; + process.env.ABLY_ACCESS_TOKEN || currentAccount?.accessToken; if (accessToken) { const controlApi = new ControlApi({ accessToken, - controlHost: flags["control-host"], + controlHost: + flags["control-host"] || process.env.ABLY_CONTROL_HOST, }); const app = await controlApi.getApp(appId); appName = app.name; @@ -654,12 +585,13 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { ); // Check auth method - token or API key - if (flags.token) { + const tokenFromEnv = process.env.ABLY_TOKEN; + if (tokenFromEnv) { // For token auth, show truncated token const truncatedToken = - flags.token.length > 20 - ? `${flags.token.slice(0, 17)}...` - : flags.token; + tokenFromEnv.length > 20 + ? `${tokenFromEnv.slice(0, 17)}...` + : tokenFromEnv; displayParts.push( `${chalk.magenta("Auth=")}${chalk.magenta.bold("Token")} ${chalk.gray(`(${truncatedToken})`)}`, ); @@ -749,20 +681,9 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { return { apiKey, appId }; } - // If token auth is being used, we don't need an API key - if (flags.token) { - // For token auth, we still need an app ID for some operations - const appId = flags.app || this.configManager.getCurrentAppId(); - if (appId) { - return { apiKey: "", appId }; - } - // If no app ID is provided, we'll try to extract it from the token if it's a JWT - // But for now, just return null and let the operation proceed with token auth only - } - // Check if we have an app and key from flags or config let appId = flags.app || this.configManager.getCurrentAppId(); - let apiKey = flags["api-key"] || this.configManager.getApiKey(appId); + let apiKey = this.configManager.getApiKey(appId); // If we have both, return them if (appId && apiKey) { @@ -771,16 +692,14 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { // Get access token for control API const accessToken = - process.env.ABLY_ACCESS_TOKEN || - flags["access-token"] || - this.configManager.getAccessToken(); + process.env.ABLY_ACCESS_TOKEN || this.configManager.getAccessToken(); if (!accessToken) { return null; } const controlApi = new ControlApi({ accessToken, - controlHost: flags["control-host"], + controlHost: flags["control-host"] || process.env.ABLY_CONTROL_HOST, }); // If no app is selected, prompt to select one @@ -895,9 +814,9 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { const options: Ably.ClientOptions = {}; const isJsonMode = this.shouldOutputJson(flags); - // Handle authentication - try token first, then api-key, then environment variable, then config - if (flags.token) { - options.token = flags.token; + // Handle authentication: ABLY_TOKEN env → flags["api-key"] (set by ensureAppAndKey) → ABLY_API_KEY env → config + if (process.env.ABLY_TOKEN) { + options.token = process.env.ABLY_TOKEN; // When using token auth, we don't set the clientId as it may conflict // with any clientId embedded in the token @@ -909,53 +828,9 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { ); } } else if (flags["api-key"]) { - options.key = flags["api-key"]; - - // In web CLI mode, validate the API key format - if (this.isWebCliMode) { - const parsedKey = this.parseApiKey(flags["api-key"]); - if (parsedKey) { - this.debug( - `Using API key with appId=${parsedKey.appId}, keyId=${parsedKey.keyId}`, - ); - // In web CLI mode, we need to explicitly configure the client for Ably.js browser library - options.key = flags["api-key"]; - } else { - this.log( - chalk.yellow( - `Warning: API key format appears to be invalid. Expected format: APP_ID.KEY_ID:KEY_SECRET`, - ), - ); - } - } - - // Handle client ID for API key auth - this.setClientId(options, flags); + this.applyApiKeyAuth(options, flags["api-key"], flags); } else if (process.env.ABLY_API_KEY) { - const apiKey = process.env.ABLY_API_KEY; - options.key = apiKey; - - // In web CLI mode, validate the API key format - if (this.isWebCliMode) { - const parsedKey = this.parseApiKey(apiKey); - if (parsedKey) { - this.debug( - `Using API key with appId=${parsedKey.appId}, keyId=${parsedKey.keyId}`, - ); - - // Ensure API key is properly formatted for Node.js SDK - options.key = apiKey; - } else { - this.log( - chalk.yellow( - `Warning: API key format appears to be invalid. Expected format: APP_ID.KEY_ID:KEY_SECRET`, - ), - ); - } - } - - // Handle client ID for API key auth - this.setClientId(options, flags); + this.applyApiKeyAuth(options, process.env.ABLY_API_KEY, flags); } else { const apiKey = this.configManager.getApiKey(); if (apiKey) { @@ -966,18 +841,11 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { } } - // Handle host and environment options - if (flags.host) { - options.realtimeHost = flags.host; - options.restHost = flags.host; - } - - if (flags.env) { - options.environment = flags.env; - } - - if (flags.endpoint) { - options.endpoint = flags.endpoint; + // Endpoint resolution: ABLY_ENDPOINT env → account config + const endpoint = + process.env.ABLY_ENDPOINT || this.configManager.getEndpoint(); + if (endpoint) { + options.endpoint = endpoint; } if (flags.port) { @@ -1300,6 +1168,32 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { } } + private applyApiKeyAuth( + options: Ably.ClientOptions, + apiKey: string, + flags: BaseFlags, + ): void { + options.key = apiKey; + + // In web CLI mode, validate the API key format + if (this.isWebCliMode) { + const parsedKey = this.parseApiKey(apiKey); + if (parsedKey) { + this.debug( + `Using API key with appId=${parsedKey.appId}, keyId=${parsedKey.keyId}`, + ); + } else { + this.log( + chalk.yellow( + `Warning: API key format appears to be invalid. Expected format: APP_ID.KEY_ID:KEY_SECRET`, + ), + ); + } + } + + this.setClientId(options, flags); + } + private setClientId(options: Ably.ClientOptions, flags: BaseFlags): void { if (flags["client-id"]) { // Special case: "none" means explicitly no client ID @@ -1383,12 +1277,10 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { return true; } - // Hide account info if explicit auth credentials are provided + // Hide account info if explicit auth credentials are provided via env vars return ( - Boolean(flags["api-key"]) || - Boolean(flags.token) || - Boolean(flags["access-token"]) || Boolean(process.env.ABLY_API_KEY) || + Boolean(process.env.ABLY_TOKEN) || Boolean(process.env.ABLY_ACCESS_TOKEN) ); } diff --git a/src/chat-base-command.ts b/src/chat-base-command.ts index bc32d93a..acb9f546 100644 --- a/src/chat-base-command.ts +++ b/src/chat-base-command.ts @@ -2,10 +2,12 @@ import { ChatClient } from "@ably/chat"; import * as Ably from "ably"; import { AblyBaseCommand } from "./base-command.js"; +import { productApiFlags } from "./flags.js"; import { BaseFlags } from "./types/cli.js"; import isTestMode from "./utils/test-mode.js"; export abstract class ChatBaseCommand extends AblyBaseCommand { + static globalFlags = { ...productApiFlags }; protected _chatRealtimeClient: Ably.Realtime | null = null; private _chatClient: ChatClient | null = null; private _cleanupTimeout: NodeJS.Timeout | undefined; diff --git a/src/commands/accounts/login.ts b/src/commands/accounts/login.ts index f1575dda..6f73da2b 100644 --- a/src/commands/accounts/login.ts +++ b/src/commands/accounts/login.ts @@ -4,6 +4,7 @@ import * as readline from "node:readline"; import open from "open"; import { ControlBaseCommand } from "../../control-base-command.js"; +import { endpointFlag } from "../../flags.js"; import { ControlApi } from "../../services/control-api.js"; import { displayLogo } from "../../utils/logo.js"; import { promptForConfirmation } from "../../utils/prompt-confirmation.js"; @@ -57,6 +58,7 @@ export default class AccountsLogin extends ControlBaseCommand { static override flags = { ...ControlBaseCommand.globalFlags, + ...endpointFlag, alias: Flags.string({ char: "a", description: "Alias for this account (default account if not specified)", @@ -181,6 +183,11 @@ export default class AccountsLogin extends ControlBaseCommand { // Switch to this account this.configManager.switchAccount(alias); + // Store custom endpoint if provided + if (flags.endpoint) { + this.configManager.storeEndpoint(flags.endpoint as string); + } + // Handle app selection based on available apps let selectedApp = null; let isAutoSelected = false; diff --git a/src/commands/accounts/switch.ts b/src/commands/accounts/switch.ts index c47feb4e..0910f7d1 100644 --- a/src/commands/accounts/switch.ts +++ b/src/commands/accounts/switch.ts @@ -1,6 +1,7 @@ import { Args } from "@oclif/core"; import { ControlBaseCommand } from "../../control-base-command.js"; +import { endpointFlag } from "../../flags.js"; import { ControlApi } from "../../services/control-api.js"; export default class AccountsSwitch extends ControlBaseCommand { @@ -22,6 +23,7 @@ export default class AccountsSwitch extends ControlBaseCommand { static override flags = { ...ControlBaseCommand.globalFlags, + ...endpointFlag, }; public async run(): Promise { @@ -123,6 +125,11 @@ export default class AccountsSwitch extends ControlBaseCommand { // Switch to the account this.configManager.switchAccount(alias); + // Store custom endpoint if provided + if (flags.endpoint) { + this.configManager.storeEndpoint(flags.endpoint as string); + } + // Verify the account is valid by making an API call try { const accessToken = this.configManager.getAccessToken(); diff --git a/src/commands/apps/create.ts b/src/commands/apps/create.ts index 0035410f..60c95b71 100644 --- a/src/commands/apps/create.ts +++ b/src/commands/apps/create.ts @@ -9,7 +9,7 @@ export default class AppsCreateCommand extends ControlBaseCommand { static examples = [ '$ ably apps create --name "My New App"', '$ ably apps create --name "My New App" --tls-only', - '$ ably apps create --name "My New App" --access-token "YOUR_ACCESS_TOKEN"', + '$ ABLY_ACCESS_TOKEN="YOUR_ACCESS_TOKEN" ably apps create --name "My New App"', ]; static flags = { diff --git a/src/commands/apps/delete.ts b/src/commands/apps/delete.ts index 570b2449..5e1edc21 100644 --- a/src/commands/apps/delete.ts +++ b/src/commands/apps/delete.ts @@ -19,7 +19,7 @@ export default class AppsDeleteCommand extends ControlBaseCommand { "$ ably apps delete", "$ ably apps delete app-id", "$ ably apps delete --app app-id", - '$ ably apps delete app-id --access-token "YOUR_ACCESS_TOKEN"', + '$ ABLY_ACCESS_TOKEN="YOUR_ACCESS_TOKEN" ably apps delete app-id', "$ ably apps delete app-id --force", "$ ably apps delete app-id --json", "$ ably apps delete app-id --pretty-json", diff --git a/src/commands/apps/update.ts b/src/commands/apps/update.ts index dba395c8..4c00af2c 100644 --- a/src/commands/apps/update.ts +++ b/src/commands/apps/update.ts @@ -16,7 +16,7 @@ export default class AppsUpdateCommand extends ControlBaseCommand { '$ ably apps update app-id --name "Updated App Name"', "$ ably apps update app-id --tls-only", '$ ably apps update app-id --name "Updated App Name" --tls-only', - '$ ably apps update app-id --name "Updated App Name" --access-token "YOUR_ACCESS_TOKEN"', + '$ ABLY_ACCESS_TOKEN="YOUR_ACCESS_TOKEN" ably apps update app-id --name "Updated App Name"', ]; static flags = { diff --git a/src/commands/auth/issue-ably-token.ts b/src/commands/auth/issue-ably-token.ts index 9989710d..37946850 100644 --- a/src/commands/auth/issue-ably-token.ts +++ b/src/commands/auth/issue-ably-token.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import { randomUUID } from "node:crypto"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; export default class IssueAblyTokenCommand extends AblyBaseCommand { static description = "Creates an Ably Token with capabilities"; @@ -16,11 +17,11 @@ export default class IssueAblyTokenCommand extends AblyBaseCommand { "$ ably auth issue-ably-token --json", "$ ably auth issue-ably-token --pretty-json", "$ ably auth issue-ably-token --token-only", - '$ ably channels publish --token "$(ably auth issue-ably-token --token-only)" my-channel "Hello"', + '$ ABLY_TOKEN="$(ably auth issue-ably-token --token-only)" ably channels publish my-channel "Hello"', ]; static flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, app: Flags.string({ description: "App ID to use (uses current app if not specified)", env: "ABLY_APP_ID", diff --git a/src/commands/auth/issue-jwt-token.ts b/src/commands/auth/issue-jwt-token.ts index ab0eeaf2..409f2eca 100644 --- a/src/commands/auth/issue-jwt-token.ts +++ b/src/commands/auth/issue-jwt-token.ts @@ -3,6 +3,7 @@ import jwt from "jsonwebtoken"; import { randomUUID } from "node:crypto"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; interface JwtPayload { exp: number; @@ -24,11 +25,11 @@ export default class IssueJwtTokenCommand extends AblyBaseCommand { "$ ably auth issue-jwt-token --json", "$ ably auth issue-jwt-token --pretty-json", "$ ably auth issue-jwt-token --token-only", - '$ ably channels publish --token "$(ably auth issue-jwt-token --token-only)" my-channel "Hello"', + '$ ABLY_TOKEN="$(ably auth issue-jwt-token --token-only)" ably channels publish my-channel "Hello"', ]; static flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, app: Flags.string({ description: "App ID to use (uses current app if not specified)", env: "ABLY_APP_ID", diff --git a/src/commands/auth/revoke-token.ts b/src/commands/auth/revoke-token.ts index bec32426..a59d5abf 100644 --- a/src/commands/auth/revoke-token.ts +++ b/src/commands/auth/revoke-token.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import * as https from "node:https"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; export default class RevokeTokenCommand extends AblyBaseCommand { static args = { @@ -23,7 +24,7 @@ export default class RevokeTokenCommand extends AblyBaseCommand { ]; static flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, app: Flags.string({ description: "App ID to use (uses current app if not specified)", env: "ABLY_APP_ID", diff --git a/src/commands/bench/publisher.ts b/src/commands/bench/publisher.ts index 398af30d..629fc3bc 100644 --- a/src/commands/bench/publisher.ts +++ b/src/commands/bench/publisher.ts @@ -4,6 +4,7 @@ import chalk from "chalk"; import Table from "cli-table3"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; interface TestMetrics { batchCount: number; @@ -66,7 +67,7 @@ export default class BenchPublisher extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, "message-size": Flags.integer({ default: 100, description: "Size of the message payload in bytes", diff --git a/src/commands/bench/subscriber.ts b/src/commands/bench/subscriber.ts index 18b60a24..1f83ecea 100644 --- a/src/commands/bench/subscriber.ts +++ b/src/commands/bench/subscriber.ts @@ -4,6 +4,7 @@ import chalk from "chalk"; import Table from "cli-table3"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; import { waitUntilInterruptedOrTimeout } from "../../utils/long-running.js"; interface TestMetrics { @@ -30,7 +31,7 @@ export default class BenchSubscriber extends AblyBaseCommand { static override examples = ["$ ably bench subscriber my-channel"]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, duration: Flags.integer({ char: "d", description: diff --git a/src/commands/channels/batch-publish.ts b/src/commands/channels/batch-publish.ts index 8ed316a1..6320e4e4 100644 --- a/src/commands/channels/batch-publish.ts +++ b/src/commands/channels/batch-publish.ts @@ -1,5 +1,6 @@ import { Args, Flags } from "@oclif/core"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; // Define interfaces for the batch-publish command interface BatchMessage { @@ -64,7 +65,7 @@ export default class ChannelsBatchPublish extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, channels: Flags.string({ description: "Comma-separated list of channel names to publish to (mutually exclusive with --channels-json and --spec)", diff --git a/src/commands/channels/history.ts b/src/commands/channels/history.ts index 2d35f589..05655bce 100644 --- a/src/commands/channels/history.ts +++ b/src/commands/channels/history.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; import { formatJson, isJsonData } from "../../utils/json-formatter.js"; export default class ChannelsHistory extends AblyBaseCommand { @@ -25,7 +26,7 @@ export default class ChannelsHistory extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, cipher: Flags.string({ description: "Decryption key for encrypted messages (AES-128)", }), diff --git a/src/commands/channels/inspect.ts b/src/commands/channels/inspect.ts index c83935b9..2f7322bd 100644 --- a/src/commands/channels/inspect.ts +++ b/src/commands/channels/inspect.ts @@ -2,6 +2,7 @@ import { Args, Flags } from "@oclif/core"; import chalk from "chalk"; import { AblyBaseCommand } from "../../base-command.js"; +import { hiddenControlApiFlags, productApiFlags } from "../../flags.js"; import openUrl from "../../utils/open-url.js"; export default class ChannelsInspect extends AblyBaseCommand { @@ -18,7 +19,8 @@ export default class ChannelsInspect extends AblyBaseCommand { static override examples = ["<%= config.bin %> <%= command.id %> my-channel"]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, + ...hiddenControlApiFlags, app: Flags.string({ description: "App ID to use (uses current app if not specified)", env: "ABLY_APP_ID", diff --git a/src/commands/channels/list.ts b/src/commands/channels/list.ts index bc5ba917..84b7caec 100644 --- a/src/commands/channels/list.ts +++ b/src/commands/channels/list.ts @@ -1,5 +1,6 @@ import { Flags } from "@oclif/core"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; import chalk from "chalk"; interface ChannelMetrics { @@ -40,7 +41,7 @@ export default class ChannelsList extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, limit: Flags.integer({ default: 100, description: "Maximum number of channels to return (default: 100)", diff --git a/src/commands/channels/occupancy/get.ts b/src/commands/channels/occupancy/get.ts index e562bb8e..84e04471 100644 --- a/src/commands/channels/occupancy/get.ts +++ b/src/commands/channels/occupancy/get.ts @@ -1,6 +1,7 @@ import { Args } from "@oclif/core"; import { AblyBaseCommand } from "../../../base-command.js"; +import { productApiFlags } from "../../../flags.js"; interface OccupancyMetrics { connections: number; @@ -23,13 +24,13 @@ export default class ChannelsOccupancyGet extends AblyBaseCommand { static examples = [ "$ ably channels occupancy get my-channel", - '$ ably channels occupancy get --api-key "YOUR_API_KEY" my-channel', "$ ably channels occupancy get my-channel --json", "$ ably channels occupancy get my-channel --pretty-json", + '$ ABLY_API_KEY="YOUR_API_KEY" ably channels occupancy get my-channel', ]; static flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, }; async run(): Promise { diff --git a/src/commands/channels/occupancy/subscribe.ts b/src/commands/channels/occupancy/subscribe.ts index 9e0f187c..66fdbcde 100644 --- a/src/commands/channels/occupancy/subscribe.ts +++ b/src/commands/channels/occupancy/subscribe.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; +import { productApiFlags } from "../../../flags.js"; import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; export default class ChannelsOccupancySubscribe extends AblyBaseCommand { @@ -17,15 +18,14 @@ export default class ChannelsOccupancySubscribe extends AblyBaseCommand { static override examples = [ "$ ably channels occupancy subscribe my-channel", - '$ ably channels occupancy subscribe my-channel --api-key "YOUR_API_KEY"', - '$ ably channels occupancy subscribe my-channel --token "YOUR_ABLY_TOKEN"', "$ ably channels occupancy subscribe my-channel --json", "$ ably channels occupancy subscribe my-channel --pretty-json", "$ ably channels occupancy subscribe my-channel --duration 30", + '$ ABLY_API_KEY="YOUR_API_KEY" ably channels occupancy subscribe my-channel', ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, duration: Flags.integer({ description: "Automatically exit after the given number of seconds (0 = run indefinitely)", diff --git a/src/commands/channels/presence/enter.ts b/src/commands/channels/presence/enter.ts index 177a8942..965f69e3 100644 --- a/src/commands/channels/presence/enter.ts +++ b/src/commands/channels/presence/enter.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; +import { clientIdFlag, productApiFlags } from "../../../flags.js"; import { isJsonData } from "../../../utils/json-formatter.js"; import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; @@ -20,16 +21,16 @@ export default class ChannelsPresenceEnter extends AblyBaseCommand { static override examples = [ '$ ably channels presence enter my-channel --client-id "client123"', '$ ably channels presence enter my-channel --client-id "client123" --data \'{"name":"John","status":"online"}\'', - '$ ably channels presence enter my-channel --api-key "YOUR_API_KEY"', - '$ ably channels presence enter my-channel --token "YOUR_ABLY_TOKEN"', "$ ably channels presence enter my-channel --show-others", "$ ably channels presence enter my-channel --json", "$ ably channels presence enter my-channel --pretty-json", "$ ably channels presence enter my-channel --duration 30", + '$ ABLY_API_KEY="YOUR_API_KEY" ably channels presence enter my-channel', ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, + ...clientIdFlag, data: Flags.string({ description: "Optional JSON data to associate with the presence", }), diff --git a/src/commands/channels/presence/subscribe.ts b/src/commands/channels/presence/subscribe.ts index d5cb99c7..010a3261 100644 --- a/src/commands/channels/presence/subscribe.ts +++ b/src/commands/channels/presence/subscribe.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; +import { clientIdFlag, productApiFlags } from "../../../flags.js"; import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; export default class ChannelsPresenceSubscribe extends AblyBaseCommand { @@ -18,15 +19,15 @@ export default class ChannelsPresenceSubscribe extends AblyBaseCommand { static override examples = [ "$ ably channels presence subscribe my-channel", '$ ably channels presence subscribe my-channel --client-id "filter123"', - '$ ably channels presence subscribe my-channel --api-key "YOUR_API_KEY"', - '$ ably channels presence subscribe my-channel --token "YOUR_ABLY_TOKEN"', "$ ably channels presence subscribe my-channel --json", "$ ably channels presence subscribe my-channel --pretty-json", "$ ably channels presence subscribe my-channel --duration 30", + '$ ABLY_API_KEY="YOUR_API_KEY" ably channels presence subscribe my-channel', ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, + ...clientIdFlag, duration: Flags.integer({ description: "Automatically exit after the given number of seconds (0 = run indefinitely)", diff --git a/src/commands/channels/publish.ts b/src/commands/channels/publish.ts index 39078039..3bba96de 100644 --- a/src/commands/channels/publish.ts +++ b/src/commands/channels/publish.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; import { BaseFlags } from "../../types/cli.js"; export default class ChannelsPublish extends AblyBaseCommand { @@ -21,8 +22,6 @@ export default class ChannelsPublish extends AblyBaseCommand { static override examples = [ '$ ably channels publish my-channel \'{"name":"event","data":"Hello World"}\'', - '$ ably channels publish --api-key "YOUR_API_KEY" my-channel \'{"data":"Simple message"}\'', - '$ ably channels publish --token "YOUR_ABLY_TOKEN" my-channel \'{"data":"Using token auth"}\'', '$ ably channels publish --name event my-channel \'{"text":"Hello World"}\'', '$ ably channels publish my-channel "Hello World"', '$ ably channels publish --name event my-channel "Plain text message"', @@ -32,10 +31,11 @@ export default class ChannelsPublish extends AblyBaseCommand { '$ ably channels publish my-channel "Hello World" --json', '$ ably channels publish my-channel "Hello World" --pretty-json', '$ ably channels publish my-channel \'{"data":"Push notification","extras":{"push":{"notification":{"title":"Hello","body":"World"}}}}\'', + '$ ABLY_API_KEY="YOUR_API_KEY" ably channels publish my-channel \'{"data":"Simple message"}\'', ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, count: Flags.integer({ char: "c", default: 1, diff --git a/src/commands/channels/subscribe.ts b/src/commands/channels/subscribe.ts index bbefec8c..1853e3ad 100644 --- a/src/commands/channels/subscribe.ts +++ b/src/commands/channels/subscribe.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; import { formatJson, isJsonData } from "../../utils/json-formatter.js"; import { waitUntilInterruptedOrTimeout } from "../../utils/long-running.js"; @@ -21,18 +22,17 @@ export default class ChannelsSubscribe extends AblyBaseCommand { static override examples = [ "$ ably channels subscribe my-channel", "$ ably channels subscribe my-channel another-channel", - '$ ably channels subscribe --api-key "YOUR_API_KEY" my-channel', - '$ ably channels subscribe --token "YOUR_ABLY_TOKEN" my-channel', "$ ably channels subscribe --rewind 10 my-channel", "$ ably channels subscribe --delta my-channel", "$ ably channels subscribe --cipher-key YOUR_CIPHER_KEY my-channel", "$ ably channels subscribe my-channel --json", "$ ably channels subscribe my-channel --pretty-json", "$ ably channels subscribe my-channel --duration 30", + '$ ABLY_API_KEY="YOUR_API_KEY" ably channels subscribe my-channel', ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, "cipher-algorithm": Flags.string({ default: "aes", description: "Encryption algorithm to use (default: aes)", diff --git a/src/commands/config/index.ts b/src/commands/config/index.ts index 73e9ecaf..1f45ad5c 100644 --- a/src/commands/config/index.ts +++ b/src/commands/config/index.ts @@ -1,4 +1,5 @@ import { AblyBaseCommand } from "../../base-command.js"; +import { coreGlobalFlags } from "../../flags.js"; export default class ConfigIndex extends AblyBaseCommand { static override description = "Manage Ably CLI configuration"; @@ -9,7 +10,7 @@ export default class ConfigIndex extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...coreGlobalFlags, }; async run(): Promise { diff --git a/src/commands/config/path.ts b/src/commands/config/path.ts index ec942ac3..e344c1dc 100644 --- a/src/commands/config/path.ts +++ b/src/commands/config/path.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs"; import { AblyBaseCommand } from "../../base-command.js"; +import { coreGlobalFlags } from "../../flags.js"; export default class ConfigPath extends AblyBaseCommand { static override description = "Print the path to the Ably CLI config file"; @@ -13,7 +14,7 @@ export default class ConfigPath extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...coreGlobalFlags, }; public async run(): Promise { diff --git a/src/commands/config/show.ts b/src/commands/config/show.ts index 5b70b6e5..cbff16f2 100644 --- a/src/commands/config/show.ts +++ b/src/commands/config/show.ts @@ -2,6 +2,7 @@ import * as fs from "node:fs"; import { parse } from "smol-toml"; import { AblyBaseCommand } from "../../base-command.js"; +import { coreGlobalFlags } from "../../flags.js"; export default class ConfigShow extends AblyBaseCommand { static override description = @@ -13,7 +14,7 @@ export default class ConfigShow extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...coreGlobalFlags, }; public async run(): Promise { diff --git a/src/commands/connections/test.ts b/src/commands/connections/test.ts index 3e86ff0f..36aa1a3c 100644 --- a/src/commands/connections/test.ts +++ b/src/commands/connections/test.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; export default class ConnectionsTest extends AblyBaseCommand { static override description = "Test connection to Ably"; @@ -14,7 +15,7 @@ export default class ConnectionsTest extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, transport: Flags.string({ default: "all", description: diff --git a/src/commands/logs/channel-lifecycle/subscribe.ts b/src/commands/logs/channel-lifecycle/subscribe.ts index 2589fdc0..f1f38f29 100644 --- a/src/commands/logs/channel-lifecycle/subscribe.ts +++ b/src/commands/logs/channel-lifecycle/subscribe.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; +import { productApiFlags } from "../../../flags.js"; import { formatJson, isJsonData } from "../../../utils/json-formatter.js"; import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; @@ -16,7 +17,7 @@ export default class LogsChannelLifecycleSubscribe extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, json: Flags.boolean({ default: false, description: "Output results as JSON", diff --git a/src/commands/logs/connection-lifecycle/history.ts b/src/commands/logs/connection-lifecycle/history.ts index a99d3102..22574ba0 100644 --- a/src/commands/logs/connection-lifecycle/history.ts +++ b/src/commands/logs/connection-lifecycle/history.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; +import { productApiFlags } from "../../../flags.js"; import { formatJson, isJsonData } from "../../../utils/json-formatter.js"; export default class LogsConnectionLifecycleHistory extends AblyBaseCommand { @@ -17,7 +18,7 @@ export default class LogsConnectionLifecycleHistory extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, direction: Flags.string({ default: "backwards", description: "Direction of log retrieval", diff --git a/src/commands/logs/connection-lifecycle/subscribe.ts b/src/commands/logs/connection-lifecycle/subscribe.ts index c45bb174..198f8a3a 100644 --- a/src/commands/logs/connection-lifecycle/subscribe.ts +++ b/src/commands/logs/connection-lifecycle/subscribe.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; +import { productApiFlags } from "../../../flags.js"; import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; export default class LogsConnectionLifecycleSubscribe extends AblyBaseCommand { @@ -16,7 +17,7 @@ export default class LogsConnectionLifecycleSubscribe extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, duration: Flags.integer({ description: "Automatically exit after the given number of seconds (0 = run indefinitely)", diff --git a/src/commands/logs/history.ts b/src/commands/logs/history.ts index 033b3964..05b85597 100644 --- a/src/commands/logs/history.ts +++ b/src/commands/logs/history.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; import { formatJson, isJsonData } from "../../utils/json-formatter.js"; export default class LogsHistory extends AblyBaseCommand { @@ -17,7 +18,7 @@ export default class LogsHistory extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, direction: Flags.string({ default: "backwards", description: "Direction of log retrieval", diff --git a/src/commands/logs/push/history.ts b/src/commands/logs/push/history.ts index 22b45d4a..abfc5eaf 100644 --- a/src/commands/logs/push/history.ts +++ b/src/commands/logs/push/history.ts @@ -2,6 +2,7 @@ import { Flags } from "@oclif/core"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; +import { productApiFlags } from "../../../flags.js"; import { formatJson, isJsonData } from "../../../utils/json-formatter.js"; export default class LogsPushHistory extends AblyBaseCommand { @@ -16,7 +17,7 @@ export default class LogsPushHistory extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, direction: Flags.string({ default: "backwards", description: "Direction of log retrieval", diff --git a/src/commands/logs/push/subscribe.ts b/src/commands/logs/push/subscribe.ts index 60cf1735..d1da5a7b 100644 --- a/src/commands/logs/push/subscribe.ts +++ b/src/commands/logs/push/subscribe.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; +import { productApiFlags } from "../../../flags.js"; import { formatJson, isJsonData } from "../../../utils/json-formatter.js"; export default class LogsPushSubscribe extends AblyBaseCommand { @@ -15,7 +16,7 @@ export default class LogsPushSubscribe extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, json: Flags.boolean({ default: false, description: "Output results as JSON", diff --git a/src/commands/logs/subscribe.ts b/src/commands/logs/subscribe.ts index 27024d21..2178a3c8 100644 --- a/src/commands/logs/subscribe.ts +++ b/src/commands/logs/subscribe.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import chalk from "chalk"; import { AblyBaseCommand } from "../../base-command.js"; +import { productApiFlags } from "../../flags.js"; import { waitUntilInterruptedOrTimeout } from "../../utils/long-running.js"; export default class LogsSubscribe extends AblyBaseCommand { @@ -18,7 +19,7 @@ export default class LogsSubscribe extends AblyBaseCommand { ]; static override flags = { - ...AblyBaseCommand.globalFlags, + ...productApiFlags, duration: Flags.integer({ description: "Automatically exit after the given number of seconds (0 = run indefinitely)", diff --git a/src/commands/rooms/messages/history.ts b/src/commands/rooms/messages/history.ts index 3feaf520..276fc618 100644 --- a/src/commands/rooms/messages/history.ts +++ b/src/commands/rooms/messages/history.ts @@ -17,7 +17,7 @@ export default class MessagesHistory extends ChatBaseCommand { static override examples = [ "$ ably rooms messages history my-room", - '$ ably rooms messages history --api-key "YOUR_API_KEY" my-room', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms messages history my-room', "$ ably rooms messages history --limit 50 my-room", "$ ably rooms messages history --show-metadata my-room", '$ ably rooms messages history my-room --start "2025-01-01T00:00:00Z"', diff --git a/src/commands/rooms/messages/reactions/remove.ts b/src/commands/rooms/messages/reactions/remove.ts index f727c61b..22bec436 100644 --- a/src/commands/rooms/messages/reactions/remove.ts +++ b/src/commands/rooms/messages/reactions/remove.ts @@ -47,7 +47,7 @@ export default class MessagesReactionsRemove extends ChatBaseCommand { static override examples = [ "$ ably rooms messages reactions remove my-room message-serial 👍", - '$ ably rooms messages reactions remove --api-key "YOUR_API_KEY" my-room message-serial ❤️', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms messages reactions remove my-room message-serial ❤️', "$ ably rooms messages reactions remove my-room message-serial 👍 --type unique", "$ ably rooms messages reactions remove my-room message-serial 👍 --json", ]; diff --git a/src/commands/rooms/messages/reactions/send.ts b/src/commands/rooms/messages/reactions/send.ts index f3d52329..16caf278 100644 --- a/src/commands/rooms/messages/reactions/send.ts +++ b/src/commands/rooms/messages/reactions/send.ts @@ -48,7 +48,7 @@ export default class MessagesReactionsSend extends ChatBaseCommand { static override examples = [ "$ ably rooms messages reactions send my-room message-serial 👍", - '$ ably rooms messages reactions send --api-key "YOUR_API_KEY" my-room message-serial ❤️', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms messages reactions send my-room message-serial ❤️', "$ ably rooms messages reactions send my-room message-serial 👍 --type multiple --count 10", "$ ably rooms messages reactions send my-room message-serial 👍 --type unique", "$ ably rooms messages reactions send my-room message-serial 👍 --json", diff --git a/src/commands/rooms/messages/send.ts b/src/commands/rooms/messages/send.ts index c893d0e1..2ea626ad 100644 --- a/src/commands/rooms/messages/send.ts +++ b/src/commands/rooms/messages/send.ts @@ -44,7 +44,7 @@ export default class MessagesSend extends ChatBaseCommand { static override examples = [ '$ ably rooms messages send my-room "Hello World!"', - '$ ably rooms messages send --api-key "YOUR_API_KEY" my-room "Welcome to the chat!"', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms messages send my-room "Welcome to the chat!"', '$ ably rooms messages send --metadata \'{"isImportant":true}\' my-room "Attention please!"', '$ ably rooms messages send --count 5 my-room "Message number {{.Count}}"', '$ ably rooms messages send --count 10 --delay 1000 my-room "Message at {{.Timestamp}}"', diff --git a/src/commands/rooms/messages/subscribe.ts b/src/commands/rooms/messages/subscribe.ts index 100e2584..7562e208 100644 --- a/src/commands/rooms/messages/subscribe.ts +++ b/src/commands/rooms/messages/subscribe.ts @@ -40,7 +40,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { static override examples = [ "$ ably rooms messages subscribe my-room", "$ ably rooms messages subscribe room1 room2 room3", - '$ ably rooms messages subscribe --api-key "YOUR_API_KEY" my-room', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms messages subscribe my-room', "$ ably rooms messages subscribe --show-metadata my-room", "$ ably rooms messages subscribe my-room --duration 30", "$ ably rooms messages subscribe my-room --json", diff --git a/src/commands/rooms/occupancy/get.ts b/src/commands/rooms/occupancy/get.ts index f8950338..23330380 100644 --- a/src/commands/rooms/occupancy/get.ts +++ b/src/commands/rooms/occupancy/get.ts @@ -14,7 +14,7 @@ export default class RoomsOccupancyGet extends ChatBaseCommand { static examples = [ "$ ably rooms occupancy get my-room", - '$ ably rooms occupancy get --api-key "YOUR_API_KEY" my-room', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms occupancy get my-room', "$ ably rooms occupancy get my-room --json", "$ ably rooms occupancy get my-room --pretty-json", ]; diff --git a/src/commands/rooms/reactions/send.ts b/src/commands/rooms/reactions/send.ts index bc603196..f73c800d 100644 --- a/src/commands/rooms/reactions/send.ts +++ b/src/commands/rooms/reactions/send.ts @@ -26,7 +26,7 @@ export default class RoomsReactionsSend extends ChatBaseCommand { static override examples = [ "$ ably rooms reactions send my-room 👍", - '$ ably rooms reactions send --api-key "YOUR_API_KEY" my-room 🎉', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms reactions send my-room 🎉', "$ ably rooms reactions send my-room ❤️ --json", "$ ably rooms reactions send my-room 😂 --pretty-json", ]; diff --git a/src/commands/rooms/typing/keystroke.ts b/src/commands/rooms/typing/keystroke.ts index 9391c236..86dac36b 100644 --- a/src/commands/rooms/typing/keystroke.ts +++ b/src/commands/rooms/typing/keystroke.ts @@ -28,7 +28,7 @@ export default class TypingKeystroke extends ChatBaseCommand { static override examples = [ "$ ably rooms typing keystroke my-room", "$ ably rooms typing keystroke my-room --autoType", - '$ ably rooms typing keystroke --api-key "YOUR_API_KEY" my-room', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms typing keystroke my-room', "$ ably rooms typing keystroke my-room --json", "$ ably rooms typing keystroke my-room --pretty-json", ]; diff --git a/src/commands/rooms/typing/subscribe.ts b/src/commands/rooms/typing/subscribe.ts index 5e71f40d..61df30d1 100644 --- a/src/commands/rooms/typing/subscribe.ts +++ b/src/commands/rooms/typing/subscribe.ts @@ -18,7 +18,7 @@ export default class TypingSubscribe extends ChatBaseCommand { static override examples = [ "$ ably rooms typing subscribe my-room", - '$ ably rooms typing subscribe --api-key "YOUR_API_KEY" my-room', + '$ ABLY_API_KEY="YOUR_API_KEY" ably rooms typing subscribe my-room', "$ ably rooms typing subscribe my-room --json", "$ ably rooms typing subscribe my-room --pretty-json", ]; diff --git a/src/commands/spaces/cursors/set.ts b/src/commands/spaces/cursors/set.ts index 689f8625..51589d36 100644 --- a/src/commands/spaces/cursors/set.ts +++ b/src/commands/spaces/cursors/set.ts @@ -34,7 +34,7 @@ export default class SpacesCursorsSet extends SpacesBaseCommand { "$ ably spaces cursors set my-space --simulate --x 500 --y 500", '$ ably spaces cursors set my-space --data \'{"position": {"x": 100, "y": 200}}\'', '$ ably spaces cursors set my-space --data \'{"position": {"x": 100, "y": 200}, "data": {"name": "John", "color": "#ff0000"}}\'', - '$ ably spaces cursors set --api-key "YOUR_API_KEY" my-space --x 100 --y 200', + '$ ABLY_API_KEY="YOUR_API_KEY" ably spaces cursors set my-space --x 100 --y 200', "$ ably spaces cursors set my-space --x 100 --y 200 --json", "$ ably spaces cursors set my-space --x 100 --y 200 --pretty-json", ]; diff --git a/src/commands/version.ts b/src/commands/version.ts index 332c6273..e7416530 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -1,4 +1,5 @@ import { AblyBaseCommand } from "../base-command.js"; +import { coreGlobalFlags } from "../flags.js"; import { getVersionInfo, formatVersionString, @@ -18,7 +19,7 @@ export default class Version extends AblyBaseCommand { // Import global flags (like --json and --pretty-json) static flags = { - ...AblyBaseCommand.globalFlags, + ...coreGlobalFlags, }; async run(): Promise { diff --git a/src/control-base-command.ts b/src/control-base-command.ts index 5141443c..d840ee4e 100644 --- a/src/control-base-command.ts +++ b/src/control-base-command.ts @@ -1,27 +1,25 @@ import chalk from "chalk"; import { AblyBaseCommand } from "./base-command.js"; +import { controlApiFlags } from "./flags.js"; import { ControlApi, App } from "./services/control-api.js"; import { BaseFlags, ErrorDetails } from "./types/cli.js"; export abstract class ControlBaseCommand extends AblyBaseCommand { - // Add flags specific to control API commands - static globalFlags = { - ...AblyBaseCommand.globalFlags, - // Other Control API specific flags can be added here - }; + // Control API commands get core + hidden control API flags + static globalFlags = { ...controlApiFlags }; /** * Create a Control API instance for making requests */ protected createControlApi(flags: BaseFlags): ControlApi { - let accessToken = flags["access-token"] || process.env.ABLY_ACCESS_TOKEN; + let accessToken = process.env.ABLY_ACCESS_TOKEN; if (!accessToken) { const account = this.configManager.getCurrentAccount(); if (!account) { this.error( - `No access token provided. Please specify --access-token or configure an account with "ably accounts login".`, + `No access token provided. Please set the ABLY_ACCESS_TOKEN environment variable or configure an account with "ably accounts login".`, ); } @@ -30,7 +28,7 @@ export abstract class ControlBaseCommand extends AblyBaseCommand { if (!accessToken) { this.error( - `No access token provided. Please specify --access-token or configure an account with "ably accounts login".`, + `No access token provided. Please set the ABLY_ACCESS_TOKEN environment variable or configure an account with "ably accounts login".`, ); } diff --git a/src/flags.ts b/src/flags.ts new file mode 100644 index 00000000..6e380bc6 --- /dev/null +++ b/src/flags.ts @@ -0,0 +1,99 @@ +import { Flags } from "@oclif/core"; + +/** + * Core global flags available on every command. + */ +export const coreGlobalFlags = { + verbose: Flags.boolean({ + char: "v", + default: false, + description: "Output verbose logs", + required: false, + }), + json: Flags.boolean({ + description: "Output in JSON format", + exclusive: ["pretty-json"], + }), + "pretty-json": Flags.boolean({ + description: "Output in colorized JSON format", + exclusive: ["json"], + }), + "web-cli-help": Flags.boolean({ + description: "Show help formatted for the web CLI", + hidden: true, + }), +}; + +/** + * Hidden flags for product API (Ably SDK) commands — port, tls, tlsPort. + */ +export const hiddenProductApiFlags = { + port: Flags.integer({ + description: "Override the port for product API calls", + hidden: process.env.ABLY_SHOW_DEV_FLAGS !== "true", + }), + tlsPort: Flags.integer({ + description: "Override the TLS port for product API calls", + hidden: process.env.ABLY_SHOW_DEV_FLAGS !== "true", + }), + tls: Flags.string({ + description: "Use TLS for product API calls (default is true)", + hidden: process.env.ABLY_SHOW_DEV_FLAGS !== "true", + }), +}; + +/** + * Hidden flags for control API commands — control-host, dashboard-host. + */ +export const hiddenControlApiFlags = { + "control-host": Flags.string({ + description: + "Override the host endpoint for the control API, which defaults to control.ably.net", + 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", + }), +}; + +/** + * client-id flag for commands that support it (e.g., presence). + */ +export const clientIdFlag = { + "client-id": Flags.string({ + description: + 'Overrides any default client ID when using API authentication. Use "none" to explicitly set no client ID. Not applicable when using token authentication.', + }), +}; + +/** + * endpoint flag for login / accounts switch commands only. + */ +export const endpointFlag = { + endpoint: Flags.string({ + description: + "Set a custom endpoint for all product API calls, stored in account config", + }), +}; + +/** + * Composite: core + hidden product API flags. + * Use for product API commands (channels, connections, logs, bench, etc.) + */ +export const productApiFlags = { + ...coreGlobalFlags, + ...hiddenProductApiFlags, +}; + +/** + * Composite: core + hidden control API flags. + * Use for control API commands (accounts, apps, keys, integrations, queues, etc.) + */ +export const controlApiFlags = { + ...coreGlobalFlags, + ...hiddenControlApiFlags, +}; diff --git a/src/services/config-manager.ts b/src/services/config-manager.ts index c3bce075..311f9aab 100644 --- a/src/services/config-manager.ts +++ b/src/services/config-manager.ts @@ -20,6 +20,7 @@ export interface AccountConfig { [appId: string]: AppConfig; }; currentAppId?: string; + endpoint?: string; tokenId?: string; userEmail?: string; } @@ -79,6 +80,10 @@ export interface ConfigManager { ): void; removeApiKey(appId: string): boolean; + // Endpoint management + getEndpoint(alias?: string): string | undefined; + storeEndpoint(endpoint: string, alias?: string): void; + // Help context (AI conversation) getHelpContext(): | { @@ -193,6 +198,16 @@ export class TomlConfigManager implements ConfigManager { return cfg ? { ...cfg } : undefined; } + // Get endpoint for the current account or specific alias + public getEndpoint(alias?: string): string | undefined { + if (alias) { + return this.config.accounts[alias]?.endpoint; + } + + const currentAccount = this.getCurrentAccount(); + return currentAccount?.endpoint; + } + // Get path to config file public getConfigPath(): string { return this.configPath; @@ -359,6 +374,18 @@ export class TomlConfigManager implements ConfigManager { this.saveConfig(); } + // Store endpoint for the current account or specific alias + public storeEndpoint(endpoint: string, alias?: string): void { + const targetAlias = alias || this.getCurrentAccountAlias() || "default"; + + if (!this.config.accounts[targetAlias]) { + throw new Error(`Account "${targetAlias}" not found`); + } + + this.config.accounts[targetAlias].endpoint = endpoint; + this.saveConfig(); + } + // Store app information (like name) in the config public storeAppInfo( appId: string, diff --git a/src/spaces-base-command.ts b/src/spaces-base-command.ts index 21f91110..cca9a693 100644 --- a/src/spaces-base-command.ts +++ b/src/spaces-base-command.ts @@ -1,6 +1,7 @@ import * as Ably from "ably"; import Spaces, { type Space, type SpaceOptions } from "@ably/spaces"; import { AblyBaseCommand } from "./base-command.js"; +import { productApiFlags } from "./flags.js"; import { BaseFlags } from "./types/cli.js"; import isTestMode from "./utils/test-mode.js"; @@ -24,6 +25,7 @@ async function getSpacesConstructor(): Promise< } export abstract class SpacesBaseCommand extends AblyBaseCommand { + static globalFlags = { ...productApiFlags }; protected space: Space | null = null; protected spaces: Spaces | null = null; protected realtimeClient: Ably.Realtime | null = null; diff --git a/src/types/cli.ts b/src/types/cli.ts index 7b075aae..71e52e16 100644 --- a/src/types/cli.ts +++ b/src/types/cli.ts @@ -5,20 +5,16 @@ import * as Ably from "ably"; * Base interface for CLI flags. */ export interface BaseFlags { - "access-token"?: string; - "api-key"?: string; + "api-key"?: string; // Not a CLI flag; set internally by ensureAppAndKey "client-id"?: string; "control-host"?: string; "dashboard-host"?: string; - env?: string; endpoint?: string; - host?: string; port?: number; tls?: string; tlsPort?: number; json?: boolean; "pretty-json"?: boolean; - token?: string; verbose?: boolean; "web-cli-help"?: boolean; format?: string; diff --git a/test/helpers/mock-config-manager.ts b/test/helpers/mock-config-manager.ts index b1011de0..2f70a031 100644 --- a/test/helpers/mock-config-manager.ts +++ b/test/helpers/mock-config-manager.ts @@ -254,6 +254,15 @@ export class MockConfigManager implements ConfigManager { return "/mock/config/path"; } + public getEndpoint(alias?: string): string | undefined { + if (alias) { + return this.config.accounts[alias]?.endpoint; + } + + const currentAccount = this.getCurrentAccount(); + return currentAccount?.endpoint; + } + public getCurrentAccount(): AccountConfig | undefined { const currentAlias = this.getCurrentAccountAlias(); if (!currentAlias) return undefined; @@ -384,6 +393,16 @@ export class MockConfigManager implements ConfigManager { } } + public storeEndpoint(endpoint: string, alias?: string): void { + const targetAlias = alias || this.getCurrentAccountAlias() || "default"; + + if (!this.config.accounts[targetAlias]) { + throw new Error(`Account "${targetAlias}" not found`); + } + + this.config.accounts[targetAlias].endpoint = endpoint; + } + public storeAppInfo( appId: string, appInfo: { appName: string }, diff --git a/test/unit/base/auth-info-display.test.ts b/test/unit/base/auth-info-display.test.ts index d3f8d426..ee117010 100644 --- a/test/unit/base/auth-info-display.test.ts +++ b/test/unit/base/auth-info-display.test.ts @@ -56,6 +56,7 @@ describe("Auth Info Display", function () { getCurrentAppId: ReturnType; getAppName: ReturnType; getApiKey: ReturnType; + getEndpoint: ReturnType; getKeyName: ReturnType; }; let logStub: ReturnType; @@ -68,6 +69,7 @@ describe("Auth Info Display", function () { getCurrentAppId: vi.fn(), getAppName: vi.fn(), getApiKey: vi.fn(), + getEndpoint: vi.fn(), getKeyName: vi.fn(), }; @@ -90,6 +92,7 @@ describe("Auth Info Display", function () { // Make sure environment variables are clean delete process.env.ABLY_API_KEY; + delete process.env.ABLY_TOKEN; delete process.env.ABLY_ACCESS_TOKEN; }); @@ -103,31 +106,17 @@ describe("Auth Info Display", function () { expect(command.testShouldHideAccountInfo({})).toBe(true); }); - it("should return true when API key is provided explicitly", function () { - expect( - command.testShouldHideAccountInfo({ "api-key": "app-id.key:secret" }), - ).toBe(true); - }); - - it("should return true when token is provided explicitly", function () { - expect(command.testShouldHideAccountInfo({ token: "some-token" })).toBe( - true, - ); - }); - - it("should return true when access token is provided explicitly", function () { - expect( - command.testShouldHideAccountInfo({ - "access-token": "some-access-token", - }), - ).toBe(true); - }); - it("should return true when ABLY_API_KEY environment variable is set", function () { process.env.ABLY_API_KEY = "app-id.key:secret"; expect(command.testShouldHideAccountInfo({})).toBe(true); }); + it("should return true when ABLY_TOKEN environment variable is set", function () { + process.env.ABLY_TOKEN = "some-token"; + expect(command.testShouldHideAccountInfo({})).toBe(true); + delete process.env.ABLY_TOKEN; + }); + it("should return true when ABLY_ACCESS_TOKEN environment variable is set", function () { process.env.ABLY_ACCESS_TOKEN = "some-access-token"; expect(command.testShouldHideAccountInfo({})).toBe(true); @@ -199,13 +188,16 @@ describe("Auth Info Display", function () { expect(logStub).not.toHaveBeenCalled(); }); - it("should display app and auth info when token is provided", async function () { + it("should display app and auth info when ABLY_TOKEN env var is set", async function () { // Setup shouldHideAccountInfoStub.mockReturnValue(true); + process.env.ABLY_TOKEN = "test-token-value"; + + // Execute + await command.testDisplayAuthInfo({}); - // Execute with token - also need to ensure the command has a token that's reflected in output - const flags = { token: "test-token" }; - await command.testDisplayAuthInfo(flags); + // Cleanup + delete process.env.ABLY_TOKEN; // Verify output includes token info but not account info expect(logStub).toHaveBeenCalled(); diff --git a/test/unit/base/base-command.test.ts b/test/unit/base/base-command.test.ts index 06c98b3b..fa420d7b 100644 --- a/test/unit/base/base-command.test.ts +++ b/test/unit/base/base-command.test.ts @@ -95,6 +95,7 @@ type MockConfigManager = ConfigManager & { getCurrentAppId: ReturnType; getApiKey: ReturnType; getAccessToken: ReturnType; + getEndpoint: ReturnType; selectKey: ReturnType; selectApp: ReturnType; setCurrentApp: ReturnType; @@ -133,6 +134,7 @@ describe("AblyBaseCommand", function () { getCurrentAppId: vi.fn(), getApiKey: vi.fn(), getAccessToken: vi.fn(), + getEndpoint: vi.fn(), selectKey: vi.fn(), selectApp: vi.fn(), setCurrentApp: vi.fn(), @@ -555,17 +557,20 @@ describe("AblyBaseCommand", function () { }); describe("ensureAppAndKey", function () { - it("should use app and key from flags if available", async function () { + it("should use app and key from flags and config if available", async function () { const flags: BaseFlags = { app: "testAppId", - "api-key": "testApiKey", }; + // Configure config manager to return the API key for the given app + configManagerStub.getApiKey.mockReturnValue("testApiKey"); + const result = await command.testEnsureAppAndKey(flags); expect(result).not.toBeNull(); expect(result?.appId).toBe("testAppId"); expect(result?.apiKey).toBe("testApiKey"); + expect(configManagerStub.getApiKey).toHaveBeenCalledWith("testAppId"); }); it("should use app and key from config if available", async function () { @@ -645,42 +650,47 @@ describe("AblyBaseCommand", function () { }); }); - describe("endpoint flag handling", function () { - it("should set endpoint in client options when endpoint flag is provided", function () { - const flags: BaseFlags = { - endpoint: "custom-endpoint.example.com", - "api-key": "test-key:secret", - }; + describe("endpoint handling", function () { + it("should set endpoint in client options when ABLY_ENDPOINT env var is provided", function () { + process.env.ABLY_ENDPOINT = "custom-endpoint.example.com"; + process.env.ABLY_API_KEY = "test-key:secret"; + + const flags: BaseFlags = {}; const clientOptions = command.testGetClientOptions(flags); expect(clientOptions.endpoint).toBe("custom-endpoint.example.com"); + + delete process.env.ABLY_ENDPOINT; + delete process.env.ABLY_API_KEY; }); - it("should not set endpoint when flag is not provided", function () { - const flags: BaseFlags = { - "api-key": "test-key:secret", - }; + it("should not set endpoint when env var is not provided", function () { + delete process.env.ABLY_ENDPOINT; + process.env.ABLY_API_KEY = "test-key:secret"; + + const flags: BaseFlags = {}; const clientOptions = command.testGetClientOptions(flags); expect(clientOptions.endpoint).toBeUndefined(); + + delete process.env.ABLY_API_KEY; }); - it("should work alongside other flags like env and host", function () { - const flags: BaseFlags = { - endpoint: "custom-endpoint.example.com", - env: "sandbox", - host: "custom-host.example.com", - "api-key": "test-key:secret", - }; + it("should set endpoint from ABLY_ENDPOINT env var alongside other options", function () { + process.env.ABLY_ENDPOINT = "custom-endpoint.example.com"; + process.env.ABLY_API_KEY = "test-key:secret"; + + const flags: BaseFlags = {}; const clientOptions = command.testGetClientOptions(flags); expect(clientOptions.endpoint).toBe("custom-endpoint.example.com"); - expect(clientOptions.environment).toBe("sandbox"); - expect(clientOptions.realtimeHost).toBe("custom-host.example.com"); - expect(clientOptions.restHost).toBe("custom-host.example.com"); + expect(clientOptions.key).toBe("test-key:secret"); + + delete process.env.ABLY_ENDPOINT; + delete process.env.ABLY_API_KEY; }); }); }); diff --git a/test/unit/commands/apps/create.test.ts b/test/unit/commands/apps/create.test.ts index 0dc1cc53..a454bb94 100644 --- a/test/unit/commands/apps/create.test.ts +++ b/test/unit/commands/apps/create.test.ts @@ -13,6 +13,7 @@ describe("apps:create command", () => { afterEach(() => { nock.cleanAll(); + delete process.env.ABLY_ACCESS_TOKEN; }); describe("successful app creation", () => { @@ -139,13 +140,15 @@ describe("apps:create command", () => { expect(result).toHaveProperty("success", true); }); - it("should use custom access token when provided", async () => { + it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const mock = getMockConfigManager(); const accountId = mock.getCurrentAccount()!.accountId!; const accountName = mock.getCurrentAccount()!.accountName!; const userEmail = mock.getCurrentAccount()!.userEmail!; const customToken = "custom_access_token"; + process.env.ABLY_ACCESS_TOKEN = customToken; + // Mock the /me endpoint with custom token nock("https://control.ably.net", { reqheaders: { @@ -176,13 +179,7 @@ describe("apps:create command", () => { }); const { stdout } = await runCommand( - [ - "apps:create", - "--name", - mockAppName, - "--access-token", - "custom_access_token", - ], + ["apps:create", "--name", mockAppName], import.meta.url, ); diff --git a/test/unit/commands/apps/delete.test.ts b/test/unit/commands/apps/delete.test.ts index 8b016f35..fb4c5f4d 100644 --- a/test/unit/commands/apps/delete.test.ts +++ b/test/unit/commands/apps/delete.test.ts @@ -12,6 +12,7 @@ describe("apps:delete command", () => { afterEach(() => { nock.cleanAll(); + delete process.env.ABLY_ACCESS_TOKEN; }); describe("successful app deletion", () => { @@ -101,7 +102,7 @@ describe("apps:delete command", () => { expect(result.app).toHaveProperty("name", mockAppName); }); - it("should use custom access token when provided", async () => { + it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const mock = getMockConfigManager(); const accountId = mock.getCurrentAccount()!.accountId!; const accountName = mock.getCurrentAccount()!.accountName!; @@ -109,6 +110,8 @@ describe("apps:delete command", () => { const appId = mock.getCurrentAppId()!; const customToken = "custom_access_token"; + process.env.ABLY_ACCESS_TOKEN = customToken; + // Mock the /me endpoint with custom token nock("https://control.ably.net", { reqheaders: { @@ -150,13 +153,7 @@ describe("apps:delete command", () => { .reply(204); const { stdout } = await runCommand( - [ - "apps:delete", - appId, - "--force", - "--access-token", - "custom_access_token", - ], + ["apps:delete", appId, "--force"], import.meta.url, ); diff --git a/test/unit/commands/apps/list.test.ts b/test/unit/commands/apps/list.test.ts index 6421c38a..c5caa834 100644 --- a/test/unit/commands/apps/list.test.ts +++ b/test/unit/commands/apps/list.test.ts @@ -114,9 +114,11 @@ describe("apps:list command", () => { expect(stdout).toContain("No apps found"); }); - it("should use custom access token when provided", async () => { + it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const customToken = "custom_access_token"; + process.env.ABLY_ACCESS_TOKEN = customToken; + // Mock the /me endpoint with custom token nock("https://control.ably.net", { reqheaders: { @@ -138,10 +140,7 @@ describe("apps:list command", () => { .get(`/v1/accounts/${mockAccountId}/apps`) .reply(200, mockApps); - const { stdout } = await runCommand( - ["apps:list", "--access-token", "custom_access_token"], - import.meta.url, - ); + const { stdout } = await runCommand(["apps:list"], import.meta.url); expect(stdout).toContain("Test App 1"); expect(stdout).toContain("Test App 2"); diff --git a/test/unit/commands/apps/update.test.ts b/test/unit/commands/apps/update.test.ts index d2af59b2..b28b2c8c 100644 --- a/test/unit/commands/apps/update.test.ts +++ b/test/unit/commands/apps/update.test.ts @@ -12,6 +12,7 @@ describe("apps:update command", () => { afterEach(() => { nock.cleanAll(); + delete process.env.ABLY_ACCESS_TOKEN; }); describe("successful app update", () => { @@ -140,13 +141,15 @@ describe("apps:update command", () => { expect(result).toHaveProperty("success", true); }); - it("should use custom access token when provided", async () => { + it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const mock = getMockConfigManager(); const accountId = mock.getCurrentAccount()!.accountId!; const appId = mock.getCurrentAppId()!; const customToken = "custom_access_token"; const updatedName = "UpdatedAppName"; + process.env.ABLY_ACCESS_TOKEN = customToken; + // Mock the app update endpoint with custom token nock("https://control.ably.net", { reqheaders: { @@ -165,14 +168,7 @@ describe("apps:update command", () => { }); const { stdout } = await runCommand( - [ - "apps:update", - appId, - "--name", - updatedName, - "--access-token", - customToken, - ], + ["apps:update", appId, "--name", updatedName], import.meta.url, ); diff --git a/test/unit/commands/auth/keys/create.test.ts b/test/unit/commands/auth/keys/create.test.ts index cea74d14..3db33725 100644 --- a/test/unit/commands/auth/keys/create.test.ts +++ b/test/unit/commands/auth/keys/create.test.ts @@ -17,6 +17,7 @@ describe("auth:keys:create command", () => { afterEach(() => { nock.cleanAll(); + delete process.env.ABLY_ACCESS_TOKEN; }); describe("successful key creation", () => { @@ -134,10 +135,12 @@ describe("auth:keys:create command", () => { expect(result).toHaveProperty("success", true); }); - it("should use custom access token when provided", async () => { + it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const appId = getMockConfigManager().getRegisteredAppId(); const customToken = "custom_access_token"; + process.env.ABLY_ACCESS_TOKEN = customToken; + // Mock the key creation endpoint with custom token nock("https://control.ably.net", { reqheaders: { @@ -158,15 +161,7 @@ describe("auth:keys:create command", () => { }); const { stdout } = await runCommand( - [ - "auth:keys:create", - "--name", - `"${mockKeyName}"`, - "--app", - appId, - "--access-token", - "custom_access_token", - ], + ["auth:keys:create", "--name", `"${mockKeyName}"`, "--app", appId], import.meta.url, ); diff --git a/test/unit/commands/auth/revoke-token.test.ts b/test/unit/commands/auth/revoke-token.test.ts index f9f65036..8bc19a3c 100644 --- a/test/unit/commands/auth/revoke-token.test.ts +++ b/test/unit/commands/auth/revoke-token.test.ts @@ -68,7 +68,6 @@ describe("auth:revoke-token command", () => { it("should successfully revoke a token with client-id", async () => { const mockConfig = getMockConfigManager(); const keyId = mockConfig.getKeyId()!; - const apiKey = mockConfig.getApiKey()!; // Mock the token revocation endpoint nock("https://rest.ably.io") .post(`/keys/${keyId}/revokeTokens`, { @@ -77,14 +76,7 @@ describe("auth:revoke-token command", () => { .reply(200, {}); const { stdout } = await runCommand( - [ - "auth:revoke-token", - mockToken, - "--client-id", - mockClientId, - "--api-key", - apiKey, - ], + ["auth:revoke-token", mockToken, "--client-id", mockClientId], import.meta.url, ); @@ -94,7 +86,6 @@ describe("auth:revoke-token command", () => { it("should use token as client-id when --client-id not provided", async () => { const mockConfig = getMockConfigManager(); const keyId = mockConfig.getKeyId()!; - const apiKey = mockConfig.getApiKey()!; // When no client-id is provided, the token is used as the client-id nock("https://rest.ably.io") .post(`/keys/${keyId}/revokeTokens`, { @@ -103,7 +94,7 @@ describe("auth:revoke-token command", () => { .reply(200, {}); const { stdout, stderr } = await runCommand( - ["auth:revoke-token", mockToken, "--api-key", apiKey], + ["auth:revoke-token", mockToken], import.meta.url, ); @@ -118,7 +109,6 @@ describe("auth:revoke-token command", () => { it("should output JSON format when --json flag is used", async () => { const mockConfig = getMockConfigManager(); const keyId = mockConfig.getKeyId()!; - const apiKey = mockConfig.getApiKey()!; nock("https://rest.ably.io") .post(`/keys/${keyId}/revokeTokens`, { targets: [`clientId:${mockClientId}`], @@ -126,15 +116,7 @@ describe("auth:revoke-token command", () => { .reply(200, { issuedBefore: 1234567890 }); const { stdout } = await runCommand( - [ - "auth:revoke-token", - mockToken, - "--client-id", - mockClientId, - "--api-key", - apiKey, - "--json", - ], + ["auth:revoke-token", mockToken, "--client-id", mockClientId, "--json"], import.meta.url, ); @@ -150,21 +132,13 @@ describe("auth:revoke-token command", () => { it("should handle token not found error with special message", async () => { const mockConfig = getMockConfigManager(); const keyId = mockConfig.getKeyId()!; - const apiKey = mockConfig.getApiKey()!; // The command handles token_not_found specifically in the response body nock("https://rest.ably.io") .post(`/keys/${keyId}/revokeTokens`) .reply(404, "token_not_found"); const { stdout } = await runCommand( - [ - "auth:revoke-token", - mockToken, - "--client-id", - mockClientId, - "--api-key", - apiKey, - ], + ["auth:revoke-token", mockToken, "--client-id", mockClientId], import.meta.url, ); @@ -175,20 +149,12 @@ describe("auth:revoke-token command", () => { it("should handle authentication error (invalid API key)", async () => { const mockConfig = getMockConfigManager(); const keyId = mockConfig.getKeyId()!; - const apiKey = mockConfig.getApiKey()!; nock("https://rest.ably.io") .post(`/keys/${keyId}/revokeTokens`) .reply(401, { error: { message: "Unauthorized" } }); const { error } = await runCommand( - [ - "auth:revoke-token", - mockToken, - "--client-id", - mockClientId, - "--api-key", - apiKey, - ], + ["auth:revoke-token", mockToken, "--client-id", mockClientId], import.meta.url, ); @@ -199,20 +165,12 @@ describe("auth:revoke-token command", () => { it("should handle server error", async () => { const mockConfig = getMockConfigManager(); const keyId = mockConfig.getKeyId()!; - const apiKey = mockConfig.getApiKey()!; nock("https://rest.ably.io") .post(`/keys/${keyId}/revokeTokens`) .reply(500, { error: "Internal Server Error" }); const { error } = await runCommand( - [ - "auth:revoke-token", - mockToken, - "--client-id", - mockClientId, - "--api-key", - apiKey, - ], + ["auth:revoke-token", mockToken, "--client-id", mockClientId], import.meta.url, ); @@ -225,7 +183,6 @@ describe("auth:revoke-token command", () => { it("should show debug information when --debug flag is used", async () => { const mockConfig = getMockConfigManager(); const keyId = mockConfig.getKeyId()!; - const apiKey = mockConfig.getApiKey()!; nock("https://rest.ably.io") .post(`/keys/${keyId}/revokeTokens`) .reply(200, {}); @@ -236,8 +193,6 @@ describe("auth:revoke-token command", () => { mockToken, "--client-id", mockClientId, - "--api-key", - apiKey, "--debug", ], import.meta.url, @@ -261,8 +216,6 @@ describe("auth:revoke-token command", () => { mockToken, "--client-id", mockClientId, - "--api-key", - apiKey, "--debug", ], import.meta.url, diff --git a/test/unit/commands/bench/bench.test.ts b/test/unit/commands/bench/bench.test.ts index f09f4102..d398d23d 100644 --- a/test/unit/commands/bench/bench.test.ts +++ b/test/unit/commands/bench/bench.test.ts @@ -54,8 +54,6 @@ describe("bench publisher control envelopes", function () { [ "bench:publisher", "test-channel", - "--api-key", - "app.key:secret", "--messages", "2", "--rate", diff --git a/test/unit/commands/bench/benchmarking.test.ts b/test/unit/commands/bench/benchmarking.test.ts index c3f27d36..5fb6a02c 100644 --- a/test/unit/commands/bench/benchmarking.test.ts +++ b/test/unit/commands/bench/benchmarking.test.ts @@ -113,7 +113,7 @@ describe("benchmarking commands", { timeout: 20000 }, () => { describe("argument validation", () => { it("should require channel argument", async () => { const { error } = await runCommand( - ["bench:publisher", "--api-key", "app.key:secret"], + ["bench:publisher"], import.meta.url, ); @@ -131,8 +131,6 @@ describe("benchmarking commands", { timeout: 20000 }, () => { [ "bench:publisher", "test-channel", - "--api-key", - "app.key:secret", "--messages", "5", "--rate", @@ -164,8 +162,6 @@ describe("benchmarking commands", { timeout: 20000 }, () => { [ "bench:publisher", "test-channel", - "--api-key", - "app.key:secret", "--messages", "3", "--rate", @@ -193,8 +189,6 @@ describe("benchmarking commands", { timeout: 20000 }, () => { [ "bench:publisher", "test-channel", - "--api-key", - "app.key:secret", "--messages", "2", "--rate", @@ -238,8 +232,6 @@ describe("benchmarking commands", { timeout: 20000 }, () => { [ "bench:publisher", "test-channel", - "--api-key", - "app.key:secret", "--messages", "2", "--rate", @@ -301,7 +293,7 @@ describe("benchmarking commands", { timeout: 20000 }, () => { describe("argument validation", () => { it("should require channel argument", async () => { const { error } = await runCommand( - ["bench:subscriber", "--api-key", "app.key:secret"], + ["bench:subscriber"], import.meta.url, ); @@ -316,7 +308,7 @@ describe("benchmarking commands", { timeout: 20000 }, () => { const channel = realtimeMock.channels._getChannel("test-channel"); const { error } = await runCommand( - ["bench:subscriber", "test-channel", "--api-key", "app.key:secret"], + ["bench:subscriber", "test-channel"], import.meta.url, ); diff --git a/test/unit/commands/channels/batch-publish.test.ts b/test/unit/commands/channels/batch-publish.test.ts index a5ab9d5f..f34f5053 100644 --- a/test/unit/commands/channels/batch-publish.test.ts +++ b/test/unit/commands/channels/batch-publish.test.ts @@ -41,12 +41,7 @@ describe("channels:batch-publish command", () => { describe("argument validation", () => { it("should require channels flag when not using --spec", async () => { const { error } = await runCommand( - [ - "channels:batch-publish", - '{"data":"test"}', - "--api-key", - "app.key:secret", - ], + ["channels:batch-publish", '{"data":"test"}'], import.meta.url, ); @@ -58,13 +53,7 @@ describe("channels:batch-publish command", () => { it("should require message when not using --spec", async () => { const { error } = await runCommand( - [ - "channels:batch-publish", - "--channels", - "channel1,channel2", - "--api-key", - "app.key:secret", - ], + ["channels:batch-publish", "--channels", "channel1,channel2"], import.meta.url, ); @@ -85,8 +74,6 @@ describe("channels:batch-publish command", () => { "--channels", "channel1,channel2", '{"data":"test message"}', - "--api-key", - "app.key:secret", ], import.meta.url, ); @@ -114,8 +101,6 @@ describe("channels:batch-publish command", () => { "--channels-json", '["channel1","channel2"]', // No spaces - prevents argument splitting '{"data":"test"}', - "--api-key", - "app.key:secret", ], import.meta.url, ); @@ -143,13 +128,7 @@ describe("channels:batch-publish command", () => { }); await runCommand( - [ - "channels:batch-publish", - "--spec", - spec, - "--api-key", - "app.key:secret", - ], + ["channels:batch-publish", "--spec", spec], import.meta.url, ); @@ -176,8 +155,6 @@ describe("channels:batch-publish command", () => { "--name", "custom-event", '{"data":"test"}', - "--api-key", - "app.key:secret", ], import.meta.url, ); @@ -204,8 +181,6 @@ describe("channels:batch-publish command", () => { "--encoding", "base64", '{"data":"dGVzdA=="}', - "--api-key", - "app.key:secret", ], import.meta.url, ); @@ -228,8 +203,6 @@ describe("channels:batch-publish command", () => { "--channels", "channel1,channel2", '{"data":"test"}', - "--api-key", - "app.key:secret", "--json", ], import.meta.url, @@ -257,14 +230,7 @@ describe("channels:batch-publish command", () => { mock.request.mockRejectedValue(new Error("Publish failed")); const { error } = await runCommand( - [ - "channels:batch-publish", - "--channels", - "channel1", - '{"data":"test"}', - "--api-key", - "app.key:secret", - ], + ["channels:batch-publish", "--channels", "channel1", '{"data":"test"}'], import.meta.url, ); @@ -294,8 +260,6 @@ describe("channels:batch-publish command", () => { "--channels", "channel1,channel2", '{"data":"test"}', - "--api-key", - "app.key:secret", ], import.meta.url, ); @@ -321,8 +285,6 @@ describe("channels:batch-publish command", () => { "--channels", "channel1", '{"data":"test"}', - "--api-key", - "app.key:secret", "--json", ], import.meta.url, diff --git a/test/unit/commands/channels/history.test.ts b/test/unit/commands/channels/history.test.ts index b8f9be11..652968c0 100644 --- a/test/unit/commands/channels/history.test.ts +++ b/test/unit/commands/channels/history.test.ts @@ -67,7 +67,7 @@ describe("channels:history command", () => { const channel = mock.channels._getChannel("test-channel"); const { stdout } = await runCommand( - ["channels:history", "test-channel", "--api-key", "app.key:secret"], + ["channels:history", "test-channel"], import.meta.url, ); @@ -79,7 +79,7 @@ describe("channels:history command", () => { it("should display message details", async () => { const { stdout } = await runCommand( - ["channels:history", "test-channel", "--api-key", "app.key:secret"], + ["channels:history", "test-channel"], import.meta.url, ); @@ -94,7 +94,7 @@ describe("channels:history command", () => { channel.history.mockResolvedValue({ items: [] }); const { stdout } = await runCommand( - ["channels:history", "test-channel", "--api-key", "app.key:secret"], + ["channels:history", "test-channel"], import.meta.url, ); @@ -103,13 +103,7 @@ describe("channels:history command", () => { it("should output JSON format when --json flag is used", async () => { const { stdout } = await runCommand( - [ - "channels:history", - "test-channel", - "--api-key", - "app.key:secret", - "--json", - ], + ["channels:history", "test-channel", "--json"], import.meta.url, ); @@ -127,14 +121,7 @@ describe("channels:history command", () => { const channel = mock.channels._getChannel("test-channel"); await runCommand( - [ - "channels:history", - "test-channel", - "--api-key", - "app.key:secret", - "--limit", - "10", - ], + ["channels:history", "test-channel", "--limit", "10"], import.meta.url, ); @@ -148,14 +135,7 @@ describe("channels:history command", () => { const channel = mock.channels._getChannel("test-channel"); await runCommand( - [ - "channels:history", - "test-channel", - "--api-key", - "app.key:secret", - "--direction", - "forwards", - ], + ["channels:history", "test-channel", "--direction", "forwards"], import.meta.url, ); @@ -171,16 +151,7 @@ describe("channels:history command", () => { const end = "2023-01-02T00:00:00Z"; await runCommand( - [ - "channels:history", - "test-channel", - "--api-key", - "app.key:secret", - "--start", - start, - "--end", - end, - ], + ["channels:history", "test-channel", "--start", start, "--end", end], import.meta.url, ); @@ -198,7 +169,7 @@ describe("channels:history command", () => { channel.history.mockRejectedValue(new Error("API error")); const { error } = await runCommand( - ["channels:history", "test-channel", "--api-key", "app.key:secret"], + ["channels:history", "test-channel"], import.meta.url, ); @@ -212,14 +183,7 @@ describe("channels:history command", () => { const mock = getMockAblyRest(); await runCommand( - [ - "channels:history", - "test-channel", - "--api-key", - "app.key:secret", - "--cipher", - "my-encryption-key", - ], + ["channels:history", "test-channel", "--cipher", "my-encryption-key"], import.meta.url, ); diff --git a/test/unit/commands/channels/list.test.ts b/test/unit/commands/channels/list.test.ts index 6f24a011..20bbfd13 100644 --- a/test/unit/commands/channels/list.test.ts +++ b/test/unit/commands/channels/list.test.ts @@ -67,10 +67,7 @@ describe("channels:list command", () => { it("should list channels successfully", async () => { const mock = getMockAblyRest(); - const { stdout } = await runCommand( - ["channels:list", "--api-key", "app.key:secret"], - import.meta.url, - ); + const { stdout } = await runCommand(["channels:list"], import.meta.url); // Verify the REST client request was called with correct parameters expect(mock.request).toHaveBeenCalledOnce(); @@ -88,10 +85,7 @@ describe("channels:list command", () => { }); it("should display channel metrics", async () => { - const { stdout } = await runCommand( - ["channels:list", "--api-key", "app.key:secret"], - import.meta.url, - ); + const { stdout } = await runCommand(["channels:list"], import.meta.url); expect(stdout).toContain("Connections: 5"); expect(stdout).toContain("Publishers: 2"); @@ -102,10 +96,7 @@ describe("channels:list command", () => { const mock = getMockAblyRest(); mock.request.mockResolvedValue({ statusCode: 200, items: [] }); - const { stdout } = await runCommand( - ["channels:list", "--api-key", "app.key:secret"], - import.meta.url, - ); + const { stdout } = await runCommand(["channels:list"], import.meta.url); expect(stdout).toContain("No active channels found"); }); @@ -114,10 +105,7 @@ describe("channels:list command", () => { const mock = getMockAblyRest(); mock.request.mockResolvedValue({ statusCode: 400, error: "Bad Request" }); - const { error } = await runCommand( - ["channels:list", "--api-key", "app.key:secret"], - import.meta.url, - ); + const { error } = await runCommand(["channels:list"], import.meta.url); expect(error).toBeDefined(); expect(error?.message).toContain("Failed to list channels"); @@ -126,10 +114,7 @@ describe("channels:list command", () => { it("should respect limit flag", async () => { const mock = getMockAblyRest(); - await runCommand( - ["channels:list", "--api-key", "app.key:secret", "--limit", "50"], - import.meta.url, - ); + await runCommand(["channels:list", "--limit", "50"], import.meta.url); expect(mock.request).toHaveBeenCalledOnce(); expect(mock.request.mock.calls[0][3]).toEqual({ limit: 50 }); @@ -138,10 +123,7 @@ describe("channels:list command", () => { it("should respect prefix flag", async () => { const mock = getMockAblyRest(); - await runCommand( - ["channels:list", "--api-key", "app.key:secret", "--prefix", "test-"], - import.meta.url, - ); + await runCommand(["channels:list", "--prefix", "test-"], import.meta.url); expect(mock.request).toHaveBeenCalledOnce(); expect(mock.request.mock.calls[0][3]).toEqual({ @@ -154,7 +136,7 @@ describe("channels:list command", () => { describe("JSON output", () => { it("should output JSON when requested", async () => { const { stdout } = await runCommand( - ["channels:list", "--api-key", "app.key:secret", "--json"], + ["channels:list", "--json"], import.meta.url, ); @@ -178,7 +160,7 @@ describe("channels:list command", () => { it("should include channel metrics in JSON output", async () => { const { stdout } = await runCommand( - ["channels:list", "--api-key", "app.key:secret", "--json"], + ["channels:list", "--json"], import.meta.url, ); @@ -199,7 +181,7 @@ describe("channels:list command", () => { mock.request.mockRejectedValue(new Error("Network error")); const { stdout } = await runCommand( - ["channels:list", "--api-key", "app.key:secret", "--json"], + ["channels:list", "--json"], import.meta.url, ); diff --git a/test/unit/commands/channels/presence/enter.test.ts b/test/unit/commands/channels/presence/enter.test.ts index 9f063e59..9ee10a3c 100644 --- a/test/unit/commands/channels/presence/enter.test.ts +++ b/test/unit/commands/channels/presence/enter.test.ts @@ -68,12 +68,7 @@ describe("channels:presence:enter command", () => { const channel = mock.channels._getChannel("test-channel"); const { stdout } = await runCommand( - [ - "channels:presence:enter", - "test-channel", - "--api-key", - "app.key:secret", - ], + ["channels:presence:enter", "test-channel"], import.meta.url, ); @@ -92,8 +87,6 @@ describe("channels:presence:enter command", () => { [ "channels:presence:enter", "test-channel", - "--api-key", - "app.key:secret", "--data", '{"status":"online","name":"TestUser"}', ], @@ -128,13 +121,7 @@ describe("channels:presence:enter command", () => { ); const { stdout } = await runCommand( - [ - "channels:presence:enter", - "test-channel", - "--api-key", - "app.key:secret", - "--show-others", - ], + ["channels:presence:enter", "test-channel", "--show-others"], import.meta.url, ); @@ -148,13 +135,7 @@ describe("channels:presence:enter command", () => { const channel = mock.channels._getChannel("test-channel"); const { error } = await runCommand( - [ - "channels:presence:enter", - "test-channel", - "--api-key", - "app.key:secret", - "--json", - ], + ["channels:presence:enter", "test-channel", "--json"], import.meta.url, ); @@ -166,14 +147,7 @@ describe("channels:presence:enter command", () => { it("should handle invalid JSON data gracefully", async () => { const { error } = await runCommand( - [ - "channels:presence:enter", - "test-channel", - "--api-key", - "app.key:secret", - "--data", - "not-valid-json", - ], + ["channels:presence:enter", "test-channel", "--data", "not-valid-json"], import.meta.url, ); @@ -187,12 +161,7 @@ describe("channels:presence:enter command", () => { const channel = mock.channels._getChannel("test-channel"); const { stdout } = await runCommand( - [ - "channels:presence:enter", - "test-channel", - "--api-key", - "app.key:secret", - ], + ["channels:presence:enter", "test-channel"], import.meta.url, ); diff --git a/test/unit/commands/channels/presence/subscribe.test.ts b/test/unit/commands/channels/presence/subscribe.test.ts index 53ac0176..9484d382 100644 --- a/test/unit/commands/channels/presence/subscribe.test.ts +++ b/test/unit/commands/channels/presence/subscribe.test.ts @@ -74,12 +74,7 @@ describe("channels:presence:subscribe command", () => { const channel = mock.channels._getChannel("test-channel"); const { stdout } = await runCommand( - [ - "channels:presence:subscribe", - "test-channel", - "--api-key", - "app.key:secret", - ], + ["channels:presence:subscribe", "test-channel"], import.meta.url, ); @@ -93,12 +88,7 @@ describe("channels:presence:subscribe command", () => { it("should receive and display presence events with action, client and data", async () => { // Run command const commandPromise = runCommand( - [ - "channels:presence:subscribe", - "test-channel", - "--api-key", - "app.key:secret", - ], + ["channels:presence:subscribe", "test-channel"], import.meta.url, ); @@ -131,13 +121,7 @@ describe("channels:presence:subscribe command", () => { const channel = mock.channels._getChannel("test-channel"); const { error } = await runCommand( - [ - "channels:presence:subscribe", - "test-channel", - "--api-key", - "app.key:secret", - "--json", - ], + ["channels:presence:subscribe", "test-channel", "--json"], import.meta.url, ); @@ -149,12 +133,7 @@ describe("channels:presence:subscribe command", () => { it("should handle multiple presence events", async () => { const commandPromise = runCommand( - [ - "channels:presence:subscribe", - "test-channel", - "--api-key", - "app.key:secret", - ], + ["channels:presence:subscribe", "test-channel"], import.meta.url, ); diff --git a/test/unit/commands/channels/subscribe.test.ts b/test/unit/commands/channels/subscribe.test.ts index d2481f3f..81279070 100644 --- a/test/unit/commands/channels/subscribe.test.ts +++ b/test/unit/commands/channels/subscribe.test.ts @@ -88,7 +88,7 @@ describe("channels:subscribe command", () => { const mock = getMockAblyRealtime(); const { stdout } = await runCommand( - ["channels:subscribe", "test-channel", "--api-key", "app.key:secret"], + ["channels:subscribe", "test-channel"], import.meta.url, ); @@ -104,7 +104,7 @@ describe("channels:subscribe command", () => { it("should receive and display messages with event name and data", async () => { // Run command in background-like manner const commandPromise = runCommand( - ["channels:subscribe", "test-channel", "--api-key", "app.key:secret"], + ["channels:subscribe", "test-channel"], import.meta.url, ); @@ -133,13 +133,7 @@ describe("channels:subscribe command", () => { it("should run with --json flag without errors", async () => { const { stdout, error } = await runCommand( - [ - "channels:subscribe", - "test-channel", - "--api-key", - "app.key:secret", - "--json", - ], + ["channels:subscribe", "test-channel", "--json"], import.meta.url, ); @@ -193,14 +187,7 @@ describe("channels:subscribe command", () => { const mock = getMockAblyRealtime(); await runCommand( - [ - "channels:subscribe", - "test-channel", - "--api-key", - "app.key:secret", - "--rewind", - "5", - ], + ["channels:subscribe", "test-channel", "--rewind", "5"], import.meta.url, ); @@ -216,13 +203,7 @@ describe("channels:subscribe command", () => { const mock = getMockAblyRealtime(); await runCommand( - [ - "channels:subscribe", - "test-channel", - "--api-key", - "app.key:secret", - "--delta", - ], + ["channels:subscribe", "test-channel", "--delta"], import.meta.url, ); diff --git a/test/unit/commands/interactive-autocomplete.test.ts b/test/unit/commands/interactive-autocomplete.test.ts index 334c115b..cf2e473d 100644 --- a/test/unit/commands/interactive-autocomplete.test.ts +++ b/test/unit/commands/interactive-autocomplete.test.ts @@ -528,11 +528,17 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { description: "Output in colorized JSON format", type: "boolean", }, - "api-key": { - name: "api-key", - description: - "Overrides any configured API key used for the product APIs", - type: "option", + verbose: { + name: "verbose", + char: "v", + description: "Enable verbose output", + type: "boolean", + }, + "web-cli-help": { + name: "web-cli-help", + description: "Show help in web CLI mode", + type: "boolean", + hidden: true, }, help: { name: "help", @@ -573,7 +579,8 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { expect(flags).toContain("--spec"); expect(flags).toContain("--json"); expect(flags).toContain("--pretty-json"); - expect(flags).toContain("--api-key"); + expect(flags).toContain("--verbose"); + expect(flags).toContain("-v"); expect(flags).toContain("--help"); expect(flags).toContain("-h"); }); @@ -682,12 +689,27 @@ describe("Interactive Mode - Autocomplete & Command Filtering", () => { expect(batchPublish.flags).toHaveProperty("name"); expect(batchPublish.flags).toHaveProperty("spec"); - // Check for global flags + // Check for base flags from productApiFlags expect(batchPublish.flags).toHaveProperty("json"); expect(batchPublish.flags).toHaveProperty("pretty-json"); - expect(batchPublish.flags).toHaveProperty("api-key"); - expect(batchPublish.flags).toHaveProperty("access-token"); expect(batchPublish.flags).toHaveProperty("verbose"); + expect(batchPublish.flags).toHaveProperty("web-cli-help"); + + // Check for product API flags (hidden) + expect(batchPublish.flags).toHaveProperty("port"); + expect(batchPublish.flags).toHaveProperty("tlsPort"); + expect(batchPublish.flags).toHaveProperty("tls"); + + // Verify removed global flags are no longer present + expect(batchPublish.flags).not.toHaveProperty("api-key"); + expect(batchPublish.flags).not.toHaveProperty("access-token"); + expect(batchPublish.flags).not.toHaveProperty("token"); + expect(batchPublish.flags).not.toHaveProperty("client-id"); + expect(batchPublish.flags).not.toHaveProperty("control-host"); + expect(batchPublish.flags).not.toHaveProperty("dashboard-host"); + expect(batchPublish.flags).not.toHaveProperty("env"); + expect(batchPublish.flags).not.toHaveProperty("host"); + expect(batchPublish.flags).not.toHaveProperty("endpoint"); // Check flag details expect(batchPublish.flags.encoding).toHaveProperty("char", "e"); diff --git a/test/unit/commands/queues/create.test.ts b/test/unit/commands/queues/create.test.ts index a063a5a1..98b53765 100644 --- a/test/unit/commands/queues/create.test.ts +++ b/test/unit/commands/queues/create.test.ts @@ -9,6 +9,7 @@ describe("queues:create command", () => { afterEach(() => { nock.cleanAll(); + delete process.env.ABLY_ACCESS_TOKEN; }); function createMockQueueResponse(appId: string) { @@ -189,12 +190,14 @@ describe("queues:create command", () => { expect(stdout).toContain("Queue created successfully"); }); - it("should use custom access token when provided", async () => { + it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const mockConfig = getMockConfigManager(); const appId = mockConfig.getCurrentAppId()!; const accountId = mockConfig.getCurrentAccount()!.accountId!; const customToken = "custom_access_token"; + process.env.ABLY_ACCESS_TOKEN = customToken; + nock("https://control.ably.net", { reqheaders: { authorization: `Bearer ${customToken}`, @@ -219,13 +222,7 @@ describe("queues:create command", () => { .reply(201, createMockQueueResponse(appId)); const { stdout } = await runCommand( - [ - "queues:create", - "--name", - mockQueueName, - "--access-token", - "custom_access_token", - ], + ["queues:create", "--name", mockQueueName], import.meta.url, ); diff --git a/test/unit/commands/queues/delete.test.ts b/test/unit/commands/queues/delete.test.ts index 7bcda39e..bd2263ad 100644 --- a/test/unit/commands/queues/delete.test.ts +++ b/test/unit/commands/queues/delete.test.ts @@ -8,6 +8,7 @@ describe("queues:delete command", () => { afterEach(() => { nock.cleanAll(); + delete process.env.ABLY_ACCESS_TOKEN; }); function createMockQueue(appId: string, queueId: string) { @@ -100,11 +101,13 @@ describe("queues:delete command", () => { ); }); - it("should use custom access token when provided", async () => { + it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const appId = getMockConfigManager().getCurrentAppId()!; const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; const customToken = "custom_access_token"; + process.env.ABLY_ACCESS_TOKEN = customToken; + nock("https://control.ably.net", { reqheaders: { authorization: `Bearer ${customToken}`, @@ -122,13 +125,7 @@ describe("queues:delete command", () => { .reply(204); const { error } = await runCommand( - [ - "queues:delete", - mockQueueId, - "--access-token", - "custom_access_token", - "--force", - ], + ["queues:delete", mockQueueId, "--force"], import.meta.url, ); diff --git a/test/unit/commands/queues/list.test.ts b/test/unit/commands/queues/list.test.ts index 89187294..d182f548 100644 --- a/test/unit/commands/queues/list.test.ts +++ b/test/unit/commands/queues/list.test.ts @@ -6,6 +6,7 @@ import { getMockConfigManager } from "../../../helpers/mock-config-manager.js"; describe("queues:list command", () => { afterEach(() => { nock.cleanAll(); + delete process.env.ABLY_ACCESS_TOKEN; }); describe("successful queue listing", () => { @@ -247,10 +248,12 @@ describe("queues:list command", () => { expect(stdout).toContain("Queue ID: queue-1"); }); - it("should use custom access token when provided", async () => { + it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { const appId = getMockConfigManager().getCurrentAppId()!; const customToken = "custom_access_token"; + process.env.ABLY_ACCESS_TOKEN = customToken; + nock("https://control.ably.net", { reqheaders: { authorization: `Bearer ${customToken}`, @@ -259,10 +262,7 @@ describe("queues:list command", () => { .get(`/v1/apps/${appId}/queues`) .reply(200, []); - const { stdout } = await runCommand( - ["queues:list", "--access-token", "custom_access_token"], - import.meta.url, - ); + const { stdout } = await runCommand(["queues:list"], import.meta.url); expect(stdout).toContain("No queues found"); }); diff --git a/test/unit/commands/spaces/spaces.test.ts b/test/unit/commands/spaces/spaces.test.ts index c7b51ad1..7cf0d93a 100644 --- a/test/unit/commands/spaces/spaces.test.ts +++ b/test/unit/commands/spaces/spaces.test.ts @@ -65,7 +65,7 @@ describe("spaces commands", () => { const space = spacesMock._getSpace("test-space"); const { stdout } = await runCommand( - ["spaces:members:enter", "test-space", "--api-key", "app.key:secret"], + ["spaces:members:enter", "test-space"], import.meta.url, ); @@ -81,8 +81,6 @@ describe("spaces commands", () => { [ "spaces:members:enter", "test-space", - "--api-key", - "app.key:secret", "--profile", '{"name":"TestUser","status":"online"}', ], @@ -123,12 +121,7 @@ describe("spaces commands", () => { ); const commandPromise = runCommand( - [ - "spaces:members:subscribe", - "test-space", - "--api-key", - "app.key:secret", - ], + ["spaces:members:subscribe", "test-space"], import.meta.url, ); @@ -174,8 +167,6 @@ describe("spaces commands", () => { [ "spaces:locations:set", "test-space", - "--api-key", - "app.key:secret", "--location", '{"x":100,"y":200}', ], @@ -209,8 +200,6 @@ describe("spaces commands", () => { "spaces:locks:acquire", "test-space", "my-lock", - "--api-key", - "app.key:secret", "--data", '{"reason":"editing"}', ], @@ -241,16 +230,7 @@ describe("spaces commands", () => { const space = spacesMock._getSpace("test-space"); const { stdout } = await runCommand( - [ - "spaces:cursors:set", - "test-space", - "--api-key", - "app.key:secret", - "--x", - "50", - "--y", - "75", - ], + ["spaces:cursors:set", "test-space", "--x", "50", "--y", "75"], import.meta.url, ); From cac20ce9124b23344bb5c2597eb8a1399201723a Mon Sep 17 00:00:00 2001 From: Vlad Velici Date: Wed, 31 Dec 2025 11:08:45 +0000 Subject: [PATCH 2/7] regenerate readme --- README.md | 793 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 549 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index fd59a43a..11cac968 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ -# Ably CLI +# Ably CLI and MCP server [![npm version](https://badge.fury.io/js/@ably%2Fcli.svg)](https://badge.fury.io/js/@ably%2Fcli) -[Ably](https://ably.com) CLI for [Ably Pub/Sub](https://ably.com/pubsub), [Ably Spaces](https://ably.com/spaces), [Ably Chat](https://ably.com/chat) and the [Ably Control API](https://ably.com/docs/account/control-api). +[Ably](https://ably.com) CLI and MCP server for [Ably Pub/Sub](https://ably.com/pubsub), [Ably Spaces](https://ably.com/spaces), [Ably Chat](https://ably.com/chat) and the [Ably Control API](https://ably.com/docs/account/control-api). + +> [!NOTE] +> This project is in beta and this CLI and MCP server project is being actively developed. +> Please [raise an issue](https://github.com/ably/ably-cli/issues) if you have feedback, feature requests or want to report a bug. We welcome [pull requests too](https://github.com/ably/ably-cli/pulls). ![Ably CLI screenshot](assets/cli-screenshot.png) -* [Ably CLI](#ably-cli) +* [Ably CLI and MCP server](#ably-cli-and-mcp-server) * [CLI Usage](#cli-usage) +* [MCP Usage](#mcp-usage) * [Commands](#commands) +* [MCP Server](#mcp-server) * [Contributing](#contributing) +* [or](#or) # CLI Usage -> [!NOTE] -> The Ably CLI is currently in Public Preview status. Please [raise an issue](https://github.com/ably/ably-cli/issues) if you have feedback, feature requests or want to report a bug. - ```sh-session $ npm install -g @ably/cli @@ -67,6 +71,20 @@ $ ably-interactive - Double Ctrl+C (within 500ms) force quits the shell - **No "ably" prefix needed**: Commands can be typed directly (e.g., just `channels list` instead of `ably channels list`) +# MCP Usage + +> [!WARNING] +> The MCP server is currently experimental. Please [raise an issue](https://github.com/ably/ably-cli/issues) if you have feedback or suggestions for features. + +1. Install the CLI following the [CLI usage](#cli-usage) steps. +2. Follow the instructions for your tool to set up an MCP server, such as [Claude desktop](https://modelcontextprotocol.io/quickstart/user), and configure: + 1. `command` as ably mcp start-server + +See [MCP Server section](#mcp-server) for more details on how to use the MCP Server. + +> [!NOTE] +> If you are having trouble getting the MCP server running, use [MCP inspector](https://github.com/modelcontextprotocol/inspector) + # Commands @@ -75,6 +93,7 @@ $ ably-interactive * [`ably accounts list`](#ably-accounts-list) * [`ably accounts login [TOKEN]`](#ably-accounts-login-token) * [`ably accounts logout [ALIAS]`](#ably-accounts-logout-alias) +* [`ably accounts stats`](#ably-accounts-stats) * [`ably accounts switch [ALIAS]`](#ably-accounts-switch-alias) * [`ably apps`](#ably-apps) * [`ably apps channel-rules`](#ably-apps-channel-rules) @@ -86,7 +105,11 @@ $ ably-interactive * [`ably apps current`](#ably-apps-current) * [`ably apps delete [APPID]`](#ably-apps-delete-appid) * [`ably apps list`](#ably-apps-list) +* [`ably apps logs`](#ably-apps-logs) +* [`ably apps logs history`](#ably-apps-logs-history) +* [`ably apps logs subscribe`](#ably-apps-logs-subscribe) * [`ably apps set-apns-p12 ID`](#ably-apps-set-apns-p12-id) +* [`ably apps stats [ID]`](#ably-apps-stats-id) * [`ably apps switch [APPID]`](#ably-apps-switch-appid) * [`ably apps update ID`](#ably-apps-update-id) * [`ably auth`](#ably-auth) @@ -109,6 +132,7 @@ $ ably-interactive * [`ably channels batch-publish [MESSAGE]`](#ably-channels-batch-publish-message) * [`ably channels history CHANNEL`](#ably-channels-history-channel) * [`ably channels list`](#ably-channels-list) +* [`ably channels logs [TOPIC]`](#ably-channels-logs-topic) * [`ably channels occupancy`](#ably-channels-occupancy) * [`ably channels occupancy get CHANNEL`](#ably-channels-occupancy-get-channel) * [`ably channels occupancy subscribe CHANNEL`](#ably-channels-occupancy-subscribe-channel) @@ -121,6 +145,7 @@ $ ably-interactive * [`ably config path`](#ably-config-path) * [`ably config show`](#ably-config-show) * [`ably connections`](#ably-connections) +* [`ably connections logs [TOPIC]`](#ably-connections-logs-topic) * [`ably connections test`](#ably-connections-test) * [`ably help [COMMANDS]`](#ably-help-commands) * [`ably integrations`](#ably-integrations) @@ -131,16 +156,18 @@ $ ably-interactive * [`ably integrations update RULEID`](#ably-integrations-update-ruleid) * [`ably login [TOKEN]`](#ably-login-token) * [`ably logs`](#ably-logs) +* [`ably logs app`](#ably-logs-app) +* [`ably logs app history`](#ably-logs-app-history) +* [`ably logs app subscribe`](#ably-logs-app-subscribe) * [`ably logs channel-lifecycle`](#ably-logs-channel-lifecycle) * [`ably logs channel-lifecycle subscribe`](#ably-logs-channel-lifecycle-subscribe) * [`ably logs connection-lifecycle`](#ably-logs-connection-lifecycle) * [`ably logs connection-lifecycle history`](#ably-logs-connection-lifecycle-history) * [`ably logs connection-lifecycle subscribe`](#ably-logs-connection-lifecycle-subscribe) -* [`ably logs history`](#ably-logs-history) +* [`ably logs connection subscribe`](#ably-logs-connection-subscribe) * [`ably logs push`](#ably-logs-push) * [`ably logs push history`](#ably-logs-push-history) * [`ably logs push subscribe`](#ably-logs-push-subscribe) -* [`ably logs subscribe`](#ably-logs-subscribe) * [`ably mcp`](#ably-mcp) * [`ably mcp start-server`](#ably-mcp-start-server) * [`ably queues`](#ably-queues) @@ -187,9 +214,6 @@ $ ably-interactive * [`ably spaces members`](#ably-spaces-members) * [`ably spaces members enter SPACE`](#ably-spaces-members-enter-space) * [`ably spaces members subscribe SPACE`](#ably-spaces-members-subscribe-space) -* [`ably stats`](#ably-stats) -* [`ably stats account`](#ably-stats-account) -* [`ably stats app [ID]`](#ably-stats-app-id) * [`ably status`](#ably-status) * [`ably support`](#ably-support) * [`ably support ask QUESTION`](#ably-support-ask-question) @@ -217,11 +241,14 @@ EXAMPLES $ ably accounts switch my-account + $ ably accounts stats + COMMANDS ably accounts current Show the current Ably account ably accounts list List locally configured Ably accounts ably accounts login Log in to your Ably account ably accounts logout Log out from an Ably account + ably accounts stats Get account stats with optional live updates ably accounts switch Switch to a different Ably account ``` @@ -380,6 +407,60 @@ EXAMPLES _See code: [src/commands/accounts/logout.ts](https://github.com/ably/ably-cli/blob/v0.15.0/src/commands/accounts/logout.ts)_ +## `ably accounts stats` + +Get account stats with optional live updates + +``` +USAGE + $ ably accounts stats [--access-token ] [--api-key ] [--client-id ] [--env ] + [--endpoint ] [--host ] [--json | --pretty-json] [--token ] [-v] [--debug] [--end ] + [--interval ] [--limit ] [--live] [--start ] [--unit minute|hour|day|month] + +FLAGS + -v, --verbose Output verbose logs + --access-token= Overrides any configured access token used for the Control API + --api-key= Overrides any configured API key used for the product APIs + --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly + set no client ID. Not applicable when using token authentication. + --debug Show debug information for live stats polling + --end= End time in milliseconds since epoch + --endpoint= Override the endpoint for all product API calls + --env= Override the environment for all product API calls + --host= Override the host endpoint for all product API calls + --interval= [default: 6] Polling interval in seconds (only used with --live) + --json Output in JSON format + --limit= [default: 10] Maximum number of stats records to return + --live Subscribe to live stats updates (uses minute interval) + --pretty-json Output in colorized JSON format + --start= Start time in milliseconds since epoch + --token= Authenticate using an Ably Token or JWT Token instead of an API key + --unit=