mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(cli): ♻️ Begin new CLI rewrite with oclif
This commit is contained in:
parent
7b05a34cce
commit
06c30b8af2
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -179,4 +179,5 @@ glitch-old
|
||||||
glitch
|
glitch
|
||||||
glitch.tar.gz
|
glitch.tar.gz
|
||||||
glitch-dev
|
glitch-dev
|
||||||
*.pem
|
*.pem
|
||||||
|
oclif.manifest.json
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { config } from "config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||||
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
||||||
|
|
@ -13,7 +13,10 @@ export const client = new Client({
|
||||||
database: config.database.database,
|
database: config.database.database,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupDatabase = async (logger: LogManager | MultiLogManager) => {
|
export const setupDatabase = async (
|
||||||
|
logger: LogManager | MultiLogManager,
|
||||||
|
info = true,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
await client.connect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -28,7 +31,8 @@ export const setupDatabase = async (logger: LogManager | MultiLogManager) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate the database
|
// Migrate the database
|
||||||
await logger.log(LogLevel.INFO, "Database", "Migrating database...");
|
info &&
|
||||||
|
(await logger.log(LogLevel.INFO, "Database", "Migrating database..."));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await migrate(db, {
|
await migrate(db, {
|
||||||
|
|
@ -44,7 +48,7 @@ export const setupDatabase = async (logger: LogManager | MultiLogManager) => {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
await logger.log(LogLevel.INFO, "Database", "Database migrated");
|
info && (await logger.log(LogLevel.INFO, "Database", "Database migrated"));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const db = drizzle(client, { schema });
|
export const db = drizzle(client, { schema });
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
"build": "bun run build.ts",
|
"build": "bun run build.ts",
|
||||||
"cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs,glitch,glitch-dev --exclude-ext sql,log,pem",
|
"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=-",
|
"wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-",
|
||||||
"cli": "bun run cli.ts",
|
"cli": "bun run packages/cli/bin/run.ts",
|
||||||
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'"
|
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
|
|
|
||||||
14
packages/cli/base.ts
Normal file
14
packages/cli/base.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Command } from "@oclif/core";
|
||||||
|
|
||||||
|
export abstract class BaseCommand<T extends typeof Command> extends Command {
|
||||||
|
protected async init(): Promise<void> {
|
||||||
|
await super.init();
|
||||||
|
|
||||||
|
const { setupDatabase } = await import("~drizzle/db");
|
||||||
|
const { consoleLogger } = await import("@loggers");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await setupDatabase(consoleLogger, false);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/cli/bin/dev.cmd
Normal file
3
packages/cli/bin/dev.cmd
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
bun "%~dp0\dev" %*
|
||||||
5
packages/cli/bin/dev.ts
Executable file
5
packages/cli/bin/dev.ts
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env -S bun
|
||||||
|
|
||||||
|
import { execute } from "@oclif/core";
|
||||||
|
|
||||||
|
await execute({ development: true, dir: import.meta.url });
|
||||||
3
packages/cli/bin/run.cmd
Normal file
3
packages/cli/bin/run.cmd
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
bun "%~dp0\run" %*
|
||||||
5
packages/cli/bin/run.ts
Executable file
5
packages/cli/bin/run.ts
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { execute } from "@oclif/core";
|
||||||
|
|
||||||
|
await execute({ dir: import.meta.url });
|
||||||
96
packages/cli/classes.ts
Normal file
96
packages/cli/classes.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
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"]
|
||||||
|
>;
|
||||||
|
export type ArgsType<T extends typeof Command> = Interfaces.InferredArgs<
|
||||||
|
T["args"]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export abstract class UserFinderCommand<
|
||||||
|
T extends typeof BaseCommand,
|
||||||
|
> extends BaseCommand<typeof UserFinderCommand> {
|
||||||
|
static baseFlags = {
|
||||||
|
pattern: Flags.boolean({
|
||||||
|
char: "p",
|
||||||
|
description:
|
||||||
|
"Process as a wildcard pattern (don't forget to escape)",
|
||||||
|
}),
|
||||||
|
type: Flags.string({
|
||||||
|
char: "t",
|
||||||
|
description: "Type of identifier",
|
||||||
|
options: ["id", "username", "note", "display-name", "email"],
|
||||||
|
default: "id",
|
||||||
|
}),
|
||||||
|
limit: Flags.integer({
|
||||||
|
char: "n",
|
||||||
|
description: "Limit the number of users",
|
||||||
|
default: 100,
|
||||||
|
}),
|
||||||
|
print: Flags.boolean({
|
||||||
|
allowNo: true,
|
||||||
|
default: true,
|
||||||
|
char: "P",
|
||||||
|
description: "Print user(s) found before processing",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static baseArgs = {
|
||||||
|
identifier: Args.string({
|
||||||
|
description:
|
||||||
|
"Identifier of the user (by default this must be an ID)",
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected flags!: FlagsType<T>;
|
||||||
|
protected args!: ArgsType<T>;
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
await super.init();
|
||||||
|
const { args, flags } = await this.parse({
|
||||||
|
flags: this.ctor.flags,
|
||||||
|
baseFlags: (super.ctor as typeof BaseCommand).baseFlags,
|
||||||
|
args: this.ctor.args,
|
||||||
|
strict: this.ctor.strict,
|
||||||
|
});
|
||||||
|
this.flags = flags as FlagsType<T>;
|
||||||
|
this.args = args as ArgsType<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findUsers(): Promise<User[]> {
|
||||||
|
const operator = this.flags.pattern ? like : eq;
|
||||||
|
// Replace wildcards with an SQL LIKE pattern
|
||||||
|
const identifier = this.flags.pattern
|
||||||
|
? this.args.identifier.replace(/\*/g, "%")
|
||||||
|
: this.args.identifier;
|
||||||
|
|
||||||
|
const { User } = await import("~packages/database-interface/user");
|
||||||
|
|
||||||
|
return await User.manyFromSql(
|
||||||
|
and(
|
||||||
|
this.flags.type === "id"
|
||||||
|
? operator(Users.id, identifier)
|
||||||
|
: undefined,
|
||||||
|
this.flags.type === "username"
|
||||||
|
? operator(Users.username, identifier)
|
||||||
|
: undefined,
|
||||||
|
this.flags.type === "note"
|
||||||
|
? operator(Users.note, identifier)
|
||||||
|
: undefined,
|
||||||
|
this.flags.type === "display-name"
|
||||||
|
? operator(Users.displayName, identifier)
|
||||||
|
: undefined,
|
||||||
|
this.flags.type === "email"
|
||||||
|
? operator(Users.email, identifier)
|
||||||
|
: undefined,
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
this.flags.limit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
packages/cli/commands/user/delete.ts
Normal file
90
packages/cli/commands/user/delete.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default class UserDelete extends UserFinderCommand<typeof UserDelete> {
|
||||||
|
static override description = "Deletes users";
|
||||||
|
|
||||||
|
static override examples = [
|
||||||
|
"<%= config.bin %> <%= command.id %> johngastron --type username",
|
||||||
|
"<%= config.bin %> <%= command.id %> 018ec11c-c6cb-7a67-bd20-a4c81bf42912",
|
||||||
|
'<%= config.bin %> <%= command.id %> "*badword*" --pattern --type username',
|
||||||
|
];
|
||||||
|
|
||||||
|
static override flags = {
|
||||||
|
confirm: Flags.boolean({
|
||||||
|
description:
|
||||||
|
"Ask for confirmation before deleting the user (default yes)",
|
||||||
|
allowNo: true,
|
||||||
|
default: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static override args = {
|
||||||
|
identifier: UserFinderCommand.baseArgs.identifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
const { flags, args } = await this.parse(UserDelete);
|
||||||
|
|
||||||
|
const users = await this.findUsers();
|
||||||
|
|
||||||
|
if (!users || users.length === 0) {
|
||||||
|
this.log(chalk.bold(`${chalk.red("✗")} No users found`));
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display user
|
||||||
|
flags.print &&
|
||||||
|
this.log(
|
||||||
|
chalk.bold(
|
||||||
|
`${chalk.green("✓")} Found ${chalk.green(
|
||||||
|
users.length,
|
||||||
|
)} user(s)`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
flags.print &&
|
||||||
|
this.log(
|
||||||
|
formatArray(
|
||||||
|
users.map((u) => u.getUser()),
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"displayName",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"isAdmin",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (flags.confirm) {
|
||||||
|
const choice = await confirm({
|
||||||
|
message: `Are you sure you want to delete these users? ${chalk.red(
|
||||||
|
"This is irreversible.",
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!choice) {
|
||||||
|
this.log(chalk.bold(`${chalk.red("✗")} Aborted operation`));
|
||||||
|
return this.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora("Deleting user(s)").start();
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
await user.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.stop();
|
||||||
|
|
||||||
|
this.log(chalk.bold(`${chalk.green("✓")} User(s) deleted`));
|
||||||
|
|
||||||
|
this.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
packages/cli/commands/user/list.ts
Normal file
83
packages/cli/commands/user/list.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { Flags } from "@oclif/core";
|
||||||
|
import { and, eq, isNotNull, isNull } from "drizzle-orm";
|
||||||
|
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> {
|
||||||
|
static override args = {};
|
||||||
|
|
||||||
|
static override description = "List all users";
|
||||||
|
|
||||||
|
static override examples = [
|
||||||
|
"<%= config.bin %> <%= command.id %> --format json --local",
|
||||||
|
"<%= config.bin %> <%= command.id %>",
|
||||||
|
];
|
||||||
|
|
||||||
|
static override flags = {
|
||||||
|
format: Flags.string({
|
||||||
|
char: "f",
|
||||||
|
description: "Output format",
|
||||||
|
options: ["json", "csv"],
|
||||||
|
}),
|
||||||
|
local: Flags.boolean({
|
||||||
|
char: "l",
|
||||||
|
description: "Local users only",
|
||||||
|
exclusive: ["remote"],
|
||||||
|
}),
|
||||||
|
remote: Flags.boolean({
|
||||||
|
char: "r",
|
||||||
|
description: "Remote users only",
|
||||||
|
exclusive: ["local"],
|
||||||
|
}),
|
||||||
|
limit: Flags.integer({
|
||||||
|
char: "n",
|
||||||
|
description: "Limit the number of users",
|
||||||
|
default: 200,
|
||||||
|
}),
|
||||||
|
admin: Flags.boolean({
|
||||||
|
char: "a",
|
||||||
|
description: "Admin users only",
|
||||||
|
allowNo: true,
|
||||||
|
}),
|
||||||
|
"pretty-dates": Flags.boolean({
|
||||||
|
char: "p",
|
||||||
|
description: "Pretty print dates",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
const { flags } = await this.parse(UserList);
|
||||||
|
|
||||||
|
const users = await User.manyFromSql(
|
||||||
|
and(
|
||||||
|
flags.local ? isNull(Users.instanceId) : undefined,
|
||||||
|
flags.remote ? isNotNull(Users.instanceId) : undefined,
|
||||||
|
flags.admin ? eq(Users.isAdmin, flags.admin) : undefined,
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
flags.limit,
|
||||||
|
);
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"displayName",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"isAdmin",
|
||||||
|
];
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
formatArray(
|
||||||
|
users.map((u) => u.getUser()),
|
||||||
|
keys,
|
||||||
|
flags.format as "json" | "csv" | undefined,
|
||||||
|
flags["pretty-dates"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
packages/cli/commands/user/reset.ts
Normal file
114
packages/cli/commands/user/reset.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default class UserReset extends UserFinderCommand<typeof UserReset> {
|
||||||
|
static override description = "Resets users' passwords";
|
||||||
|
|
||||||
|
static override examples = [
|
||||||
|
"<%= config.bin %> <%= command.id %> johngastron --type username",
|
||||||
|
"<%= config.bin %> <%= command.id %> 018ec11c-c6cb-7a67-bd20-a4c81bf42912",
|
||||||
|
];
|
||||||
|
|
||||||
|
static override flags = {
|
||||||
|
confirm: Flags.boolean({
|
||||||
|
description:
|
||||||
|
"Ask for confirmation before deleting the user (default yes)",
|
||||||
|
allowNo: true,
|
||||||
|
default: true,
|
||||||
|
}),
|
||||||
|
limit: Flags.integer({
|
||||||
|
char: "n",
|
||||||
|
description: "Limit the number of users",
|
||||||
|
default: 1,
|
||||||
|
}),
|
||||||
|
raw: Flags.boolean({
|
||||||
|
description:
|
||||||
|
"Only output the password reset link (implies --no-print and --no-confirm)",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static override args = {
|
||||||
|
identifier: UserFinderCommand.baseArgs.identifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
const { flags, args } = await this.parse(UserReset);
|
||||||
|
|
||||||
|
const users = await this.findUsers();
|
||||||
|
|
||||||
|
if (!users || users.length === 0) {
|
||||||
|
this.log(chalk.bold(`${chalk.red("✗")} No users found`));
|
||||||
|
this.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display user
|
||||||
|
!flags.raw &&
|
||||||
|
this.log(
|
||||||
|
chalk.bold(
|
||||||
|
`${chalk.green("✓")} Found ${chalk.green(
|
||||||
|
users.length,
|
||||||
|
)} user(s)`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
!flags.raw &&
|
||||||
|
flags.print &&
|
||||||
|
this.log(
|
||||||
|
formatArray(
|
||||||
|
users.map((u) => u.getUser()),
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"displayName",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"isAdmin",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (flags.confirm && !flags.raw) {
|
||||||
|
const choice = await confirm({
|
||||||
|
message: `Reset these users's passwords? ${chalk.red(
|
||||||
|
"This is irreversible.",
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!choice) {
|
||||||
|
this.log(chalk.bold(`${chalk.red("✗")} Aborted operation`));
|
||||||
|
return this.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = "https://example.com/reset-password";
|
||||||
|
|
||||||
|
!flags.raw &&
|
||||||
|
this.log(
|
||||||
|
`${chalk.green("✓")} Password reset for ${
|
||||||
|
users.length
|
||||||
|
} user(s)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
flags.raw
|
||||||
|
? link
|
||||||
|
: `\nPassword reset link for ${chalk.bold(
|
||||||
|
"@testuser",
|
||||||
|
)}: ${chalk.underline(chalk.blue(link))}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const qrcode = renderUnicodeCompact(link, {
|
||||||
|
border: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pad all lines of QR code with spaces
|
||||||
|
|
||||||
|
!flags.raw && this.log(` ${qrcode.replaceAll("\n", "\n ")}`);
|
||||||
|
|
||||||
|
this.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/cli/index.ts
Normal file
9
packages/cli/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/* 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,
|
||||||
|
};
|
||||||
|
*/
|
||||||
40
packages/cli/package.json
Normal file
40
packages/cli/package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/cli/theme.json
Normal file
15
packages/cli/theme.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"bin": "white",
|
||||||
|
"command": "cyan",
|
||||||
|
"commandSummary": "white",
|
||||||
|
"dollarSign": "white",
|
||||||
|
"flag": "white",
|
||||||
|
"flagDefaultValue": "blue",
|
||||||
|
"flagOptions": "white",
|
||||||
|
"flagRequired": "red",
|
||||||
|
"flagSeparator": "white",
|
||||||
|
"sectionDescription": "white",
|
||||||
|
"sectionHeader": "underline",
|
||||||
|
"topic": "white",
|
||||||
|
"version": "green"
|
||||||
|
}
|
||||||
70
packages/cli/utils/format.ts
Normal file
70
packages/cli/utils/format.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { getBorderCharacters, table } from "table";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a JS array, return a string output to be passed to console.log
|
||||||
|
* @param arr The array to be formatted
|
||||||
|
* @param keys The keys to be displayed (removes all other keys from the output)
|
||||||
|
* @param type Either "json", "csv" or nothing for a table
|
||||||
|
* @returns The formatted string
|
||||||
|
*/
|
||||||
|
export const formatArray = (
|
||||||
|
arr: Record<string, unknown>[],
|
||||||
|
keys: string[],
|
||||||
|
type?: "json" | "csv",
|
||||||
|
prettyDates = false,
|
||||||
|
): string => {
|
||||||
|
const output = arr.map((item) => {
|
||||||
|
const newItem = {} as Record<string, unknown>;
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
newItem[key] = item[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prettyDates) {
|
||||||
|
for (const item of output) {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = item[key];
|
||||||
|
// If this is an ISO string, convert it to a nice date
|
||||||
|
if (
|
||||||
|
typeof value === "string" &&
|
||||||
|
value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}$/)
|
||||||
|
) {
|
||||||
|
item[key] = Intl.DateTimeFormat(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
}).format(new Date(value));
|
||||||
|
// Format using Chalk
|
||||||
|
item[key] = chalk.underline(item[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "json":
|
||||||
|
return JSON.stringify(output, null, 2);
|
||||||
|
case "csv":
|
||||||
|
return `${keys.join(",")}\n${output
|
||||||
|
.map((item) => keys.map((key) => item[key]).join(","))
|
||||||
|
.join("\n")}`;
|
||||||
|
default:
|
||||||
|
// Convert output to array of arrays for table
|
||||||
|
return table(
|
||||||
|
[
|
||||||
|
keys.map((k) => chalk.bold(k)),
|
||||||
|
...output.map((item) => keys.map((key) => item[key])),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
border: getBorderCharacters("norc"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
"name": "config-manager",
|
"name": "config-manager",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^2.2.5",
|
"c12": "^1.10.0"
|
||||||
"c12": "^1.10.0",
|
|
||||||
"merge-deep-ts": "^1.2.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,12 @@ export class User {
|
||||||
)[0].count;
|
)[0].count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete() {
|
||||||
|
return (
|
||||||
|
await db.delete(Users).where(eq(Users.id, this.id)).returning()
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
|
||||||
async pin(note: Note) {
|
async pin(note: Note) {
|
||||||
return (
|
return (
|
||||||
await db
|
await db
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
"*.d.ts",
|
"*.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.d.ts",
|
"**/*.d.ts",
|
||||||
"server/api/well-known/**/*.ts"
|
"server/api/well-known/**/*.ts",
|
||||||
|
"packages/cli/index.mts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { config } from "config-manager";
|
import { config } from "~packages/config-manager";
|
||||||
import { LogManager, MultiLogManager } from "log-manager";
|
import { LogManager, MultiLogManager } from "log-manager";
|
||||||
|
|
||||||
const noColors = process.env.NO_COLORS === "true";
|
const noColors = process.env.NO_COLORS === "true";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue