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
8 changes: 4 additions & 4 deletions packages/cli/commands/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ export const listAppsAction = async () => {
const spinner = ora(`Loading apps from ${chalk.cyan(baseUrl)}...`).start();
try {
const apps = await listApps();
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}`);
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}.`);
console.table(apps);
} catch (error) {
spinner.fail("Failed to list apps");
spinner.fail("Failed to list apps.");
void handleError(error, "Apps List");
}
};

export function registerAppCommands(cli: Command) {
const appsCommand = new Command("apps").description("Manage apps");
const appsCommand = new Command("apps").description("Manage apps.");

appsCommand
.command("list")
.description("List all available apps")
.description("List all available apps.")
.action(listAppsAction);

cli.addCommand(appsCommand);
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const loginAction = async () => {
await authenticateUser(baseUrl);
spinner.succeed(`Logged in to ${chalk.cyan(baseUrl)} successfully! 🎉`);
} catch (error) {
spinner.fail("Login failed");
spinner.fail("Login failed.");
void handleError(error, "Login");
}
};
Expand All @@ -26,13 +26,13 @@ export const logoutAction = async () => {
await authStore.setToken(baseUrl, undefined);
spinner.succeed("Logged out successfully! 👋");
} catch (error) {
spinner.fail("Logout failed");
spinner.fail("Logout failed.");
void handleError(error, "Logout");
}
};

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

cli.command("logout").description("Logout from Bucket").action(logoutAction);
cli.command("logout").description("Logout from Bucket.").action(logoutAction);
}
52 changes: 35 additions & 17 deletions packages/cli/commands/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { dirname, isAbsolute, join, relative } from "node:path";
import ora, { Ora } from "ora";

import { createFeature, Feature, listFeatures } from "../services/features.js";
import { listStages, Stage } from "../services/stages.js";
import { configStore } from "../stores/config.js";
import { handleError, MissingAppIdError } from "../utils/errors.js";
import { genFeatureKey, genTypes, KeyFormatPatterns } from "../utils/gen.js";
Expand Down Expand Up @@ -33,7 +34,7 @@ export const createFeatureAction = async (
if (!name) {
name = await input({
message: "New feature name:",
validate: (text) => text.length > 0 || "Name is required",
validate: (text) => text.length > 0 || "Name is required.",
});
}

Expand All @@ -55,7 +56,7 @@ export const createFeatureAction = async (
`Created feature ${chalk.cyan(feature.name)} with key ${chalk.cyan(feature.key)} at ${chalk.cyan(baseUrl)}. 🎉`,
);
} catch (error) {
spinner?.fail("Feature creation failed");
spinner?.fail("Feature creation failed.");
void handleError(error, "Features Create");
}
};
Expand All @@ -71,11 +72,17 @@ export const listFeaturesAction = async () => {
).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(appId)} at ${chalk.cyan(baseUrl)}.`,
);
console.table(
features.map(({ key, name, stage }) => ({
key,
name,
stage: stage?.name,
})),
);
console.table(features);
} catch (error) {
spinner?.fail("Loading features failed");
spinner?.fail("Loading features failed.");
void handleError(error, "Features List");
}
};
Expand All @@ -86,6 +93,7 @@ export const generateTypesAction = async () => {

let spinner: Ora | undefined;
let features: Feature[] = [];
let stages: Stage[] = [];
try {
if (!appId) throw new MissingAppIdError();
spinner = ora(
Expand All @@ -95,10 +103,20 @@ export const generateTypesAction = async () => {
includeRemoteConfigs: true,
});
spinner.succeed(
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}`,
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}.`,
);
} catch (error) {
spinner?.fail("Loading features failed");
spinner?.fail("Loading features failed.");
void handleError(error, "Features Types");
return;
}

try {
spinner = ora(`Loading stages...`).start();
stages = await listStages(appId);
spinner.succeed(`Loaded stages.`);
} catch (error) {
spinner?.fail("Loading stages failed.");
void handleError(error, "Features Types");
return;
}
Expand All @@ -109,33 +127,33 @@ export const generateTypesAction = async () => {

// Generate types for each output configuration
for (const output of typesOutput) {
const types = await genTypes(features, output.format);
const types = await genTypes(features, stages, 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 ${output.format} types in ${chalk.cyan(relative(projectPath, outPath))}.`,
);
}

spinner.succeed(
`Generated types for ${chalk.cyan(appId)} in ${typesOutput.length} location(s).`,
);
spinner.succeed(`Generated types for ${chalk.cyan(appId)}.`);
} catch (error) {
spinner?.fail("Type generation failed");
spinner?.fail("Type generation failed.");
void handleError(error, "Features Types");
}
};

