Skip to content

Commit 52e77da

Browse files
committed
feat: removed key format and created link to feature
1 parent 35b1a78 commit 52e77da

File tree

15 files changed

+109
-90
lines changed

15 files changed

+109
-90
lines changed

packages/cli/README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ Here's a comprehensive list of configuration options available in the `bucket.co
7474
"path": "gen/features.d.ts",
7575
"format": "react"
7676
}
77-
],
78-
"keyFormat": "camelCase"
77+
]
7978
}
8079
```
8180

@@ -86,7 +85,6 @@ Here's a comprehensive list of configuration options available in the `bucket.co
8685
| `apiUrl` | API URL for Bucket services (overrides baseUrl for API calls). | "https://app.bucket.co/api" |
8786
| `appId` | Your Bucket application ID. | Required |
8887
| `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" |
89-
| `keyFormat` | Format for feature keys (options: custom, pascalCase, camelCase, snakeCaseUpper, snakeCaseLower, kebabCaseUpper, kebabCaseLower). | "custom" |
9088

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

packages/cli/commands/features.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,19 @@ import { mkdir, writeFile } from "node:fs/promises";
55
import { dirname, isAbsolute, join, relative } from "node:path";
66
import ora, { Ora } from "ora";
77

8-
import { App, getApp } from "../services/bootstrap.js";
8+
import { App, getApp, getOrg } from "../services/bootstrap.js";
99
import { createFeature, Feature, listFeatures } from "../services/features.js";
1010
import { configStore } from "../stores/config.js";
11-
import { baseUrlSuffix } from "../utils/constants.js";
1211
import { handleError, MissingAppIdError } from "../utils/errors.js";
1312
import { genFeatureKey, genTypes, KeyFormatPatterns } from "../utils/gen.js";
1413
import {
1514
appIdOption,
1615
featureKeyOption,
1716
featureNameArgument,
18-
keyFormatOption,
1917
typesFormatOption,
2018
typesOutOption,
2119
} from "../utils/options.js";
20+
import { baseUrlSuffix, featureUrl } from "../utils/path.js";
2221

2322
type CreateFeatureArgs = {
2423
key?: string;
@@ -32,6 +31,7 @@ export const createFeatureAction = async (
3231
let spinner: Ora | undefined;
3332
try {
3433
if (!appId) throw new MissingAppIdError();
34+
const org = getOrg();
3535
const app = getApp(appId);
3636
console.log(
3737
`Creating feature for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
@@ -44,7 +44,7 @@ export const createFeatureAction = async (
4444
}
4545

