Skip to content

Commit

Permalink
refactor(cli): convert CLI to class and use zod for validation
Browse files Browse the repository at this point in the history
  • Loading branch information
tyler-dane committed Dec 30, 2024
1 parent 407ef2d commit 2bda7f0
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 130 deletions.
164 changes: 88 additions & 76 deletions packages/scripts/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,101 +6,113 @@ dotenv.config({
import { Command } from "commander";

import { runBuild } from "./commands/build";
import { ALL_PACKAGES, CATEGORY_VM, PCKG } from "./common/cli.constants";
import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants";
import { startDeleteFlow } from "./commands/delete";
import { Options_Cli, Schema_Options_Cli } from "./common/cli.types";
import { log } from "./common/cli.utils";
import { Options_Cli } from "./common/cli.types";

const createProgram = () => {
const program = new Command();
program.option(
`-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`,
"specify environment"
);
program.option("-f, --force", "forces operation, no cautionary prompts");
program.option(
"-u, --user [id | email]",
"specifies which user to run script for"
);
class CompassCli {
private program: Command;
private options: Options_Cli;

program
.command("build")
.description("build compass package(s)")
.argument(
`[${ALL_PACKAGES.join(" | ")}]`,
"package(s) to build, separated by comma"
)
.option("--skip-env", "skips copying env files to build");
constructor(args: string[]) {
this.program = this.createProgram();
this.program.parse(args);
this.options = this.getCliOptions();
}

program
.command("delete")
.description("delete user data from compass database");
return program;
};
private createProgram(): Command {
const program = new Command();
program.option(
`-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`,
"specify environment"
);
program.option("-f, --force", "force operation, no cautionary prompts");
program.option(
"-u, --user [id | email]",
"specify which user to run script for"
);

const exitHelpfully = (program: Command, msg?: string) => {
msg && log.error(msg);
console.log(program.helpInformation());
process.exit(1);
};
program
.command("build")
.description("build compass package(s)")
.argument(
`[${ALL_PACKAGES.join(" | ")}]`,
"package(s) to build, separated by comma"
)
.option("--skip-env", "skip copying env files to build");

const getCliOptions = (program: Command): Options_Cli => {
const _options = program.opts();
const packages = program.args[1]?.split(",");
program
.command("delete")
.description("delete user data from compass database");
return program;
}

const options = {
..._options,
packages,
force: _options["force"] === true,
user: _options["user"] as string,
};
private getCliOptions(): Options_Cli {
const _options = this.program.opts();
const packages = this.program.args[1]?.split(",");
const options: Options_Cli = {
..._options,
force: _options["force"] === true,
packages,
};

return options;
};
const { data, error } = Schema_Options_Cli.safeParse(options);
if (error) {
log.error(`Invalid CLI options: ${JSON.stringify(error.format())}`);
process.exit(1);
}

const validatePackages = (packages: string[] | undefined) => {
if (!packages) {
log.error("Packages must be defined");
return data;
}
if (!packages?.includes(PCKG.NODE) && !packages?.includes(PCKG.WEB)) {
log.error(
`One or more of these pckgs isn't supported: ${(
packages as string[]
)?.toString()}`
);

process.exit(1);
private validatePackages(packages: string[] | undefined) {
if (!packages) {
log.error("Packages must be defined");
process.exit(1);
}
const unsupportedPackages = packages.filter(
(pkg) => !ALL_PACKAGES.includes(pkg)
);
if (unsupportedPackages.length > 0) {
log.error(
`One or more of these packages isn't supported: ${unsupportedPackages.toString()}`
);
process.exit(1);
}
}
};

const runScript = async () => {
const program = createProgram();
program.parse(process.argv);

const options = getCliOptions(program);
const { user, force } = options;
public async run() {
const { user, force, packages } = this.options;
const cmd = this.program.args[0];

const cmd = program.args[0];
switch (true) {
case cmd === "build": {
validatePackages(options.packages);
await runBuild(options);
break;
}
case cmd === "delete": {
if (!user || typeof user !== "string") {
exitHelpfully(program, "You must supply a user");
switch (true) {
case cmd === "build": {
this.validatePackages(packages);
await runBuild(this.options);
break;
}

await startDeleteFlow(user as string, force);
break;
case cmd === "delete": {
if (!user || typeof user !== "string") {
this.exitHelpfully("You must supply a user");
}
await startDeleteFlow(user as string, force);
break;
}
default:
this.exitHelpfully("Unsupported cmd");
}
default:
exitHelpfully(program, "Unsupported cmd");
}
};

runScript().catch((err) => {
private exitHelpfully(msg?: string) {
msg && log.error(msg);
console.log(this.program.helpInformation());
process.exit(1);
}
}

const cli = new CompassCli(process.argv);
cli.run().catch((err) => {
console.log(err);
process.exit(1);
});
44 changes: 21 additions & 23 deletions packages/scripts/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
import dotenv from "dotenv";
import path from "path";
import shell from "shelljs";
import { Options_Cli, Info_VM } from "@scripts/common/cli.types";
import { Options_Cli } from "@scripts/common/cli.types";
import {
COMPASS_BUILD_DEV,
COMPASS_ROOT_DEV,
NODE_BUILD,
PCKG,
} from "@scripts/common/cli.constants";
import {
getVmInfo,
getPckgsTo,
_confirm,
log,
fileExists,
getClientId,
getApiBaseUrl,
getEnvironmentAnswer,
} from "@scripts/common/cli.utils";

export const runBuild = async (options: Options_Cli) => {
const vmInfo = await getVmInfo(options.environment);

const pckgs =
options?.packages?.length === 0
? await getPckgsTo("build")
: (options.packages as string[]);

if (pckgs.includes(PCKG.NODE)) {
await buildNodePckgs(vmInfo, options);
await buildNodePckgs(options);
}
if (pckgs.includes(PCKG.WEB)) {
await buildWeb(vmInfo);
await buildWeb(options);
}
};

const buildNodePckgs = async (vmInfo: Info_VM, options: Options_Cli) => {
const buildNodePckgs = async (options: Options_Cli) => {
removeOldBuildFor(PCKG.NODE);
createNodeDirs();
await copyNodeConfigsToBuild(vmInfo, options.skipEnv, options.force);
await copyNodeConfigsToBuild(options);

log.info("Compiling node packages ...");
shell.exec(
Expand All @@ -55,11 +54,15 @@ const buildNodePckgs = async (vmInfo: Info_VM, options: Options_Cli) => {
);
};

const buildWeb = async (vmInfo: Info_VM) => {
const { baseUrl, destination } = vmInfo;
const envFile = destination === "staging" ? ".env" : ".env.prod";
const buildWeb = async (options: Options_Cli) => {
const environment =
options.environment !== undefined
? options.environment
: await getEnvironmentAnswer();

const gClientId = await getClientId(destination);
const envFile = environment === "staging" ? ".env" : ".env.prod";
const baseUrl = await getApiBaseUrl(environment);
const gClientId = await getClientId(environment);

const envPath = path.join(__dirname, "..", "..", "..", "backend", envFile);
dotenv.config({ path: envPath });
Expand All @@ -76,18 +79,15 @@ const buildWeb = async (vmInfo: Info_VM) => {
log.tip(`
Now you'll probably want to:
- zip the build dir
- copy it to your ${destination} server
- unzip it and serve as the static assets
- copy it to your ${environment} environment
- unzip it to expose the static assets
- serve assets
`);
process.exit(0);
};

const copyNodeConfigsToBuild = async (
vmInfo: Info_VM,
skipEnv?: boolean,
force?: boolean
) => {
const envName = vmInfo.destination === "production" ? ".prod.env" : ".env";
const copyNodeConfigsToBuild = async (options: Options_Cli) => {
const envName = options.environment === "production" ? ".prod.env" : ".env";

const envPath = `${COMPASS_ROOT_DEV}/packages/backend/${envName}`;

Expand All @@ -100,9 +100,7 @@ const copyNodeConfigsToBuild = async (
log.warning(`Env file does not exist: ${envPath}`);

const keepGoing =
skipEnv === true || force === true
? true
: await _confirm("Continue anyway?");
options.force === true ? true : await _confirm("Continue anyway?");

if (!keepGoing) {
log.error("Exiting due to missing env file");
Expand Down
25 changes: 11 additions & 14 deletions packages/scripts/src/common/cli.types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
export type Category_VM = "staging" | "production";
import { z } from "zod";

export interface Info_VM {
baseUrl: string;
destination: Category_VM;
}
export type Environment_Cli = "staging" | "production";

export interface Options_Cli {
build?: boolean;
delete?: boolean;
environment?: Category_VM;
force?: boolean;
packages?: string[];
skipEnv?: boolean;
user?: string;
}
export const Schema_Options_Cli = z.object({
clientId: z.string().optional(),
environment: z.enum(["staging", "production"]).optional(),
force: z.boolean().optional(),
packages: z.array(z.string()).optional(),
user: z.string().optional(),
});

export type Options_Cli = z.infer<typeof Schema_Options_Cli>;
38 changes: 21 additions & 17 deletions packages/scripts/src/common/cli.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@ const { prompt } = pkg;
import shell from "shelljs";

import { ALL_PACKAGES, CLI_ENV } from "./cli.constants";
import { Category_VM } from "./cli.types";
import { Environment_Cli } from "./cli.types";

export const fileExists = (file: string) => {
return shell.test("-e", file);
};

export const getClientId = async (destination: Category_VM) => {
if (destination === "staging") {
export const getApiBaseUrl = async (environment: Environment_Cli) => {
const category = environment ? environment : await getEnvironmentAnswer();
const isStaging = category === "staging";
const domain = await getDomainAnswer(isStaging);
const baseUrl = `https://${domain}/api`;

return baseUrl;
};

export const getClientId = async (environment: Environment_Cli) => {
if (environment === "staging") {
return process.env["CLIENT_ID"] as string;
}

if (destination === "production") {
if (environment === "production") {
const q = `Enter the googleClientId for the production environment:`;

return prompt([{ type: "input", name: "answer", message: q }])
Expand Down Expand Up @@ -58,22 +67,17 @@ const getDomainAnswer = async (isStaging: boolean) => {
process.exit(1);
});
};
export const getVmInfo = async (environment?: Category_VM) => {
const destination = environment
? environment
: ((await getListAnswer("Select environment to use:", [
"staging",
"production",
])) as Category_VM);

const isStaging = destination === "staging";
const domain = await getDomainAnswer(isStaging);
const baseUrl = `https://${domain}/api`;

return { baseUrl, destination };
export const getEnvironmentAnswer = async (): Promise<Environment_Cli> => {
const environment = (await getListAnswer("Select environment to use:", [
"staging",
"production",
])) as Environment_Cli;

return environment;
};

const getListAnswer = async (question: string, choices: string[]) => {
export const getListAnswer = async (question: string, choices: string[]) => {
const q = [
{
type: "list",
Expand Down

0 comments on commit 2bda7f0

Please sign in to comment.