export function registerFeatureCommands(cli: Command) {
const featuresCommand = new Command("features").description(
"Manage features",
"Manage features.",
);

featuresCommand
.command("create")
.description("Create a new feature")
.description("Create a new feature.")
.addOption(appIdOption)
.addOption(keyFormatOption)
.addOption(featureKeyOption)
Expand All @@ -144,13 +162,13 @@ export function registerFeatureCommands(cli: Command) {

featuresCommand
.command("list")
.description("List all features")
.description("List all features.")
.addOption(appIdOption)
.action(listFeaturesAction);

featuresCommand
.command("types")
.description("Generate feature types")
.description("Generate feature types.")
.addOption(appIdOption)
.addOption(typesOutOption)
.addOption(typesFormatOption)
Expand Down
18 changes: 8 additions & 10 deletions packages/cli/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ export const initAction = async (args: InitArgs = {}) => {
// Load apps
spinner = ora(`Loading apps from ${chalk.cyan(baseUrl)}...`).start();
apps = await listApps();
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}`);
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}.`);
} catch (error) {
spinner?.fail("Loading apps failed");
spinner?.fail("Loading apps failed.");
void handleError(error, "Initialization");
return;
}
Expand All @@ -50,17 +50,15 @@ export const initAction = async (args: InitArgs = {}) => {
} else if (nonDemoApps.length === 1) {
appId = nonDemoApps[0].id;
console.log(
chalk.gray(
`Automatically selected app ${nonDemoApps[0].name} (${appId})`,
),
`Automatically selected app ${chalk.cyan(nonDemoApps[0].name)} (${chalk.cyan(appId)}).`,
);
} else {
const longestName = Math.max(...apps.map((app) => app.name.length));
appId = await select({
message: "Select an app",
choices: apps.map((app) => ({
name: app.name,
name: `${app.name.padEnd(longestName, " ")}${app.demo ? " [Demo]" : ""}`,
value: app.id,
description: app.demo ? "Demo" : undefined,
})),
});
}
Expand Down Expand Up @@ -95,18 +93,18 @@ export const initAction = async (args: InitArgs = {}) => {
spinner = ora("Creating configuration...").start();
await configStore.saveConfigFile(args.overwrite);
spinner.succeed(
`Configuration created at ${chalk.cyan(relative(process.cwd(), configStore.getConfigPath()!))}`,
`Configuration created at ${chalk.cyan(relative(process.cwd(), configStore.getConfigPath()!))}.`,
);
} catch (error) {
spinner?.fail("Configuration creation failed");
spinner?.fail("Configuration creation failed.");
void handleError(error, "Initialization");
}
};

export function registerInitCommand(cli: Command) {
cli
.command("init")
.description("Initialize a new Bucket configuration")
.description("Initialize a new Bucket configuration.")
.addOption(overwriteOption)
.action(initAction);
}
2 changes: 1 addition & 1 deletion packages/cli/commands/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function registerNewCommand(cli: Command) {
cli
.command("new")
.description(
"Initialize the Bucket CLI, authenticates, and creates a new feature",
"Initialize the Bucket CLI, authenticates, and creates a new feature.",
)
.addOption(appIdOption)
.addOption(keyFormatOption)
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bucketco/cli",
"version": "0.1.4",
"version": "0.2.0",
"packageManager": "yarn@4.1.1",
"description": "CLI for Bucket service",
"main": "./dist/index.js",
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/services/features.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { authRequest } from "../utils/auth.js";

import { Stage } from "./stages.js";

export type RemoteConfigVariant = {
key?: string;
payload?: any;
Expand All @@ -17,6 +19,7 @@ export type Feature = {
name: string;
key: string;
remoteConfigs: RemoteConfig[];
stage: Stage | null;
};

export type FeaturesResponse = {
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/services/stages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { authRequest } from "../utils/auth.js";

export type Stage = {
id: string;
name: string;
order: number;
};

type StagesResponse = {
stages: Stage[];
};

export async function listStages(appId: string): Promise<Stage[]> {
const response = await authRequest<StagesResponse>(`/apps/${appId}/stages`);
return response.stages;
}
10 changes: 4 additions & 6 deletions packages/cli/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function authenticateUser(baseUrl: string) {
let isResolved = false;

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

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

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

function cleanupAndResolve(token: string) {
if (isResolved) return;
Expand Down Expand Up @@ -94,9 +94,7 @@ export async function authenticateUser(baseUrl: string) {
const address = server.address();
if (address && typeof address === "object") {
const port = address.port;
void open(loginUrl(baseUrl, port), {
newInstance: true,
});
void open(loginUrl(baseUrl, port));
}
});
}
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/utils/gen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { camelCase, kebabCase, pascalCase, snakeCase } from "change-case";

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

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

Expand Down Expand Up @@ -89,7 +90,11 @@ export function genRemoteConfig(remoteConfigs?: RemoteConfig[]) {
);
}

export function genTypes(features: Feature[], format: GenFormat = "react") {
export function genTypes(
features: Feature[],
stages: Stage[],
format: GenFormat = "react",
) {
const configDefs = new Map<string, { name: string; definition: string }>();
features.forEach(({ key, name, remoteConfigs }) => {
const definition = genRemoteConfig(remoteConfigs);
Expand All @@ -105,6 +110,11 @@ export function genTypes(features: Feature[], format: GenFormat = "react") {
import "@bucketco/${format}-sdk";

declare module "@bucketco/${format}-sdk" {
${
stages.length
? /* ts */ `export type Stage = ${stages.map(({ name }) => `"${name}"`).join(" | ")};\n`
: ""
}
export interface Features {
${features
.map(({ key }) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bucketco/node-sdk",
"version": "1.6.1",
"version": "1.6.2",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
8 changes: 6 additions & 2 deletions packages/node-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export type FeatureRemoteConfig =
* Describes a feature
*/
export interface Feature<
TConfig extends FeatureRemoteConfig | undefined = EmptyFeatureRemoteConfig,
TConfig extends FeatureType["config"] | undefined = EmptyFeatureRemoteConfig,
> {
/**
* The key of the feature.
Expand All @@ -163,7 +163,11 @@ export interface Feature<
/*
* Optional user-defined configuration.
*/
config: TConfig extends undefined ? EmptyFeatureRemoteConfig : TConfig;
config: TConfig extends undefined
? EmptyFeatureRemoteConfig
: TConfig & {
key: string;
};

/**
* Track feature usage in Bucket.
Expand Down