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: 9 additions & 29 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,9 @@ Command-line interface for interacting with Bucket services. The CLI allows you
features, authentication, and generate TypeScript types for your Bucket features. With this tool,
you can streamline your feature flagging workflow directly from your terminal.

## Quick Start
## Usage

Get started quickly by running the CLI directly from your project's root directory:
initializing the CLI (if not already setup), creating a feature, and generate the types all at once.

```bash
# Initialize CLI (if not already setup), create a feature, and generate types all at once
npx @bucketco/cli new
```

or install it locally
Get started by installing the CLI locally in your project:

```bash
# npm
Expand All @@ -24,7 +16,8 @@ npm install --save-dev @bucketco/cli
yarn add --dev @bucketco/cli
```

then
Then running the `new` command from your project's root directory,
initializing the CLI, creating a feature, and generating the types all at once:

```bash
# npm
Expand All @@ -34,30 +27,19 @@ npx bucket new
yarn bucket new
```

### Global installation

You can also install the CLI globally adding the it to your PATH allowing you to use the shorthand `bucket`,
but we recommend installing it locally instead to better maintain the version.

```bash
npm install -g @bucketco/cli

bucket <command>
```

### Individual commands

Instead of running `new` you can call each step individually.

```bash
# Initialize Bucket in your project (if not already setup)
bucket init
npx bucket init

# Create a new feature
bucket features create "My Feature"
npx bucket features create "My Feature"

# Generate TypeScript types for your features
bucket features types
npx bucket features types
```

## Configuration
Expand All @@ -77,11 +59,10 @@ Here's a comprehensive list of configuration options available in the `bucket.co
"appId": "ap123456789",
"typesOutput": [
{
"path": "gen/features.ts",
"path": "gen/features.d.ts",
"format": "react"
}
],
"keyFormat": "camelCase"
]
}
```

Expand All @@ -92,7 +73,6 @@ Here's a comprehensive list of configuration options available in the `bucket.co
| `apiUrl` | API URL for Bucket services (overrides baseUrl for API calls). | "https://app.bucket.co/api" |
| `appId` | Your Bucket application ID. | Required |
| `typesOutput` | Path(s) where TypeScript types will be generated. Can be a string or an array of objects with `path` and `format` properties. Available formats: `react` and `node`. | "gen/features.ts" with format "react" |
| `keyFormat` | Format for feature keys (options: custom, pascalCase, camelCase, snakeCaseUpper, snakeCaseLower, kebabCaseUpper, kebabCaseLower). | "custom" |

