Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,44 @@ Options:
- `--out`: Path to generate TypeScript types
- `--format`: Format of the generated types (react or node)

### `bucket companies`

Manage company data and feature access with the following subcommands.

#### `bucket companies list`

List all companies for the current app.
This helps you visualize the companies using your features and their basic metrics.

```bash
bucket companies list [--app-id ap123456789] [--filter nameOrId]
```

Options:

- `--app-id`: App ID to use
- `--filter`: Filter companies by name or ID

#### `bucket companies features access`

Grant or revoke access to specific features for a company.
If no feature key is provided, you'll be prompted to select one from a list.

```bash
bucket companies features access <companyId> [featureKey] [--enable|--disable] [--app-id ap123456789]
```

Arguments:

- `companyId`: ID of the company to manage
- `featureKey`: Key of the feature to grant/revoke access to (optional, interactive selection if omitted)

Options:

- `--enable`: Enable the feature for this company
- `--disable`: Disable the feature for this company
- `--app-id`: App ID to use

### `bucket apps`

Commands for managing Bucket apps.
Expand Down
211 changes: 211 additions & 0 deletions packages/cli/commands/companies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { select } from "@inquirer/prompts";
import chalk from "chalk";
import { Argument, Command } from "commander";
import ora, { Ora } from "ora";

import { getApp } from "../services/bootstrap.js";
import {
CompanyFeatureAccess,
companyFeatureAccess,
listCompanies,
} from "../services/companies.js";
import { listFeatures } from "../services/features.js";
import { configStore } from "../stores/config.js";
import {
handleError,
MissingAppIdError,
MissingEnvIdError,
} from "../utils/errors.js";
import {
appIdOption,
companyFilterOption,
companyIdArgument,
disableFeatureOption,
enableFeatureOption,
} from "../utils/options.js";
import { baseUrlSuffix } from "../utils/path.js";

export const listCompaniesAction = async (options: { filter?: string }) => {
const { baseUrl, appId } = configStore.getConfig();
let spinner: Ora | undefined;

if (!appId) {
return handleError(new MissingAppIdError(), "Companies List");
}
const app = getApp(appId);
const production = app.environments.find((e) => e.isProduction);
if (!production) {
return handleError(new MissingEnvIdError(), "Companies List");
}

try {
spinner = ora(
`Loading companies for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`,
).start();

const companiesResponse = await listCompanies(appId, {
envId: production.id,
// Use the filter for name/ID filtering if provided
idNameFilter: options.filter,
});

spinner.succeed(
`Loaded companies for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
);

console.table(
companiesResponse.data.map(({ id, name, userCount, lastSeen }) => ({
id,
name: name || "(unnamed)",
users: userCount,
lastSeen: lastSeen ? new Date(lastSeen).toLocaleDateString() : "Never",
})),
);

console.log(`Total companies: ${companiesResponse.totalCount}`);
} catch (error) {
spinner?.fail("Loading companies failed.");
void handleError(error, "Companies List");
}
};

export const companyFeatureAccessAction = async (
companyId: string,
featureKey: string | undefined,
options: { enable: boolean; disable: boolean },
) => {
const { baseUrl, appId } = configStore.getConfig();
let spinner: Ora | undefined;

if (!appId) {
return handleError(new MissingAppIdError(), "Company Feature Access");
}

const app = getApp(appId);
const production = app.environments.find((e) => e.isProduction);
if (!production) {
return handleError(new MissingEnvIdError(), "Company Feature Access");
}

// Validate conflicting options
if (options.enable && options.disable) {
return handleError(
"Cannot both enable and disable a feature.",
"Company Feature Access",
);
}

if (!options.enable && !options.disable) {
return handleError(
"Must specify either --enable or --disable.",
"Company Feature Access",
);
}

// If feature key is not provided, let user select one
if (!featureKey) {
try {
spinner = ora(
`Loading features for app ${chalk.cyan(app.name)}${baseUrlSuffix(
baseUrl,
)}...`,
).start();

const featuresResponse = await listFeatures(appId, {
envId: production.id,
});

if (featuresResponse.data.length === 0) {
return handleError(
"No features found for this app.",
"Company Feature Access",
);
}

spinner.succeed(
`Loaded features for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
);

featureKey = await select({
message: "Select a feature to manage access:",
choices: featuresResponse.data.map((feature) => ({
name: `${feature.name} (${feature.key})`,
value: feature.key,
})),
});
} catch (error) {
spinner?.fail("Loading features failed.");
return handleError(error, "Company Feature Access");
}
}

// Determine if enabling or disabling
const isEnabled = options.enable;

try {
spinner = ora(
`${isEnabled ? "Enabling" : "Disabling"} feature ${chalk.cyan(featureKey)} for company ${chalk.cyan(companyId)}...`,
).start();

const request: CompanyFeatureAccess = {
envId: production.id,
companyId,
featureKey,
isEnabled,
};

await companyFeatureAccess(appId, request);

spinner.succeed(
`${isEnabled ? "Enabled" : "Disabled"} feature ${chalk.cyan(featureKey)} for company ${chalk.cyan(companyId)}.`,
);
} catch (error) {
spinner?.fail(`Feature access update failed.`);
void handleError(error, "Company Feature Access");
}
};

export function registerCompanyCommands(cli: Command) {
const companiesCommand = new Command("companies").description(
"Manage companies.",
);

const companyFeaturesCommand = new Command("features").description(
"Manage company features.",
);

companiesCommand
.command("list")
.description("List all companies.")
.addOption(appIdOption)
.addOption(companyFilterOption)
.action(listCompaniesAction);

// Feature access command
companyFeaturesCommand
.command("access")
.description("Grant or revoke feature access for a specific company.")
.addOption(appIdOption)
.addArgument(companyIdArgument)
.addArgument(
new Argument(
"[featureKey]",
"Feature key. If not provided, you'll be prompted to select one",
),
)
.addOption(enableFeatureOption)
.addOption(disableFeatureOption)
.action(companyFeatureAccessAction);

companiesCommand.addCommand(companyFeaturesCommand);

// Update the config with the cli override values
companiesCommand.hook("preAction", (_, command) => {
const { appId } = command.opts();
configStore.setConfig({
appId,
});
});

cli.addCommand(companiesCommand);
}
Loading