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
33 changes: 20 additions & 13 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,24 @@ Here's a comprehensive list of configuration options available in the `bucket.co
"baseUrl": "https://app.bucket.co",
"apiUrl": "https://app.bucket.co/api",
"appId": "ap123456789",
"typesOutput": "gen/features.ts",
"typesOutput": [
{
"path": "gen/features.ts",
"format": "react"
}
],
"keyFormat": "camelCase"
}
```

| Option | Description | Default |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `$schema` | Autocompletion for the config. `latest` can be replaced with a specific version. | "https://unpkg.com/@bucketco/cli@latest/schema.json" |
| `baseUrl` | Base URL for Bucket services. | "https://app.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 where TypeScript types will be generated. | "gen/features.ts" |
| `keyFormat` | Format for feature keys (options: custom, pascalCase, camelCase, snakeCaseUpper, snakeCaseLower, kebabCaseUpper, kebabCaseLower). | "custom" |
| Option | Description | Default |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `$schema` | Autocompletion for the config. `latest` can be replaced with a specific version. | "https://unpkg.com/@bucketco/cli@latest/schema.json" |
| `baseUrl` | Base URL for Bucket services. | "https://app.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 All @@ -93,12 +98,12 @@ You can override these settings using command-line options for individual comman
Initialize a new Bucket configuration in your project. This creates a `bucket.config.json` file with your settings and prompts for any required information not provided via options.

```bash
bucket init [--force]
bucket init [--overwrite]
```

Options:

- `--force`: Overwrite existing configuration file if one exists
- `--overwrite`: Overwrite existing configuration file if one exists
- `--app-id <id>`: Set the application ID
- `--key-format <format>`: Set the key format for features

Expand All @@ -107,7 +112,7 @@ Options:
All-in-one command to get started quickly. This command combines `init`, feature creation, and type generation in a single step. Use this for the fastest way to get up and running with Bucket.

```bash
bucket new "My Feature" [--key my-feature] [--app-id ap123456789] [--key-format custom] [--out gen/features.ts]
bucket new "My Feature" [--key my-feature] [--app-id ap123456789] [--key-format custom] [--out gen/features.ts] [--format react]
```

Options:
Expand All @@ -116,6 +121,7 @@ Options:
- `--app-id`: App ID to use
- `--key-format`: Format for feature keys (custom, snake, camel, etc.)
- `--out`: Path to generate TypeScript types
- `--format`: Format of the generated types (react or node)

If you prefer more control over each step, you can use the individual commands (`init`, `features create`, `features types`) instead.

Expand Down Expand Up @@ -170,13 +176,14 @@ Options:
Generate TypeScript types for your features. This ensures type safety when using Bucket features in your TypeScript/JavaScript applications.

```bash
bucket features types [--app-id ap123456789] [--out gen/features.ts]
bucket features types [--app-id ap123456789] [--out gen/features.ts] [--format react]
```

Options:

- `--app-id`: App ID to use
- `--out`: Path to generate TypeScript types
- `--format`: Format of the generated types (react or node)

### `bucket apps`

Expand Down
44 changes: 30 additions & 14 deletions packages/cli/commands/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import { mkdir, writeFile } from "node:fs/promises";
import { dirname, isAbsolute, join, relative } from "node:path";
import ora, { Ora } from "ora";

import { createFeature, listFeatures } from "../services/features.js";
import { createFeature, Feature, listFeatures } from "../services/features.js";
import { configStore } from "../stores/config.js";
import { handleError, MissingAppIdError } from "../utils/errors.js";
import { genDTS, genFeatureKey, KeyFormatPatterns } from "../utils/gen.js";
import { genFeatureKey, genTypes, KeyFormatPatterns } from "../utils/gen.js";
import {
appIdOption,
featureKeyOption,
featureNameArgument,
keyFormatOption,
typesFormatOption,
typesOutOption,
} from "../utils/options.js";

Expand Down Expand Up @@ -80,16 +81,19 @@ export const listFeaturesAction = async () => {
};

export const generateTypesAction = async () => {
const { baseUrl, appId, typesOutput } = configStore.getConfig();
const { baseUrl, appId } = configStore.getConfig();
const typesOutput = configStore.getConfig("typesOutput");

let spinner: Ora | undefined;
let featureKeys: string[] = [];
let features: Feature[] = [];
try {
if (!appId) throw new MissingAppIdError();
spinner = ora(
`Loading features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}...`,
).start();
featureKeys = (await listFeatures(appId)).map(({ key }) => key);
features = await listFeatures(appId, {
includeRemoteConfigs: true,
});
spinner.succeed(
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}`,
);
Expand All @@ -101,15 +105,22 @@ export const generateTypesAction = async () => {

try {
spinner = ora("Generating feature types...").start();
const types = genDTS(featureKeys);
const projectPath = configStore.getProjectPath();
const outPath = isAbsolute(typesOutput)
? typesOutput
: join(projectPath, typesOutput);
await mkdir(dirname(outPath), { recursive: true });
await writeFile(outPath, types);

// Generate types for each output configuration
for (const output of typesOutput) {
const types = await genTypes(features, output.format);
const outPath = isAbsolute(output.path)
? output.path
: join(projectPath, output.path);

await mkdir(dirname(outPath), { recursive: true });
await writeFile(outPath, types);
spinner.text = `Generated ${output.format} types for ${chalk.cyan(appId)} in ${chalk.cyan(relative(projectPath, outPath))}.`;
}

spinner.succeed(
`Generated types for ${chalk.cyan(appId)} in ${chalk.cyan(relative(projectPath, outPath))}.`,
`Generated types for ${chalk.cyan(appId)} in ${typesOutput.length} location(s).`,
);
} catch (error) {
spinner?.fail("Type generation failed");
Expand Down Expand Up @@ -142,12 +153,17 @@ export function registerFeatureCommands(cli: Command) {
.description("Generate feature types")
.addOption(appIdOption)
.addOption(typesOutOption)
.addOption(typesFormatOption)
.action(generateTypesAction);

// Update the config with the cli override values
featuresCommand.hook("preAction", (_, command) => {
const { appId, keyFormat, out } = command.opts();
configStore.setConfig({ appId, keyFormat, typesOutput: out });
const { appId, keyFormat, out, format } = command.opts();
configStore.setConfig({
appId,
keyFormat,
typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
});
});

cli.addCommand(featuresCommand);
Expand Down
26 changes: 18 additions & 8 deletions packages/cli/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { relative } from "node:path";
import ora, { Ora } from "ora";

import { App, listApps } from "../services/bootstrap.js";
import { configStore } from "../stores/config.js";
import { configStore, typeFormats } from "../stores/config.js";
import { chalkBrand, DEFAULT_TYPES_OUTPUT } from "../utils/constants.js";
import { handleError } from "../utils/errors.js";
import { initOverrideOption } from "../utils/options.js";
import { overwriteOption } from "../utils/options.js";

type InitArgs = {
force?: boolean;
overwrite?: boolean;
};

export const initAction = async (args: InitArgs = {}) => {
Expand All @@ -21,9 +21,9 @@ export const initAction = async (args: InitArgs = {}) => {
try {
// Check if config already exists
const configPath = configStore.getConfigPath();
if (configPath && !args.force) {
if (configPath && !args.overwrite) {
throw new Error(
"Bucket is already initialized. Use --force to overwrite.",
"Bucket is already initialized. Use --overwrite to overwrite.",
);
}

Expand Down Expand Up @@ -74,16 +74,26 @@ export const initAction = async (args: InitArgs = {}) => {
default: DEFAULT_TYPES_OUTPUT,
});

// Get types output format
const typesFormat = await select({
message: "What is the output format?",
choices: typeFormats.map((format) => ({
name: format,
value: format,
})),
default: "react",
});

// Update config
configStore.setConfig({
appId,
keyFormat,
typesOutput,
typesOutput: [{ path: typesOutput, format: typesFormat }],
});

// Create config file
spinner = ora("Creating configuration...").start();
await configStore.saveConfigFile(args.force);
await configStore.saveConfigFile(args.overwrite);
spinner.succeed(
`Configuration created at ${chalk.cyan(relative(process.cwd(), configStore.getConfigPath()!))}`,
);
Expand All @@ -97,6 +107,6 @@ export function registerInitCommand(cli: Command) {
cli
.command("init")
.description("Initialize a new Bucket configuration")
.addOption(initOverrideOption)
.addOption(overwriteOption)
.action(initAction);
}
10 changes: 8 additions & 2 deletions packages/cli/commands/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
featureKeyOption,
featureNameArgument,
keyFormatOption,
typesFormatOption,
typesOutOption,
} from "../utils/options.js";

Expand Down Expand Up @@ -39,13 +40,18 @@ export function registerNewCommand(cli: Command) {
.addOption(appIdOption)
.addOption(keyFormatOption)
.addOption(typesOutOption)
.addOption(typesFormatOption)
.addOption(featureKeyOption)
.addArgument(featureNameArgument)
.action(newAction);

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

async function main() {
// Must load tokens and config before anything else
Expand All @@ -25,9 +26,12 @@ async function main() {
// Pre-action hook
program.hook("preAction", () => {
const { debug, baseUrl, apiUrl } = program.opts();
const cleanedBaseUrl = stripTrailingSlash(baseUrl?.trim());
configStore.setConfig({
baseUrl,
apiUrl: apiUrl || (baseUrl && `${baseUrl}/api`),
baseUrl: cleanedBaseUrl,
apiUrl:
stripTrailingSlash(apiUrl) ||
(cleanedBaseUrl && `${cleanedBaseUrl}/api`),
});

if (debug) {
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bucketco/cli",
"version": "0.1.2",
"version": "0.1.3",
"packageManager": "yarn@4.1.1",
"description": "CLI for Bucket service",
"main": "./dist/index.js",
Expand Down Expand Up @@ -31,17 +31,22 @@
"scripts": {
"build": "tsc && shx chmod +x dist/index.js",
"bucket": "yarn build && ./dist/index.js",
"test": "vitest -c vite.config.js",
"test:ci": "vitest run -c vite.config.js --reporter=default --reporter=junit --outputFile=junit.xml",
"coverage": "vitest run --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write"
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn vitest run -c vite.config.js && yarn build"
},
"dependencies": {
"@inquirer/prompts": "^5.3.8",
"ajv": "^8.17.1",
"chalk": "^5.3.0",
"change-case": "^5.4.4",
"commander": "^12.1.0",
"fast-deep-equal": "^3.1.3",
"find-up": "^7.0.0",
"json5": "^2.2.3",
"open": "^10.1.0",
Expand All @@ -54,6 +59,7 @@
"eslint": "^9.21.0",
"prettier": "^3.5.2",
"shx": "^0.3.4",
"typescript": "^5.5.4"
"typescript": "^5.5.4",
"vitest": "^3.0.8"
}
}
10 changes: 9 additions & 1 deletion packages/cli/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
"type": "string"
},
"typesOutput": {
"type": "string"
"type": "array",
"items": {
"type": "object",
"properties": {
"path": { "type": "string" },
"format": { "type": "string", "enum": ["react", "node"] }
},
"required": ["path"]
}
},
"keyFormat": {
"type": "string",
Expand Down
Loading