mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(cli): ✨ Add new CLI commands, move to project root
This commit is contained in:
parent
68f16f9101
commit
fc06b35c09
|
|
@ -1,8 +1,9 @@
|
|||
import { Args, type Command, Flags, type Interfaces } from "@oclif/core";
|
||||
import chalk from "chalk";
|
||||
import { and, eq, like } from "drizzle-orm";
|
||||
import { Users } from "~drizzle/schema";
|
||||
import type { User } from "~packages/database-interface/user";
|
||||
import { BaseCommand } from "./base";
|
||||
import { Args, Flags, type Command, type Interfaces } from "@oclif/core";
|
||||
|
||||
export type FlagsType<T extends typeof Command> = Interfaces.InferredFlags<
|
||||
(typeof BaseCommand)["baseFlags"] & T["flags"]
|
||||
|
|
@ -63,6 +64,17 @@ export abstract class UserFinderCommand<
|
|||
}
|
||||
|
||||
public async findUsers(): Promise<User[]> {
|
||||
// Check if there are asterisks in the identifier but no pattern flag, warn the user if so
|
||||
if (this.args.identifier.includes("*") && !this.flags.pattern) {
|
||||
this.log(
|
||||
chalk.bold(
|
||||
`${chalk.yellow(
|
||||
"⚠",
|
||||
)} Your identifier has asterisks but the --pattern flag is not set. This will match a literal string. If you want to use wildcards, set the --pattern flag.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const operator = this.flags.pattern ? like : eq;
|
||||
// Replace wildcards with an SQL LIKE pattern
|
||||
const identifier = this.flags.pattern
|
||||
94
cli/commands/emoji/add.ts
Normal file
94
cli/commands/emoji/add.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { Args } from "@oclif/core";
|
||||
import chalk from "chalk";
|
||||
import { BaseCommand } from "~/cli/base";
|
||||
import { db } from "~drizzle/db";
|
||||
|
||||
export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
||||
static override args = {
|
||||
shortcode: Args.string({
|
||||
description: "Shortcode of the emoji",
|
||||
required: true,
|
||||
}),
|
||||
file: Args.string({
|
||||
description: "Path to the image file (can be an URL)",
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
static override description = "Adds a new emoji";
|
||||
|
||||
static override examples = ["<%= config.bin %> <%= command.id %>"];
|
||||
|
||||
static override flags = {};
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const { flags, args } = await this.parse(EmojiAdd);
|
||||
|
||||
// Check if emoji already exists
|
||||
const existingEmoji = await db.query.Emojis.findFirst({
|
||||
where: (Emojis, { eq }) => eq(Emojis.shortcode, args.shortcode),
|
||||
});
|
||||
|
||||
if (existingEmoji) {
|
||||
this.log(
|
||||
`${chalk.red("✗")} Emoji with shortcode ${chalk.red(
|
||||
args.shortcode,
|
||||
)} already exists`,
|
||||
);
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
/* if (!user) {
|
||||
this.log(
|
||||
`${chalk.red("✗")} Failed to create user ${chalk.red(
|
||||
args.username,
|
||||
)}`,
|
||||
);
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
!flags.format &&
|
||||
this.log(
|
||||
`${chalk.green("✓")} Created user ${chalk.green(
|
||||
user.getUser().username,
|
||||
)} with id ${chalk.green(user.id)}`,
|
||||
);
|
||||
|
||||
this.log(
|
||||
formatArray(
|
||||
[user.getUser()],
|
||||
[
|
||||
"id",
|
||||
"username",
|
||||
"displayName",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"isAdmin",
|
||||
],
|
||||
flags.format as "json" | "csv" | undefined,
|
||||
),
|
||||
);
|
||||
|
||||
if (!flags.format && !flags["set-password"]) {
|
||||
const link = "";
|
||||
|
||||
this.log(
|
||||
flags.format
|
||||
? link
|
||||
: `\nPassword reset link for ${chalk.bold(
|
||||
`@${user.getUser().username}`,
|
||||
)}: ${chalk.underline(chalk.blue(link))}\n`,
|
||||
);
|
||||
|
||||
const qrcode = renderUnicodeCompact(link, {
|
||||
border: 2,
|
||||
});
|
||||
|
||||
// Pad all lines of QR code with spaces
|
||||
|
||||
this.log(` ${qrcode.replaceAll("\n", "\n ")}`);
|
||||
} */
|
||||
|
||||
this.exit(0);
|
||||
}
|
||||
}
|
||||
162
cli/commands/user/create.ts
Normal file
162
cli/commands/user/create.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import input from "@inquirer/input";
|
||||
import { Args, Flags } from "@oclif/core";
|
||||
import chalk from "chalk";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { renderUnicodeCompact } from "uqr";
|
||||
import { BaseCommand } from "~cli/base";
|
||||
import { formatArray } from "~cli/utils/format";
|
||||
import { Users } from "~drizzle/schema";
|
||||
import { User } from "~packages/database-interface/user";
|
||||
|
||||
export default class UserCreate extends BaseCommand<typeof UserCreate> {
|
||||
static override args = {
|
||||
username: Args.string({
|
||||
description: "Username",
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
static override description = "Creates a new user";
|
||||
|
||||
static override examples = [
|
||||
"<%= config.bin %> <%= command.id %> johngastron --email joe@gamer.com",
|
||||
"<%= config.bin %> <%= command.id %> bimbobaggins",
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
format: Flags.string({
|
||||
char: "f",
|
||||
description:
|
||||
"Output format (when set, no password reset link is generated)",
|
||||
options: ["json", "csv"],
|
||||
}),
|
||||
admin: Flags.boolean({
|
||||
char: "a",
|
||||
description: "Admin user",
|
||||
allowNo: true,
|
||||
default: false,
|
||||
}),
|
||||
email: Flags.string({
|
||||
char: "e",
|
||||
description: "Email",
|
||||
}),
|
||||
"verify-email": Flags.boolean({
|
||||
description: "Send email verification",
|
||||
default: true,
|
||||
allowNo: true,
|
||||
}),
|
||||
"set-password": Flags.boolean({
|
||||
description: "Type password instead of getting a reset link",
|
||||
default: false,
|
||||
exclusive: ["format"],
|
||||
}),
|
||||
};
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const { flags, args } = await this.parse(UserCreate);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await User.fromSql(
|
||||
eq(Users.username, args.username),
|
||||
);
|
||||
|
||||
if (existingUser) {
|
||||
this.log(
|
||||
`${chalk.red("✗")} User ${chalk.red(
|
||||
args.username,
|
||||
)} already exists`,
|
||||
);
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
let password = null;
|
||||
|
||||
if (flags["set-password"]) {
|
||||
const password1 = await input({
|
||||
message: "Please enter the user's password:",
|
||||
// Set whatever the user types to stars
|
||||
transformer: (value) => "*".repeat(value.length),
|
||||
});
|
||||
|
||||
const password2 = await input({
|
||||
message: "Please confirm the user's password:",
|
||||
// Set whatever the user types to stars
|
||||
transformer: (value) => "*".repeat(value.length),
|
||||
});
|
||||
|
||||
if (password1 !== password2) {
|
||||
this.log(
|
||||
`${chalk.red(
|
||||
"✗",
|
||||
)} Passwords do not match. Please try again.`,
|
||||
);
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
password = password1;
|
||||
}
|
||||
|
||||
// TODO: Add password resets
|
||||
|
||||
const user = await User.fromDataLocal({
|
||||
email: flags.email ?? undefined,
|
||||
password: password ?? undefined,
|
||||
username: args.username,
|
||||
admin: flags.admin,
|
||||
skipPasswordHash: !!password,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
this.log(
|
||||
`${chalk.red("✗")} Failed to create user ${chalk.red(
|
||||
args.username,
|
||||
)}`,
|
||||
);
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
!flags.format &&
|
||||
this.log(
|
||||
`${chalk.green("✓")} Created user ${chalk.green(
|
||||
user.getUser().username,
|
||||
)} with id ${chalk.green(user.id)}`,
|
||||
);
|
||||
|
||||
this.log(
|
||||
formatArray(
|
||||
[user.getUser()],
|
||||
[
|
||||
"id",
|
||||
"username",
|
||||
"displayName",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"isAdmin",
|
||||
],
|
||||
flags.format as "json" | "csv" | undefined,
|
||||
),
|
||||
);
|
||||
|
||||
if (!flags.format && !flags["set-password"]) {
|
||||
const link = "";
|
||||
|
||||
this.log(
|
||||
flags.format
|
||||
? link
|
||||
: `\nPassword reset link for ${chalk.bold(
|
||||
`@${user.getUser().username}`,
|
||||
)}: ${chalk.underline(chalk.blue(link))}\n`,
|
||||
);
|
||||
|
||||
const qrcode = renderUnicodeCompact(link, {
|
||||
border: 2,
|
||||
});
|
||||
|
||||
// Pad all lines of QR code with spaces
|
||||
|
||||
this.log(` ${qrcode.replaceAll("\n", "\n ")}`);
|
||||
}
|
||||
|
||||
this.exit(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import confirm from "@inquirer/confirm";
|
||||
import { Flags } from "@oclif/core";
|
||||
import chalk from "chalk";
|
||||
import { formatArray } from "~packages/cli/utils/format";
|
||||
import confirm from "@inquirer/confirm";
|
||||
import ora from "ora";
|
||||
import { UserFinderCommand } from "~packages/cli/classes";
|
||||
import { UserFinderCommand } from "~cli/classes";
|
||||
import { formatArray } from "~cli/utils/format";
|
||||
|
||||
export default class UserDelete extends UserFinderCommand<typeof UserDelete> {
|
||||
static override description = "Deletes users";
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { Flags } from "@oclif/core";
|
||||
import { and, eq, isNotNull, isNull } from "drizzle-orm";
|
||||
import { BaseCommand } from "~cli/base";
|
||||
import { formatArray } from "~cli/utils/format";
|
||||
import { Users } from "~drizzle/schema";
|
||||
import { BaseCommand } from "~packages/cli/base";
|
||||
import { formatArray } from "~packages/cli/utils/format";
|
||||
import { User } from "~packages/database-interface/user";
|
||||
|
||||
export default class UserList extends BaseCommand<typeof UserList> {
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import confirm from "@inquirer/confirm";
|
||||
import { Flags } from "@oclif/core";
|
||||
import chalk from "chalk";
|
||||
import { formatArray } from "~packages/cli/utils/format";
|
||||
import confirm from "@inquirer/confirm";
|
||||
import { renderUnicodeCompact } from "uqr";
|
||||
import { UserFinderCommand } from "~packages/cli/classes";
|
||||
import { UserFinderCommand } from "~cli/classes";
|
||||
import { formatArray } from "~cli/utils/format";
|
||||
|
||||
export default class UserReset extends UserFinderCommand<typeof UserReset> {
|
||||
static override description = "Resets users' passwords";
|
||||
13
cli/index.ts
Normal file
13
cli/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import EmojiAdd from "./commands/emoji/add";
|
||||
import UserCreate from "./commands/user/create";
|
||||
import UserDelete from "./commands/user/delete";
|
||||
import UserList from "./commands/user/list";
|
||||
import UserReset from "./commands/user/reset";
|
||||
|
||||
export const commands = {
|
||||
"user list": UserList,
|
||||
"user delete": UserDelete,
|
||||
"user create": UserCreate,
|
||||
"user reset": UserReset,
|
||||
"emoji add": EmojiAdd,
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { config } from "~/packages/config-manager";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
||||
import { Client } from "pg";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import * as schema from "./schema";
|
||||
|
||||
export const client = new Client({
|
||||
|
|
|
|||
36
package.json
36
package.json
|
|
@ -35,7 +35,7 @@
|
|||
"build": "bun run build.ts",
|
||||
"cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs,glitch,glitch-dev --exclude-ext sql,log,pem",
|
||||
"wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-",
|
||||
"cli": "bun run packages/cli/bin/run.ts",
|
||||
"cli": "bun run cli/bin/run.ts",
|
||||
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
|
|
@ -51,6 +51,27 @@
|
|||
"sharp",
|
||||
"vue-demi"
|
||||
],
|
||||
"oclif": {
|
||||
"bin": "cli",
|
||||
"dirname": "cli",
|
||||
"commands": {
|
||||
"strategy": "explicit",
|
||||
"target": "./cli/index.ts",
|
||||
"identifier": "commands"
|
||||
},
|
||||
"additionalHelpFlags": ["-h"],
|
||||
"additionalVersionFlags": ["-v"],
|
||||
"plugins": ["@oclif/plugin-help"],
|
||||
"description": "CLI to interface with the Lysand project",
|
||||
"topicSeparator": " ",
|
||||
"topics": {
|
||||
"user": {
|
||||
"description": "Manage users"
|
||||
}
|
||||
},
|
||||
"theme": "./cli/theme.json",
|
||||
"flexibleTaxonomy": true
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.7.0",
|
||||
"@types/cli-table": "^0.3.4",
|
||||
|
|
@ -63,12 +84,23 @@
|
|||
"bun-types": "latest",
|
||||
"drizzle-kit": "^0.20.14",
|
||||
"ts-prune": "^0.10.3",
|
||||
"typescript": "latest"
|
||||
"typescript": "latest",
|
||||
"@types/cli-progress": "^3.11.5",
|
||||
"oclif": "^4.10.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^3.1.6",
|
||||
"@inquirer/input": "^2.1.6",
|
||||
"@oclif/core": "^3.26.6",
|
||||
"@oclif/plugin-help": "^6.0.21",
|
||||
"@oclif/plugin-plugins": "^5.0.19",
|
||||
"cli-progress": "^3.12.0",
|
||||
"ora": "^8.0.1",
|
||||
"table": "^6.8.2",
|
||||
"uqr": "^0.1.2",
|
||||
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
||||
"@hono/zod-validator": "^0.2.1",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
/* import { Command } from "@oclif/core";
|
||||
import UserList from "./commands/user/list";
|
||||
import UserDelete from "./commands/user/delete";
|
||||
|
||||
export const commands = {
|
||||
"user list": UserList,
|
||||
"user delete": UserDelete,
|
||||
};
|
||||
*/
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"name": "cli",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^3.1.6",
|
||||
"@oclif/core": "^3.26.6",
|
||||
"@oclif/plugin-help": "^6.0.21",
|
||||
"@oclif/plugin-plugins": "^5.0.19",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"ora": "^8.0.1",
|
||||
"table": "^6.8.2",
|
||||
"uqr": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cli-progress": "^3.11.5",
|
||||
"oclif": "^4.10.4"
|
||||
},
|
||||
"oclif": {
|
||||
"bin": "cli",
|
||||
"dirname": "cli",
|
||||
"commands": {
|
||||
"strategy": "pattern",
|
||||
"target": "./commands"
|
||||
},
|
||||
"additionalHelpFlags": ["-h"],
|
||||
"additionalVersionFlags": ["-v"],
|
||||
"plugins": ["@oclif/plugin-help"],
|
||||
"description": "CLI to interface with the Lysand project",
|
||||
"topicSeparator": " ",
|
||||
"topics": {
|
||||
"user": {
|
||||
"description": "Manage users"
|
||||
}
|
||||
},
|
||||
"theme": "./theme.json",
|
||||
"flexibleTaxonomy": true
|
||||
}
|
||||
}
|
||||
|
|
@ -337,8 +337,8 @@ export class User {
|
|||
static async fromDataLocal(data: {
|
||||
username: string;
|
||||
display_name?: string;
|
||||
password: string;
|
||||
email: string;
|
||||
password: string | undefined;
|
||||
email: string | undefined;
|
||||
bio?: string;
|
||||
avatar?: string;
|
||||
header?: string;
|
||||
|
|
@ -353,9 +353,10 @@ export class User {
|
|||
.values({
|
||||
username: data.username,
|
||||
displayName: data.display_name ?? data.username,
|
||||
password: data.skipPasswordHash
|
||||
? data.password
|
||||
: await Bun.password.hash(data.password),
|
||||
password:
|
||||
data.skipPasswordHash || !data.password
|
||||
? data.password
|
||||
: await Bun.password.hash(data.password),
|
||||
email: data.email,
|
||||
note: data.bio ?? "",
|
||||
avatar: data.avatar ?? config.defaults.avatar,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { config } from "~packages/config-manager";
|
||||
import { LogManager, MultiLogManager } from "log-manager";
|
||||
import { config } from "~packages/config-manager";
|
||||
|
||||
const noColors = process.env.NO_COLORS === "true";
|
||||
const noFancyDates = process.env.NO_FANCY_DATES === "true";
|
||||
|
|
|
|||
Loading…
Reference in a new issue