feat(cli): Add more emoji commands to CLI (add, delete, list, import)

This commit is contained in:
Jesse Wierzbinski 2024-05-08 16:07:33 -10:00
parent e48f57a3d8
commit 5bdb8360ea
No known key found for this signature in database
9 changed files with 753 additions and 188 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -1,9 +1,10 @@
import { Args, type Command, Flags, type Interfaces } from "@oclif/core"; import { Args, type Command, Flags, type Interfaces } from "@oclif/core";
import chalk from "chalk"; import chalk from "chalk";
import { and, eq, like } from "drizzle-orm"; import { and, eq, getTableColumns, like } from "drizzle-orm";
import { Users } from "~drizzle/schema"; import { Emojis, Instances, Users } from "~drizzle/schema";
import { User } from "~packages/database-interface/user"; import { User } from "~packages/database-interface/user";
import { BaseCommand } from "./base"; import { BaseCommand } from "./base";
import { db } from "~drizzle/db";
export type FlagsType<T extends typeof Command> = Interfaces.InferredFlags< export type FlagsType<T extends typeof Command> = Interfaces.InferredFlags<
(typeof BaseCommand)["baseFlags"] & T["flags"] (typeof BaseCommand)["baseFlags"] & T["flags"]
@ -104,3 +105,91 @@ export abstract class UserFinderCommand<
); );
} }
} }
export abstract class EmojiFinderCommand<
T extends typeof BaseCommand,
> extends BaseCommand<typeof EmojiFinderCommand> {
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: ["shortcode", "instance"],
default: "shortcode",
}),
limit: Flags.integer({
char: "n",
description: "Limit the number of emojis",
default: 100,
}),
print: Flags.boolean({
allowNo: true,
default: true,
char: "P",
description: "Print emoji(s) found before processing",
}),
};
static baseArgs = {
identifier: Args.string({
description: "Identifier of the emoji (defaults to shortcode)",
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 findEmojis() {
// 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
? this.args.identifier.replace(/\*/g, "%")
: this.args.identifier;
return await db
.select({
...getTableColumns(Emojis),
instanceUrl: Instances.baseUrl,
})
.from(Emojis)
.leftJoin(Instances, eq(Emojis.instanceId, Instances.id))
.where(
and(
this.flags.type === "shortcode"
? operator(Emojis.shortcode, identifier)
: undefined,
this.flags.type === "instance"
? operator(Instances.baseUrl, identifier)
: undefined,
),
);
}
}

View file

@ -1,7 +1,12 @@
import { Args } from "@oclif/core"; import { Args } from "@oclif/core";
import chalk from "chalk"; import chalk from "chalk";
import ora from "ora";
import { BaseCommand } from "~/cli/base"; import { BaseCommand } from "~/cli/base";
import { getUrl } from "~database/entities/Attachment";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
import { Emojis } from "~drizzle/schema";
import { config } from "~packages/config-manager";
import { MediaBackend } from "~packages/media-manager";
export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> { export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
static override args = { static override args = {
@ -17,7 +22,10 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
static override description = "Adds a new emoji"; static override description = "Adds a new emoji";
static override examples = ["<%= config.bin %> <%= command.id %>"]; static override examples = [
"<%= config.bin %> <%= command.id %> baba_yassie ./emojis/baba_yassie.png",
"<%= config.bin %> <%= command.id %> baba_yassie https://example.com/emojis/baba_yassie.png",
];
static override flags = {}; static override flags = {};
@ -26,7 +34,11 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
// Check if emoji already exists // Check if emoji already exists
const existingEmoji = await db.query.Emojis.findFirst({ const existingEmoji = await db.query.Emojis.findFirst({
where: (Emojis, { eq }) => eq(Emojis.shortcode, args.shortcode), where: (Emojis, { eq, and, isNull }) =>
and(
eq(Emojis.shortcode, args.shortcode),
isNull(Emojis.instanceId),
),
}); });
if (existingEmoji) { if (existingEmoji) {
@ -38,59 +50,98 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
this.exit(1); this.exit(1);
} }
this.log("Placeholder command, this command is not implemented yet."); let file: File | null = null;
/* if (!user) { if (URL.canParse(args.file)) {
const spinner = ora(
`Downloading emoji from ${chalk.blue(
chalk.underline(args.file),
)}`,
).start();
const response = await fetch(args.file, {
headers: {
"Accept-Encoding": "identity",
},
});
if (!response.ok) {
spinner.fail();
this.log(
`${chalk.red("✗")} Request returned status code ${chalk.red(
response.status,
)}`,
);
this.exit(1);
}
const filename =
new URL(args.file).pathname.split("/").pop() ?? "emoji";
file = new File([await response.blob()], filename, {
type:
response.headers.get("Content-Type") ??
"application/octet-stream",
});
spinner.succeed();
} else {
const bunFile = Bun.file(args.file);
file = new File(
[await bunFile.arrayBuffer()],
args.file.split("/").pop() ?? "emoji",
{
type: bunFile.type,
},
);
}
const media = await MediaBackend.fromBackendType(
config.media.backend,
config,
);
const spinner = ora("Uploading emoji").start();
const uploaded = await media.addFile(file).catch((e: Error) => {
spinner.fail();
this.log(`${chalk.red("✗")} Error: ${chalk.red(e.message)}`);
return null;
});
if (!uploaded) {
return this.exit(1);
}
spinner.succeed();
const emoji = await db
.insert(Emojis)
.values({
shortcode: args.shortcode,
url: getUrl(uploaded.path, config),
visibleInPicker: true,
contentType: file.type,
})
.returning();
if (!emoji || emoji.length === 0) {
this.log( this.log(
`${chalk.red("✗")} Failed to create user ${chalk.red( `${chalk.red("✗")} Failed to create emoji ${chalk.red(
args.username, args.shortcode,
)}`, )}`,
); );
this.exit(1); this.exit(1);
} }
!flags.format &&
this.log(
`${chalk.green("✓")} Created user ${chalk.green(
user.getUser().username,
)} with id ${chalk.green(user.id)}`,
);
this.log( this.log(
formatArray( `${chalk.green("✓")} Created emoji ${chalk.green(
[user.getUser()], args.shortcode,
[ )} with url ${chalk.blue(
"id", chalk.underline(getUrl(uploaded.path, config)),
"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); this.exit(0);
} }
} }

View file

@ -0,0 +1,93 @@
import { Args, Flags } from "@oclif/core";
import chalk from "chalk";
import { and, eq, inArray, isNull } from "drizzle-orm";
import { EmojiFinderCommand } from "~cli/classes";
import { formatArray } from "~cli/utils/format";
import { db } from "~drizzle/db";
import { Emojis } from "~drizzle/schema";
import confirm from "@inquirer/confirm";
import ora from "ora";
export default class EmojiDelete extends EmojiFinderCommand<
typeof EmojiDelete
> {
static override args = {
identifier: EmojiFinderCommand.baseArgs.identifier,
};
static override description = "Deletes an emoji";
static override examples = [
"<%= config.bin %> <%= command.id %> baba_yassie",
'<%= config.bin %> <%= command.id %> "baba\\*" --pattern',
];
static override flags = {
confirm: Flags.boolean({
description:
"Ask for confirmation before deleting the emoji (default yes)",
allowNo: true,
default: true,
}),
};
public async run(): Promise<void> {
const { flags, args } = await this.parse(EmojiDelete);
const emojis = await this.findEmojis();
if (!emojis || emojis.length === 0) {
this.log(chalk.bold(`${chalk.red("✗")} No emojis found`));
this.exit(1);
}
// Display user
flags.print &&
this.log(
chalk.bold(
`${chalk.green("✓")} Found ${chalk.green(
emojis.length,
)} emoji(s)`,
),
);
flags.print &&
this.log(
formatArray(emojis, [
"id",
"shortcode",
"alt",
"contentType",
"instanceUrl",
]),
);
if (flags.confirm) {
const choice = await confirm({
message: `Are you sure you want to delete these emojis? ${chalk.red(
"This is irreversible.",
)}`,
});
if (!choice) {
this.log(chalk.bold(`${chalk.red("✗")} Aborted operation`));
return this.exit(1);
}
}
const spinner = ora("Deleting emoji(s)").start();
await db.delete(Emojis).where(
inArray(
Emojis.id,
emojis.map((e) => e.id),
),
);
spinner.succeed();
this.log(chalk.bold(`${chalk.green("✓")} Emoji(s) deleted`));
this.exit(0);
}
}

View file

@ -0,0 +1,255 @@
import { Args, Flags } from "@oclif/core";
import chalk from "chalk";
import ora from "ora";
import { BaseCommand } from "~/cli/base";
import { getUrl } from "~database/entities/Attachment";
import { db } from "~drizzle/db";
import { Emojis } from "~drizzle/schema";
import { config } from "~packages/config-manager";
import { MediaBackend } from "~packages/media-manager";
import { unzip } from "unzipit";
import { and, inArray, isNull } from "drizzle-orm";
import { lookup } from "mime-types";
type MetaType = {
emojis: {
fileName: string;
emoji: {
name: string;
};
}[];
};
export default class EmojiImport extends BaseCommand<typeof EmojiImport> {
static override args = {
path: Args.string({
description: "Path to the emoji archive (can be an URL)",
required: true,
}),
};
static override description =
"Imports emojis from a zip file (which can be fetched from a zip URL, e.g. for Pleroma emoji packs)";
static override examples = [
"<%= config.bin %> <%= command.id %> https://volpeon.ink/emojis/neocat/neocat.zip",
"<%= config.bin %> <%= command.id %> export.zip",
];
static override flags = {
confirm: Flags.boolean({
description:
"Ask for confirmation before deleting the emoji (default yes)",
allowNo: true,
default: true,
}),
};
public async run(): Promise<void> {
const { flags, args } = await this.parse(EmojiImport);
// Check if path ends in .zip, warn the user if it doesn't
if (!args.path.endsWith(".zip")) {
this.log(
`${chalk.yellow(
"⚠",
)} The path you provided does not end in .zip, this may not be a zip file. Proceeding anyway.`,
);
}
let file: File | null = null;
if (URL.canParse(args.path)) {
const spinner = ora(
`Downloading pack from ${chalk.blue(
chalk.underline(args.path),
)}`,
).start();
const response = await fetch(args.path, {
headers: {
"Accept-Encoding": "identity",
},
});
if (!response.ok) {
spinner.fail();
this.log(
`${chalk.red("✗")} Request returned status code ${chalk.red(
response.status,
)}`,
);
this.exit(1);
}
const filename =
new URL(args.path).pathname.split("/").pop() ?? "archive";
file = new File([await response.blob()], filename, {
type:
response.headers.get("Content-Type") ??
"application/octet-stream",
});
spinner.succeed();
} else {
const bunFile = Bun.file(args.path);
file = new File(
[await bunFile.arrayBuffer()],
args.path.split("/").pop() ?? "archive",
{
type: bunFile.type,
},
);
}
const unzipSpinner = ora("Unzipping pack").start();
const { entries: unzipped } = await unzip(file);
unzipSpinner.succeed();
const entries = Object.entries(unzipped);
// Check if a meta.json file exists
const metaExists = entries.find(([name]) => name === "meta.json");
if (metaExists) {
this.log(`${chalk.green("✓")} Detected Pleroma meta.json, parsing`);
}
const meta = metaExists
? ((await metaExists[1].json()) as MetaType)
: ({
emojis: entries.map(([name]) => ({
fileName: name,
emoji: {
name: name.split(".")[0],
},
})),
} as MetaType);
// Get all emojis that already exist
const existingEmojis = await db
.select()
.from(Emojis)
.where(
and(
isNull(Emojis.instanceId),
inArray(
Emojis.shortcode,
meta.emojis.map((e) => e.emoji.name),
),
),
);
// Filter out existing emojis
const newEmojis = meta.emojis.filter(
(e) => !existingEmojis.find((ee) => ee.shortcode === e.emoji.name),
);
existingEmojis.length > 0 &&
this.log(
`${chalk.yellow("⚠")} Emojis with shortcode ${chalk.yellow(
existingEmojis.map((e) => e.shortcode).join(", "),
)} already exist in the database and will not be imported`,
);
if (newEmojis.length === 0) {
this.log(`${chalk.red("✗")} No new emojis to import`);
this.exit(1);
}
this.log(
`${chalk.green("✓")} Found ${chalk.green(
newEmojis.length,
)} new emoji(s)`,
);
const importSpinner = ora("Importing emojis").start();
const media = await MediaBackend.fromBackendType(
config.media.backend,
config,
);
const successfullyImported: MetaType["emojis"] = [];
for (const emoji of newEmojis) {
importSpinner.text = `Uploading ${chalk.gray(emoji.emoji.name)} (${
newEmojis.indexOf(emoji) + 1
}/${newEmojis.length})`;
const zipEntry = unzipped[emoji.fileName];
if (!zipEntry) {
this.log(
`${chalk.red(
"✗",
)} Could not find file for emoji ${chalk.red(
emoji.emoji.name,
)}`,
);
continue;
}
const fileName = emoji.fileName.split("/").pop() ?? "emoji";
const contentType = lookup(fileName) || "application/octet-stream";
const newFile = new File([await zipEntry.arrayBuffer()], fileName, {
type: contentType,
});
const uploaded = await media.addFile(newFile).catch((e: Error) => {
this.log(
`${chalk.red("✗")} Error uploading ${chalk.red(
emoji.emoji.name,
)}: ${chalk.red(e.message)}`,
);
return null;
});
if (!uploaded) {
continue;
}
await db
.insert(Emojis)
.values({
shortcode: emoji.emoji.name,
url: getUrl(uploaded.path, config),
visibleInPicker: true,
contentType: file.type,
})
.execute();
successfullyImported.push(emoji);
}
importSpinner.succeed("Imported emojis");
successfullyImported.length > 0 &&
this.log(
`${chalk.green("✓")} Successfully imported ${chalk.green(
successfullyImported.length,
)} emoji(s)`,
);
newEmojis.length - successfullyImported.length > 0 &&
this.log(
`${chalk.yellow("⚠")} Failed to import ${chalk.yellow(
newEmojis.length - successfullyImported.length,
)} emoji(s): ${chalk.yellow(
newEmojis
.filter((e) => !successfullyImported.includes(e))
.map((e) => e.emoji.name)
.join(", "),
)}`,
);
if (successfullyImported.length === 0) {
this.exit(1);
}
this.exit(0);
}
}

View file

@ -0,0 +1,70 @@
import { Flags } from "@oclif/core";
import { and, eq, getTableColumns, isNotNull, isNull } from "drizzle-orm";
import { BaseCommand } from "~cli/base";
import { formatArray } from "~cli/utils/format";
import { db } from "~drizzle/db";
import { Emojis, Instances } from "~drizzle/schema";
export default class EmojiList extends BaseCommand<typeof EmojiList> {
static override args = {};
static override description = "List all emojis";
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 emojis only",
exclusive: ["remote"],
}),
remote: Flags.boolean({
char: "r",
description: "Remote emojis only",
exclusive: ["local"],
}),
limit: Flags.integer({
char: "n",
description: "Limit the number of emojis",
default: 200,
}),
};
public async run(): Promise<void> {
const { flags } = await this.parse(EmojiList);
const emojis = await db
.select({
...getTableColumns(Emojis),
instanceUrl: Instances.baseUrl,
})
.from(Emojis)
.leftJoin(Instances, eq(Emojis.instanceId, Instances.id))
.where(
and(
flags.local ? isNull(Emojis.instanceId) : undefined,
flags.remote ? isNotNull(Emojis.instanceId) : undefined,
),
);
const keys = ["id", "shortcode", "alt", "contentType", "instanceUrl"];
this.log(
formatArray(
emojis,
keys,
flags.format as "json" | "csv" | undefined,
),
);
this.exit(0);
}
}

View file

@ -81,7 +81,7 @@ export default class UserDelete extends UserFinderCommand<typeof UserDelete> {
await user.delete(); await user.delete();
} }
spinner.stop(); spinner.succeed();
this.log(chalk.bold(`${chalk.green("✓")} User(s) deleted`)); this.log(chalk.bold(`${chalk.green("✓")} User(s) deleted`));

View file

@ -4,6 +4,9 @@ import UserCreate from "./commands/user/create";
import UserDelete from "./commands/user/delete"; import UserDelete from "./commands/user/delete";
import UserList from "./commands/user/list"; import UserList from "./commands/user/list";
import UserReset from "./commands/user/reset"; import UserReset from "./commands/user/reset";
import EmojiDelete from "./commands/emoji/delete";
import EmojiList from "./commands/emoji/list";
import EmojiImport from "./commands/emoji/import";
// Use "explicit" oclif strategy to avoid issues with oclif's module resolver and bundling // Use "explicit" oclif strategy to avoid issues with oclif's module resolver and bundling
export const commands = { export const commands = {
@ -12,6 +15,9 @@ export const commands = {
"user:create": UserCreate, "user:create": UserCreate,
"user:reset": UserReset, "user:reset": UserReset,
"emoji:add": EmojiAdd, "emoji:add": EmojiAdd,
"emoji:delete": EmojiDelete,
"emoji:list": EmojiList,
"emoji:import": EmojiImport,
}; };
if (import.meta.path === Bun.main) { if (import.meta.path === Bun.main) {

View file

@ -1,143 +1,144 @@
{ {
"name": "lysand", "name": "lysand",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "0.5.0", "version": "0.5.0",
"description": "A project to build a federated social network", "description": "A project to build a federated social network",
"author": { "author": {
"email": "contact@cpluspatch.com", "email": "contact@cpluspatch.com",
"name": "CPlusPatch", "name": "CPlusPatch",
"url": "https://cpluspatch.com" "url": "https://cpluspatch.com"
}, },
"bugs": { "bugs": {
"url": "https://github.com/lysand-org/lysand/issues" "url": "https://github.com/lysand-org/lysand/issues"
}, },
"icon": "https://github.com/lysand-org/lysand", "icon": "https://github.com/lysand-org/lysand",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"keywords": ["federated", "activitypub", "bun"], "keywords": ["federated", "activitypub", "bun"],
"workspaces": ["packages/*"], "workspaces": ["packages/*"],
"maintainers": [ "maintainers": [
{ {
"email": "contact@cpluspatch.com", "email": "contact@cpluspatch.com",
"name": "CPlusPatch", "name": "CPlusPatch",
"url": "https://cpluspatch.com" "url": "https://cpluspatch.com"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/lysand-org/lysand.git"
},
"private": true,
"scripts": {
"dev": "bun run --hot index.ts",
"start": "NODE_ENV=production bun run dist/index.js --prod",
"lint": "bunx @biomejs/biome check .",
"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 cli/index.ts",
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'"
},
"trustedDependencies": [
"@biomejs/biome",
"@fortawesome/fontawesome-common-types",
"@fortawesome/free-regular-svg-icons",
"@fortawesome/free-solid-svg-icons",
"es5-ext",
"esbuild",
"json-editor-vue",
"msgpackr-extract",
"nuxt-app",
"sharp",
"vue-demi"
],
"oclif": {
"bin": "cli",
"dirname": "cli",
"commands": {
"strategy": "explicit",
"target": "./cli/index",
"identifier": "commands"
},
"additionalHelpFlags": ["-h"],
"additionalVersionFlags": ["-v"],
"plugins": [],
"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-progress": "^3.11.5",
"@types/cli-table": "^0.3.4",
"@types/html-to-text": "^9.0.4",
"@types/ioredis": "^5.0.0",
"@types/jsonld": "^1.5.13",
"@types/markdown-it-container": "^2.0.10",
"@types/mime-types": "^2.1.4",
"@types/pg": "^8.11.5",
"@types/qs": "^6.9.15",
"bun-types": "latest",
"drizzle-kit": "^0.20.14",
"oclif": "^4.10.4",
"ts-prune": "^0.10.3",
"typescript": "latest"
},
"peerDependencies": {
"typescript": "^5.3.2"
},
"dependencies": {
"@inquirer/confirm": "^3.1.6",
"@inquirer/input": "^2.1.6",
"@oclif/core": "^3.26.6",
"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",
"@tufjs/canonical-json": "^2.0.0",
"blurhash": "^2.0.5",
"bullmq": "^5.7.1",
"chalk": "^5.3.0",
"cli-parser": "workspace:*",
"cli-table": "^0.3.11",
"config-manager": "workspace:*",
"drizzle-orm": "^0.30.7",
"extract-zip": "^2.0.1",
"hono": "^4.3.2",
"html-to-text": "^9.0.5",
"ioredis": "^5.3.2",
"ip-matching": "^2.1.2",
"iso-639-1": "^3.1.0",
"jose": "^5.2.4",
"linkify-html": "^4.1.3",
"linkify-string": "^4.1.3",
"linkifyjs": "^4.1.3",
"log-manager": "workspace:*",
"magic-regexp": "^0.8.0",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^8.6.7",
"markdown-it-container": "^4.0.0",
"markdown-it-toc-done-right": "^4.2.0",
"media-manager": "workspace:*",
"meilisearch": "^0.39.0",
"mime-types": "^2.1.35",
"oauth4webapi": "^2.4.0",
"pg": "^8.11.5",
"qs": "^6.12.1",
"sharp": "^0.33.3",
"string-comparison": "^1.3.0",
"stringify-entities": "^4.0.4",
"xss": "^1.0.15",
"zod": "^3.22.4",
"zod-validation-error": "^3.2.0"
} }
],
"repository": {
"type": "git",
"url": "git+https://github.com/lysand-org/lysand.git"
},
"private": true,
"scripts": {
"dev": "bun run --hot index.ts",
"start": "NODE_ENV=production bun run dist/index.js --prod",
"lint": "bunx @biomejs/biome check .",
"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 cli/index.ts",
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'"
},
"trustedDependencies": [
"@biomejs/biome",
"@fortawesome/fontawesome-common-types",
"@fortawesome/free-regular-svg-icons",
"@fortawesome/free-solid-svg-icons",
"es5-ext",
"esbuild",
"json-editor-vue",
"msgpackr-extract",
"nuxt-app",
"sharp",
"vue-demi"
],
"oclif": {
"bin": "cli",
"dirname": "cli",
"commands": {
"strategy": "explicit",
"target": "./cli/index",
"identifier": "commands"
},
"additionalHelpFlags": ["-h"],
"additionalVersionFlags": ["-v"],
"plugins": [],
"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-progress": "^3.11.5",
"@types/cli-table": "^0.3.4",
"@types/html-to-text": "^9.0.4",
"@types/ioredis": "^5.0.0",
"@types/jsonld": "^1.5.13",
"@types/markdown-it-container": "^2.0.10",
"@types/mime-types": "^2.1.4",
"@types/pg": "^8.11.5",
"@types/qs": "^6.9.15",
"bun-types": "latest",
"drizzle-kit": "^0.20.14",
"oclif": "^4.10.4",
"ts-prune": "^0.10.3",
"typescript": "latest"
},
"peerDependencies": {
"typescript": "^5.3.2"
},
"dependencies": {
"@hackmd/markdown-it-task-lists": "^2.1.4",
"@hono/zod-validator": "^0.2.1",
"@inquirer/confirm": "^3.1.6",
"@inquirer/input": "^2.1.6",
"@json2csv/plainjs": "^7.0.6",
"@oclif/core": "^3.26.6",
"@tufjs/canonical-json": "^2.0.0",
"blurhash": "^2.0.5",
"bullmq": "^5.7.1",
"chalk": "^5.3.0",
"cli-parser": "workspace:*",
"cli-progress": "^3.12.0",
"cli-table": "^0.3.11",
"config-manager": "workspace:*",
"drizzle-orm": "^0.30.7",
"extract-zip": "^2.0.1",
"hono": "^4.3.2",
"html-to-text": "^9.0.5",
"ioredis": "^5.3.2",
"ip-matching": "^2.1.2",
"iso-639-1": "^3.1.0",
"jose": "^5.2.4",
"linkify-html": "^4.1.3",
"linkify-string": "^4.1.3",
"linkifyjs": "^4.1.3",
"log-manager": "workspace:*",
"magic-regexp": "^0.8.0",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^8.6.7",
"markdown-it-container": "^4.0.0",
"markdown-it-toc-done-right": "^4.2.0",
"media-manager": "workspace:*",
"meilisearch": "^0.39.0",
"mime-types": "^2.1.35",
"oauth4webapi": "^2.4.0",
"ora": "^8.0.1",
"pg": "^8.11.5",
"qs": "^6.12.1",
"sharp": "^0.33.3",
"string-comparison": "^1.3.0",
"stringify-entities": "^4.0.4",
"table": "^6.8.2",
"unzipit": "^1.4.3",
"uqr": "^0.1.2",
"xss": "^1.0.15",
"zod": "^3.22.4",
"zod-validation-error": "^3.2.0"
}
} }