Skip to content

Commit 7c600eb

Browse files
committed
Merge branch 'main' into node-sdk-feature-definitions
2 parents 06ffb81 + ec60f6b commit 7c600eb

File tree

22 files changed

+3338
-139
lines changed

22 files changed

+3338
-139
lines changed

packages/cli/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,44 @@ Options:
175175
- `--out`: Path to generate TypeScript types
176176
- `--format`: Format of the generated types (react or node)
177177

178+
### `bucket companies`
179+
180+
Manage company data and feature access with the following subcommands.
181+
182+
#### `bucket companies list`
183+
184+
List all companies for the current app.
185+
This helps you visualize the companies using your features and their basic metrics.
186+
187+
```bash
188+
bucket companies list [--app-id ap123456789] [--filter nameOrId]
189+
```
190+
191+
Options:
192+
193+
- `--app-id`: App ID to use
194+
- `--filter`: Filter companies by name or ID
195+
196+
#### `bucket companies features access`
197+
198+
Grant or revoke access to specific features for a company.
199+
If no feature key is provided, you'll be prompted to select one from a list.
200+
201+
```bash
202+
bucket companies features access <companyId> [featureKey] [--enable|--disable] [--app-id ap123456789]
203+
```
204+
205+
Arguments:
206+
207+
- `companyId`: ID of the company to manage
208+
- `featureKey`: Key of the feature to grant/revoke access to (optional, interactive selection if omitted)
209+
210+
Options:
211+
212+
- `--enable`: Enable the feature for this company
213+
- `--disable`: Disable the feature for this company
214+
- `--app-id`: App ID to use
215+
178216
### `bucket apps`
179217

180218
Commands for managing Bucket apps.

