Skip to content

Commit 5cc7d5c

Browse files
authored
feat: fix node type, add stages, cleanup logs (#367)
- Added stages export as a simple union `export type Stage = "Alpha" | "Beta";`. - Fix node-sdk type to match react-sdk. - Improved type gen logging. - Made all logging more consistent.
1 parent c3ac535 commit 5cc7d5c

12 files changed

Lines changed: 94 additions & 47 deletions

File tree

packages/cli/commands/apps.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ export const listAppsAction = async () => {
1111
const spinner = ora(`Loading apps from ${chalk.cyan(baseUrl)}...`).start();
1212
try {
1313
const apps = await listApps();
14-
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}`);
14+
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}.`);
1515
console.table(apps);
1616
} catch (error) {
17-
spinner.fail("Failed to list apps");
17+
spinner.fail("Failed to list apps.");
1818
void handleError(error, "Apps List");
1919
}
2020
};
2121

2222
export function registerAppCommands(cli: Command) {
23-
const appsCommand = new Command("apps").description("Manage apps");
23+
const appsCommand = new Command("apps").description("Manage apps.");
2424

2525
appsCommand
2626
.command("list")
27-
.description("List all available apps")
27+
.description("List all available apps.")
2828
.action(listAppsAction);
2929

3030
cli.addCommand(appsCommand);

packages/cli/commands/auth.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const loginAction = async () => {
1414
await authenticateUser(baseUrl);
1515
spinner.succeed(`Logged in to ${chalk.cyan(baseUrl)} successfully! 🎉`);
1616
} catch (error) {
17-
spinner.fail("Login failed");
17+
spinner.fail("Login failed.");
1818
void handleError(error, "Login");
1919
}
2020
};
@@ -26,13 +26,13 @@ export const logoutAction = async () => {
2626
await authStore.setToken(baseUrl, undefined);
2727
spinner.succeed("Logged out successfully! 👋");
2828
} catch (error) {
29-
spinner.fail("Logout failed");
29+
spinner.fail("Logout failed.");
3030
void handleError(error, "Logout");
3131
}
3232
};
3333

3434
export function registerAuthCommands(cli: Command) {
35-
cli.command("login").description("Login to Bucket").action(loginAction);
35+
cli.command("login").description("Login to Bucket.").action(loginAction);
3636

37-
cli.command("logout").description("Logout from Bucket").action(logoutAction);
37+
cli.command("logout").description("Logout from Bucket.").action(logoutAction);
3838
}

packages/cli/commands/features.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { dirname, isAbsolute, join, relative } from "node:path";
66
import ora, { Ora } from "ora";
77

88
import { createFeature, Feature, listFeatures } from "../services/features.js";
9+
import { listStages, Stage } from "../services/stages.js";
910
import { configStore } from "../stores/config.js";
1011
import { handleError, MissingAppIdError } from "../utils/errors.js";
1112
import { genFeatureKey, genTypes, KeyFormatPatterns } from "../utils/gen.js";
@@ -33,7 +34,7 @@ export const createFeatureAction = async (
3334
if (!name) {
3435
name = await input({
3536
message: "New feature name:",
36-
validate: (text) => text.length > 0 || "Name is required",
37+
validate: (text) => text.length > 0 || "Name is required.",
3738
});
3839
}
3940

@@ -55,7 +56,7 @@ export const createFeatureAction = async (
5556
`Created feature ${chalk.cyan(feature.name)} with key ${chalk.cyan(feature.key)} at ${chalk.cyan(baseUrl)}. 🎉`,
5657
);
5758
} catch (error) {
58-
spinner?.fail("Feature creation failed");
59+
spinner?.fail("Feature creation failed.");
5960
void handleError(error, "Features Create");
6061
}
6162
};
@@ -71,11 +72,17 @@ export const listFeaturesAction = async () => {
7172
).start();
7273
const features = await listFeatures(appId);
7374
spinner.succeed(
74-
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}`,
75+
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}.`,
76+
);
77+
console.table(
78+
features.map(({ key, name, stage }) => ({
79+
key,
80+
name,
81+
stage: stage?.name,
82+
})),
7583
);
76-
console.table(features);
7784
} catch (error) {
78-
spinner?.fail("Loading features failed");
85+
spinner?.fail("Loading features failed.");
7986
void handleError(error, "Features List");
8087
}
8188
};
@@ -86,6 +93,7 @@ export const generateTypesAction = async () => {
8693

8794
let spinner: Ora | undefined;
8895
let features: Feature[] = [];
96+
let stages: Stage[] = [];
8997
try {
9098
if (!appId) throw new MissingAppIdError();
9199
spinner = ora(
@@ -95,10 +103,20 @@ export const generateTypesAction = async () => {
95103
includeRemoteConfigs: true,
96104
});
97105
spinner.succeed(
98-
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}`,
106+
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}.`,
99107
);
100108
} catch (error) {
101-
spinner?.fail("Loading features failed");
109+
spinner?.fail("Loading features failed.");
110+
void handleError(error, "Features Types");
111+
return;
112+
}
113+
114+
try {
115+
spinner = ora(`Loading stages...`).start();
116+
stages = await listStages(appId);
117+
spinner.succeed(`Loaded stages.`);
118+
} catch (error) {
119+
spinner?.fail("Loading stages failed.");
102120
void handleError(error, "Features Types");
103121
return;
104122
}
@@ -109,33 +127,33 @@ export const generateTypesAction = async () => {
109127

110128
// Generate types for each output configuration
111129
for (const output of typesOutput) {
112-
const types = await genTypes(features, output.format);
130+
const types = await genTypes(features, stages, output.format);
113131
const outPath = isAbsolute(output.path)
114132
? output.path
115133
: join(projectPath, output.path);
116134

117135
await mkdir(dirname(outPath), { recursive: true });
118136
await writeFile(outPath, types);
119-
spinner.text = `Generated ${output.format} types for ${chalk.cyan(appId)} in ${chalk.cyan(relative(projectPath, outPath))}.`;
137+
spinner.succeed(
138+
`Generated ${output.format} types in ${chalk.cyan(relative(projectPath, outPath))}.`,
139+
);
120140
}
121141

122-
spinner.succeed(
123-
`Generated types for ${chalk.cyan(appId)} in ${typesOutput.length} location(s).`,
124-
);
142+
spinner.succeed(`Generated types for ${chalk.cyan(appId)}.`);
125143
} catch (error) {
126-
spinner?.fail("Type generation failed");
144+
spinner?.fail("Type generation failed.");
127145
void handleError(error, "Features Types");
128146
}
129147
};
130148