You can override these settings using command-line options for individual commands.

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/commands/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const listAppsAction = async () => {
try {
const apps = await listApps();
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}.`);
console.table(apps);
console.table(apps.map(({ name, id, demo }) => ({ name, id, demo })));
} catch (error) {
spinner.fail("Failed to list apps.");
void handleError(error, "Apps List");
Expand Down
47 changes: 31 additions & 16 deletions packages/cli/commands/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@ import { mkdir, writeFile } from "node:fs/promises";
import { dirname, isAbsolute, join, relative } from "node:path";
import ora, { Ora } from "ora";

import { App, getApp, getOrg } from "../services/bootstrap.js";
import { createFeature, Feature, listFeatures } from "../services/features.js";
import { configStore } from "../stores/config.js";
import { handleError, MissingAppIdError } from "../utils/errors.js";
import { genFeatureKey, genTypes, KeyFormatPatterns } from "../utils/gen.js";
import {
genFeatureKey,
genTypes,
indentLines,
KeyFormatPatterns,
} from "../utils/gen.js";
import {
appIdOption,
featureKeyOption,
featureNameArgument,
keyFormatOption,
typesFormatOption,
typesOutOption,
} from "../utils/options.js";
import { baseUrlSuffix, featureUrl } from "../utils/path.js";

type CreateFeatureArgs = {
key?: string;
Expand All @@ -30,6 +36,11 @@ export const createFeatureAction = async (
let spinner: Ora | undefined;
try {
if (!appId) throw new MissingAppIdError();
const org = getOrg();
const app = getApp(appId);
console.log(
`Creating feature for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
);
if (!name) {
name = await input({
message: "New feature name:",
Expand All @@ -38,22 +49,25 @@ export const createFeatureAction = async (
}

if (!key) {
const keyFormat = configStore.getConfig("keyFormat") ?? "custom";
const keyFormat = org.featureKeyFormat;
key = await input({
message: "New feature key:",
default: genFeatureKey(name, keyFormat),
validate: KeyFormatPatterns[keyFormat].validate,
});
}

spinner = ora(
`Creating feature for app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}...`,
).start();
spinner = ora(`Creating feature...`).start();
const feature = await createFeature(appId, name, key);
// todo: would like to link to feature here but we don't have the env id, only app id
spinner.succeed(
`Created feature ${chalk.cyan(feature.name)} with key ${chalk.cyan(feature.key)} at ${chalk.cyan(baseUrl)}.`,
`Created feature ${chalk.cyan(feature.name)} with key ${chalk.cyan(feature.key)}:`,
);
const production = app.environments.find((e) => e.isProduction);
if (production) {
console.log(
indentLines(chalk.magenta(featureUrl(baseUrl, production, feature))),
);
}
} catch (error) {
spinner?.fail("Feature creation failed.");
void handleError(error, "Features Create");
Expand All @@ -66,12 +80,13 @@ export const listFeaturesAction = async () => {

try {
if (!appId) throw new MissingAppIdError();
const app = getApp(appId);
spinner = ora(
`Loading features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}...`,
`Loading features of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`,
).start();
const features = await listFeatures(appId);
spinner.succeed(
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}.`,
`Loaded features of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
);
console.table(
features.map(({ key, name, stage }) => ({
Expand All @@ -92,16 +107,18 @@ export const generateTypesAction = async () => {

let spinner: Ora | undefined;
let features: Feature[] = [];
let app: App | undefined;
try {
if (!appId) throw new MissingAppIdError();
app = getApp(appId);
spinner = ora(
`Loading features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}...`,
`Loading features of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`,
).start();
features = await listFeatures(appId, {
includeRemoteConfigs: true,
});
spinner.succeed(
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}.`,
`Loaded features of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
);
} catch (error) {
spinner?.fail("Loading features failed.");
Expand All @@ -127,7 +144,7 @@ export const generateTypesAction = async () => {
);
}

spinner.succeed(`Generated types for ${chalk.cyan(appId)}.`);
spinner.succeed(`Generated types for app ${chalk.cyan(app.name)}.`);
} catch (error) {
spinner?.fail("Type generation failed.");
void handleError(error, "Features Types");
Expand All @@ -143,7 +160,6 @@ export function registerFeatureCommands(cli: Command) {
.command("create")
.description("Create a new feature.")
.addOption(appIdOption)
.addOption(keyFormatOption)
.addOption(featureKeyOption)
.addArgument(featureNameArgument)
.action(createFeatureAction);
Expand All @@ -164,10 +180,9 @@ export function registerFeatureCommands(cli: Command) {

// Update the config with the cli override values
featuresCommand.hook("preAction", (_, command) => {
const { appId, keyFormat, out, format } = command.opts();
const { appId, out, format } = command.opts();
configStore.setConfig({
appId,
keyFormat,
typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
});
});
Expand Down
4 changes: 0 additions & 4 deletions packages/cli/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@ export const initAction = async (args: InitArgs = {}) => {
});
}

const keyFormat =
apps.find((app) => app.id === appId)?.featureKeyFormat ?? "custom";

// Get types output path
const typesOutput = await input({
message: "Where should we generate the types?",
Expand All @@ -85,7 +82,6 @@ export const initAction = async (args: InitArgs = {}) => {
// Update config
configStore.setConfig({
appId,
keyFormat,
typesOutput: [{ path: typesOutput, format: typesFormat }],
});

Expand Down
5 changes: 1 addition & 4 deletions packages/cli/commands/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
appIdOption,
featureKeyOption,
featureNameArgument,
keyFormatOption,
typesFormatOption,
typesOutOption,
} from "../utils/options.js";
Expand Down Expand Up @@ -38,7 +37,6 @@ export function registerNewCommand(cli: Command) {
"Initialize the Bucket CLI, authenticates, and creates a new feature.",
)
.addOption(appIdOption)
.addOption(keyFormatOption)
.addOption(typesOutOption)
.addOption(typesFormatOption)
.addOption(featureKeyOption)
Expand All @@ -47,10 +45,9 @@ export function registerNewCommand(cli: Command) {

// Update the config with the cli override values
cli.hook("preAction", (command) => {
const { appId, keyFormat, out, format } = command.opts();
const { appId, out, format } = command.opts();
configStore.setConfig({
appId,
keyFormat,
typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
});
});
Expand Down
27 changes: 24 additions & 3 deletions packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ import { registerAuthCommands } from "./commands/auth.js";
import { registerFeatureCommands } from "./commands/features.js";
import { registerInitCommand } from "./commands/init.js";
import { registerNewCommand } from "./commands/new.js";
import { bootstrap, getUser } from "./services/bootstrap.js";
import { authStore } from "./stores/auth.js";
import { configStore } from "./stores/config.js";
import { handleError } from "./utils/errors.js";
import { apiUrlOption, baseUrlOption, debugOption } from "./utils/options.js";
import { stripTrailingSlash } from "./utils/path.js";

type Options = {
debug?: boolean;
baseUrl?: string;
apiUrl?: string;
};

async function main() {
// Must load tokens and config before anything else
await authStore.initialize();
Expand All @@ -23,21 +31,34 @@ async function main() {
program.addOption(apiUrlOption);

// Pre-action hook
program.hook("preAction", () => {
const { debug, baseUrl, apiUrl } = program.opts();
program.hook("preAction", async () => {
const { debug, baseUrl, apiUrl } = program.opts<Options>();
const cleanedBaseUrl = stripTrailingSlash(baseUrl?.trim());
// Set baseUrl and apiUrl in config store, will skip if undefined
configStore.setConfig({
baseUrl: cleanedBaseUrl,
apiUrl:
stripTrailingSlash(apiUrl) ||
(cleanedBaseUrl && `${cleanedBaseUrl}/api`),
});

try {
// Load bootstrap data if not already loaded
await bootstrap();
} catch (error) {
void handleError(
debug ? error : `Unable to reach ${configStore.getConfig("baseUrl")}`,
"Connect",
);
}

if (debug) {
console.debug(chalk.cyan("\nDebug mode enabled"));
const user = getUser();
console.debug(`Logged in as ${chalk.cyan(user.name ?? user.email)}`);
console.debug(
"Reading config from",
chalk.green(configStore.getConfigPath()),
chalk.cyan(configStore.getConfigPath()),
);
console.table(configStore.getConfig());
}
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bucketco/cli",
"version": "0.2.2",
"version": "0.2.3",
"packageManager": "yarn@4.1.1",
"description": "CLI for Bucket service",
"main": "./dist/index.js",
Expand Down Expand Up @@ -50,12 +50,14 @@
"find-up": "^7.0.0",
"json5": "^2.2.3",
"open": "^10.1.0",
"ora": "^8.1.0"
"ora": "^8.1.0",
"slug": "^10.0.0"
},
"devDependencies": {
"@bucketco/eslint-config": "workspace:^",
"@bucketco/tsconfig": "workspace:^",
"@types/node": "^22.5.1",
"@types/slug": "^5.0.9",
"eslint": "^9.21.0",
"prettier": "^3.5.2",
"shx": "^0.3.4",
Expand Down
Loading