packages/cli/commands/companies.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { select } from "@inquirer/prompts";
2+
import chalk from "chalk";
3+
import { Argument, Command } from "commander";
4+
import ora, { Ora } from "ora";
5+
6+
import { getApp } from "../services/bootstrap.js";
7+
import {
8+
CompanyFeatureAccess,
9+
companyFeatureAccess,
10+
listCompanies,
11+
} from "../services/companies.js";
12+
import { listFeatures } from "../services/features.js";
13+
import { configStore } from "../stores/config.js";
14+
import {
15+
handleError,
16+
MissingAppIdError,
17+
MissingEnvIdError,
18+
} from "../utils/errors.js";
19+
import {
20+
appIdOption,
21+
companyFilterOption,
22+
companyIdArgument,
23+
disableFeatureOption,
24+
enableFeatureOption,
25+
} from "../utils/options.js";
26+
import { baseUrlSuffix } from "../utils/path.js";
27+
28+
export const listCompaniesAction = async (options: { filter?: string }) => {
29+
const { baseUrl, appId } = configStore.getConfig();
30+
let spinner: Ora | undefined;
31+
32+
if (!appId) {
33+
return handleError(new MissingAppIdError(), "Companies List");
34+
}
35+
const app = getApp(appId);
36+
const production = app.environments.find((e) => e.isProduction);
37+
if (!production) {
38+
return handleError(new MissingEnvIdError(), "Companies List");
39+
}
40+
41+
try {
42+
spinner = ora(
43+
`Loading companies for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`,
44+
).start();
45+
46+
const companiesResponse = await listCompanies(appId, {
47+
envId: production.id,
48+
// Use the filter for name/ID filtering if provided
49+
idNameFilter: options.filter,
50+
});
51+
52+
spinner.succeed(
53+
`Loaded companies for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
54+
);
55+
56+
console.table(
57+
companiesResponse.data.map(({ id, name, userCount, lastSeen }) => ({
58+
id,
59+
name: name || "(unnamed)",
60+
users: userCount,
61+
lastSeen: lastSeen ? new Date(lastSeen).toLocaleDateString() : "Never",
62+
})),
63+
);
64+
65+
console.log(`Total companies: ${companiesResponse.totalCount}`);
66+
} catch (error) {
67+
spinner?.fail("Loading companies failed.");
68+
void handleError(error, "Companies List");
69+
}
70+
};
71+
72+
export const companyFeatureAccessAction = async (
73+
companyId: string,
74+
featureKey: string | undefined,
75+
options: { enable: boolean; disable: boolean },
76+
) => {
77+
const { baseUrl, appId } = configStore.getConfig();
78+
let spinner: Ora | undefined;
79+
80+
if (!appId) {
81+
return handleError(new MissingAppIdError(), "Company Feature Access");
82+
}
83+
84+
const app = getApp(appId);
85+
const production = app.environments.find((e) => e.isProduction);
86+
if (!production) {
87+
return handleError(new MissingEnvIdError(), "Company Feature Access");
88+
}
89+
90+
// Validate conflicting options
91+
if (options.enable && options.disable) {
92+
return handleError(
93+
"Cannot both enable and disable a feature.",
94+
"Company Feature Access",
95+
);
96+
}
97+
98+
if (!options.enable && !options.disable) {
99+
return handleError(
100+
"Must specify either --enable or --disable.",
101+
"Company Feature Access",
102+
);
103+
}
104+
105+
// If feature key is not provided, let user select one
106+
if (!featureKey) {
107+
try {
108+
spinner = ora(
109+
`Loading features for app ${chalk.cyan(app.name)}${baseUrlSuffix(
110+
baseUrl,
111+
)}...`,
112+
).start();
113+
114+
const featuresResponse = await listFeatures(appId, {
115+
envId: production.id,
116+
});
117+
118+
if (featuresResponse.data.length === 0) {
119+
return handleError(
120+
"No features found for this app.",
121+
"Company Feature Access",
122+
);
123+
}
124+
125+
spinner.succeed(
126+
`Loaded features for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
127+
);
128+
129+
featureKey = await select({
130+
message: "Select a feature to manage access:",
131+
choices: featuresResponse.data.map((feature) => ({
132+
name: `${feature.name} (${feature.key})`,
133+
value: feature.key,
134+
})),
135+
});
136+
} catch (error) {
137+
spinner?.fail("Loading features failed.");
138+
return handleError(error, "Company Feature Access");
139+
}
140+
}
141+
142+
// Determine if enabling or disabling
143+
const isEnabled = options.enable;
144+
145+
try {
146+
spinner = ora(
147+
`${isEnabled ? "Enabling" : "Disabling"} feature ${chalk.cyan(featureKey)} for company ${chalk.cyan(companyId)}...`,
148+
).start();
149+
150+
const request: CompanyFeatureAccess = {
151+
envId: production.id,
152+
companyId,
153+
featureKey,
154+
isEnabled,
155+
};
156+
157+
await companyFeatureAccess(appId, request);
158+
159+
spinner.succeed(
160+
`${isEnabled ? "Enabled" : "Disabled"} feature ${chalk.cyan(featureKey)} for company ${chalk.cyan(companyId)}.`,
161+
);
162+
} catch (error) {
163+
spinner?.fail(`Feature access update failed.`);
164+
void handleError(error, "Company Feature Access");
165+
}
166+
};
167+
168+
export function registerCompanyCommands(cli: Command) {
169+
const companiesCommand = new Command("companies").description(
170+
"Manage companies.",
171+
);
172+
173+
const companyFeaturesCommand = new Command("features").description(
174+
"Manage company features.",
175+
);
176+
177+
companiesCommand
178+
.command("list")
179+
.description("List all companies.")
180+
.addOption(appIdOption)
181+
.addOption(companyFilterOption)
182+
.action(listCompaniesAction);
183+
184+
// Feature access command
185+
companyFeaturesCommand
186+
.command("access")
187+
.description("Grant or revoke feature access for a specific company.")
188+
.addOption(appIdOption)
189+
.addArgument(companyIdArgument)
190+
.addArgument(
191+
new Argument(
192+
"[featureKey]",
193+
"Feature key. If not provided, you'll be prompted to select one",
194+
),
195+
)
196+
.addOption(enableFeatureOption)
197+
.addOption(disableFeatureOption)
198+
.action(companyFeatureAccessAction);
199+
200+
companiesCommand.addCommand(companyFeaturesCommand);
201+
202+
// Update the config with the cli override values
203+
companiesCommand.hook("preAction", (_, command) => {
204+
const { appId } = command.opts();
205+
configStore.setConfig({
206+
appId,
207+
});
208+
});
209+
210+
cli.addCommand(companiesCommand);
211+
}

0 commit comments

Comments
 (0)