@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
129 lines (128 loc) • 5.9 kB
JavaScript
import { Flags } from "@oclif/core";
import { SpacesBaseCommand } from "../../spaces-base-command.js";
import chalk from "chalk";
export default class SpacesList extends SpacesBaseCommand {
static description = "List active spaces";
static examples = [
"$ ably spaces list",
"$ ably spaces list --prefix my-space",
"$ ably spaces list --limit 50",
"$ ably spaces list --json",
"$ ably spaces list --pretty-json",
];
static flags = {
...SpacesBaseCommand.globalFlags,
limit: Flags.integer({
default: 100,
description: "Maximum number of spaces to return",
}),
prefix: Flags.string({
char: "p",
description: "Filter spaces by prefix",
}),
};
async run() {
const { flags } = await this.parse(SpacesList);
try {
// REST client for channel enumeration
const rest = await this.createAblyRestClient(flags);
if (!rest)
return;
const params = {
limit: flags.limit * 5, // Request more to allow for filtering
};
if (flags.prefix) {
params.prefix = flags.prefix;
}
// Fetch channels
const channelsResponse = await rest.request("get", "/channels", 2, params);
if (channelsResponse.statusCode !== 200) {
this.error(`Failed to list spaces: ${channelsResponse.statusCode}`);
return;
}
// Filter to only include space channels
const allChannels = channelsResponse.items || [];
// Map to store deduplicated spaces
const spaces = new Map();
// Filter for space channels and deduplicate
for (const channel of allChannels) {
const { channelId } = channel;
// Check if this is a space channel (has ::$space suffix)
if (channelId.includes("::$space")) {
// Extract the base space name (everything before the first ::$space)
// We need to escape the $ in the regex pattern since it's a special character
const spaceNameMatch = channelId.match(/^(.+?)::\$space.*$/);
if (spaceNameMatch && spaceNameMatch[1]) {
const spaceName = spaceNameMatch[1];
// Only add if we haven't seen this space before
if (!spaces.has(spaceName)) {
// Store the original channel data but with the simple space name
const spaceData = {
...channel,
channelId: spaceName,
spaceName,
};
spaces.set(spaceName, spaceData);
}
}
}
}
// Convert map to array
const spacesList = [...spaces.values()];
// Limit the results to the requested number
const limitedSpaces = spacesList.slice(0, flags.limit);
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({
hasMore: spacesList.length > flags.limit,
shown: limitedSpaces.length,
spaces: limitedSpaces.map((space) => ({
metrics: space.status?.occupancy?.metrics || {},
spaceName: space.spaceName,
})),
success: true,
timestamp: new Date().toISOString(),
total: spacesList.length,
}, flags));
}
else {
if (limitedSpaces.length === 0) {
this.log("No active spaces found.");
return;
}
this.log(`Found ${chalk.cyan(limitedSpaces.length.toString())} active spaces:`);
limitedSpaces.forEach((space) => {
this.log(`${chalk.green(space.spaceName)}`);
// Show occupancy if available
if (space.status?.occupancy?.metrics) {
const { metrics } = space.status.occupancy;
this.log(` ${chalk.dim("Connections:")} ${metrics.connections || 0}`);
this.log(` ${chalk.dim("Publishers:")} ${metrics.publishers || 0}`);
this.log(` ${chalk.dim("Subscribers:")} ${metrics.subscribers || 0}`);
if (metrics.presenceConnections !== undefined) {
this.log(` ${chalk.dim("Presence Connections:")} ${metrics.presenceConnections}`);
}
if (metrics.presenceMembers !== undefined) {
this.log(` ${chalk.dim("Presence Members:")} ${metrics.presenceMembers}`);
}
}
this.log(""); // Add a line break between spaces
});
if (spacesList.length > flags.limit) {
this.log(chalk.yellow(`Showing ${flags.limit} of ${spacesList.length} spaces. Use --limit to show more.`));
}
}
}
catch (error) {
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({
error: error instanceof Error ? error.message : String(error),
status: "error",
success: false,
}, flags));
}
else {
this.error(`Error listing spaces: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
}