4646
if (!key) {
47-
const keyFormat = configStore.getConfig("keyFormat") ?? "custom";
47+
const keyFormat = org.featureKeyFormat;
4848
key = await input({
4949
message: "New feature key:",
5050
default: genFeatureKey(name, keyFormat),
@@ -54,9 +54,13 @@ export const createFeatureAction = async (
5454

5555
spinner = ora(`Creating feature...`).start();
5656
const feature = await createFeature(appId, name, key);
57-
// todo: would like to link to feature here but we don't have the env id, only app id
57+
const production = app.environments.find((e) => e.isProduction);
5858
spinner.succeed(
59-
`Created feature ${chalk.cyan(feature.name)} with key ${chalk.cyan(feature.key)}${baseUrlSuffix(baseUrl)}.`,
59+
`Created feature ${chalk.cyan(feature.name)} with key ${chalk.cyan(feature.key)}${
60+
production
61+
? ` at ${chalk.cyan(featureUrl(baseUrl, production, feature))}`
62+
: ""
63+
}.`,
6064
);
6165
} catch (error) {
6266
spinner?.fail("Feature creation failed.");
@@ -150,7 +154,6 @@ export function registerFeatureCommands(cli: Command) {
150154
.command("create")
151155
.description("Create a new feature.")
152156
.addOption(appIdOption)
153-
.addOption(keyFormatOption)
154157
.addOption(featureKeyOption)
155158
.addArgument(featureNameArgument)
156159
.action(createFeatureAction);
@@ -171,10 +174,9 @@ export function registerFeatureCommands(cli: Command) {
171174

172175
// Update the config with the cli override values
173176
featuresCommand.hook("preAction", (_, command) => {
174-
const { appId, keyFormat, out, format } = command.opts();
177+
const { appId, out, format } = command.opts();
175178
configStore.setConfig({
176179
appId,
177-
keyFormat,
178180
typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
179181
});
180182
});

packages/cli/commands/init.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,6 @@ export const initAction = async (args: InitArgs = {}) => {
6363
});
6464
}
6565

66-
const keyFormat =
67-
apps.find((app) => app.id === appId)?.featureKeyFormat ?? "custom";
68-
6966
// Get types output path
7067
const typesOutput = await input({
7168
message: "Where should we generate the types?",
@@ -85,7 +82,6 @@ export const initAction = async (args: InitArgs = {}) => {
8582
// Update config
8683
configStore.setConfig({
8784
appId,
88-
keyFormat,
8985
typesOutput: [{ path: typesOutput, format: typesFormat }],
9086
});
9187

packages/cli/commands/new.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
appIdOption,
88
featureKeyOption,
99
featureNameArgument,
10-
keyFormatOption,
1110
typesFormatOption,
1211
typesOutOption,
1312
} from "../utils/options.js";
@@ -38,7 +37,6 @@ export function registerNewCommand(cli: Command) {
3837
"Initialize the Bucket CLI, authenticates, and creates a new feature.",
3938
)
4039
.addOption(appIdOption)
41-
.addOption(keyFormatOption)
4240
.addOption(typesOutOption)
4341
.addOption(typesFormatOption)
4442
.addOption(featureKeyOption)
@@ -47,10 +45,9 @@ export function registerNewCommand(cli: Command) {
4745

4846
// Update the config with the cli override values
4947
cli.hook("preAction", (command) => {
50-
const { appId, keyFormat, out, format } = command.opts();
48+
const { appId, out, format } = command.opts();
5149
configStore.setConfig({
5250
appId,
53-
keyFormat,
5451
typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
5552
});
5653
});

packages/cli/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import { handleError } from "./utils/errors.js";
1414
import { apiUrlOption, baseUrlOption, debugOption } from "./utils/options.js";
1515
import { stripTrailingSlash } from "./utils/path.js";
1616

17+
type Options = {
18+
debug?: boolean;
19+
baseUrl?: string;
20+
apiUrl?: string;
21+
};
22+
1723
async function main() {
1824
// Must load tokens and config before anything else
1925
await authStore.initialize();
@@ -26,8 +32,9 @@ async function main() {
2632

2733
// Pre-action hook
2834
program.hook("preAction", async () => {
29-
const { debug, baseUrl, apiUrl } = program.opts();
35+
const { debug, baseUrl, apiUrl } = program.opts<Options>();
3036
const cleanedBaseUrl = stripTrailingSlash(baseUrl?.trim());
37+
// Set baseUrl and apiUrl in config store, will skip if undefined
3138
configStore.setConfig({
3239
baseUrl: cleanedBaseUrl,
3340
apiUrl:

packages/cli/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@
5050
"find-up": "^7.0.0",
5151
"json5": "^2.2.3",
5252
"open": "^10.1.0",
53-
"ora": "^8.1.0"
53+
"ora": "^8.1.0",
54+
"slug": "^10.0.0"
5455
},
5556
"devDependencies": {
5657
"@bucketco/eslint-config": "workspace:^",
5758
"@bucketco/tsconfig": "workspace:^",
5859
"@types/node": "^22.5.1",
60+
"@types/slug": "^5.0.9",
5961
"eslint": "^9.21.0",
6062
"prettier": "^3.5.2",
6163
"shx": "^0.3.4",

packages/cli/schema.json

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,6 @@
3030
},
3131
"required": ["path"]
3232
}
33-
},
34-
"keyFormat": {
35-
"type": "string",
36-
"description": "Format for the suggested feature keys, must match Bucket app settings. Defaults to custom.",
37-
"enum": [
38-
"custom",
39-
"pascalCase",
40-
"camelCase",
41-
"snakeCaseUpper",
42-
"snakeCaseLower",
43-
"kebabCaseUpper",
44-
"kebabCaseLower"
45-
]
4633
}
4734
},
4835
"required": ["appId"]

packages/cli/services/bootstrap.ts

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { KeyFormat } from "../stores/config.js";
21
import { authRequest } from "../utils/auth.js";
2+
import { KeyFormat } from "../utils/gen.js";
33

44
export type BootstrapResponse = {
5-
org: OrgResponse;
6-
user: UserResponse;
5+
org: Org;
6+
user: User;
77
};
88

9-
export type OrgResponse = {
9+
export type Org = {
1010
id: string;
1111
name: string;
1212
logoUrl: string;
13-
apps: AppResponse[];
13+
apps: App[];
1414
inviteKey: string;
1515
createdAt: Date;
1616
updatedAt: Date;
@@ -23,13 +23,21 @@ export type OrgResponse = {
2323
featureKeyFormat: KeyFormat;
2424
};
2525

26-
export type AppResponse = {
26+
export type Environment = {
27+
id: string;
28+
name: string;
29+
isProduction: boolean;
30+
order: number;
31+
};
32+
33+
export type App = {
2734
id: string;
2835
name: string;
2936
demo: boolean;
37+
environments: Environment[];
3038
};
3139

32-
export type UserResponse = {
40+
export type User = {
3341
id: string;
3442
email: string;
3543
name: string;
@@ -39,13 +47,6 @@ export type UserResponse = {
3947
isAdmin: boolean;
4048
};
4149

42-
export type App = {
43-
id: string;
44-
name: string;
45-
demo: boolean;
46-
featureKeyFormat: KeyFormat;
47-
};
48-
4950
let bootstrapResponse: BootstrapResponse | null = null;
5051

5152
export async function bootstrap(): Promise<BootstrapResponse> {
@@ -55,23 +56,28 @@ export async function bootstrap(): Promise<BootstrapResponse> {
5556
return bootstrapResponse;
5657
}
5758

59+
export function getOrg(): Org {
60+
if (!bootstrapResponse) {
61+
throw new Error("CLI has not been bootstrapped.");
62+
}
63+
if (!bootstrapResponse.org) {
64+
throw new Error("No organization found.");
65+
}
66+
return bootstrapResponse.org;
67+
}
68+
5869
export function listApps(): App[] {
5970
if (!bootstrapResponse) {
60-
throw new Error("Failed to load bootstrap response");
71+
throw new Error("CLI has not been bootstrapped.");
6172
}
6273
const org = bootstrapResponse.org;
6374
if (!org) {
64-
throw new Error("No organization found");
75+
throw new Error("No organization found.");
6576
}
6677
if (!org.apps?.length) {
67-
throw new Error("No apps found");
78+
throw new Error("No apps found.");
6879
}
69-
return bootstrapResponse.org.apps.map(({ id, name, demo }) => ({
70-
name,
71-
id,
72-
featureKeyFormat: org.featureKeyFormat ?? "custom",
73-
demo,
74-
}));
80+
return bootstrapResponse.org.apps;
7581
}
7682

7783
export function getApp(id: string): App {
@@ -83,9 +89,12 @@ export function getApp(id: string): App {
8389
return app;
8490
}
8591

86-
export function getUser(): UserResponse {
92+
export function getUser(): User {
8793
if (!bootstrapResponse) {
88-
throw new Error("Failed to load bootstrap response");
94+
throw new Error("CLI has not been bootstrapped.");
95+
}
96+
if (!bootstrapResponse.user) {
97+
throw new Error("No user found.");
8998
}
9099
return bootstrapResponse.user;
91100
}

packages/cli/services/features.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type RemoteConfig = {
1616
};
1717

1818
export type Feature = {
19+
id: string;
1920
name: string;
2021
key: string;
2122
remoteConfigs: RemoteConfig[];

packages/cli/stores/config.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,6 @@ import {
1616
import { ConfigValidationError, handleError } from "../utils/errors.js";
1717
import { stripTrailingSlash } from "../utils/path.js";
1818

19-
export const keyFormats = [
20-
"custom",
21-
"pascalCase",
22-
"camelCase",
23-
"snakeCaseUpper",
24-
"snakeCaseLower",
25-
"kebabCaseUpper",
26-
"kebabCaseLower",
27-
] as const;
28-
export type KeyFormat = (typeof keyFormats)[number];
29-
3019
export const typeFormats = ["react", "node"] as const;
3120
export type TypeFormat = (typeof typeFormats)[number];
3221

@@ -41,7 +30,6 @@ type Config = {
4130
apiUrl: string;
4231
appId: string | undefined;
4332
typesOutput: TypesOutput[];
44-
keyFormat: KeyFormat;
4533
};
4634

4735
const defaultConfig: Config = {
@@ -50,7 +38,6 @@ const defaultConfig: Config = {
5038
apiUrl: DEFAULT_API_URL,
5139
appId: undefined,
5240
typesOutput: [{ path: DEFAULT_TYPES_OUTPUT, format: "react" }],
53-
keyFormat: "custom",
5441
};
5542

5643
// Helper to normalize typesOutput to array format

0 commit comments

Comments
 (0)