131149
export function registerFeatureCommands(cli: Command) {
132150
const featuresCommand = new Command("features").description(
133-
"Manage features",
151+
"Manage features.",
134152
);
135153

136154
featuresCommand
137155
.command("create")
138-
.description("Create a new feature")
156+
.description("Create a new feature.")
139157
.addOption(appIdOption)
140158
.addOption(keyFormatOption)
141159
.addOption(featureKeyOption)
@@ -144,13 +162,13 @@ export function registerFeatureCommands(cli: Command) {
144162

145163
featuresCommand
146164
.command("list")
147-
.description("List all features")
165+
.description("List all features.")
148166
.addOption(appIdOption)
149167
.action(listFeaturesAction);
150168

151169
featuresCommand
152170
.command("types")
153-
.description("Generate feature types")
171+
.description("Generate feature types.")
154172
.addOption(appIdOption)
155173
.addOption(typesOutOption)
156174
.addOption(typesFormatOption)

packages/cli/commands/init.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export const initAction = async (args: InitArgs = {}) => {
3333
// Load apps
3434
spinner = ora(`Loading apps from ${chalk.cyan(baseUrl)}...`).start();
3535
apps = await listApps();
36-
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}`);
36+
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}.`);
3737
} catch (error) {
38-
spinner?.fail("Loading apps failed");
38+
spinner?.fail("Loading apps failed.");
3939
void handleError(error, "Initialization");
4040
return;
4141
}
@@ -50,17 +50,15 @@ export const initAction = async (args: InitArgs = {}) => {
5050
} else if (nonDemoApps.length === 1) {
5151
appId = nonDemoApps[0].id;
5252
console.log(
53-
chalk.gray(
54-
`Automatically selected app ${nonDemoApps[0].name} (${appId})`,
55-
),
53+
`Automatically selected app ${chalk.cyan(nonDemoApps[0].name)} (${chalk.cyan(appId)}).`,
5654
);
5755
} else {
56+
const longestName = Math.max(...apps.map((app) => app.name.length));
5857
appId = await select({
5958
message: "Select an app",
6059
choices: apps.map((app) => ({
61-
name: app.name,
60+
name: `${app.name.padEnd(longestName, " ")}${app.demo ? " [Demo]" : ""}`,
6261
value: app.id,
63-
description: app.demo ? "Demo" : undefined,
6462
})),
6563
});
6664
}
@@ -95,18 +93,18 @@ export const initAction = async (args: InitArgs = {}) => {
9593
spinner = ora("Creating configuration...").start();
9694
await configStore.saveConfigFile(args.overwrite);
9795
spinner.succeed(
98-
`Configuration created at ${chalk.cyan(relative(process.cwd(), configStore.getConfigPath()!))}`,
96+
`Configuration created at ${chalk.cyan(relative(process.cwd(), configStore.getConfigPath()!))}.`,
9997
);
10098
} catch (error) {
101-
spinner?.fail("Configuration creation failed");
99+
spinner?.fail("Configuration creation failed.");
102100
void handleError(error, "Initialization");
103101
}
104102
};
105103

