Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🏗 build(cli): extract args before running build cmd #196

Merged
merged 10 commits into from
Jan 7, 2025
3 changes: 2 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"saslprep": "^1.0.3",
"socket.io": "^4.7.5",
"supertokens-node": "^20.0.5",
"tslib": "^2.4.0"
"tslib": "^2.4.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@shelf/jest-mongodb": "^4.1.4",
Expand Down
110 changes: 60 additions & 50 deletions packages/scripts/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,76 @@ import { Command } from "commander";
import { runBuild } from "./commands/build";
import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants";
import { startDeleteFlow } from "./commands/delete";
import { log } from "./common/cli.utils";
import { CliValidator } from "./cli.validator";

const runScript = async () => {
const exitHelpfully = (msg?: string) => {
msg && log.error(msg);
console.log(program.helpInformation());
process.exit(1);
};
class CompassCli {
private program: Command;
private validator: CliValidator;

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"
);

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.validator = new CliValidator(this.program);
this.program.parse(args);
}

program
.command("delete")
.description("deletes users data from compass database");
public async run() {
const options = this.validator.getCliOptions();
const { force, user } = options;
const cmd = this.program.args[0];

program.parse(process.argv);
switch (true) {
case cmd === "build": {
await this.validator.validateBuild(options);
await runBuild(options);
break;
}
case cmd === "delete": {
this.validator.validateDelete(options);
await startDeleteFlow(user as string, force);
break;
}
default:
this.validator.exitHelpfully(
"root",
`${cmd as string} is not a supported cmd`
);
}
}

const options = program.opts();
const cmd = program.args[0];
private _createProgram(): Command {
const program = new Command();

switch (true) {
case cmd === "build": {
await runBuild(options);
break;
}
case cmd === "delete": {
const force = options["force"] as boolean;
const user = options["user"] as string;
program.option("-f, --force", "force operation, no cautionary prompts");

if (!user || typeof user !== "string") {
exitHelpfully("You must supply a user");
}
program
.command("build")
.description("build compass package")
.argument(
`[${ALL_PACKAGES.join(" | ")}]`,
"package to build (only provide 1)"
)
.option(
"-c, --clientId <clientId>",
"google client id to inject into build"
)
.option(
`-e, --environment [${CATEGORY_VM.STAG} | ${CATEGORY_VM.PROD}]`,
"specify environment"
);

await startDeleteFlow(user, force);
break;
}
default:
exitHelpfully("Unsupported cmd");
program
.command("delete")
.description("delete user data from compass database")
.option(
"-u, --user [id | email]",
"specify which user to run script for"
);
return program;
}
};
}

runScript().catch((err) => {
const cli = new CompassCli(process.argv);
cli.run().catch((err) => {
console.log(err);
process.exit(1);
});
168 changes: 168 additions & 0 deletions packages/scripts/src/cli.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { Command } from "commander";

import { ALL_PACKAGES } from "./common/cli.constants";
import {
Options_Cli,
Options_Cli_Build,
Options_Cli_Delete,
Schema_Options_Cli_Build,
Schema_Options_Cli_Delete,
Schema_Options_Cli_Root,
} from "./common/cli.types";
import { getPckgsTo, log } from "./common/cli.utils";

export class CliValidator {
private program: Command;

constructor(program: Command) {
this.program = program;
}

public exitHelpfully(cmd: "root" | "build" | "delete", msg?: string) {
msg && log.error(msg);

if (cmd === "root") {
console.log(this.program.helpInformation());
} else {
const command = this.program.commands.find(
(c) => c.name() === cmd
) as Command;
console.log(command.helpInformation());
}

process.exit(1);
}

public getCliOptions(): Options_Cli {
const options = this._mergeOptions();
const validOptions = this._validateOptions(options);

return validOptions;
}

public async validateBuild(options: Options_Cli) {
if (!options.packages) {
options.packages = await getPckgsTo("build");
}

const unsupportedPackages = options.packages.filter(
(pkg) => !ALL_PACKAGES.includes(pkg)
);
if (unsupportedPackages.length > 0) {
this.exitHelpfully(
"build",
`One or more of these packages isn't supported: ${unsupportedPackages.toString()}`
);
}
}

public validateDelete(options: Options_Cli) {
const { user } = options;
if (!user || typeof user !== "string") {
this.exitHelpfully("delete", "You must supply a user");
}
}

private _getBuildOptions() {
const buildOpts: Options_Cli_Build = {};

const buildCmd = this.program.commands.find(
(cmd) => cmd.name() === "build"
);
if (buildCmd) {
const packages = this.program.args[1]?.split(",");
if (packages) {
buildOpts.packages = packages;
}

const environment = buildCmd?.opts()[
"environment"
] as Options_Cli_Build["environment"];
if (environment) {
buildOpts.environment = environment;
}

const clientId = buildCmd?.opts()[
"clientId"
] as Options_Cli_Build["clientId"];
if (clientId) {
buildOpts.clientId = clientId;
}
}
return buildOpts;
}

private _getDeleteOptions() {
const deleteOpts: Options_Cli_Delete = {};

const deleteCmd = this.program.commands.find(
(cmd) => cmd.name() === "delete"
);
if (deleteCmd) {
const user = deleteCmd?.opts()["user"] as Options_Cli["user"];
if (user) {
deleteOpts.user = user;
}
}

return deleteOpts;
}

private _mergeOptions = (): Options_Cli => {
const _options = this.program.opts();
let options: Options_Cli = {
..._options,
force: _options["force"] === true,
};

const buildOptions = this._getBuildOptions();
if (Object.keys(buildOptions).length > 0) {
options = {
...options,
...buildOptions,
};
}

const deleteOptions = this._getDeleteOptions();
if (Object.keys(deleteOptions).length > 0) {
options = {
...options,
...deleteOptions,
};
}

return options;
};

private _validateOptions(options: Options_Cli) {
const { data: rootData, error: rootError } =
Schema_Options_Cli_Root.safeParse(options);
if (rootError) {
this.exitHelpfully(
"root",
`Invalid CLI options: ${rootError.toString()}`
);
}

const { data: buildData, error: buildError } =
Schema_Options_Cli_Build.safeParse(options);
if (buildError) {
this.exitHelpfully(
"build",
`Invalid build options: ${buildError.toString()}`
);
}

const { data: deleteData, error: deleteError } =
Schema_Options_Cli_Delete.safeParse(options);
if (deleteError) {
this.exitHelpfully(
"delete",
`Invalid delete options: ${deleteError.toString()}`
);
}

const data: Options_Cli = { ...rootData, ...buildData, ...deleteData };
return data;
}
}
Loading
Loading