106104
export function registerInitCommand(cli: Command) {
107105
cli
108106
.command("init")
109-
.description("Initialize a new Bucket configuration")
107+
.description("Initialize a new Bucket configuration.")
110108
.addOption(overwriteOption)
111109
.action(initAction);
112110
}

packages/cli/commands/new.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function registerNewCommand(cli: Command) {
3535
cli
3636
.command("new")
3737
.description(
38-
"Initialize the Bucket CLI, authenticates, and creates a new feature",
38+
"Initialize the Bucket CLI, authenticates, and creates a new feature.",
3939
)
4040
.addOption(appIdOption)
4141
.addOption(keyFormatOption)

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bucketco/cli",
3-
"version": "0.1.4",
3+
"version": "0.2.0",
44
"packageManager": "yarn@4.1.1",
55
"description": "CLI for Bucket service",
66
"main": "./dist/index.js",

packages/cli/services/features.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { authRequest } from "../utils/auth.js";
22

3+
import { Stage } from "./stages.js";
4+
35
export type RemoteConfigVariant = {
46
key?: string;
57
payload?: any;
@@ -17,6 +19,7 @@ export type Feature = {
1719
name: string;
1820
key: string;
1921
remoteConfigs: RemoteConfig[];
22+
stage: Stage | null;
2023
};
2124

2225
export type FeaturesResponse = {

packages/cli/services/stages.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { authRequest } from "../utils/auth.js";
2+
3+
export type Stage = {
4+
id: string;
5+
name: string;
6+
order: number;
7+
};
8+
9+
type StagesResponse = {
10+
stages: Stage[];
11+
};
12+
13+
export async function listStages(appId: string): Promise<Stage[]> {
14+
const response = await authRequest<StagesResponse>(`/apps/${appId}/stages`);
15+
return response.stages;
16+
}

packages/cli/utils/auth.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export async function authenticateUser(baseUrl: string) {
1919
let isResolved = false;
2020

2121
const server = http.createServer(async (req, res) => {
22-
const url = new URL(req.url ?? "/", "http://localhost");
22+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
2323
const headers = corsHeaders(baseUrl);
2424

2525
// Ensure we don't process requests after resolution
@@ -63,8 +63,8 @@ export async function authenticateUser(baseUrl: string) {
6363
});
6464

6565
const timeout = setTimeout(() => {
66-
cleanupAndReject(new Error("Authentication timed out after 30 seconds"));
67-
}, 30000);
66+
cleanupAndReject(new Error("Authentication timed out after 60 seconds"));
67+
}, 60000);
6868

6969
function cleanupAndResolve(token: string) {
7070
if (isResolved) return;
@@ -94,9 +94,7 @@ export async function authenticateUser(baseUrl: string) {
9494
const address = server.address();
9595
if (address && typeof address === "object") {
9696
const port = address.port;
97-
void open(loginUrl(baseUrl, port), {
98-
newInstance: true,
99-
});
97+
void open(loginUrl(baseUrl, port));
10098
}
10199
});
102100
}

packages/cli/utils/gen.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { camelCase, kebabCase, pascalCase, snakeCase } from "change-case";
22

33
import { Feature, RemoteConfig } from "../services/features.js";
4+
import { Stage } from "../services/stages.js";
45

56
import { JSONToType } from "./json.js";
67

@@ -89,7 +90,11 @@ export function genRemoteConfig(remoteConfigs?: RemoteConfig[]) {
8990
);
9091
}
9192

92-
export function genTypes(features: Feature[], format: GenFormat = "react") {
93+
export function genTypes(
94+
features: Feature[],
95+
stages: Stage[],
96+
format: GenFormat = "react",
97+
) {
9398
const configDefs = new Map<string, { name: string; definition: string }>();
9499
features.forEach(({ key, name, remoteConfigs }) => {
95100
const definition = genRemoteConfig(remoteConfigs);
@@ -105,6 +110,11 @@ export function genTypes(features: Feature[], format: GenFormat = "react") {
105110
import "@bucketco/${format}-sdk";
106111
107112
declare module "@bucketco/${format}-sdk" {
113+
${
114+
stages.length
115+
? /* ts */ `export type Stage = ${stages.map(({ name }) => `"${name}"`).join(" | ")};\n`
116+
: ""
117+
}
108118
export interface Features {
109119
${features
110120
.map(({ key }) => {

0 commit comments

Comments
 (0)