From 5bdb8360ea24c1938183b5175ffbee2a89c47aa0 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 8 May 2024 16:07:33 -1000 Subject: [PATCH] feat(cli): :sparkles: Add more emoji commands to CLI (add, delete, list, import) --- bun.lockb | Bin 272508 -> 273220 bytes cli/classes.ts | 93 +++++++++++- cli/commands/emoji/add.ts | 141 ++++++++++++------ cli/commands/emoji/delete.ts | 93 ++++++++++++ cli/commands/emoji/import.ts | 255 +++++++++++++++++++++++++++++++ cli/commands/emoji/list.ts | 70 +++++++++ cli/commands/user/delete.ts | 2 +- cli/index.ts | 6 + package.json | 281 ++++++++++++++++++----------------- 9 files changed, 753 insertions(+), 188 deletions(-) create mode 100644 cli/commands/emoji/delete.ts create mode 100644 cli/commands/emoji/import.ts create mode 100644 cli/commands/emoji/list.ts diff --git a/bun.lockb b/bun.lockb index 858989ad4cc27400f4e428164beb3da5a7218108..8368dabaf02dfbd0f2b67559e028af0238257848 100755 GIT binary patch delta 48175 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

%>;uve#zX&5doX#5!4hdw!9+X?{#3Jf9*3 zy4vEcF7cng85P>3T5j{7qbZEvzYMOKoFsXfc`s$}NRRLv!GH6kuSol6r>|IwZ%_Lz zTr_v-0#IF++?;jQc?7N0VCF}Xcg@1rq&>}67Cxp#1SUy7AP+Jf1mS~BW39LIhm(9w z$>-}_YYq%Dy`-{Q?#r+zeHr%PVAILg7DG9Kp$rRdKBjM3ujfyz@M&6!+8a*#n4b7A D*hExF delta 48043 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 = Interfaces.InferredFlags< (typeof BaseCommand)["baseFlags"] & T["flags"] @@ -104,3 +105,91 @@ export abstract class UserFinderCommand< ); } } + +export abstract class EmojiFinderCommand< + T extends typeof BaseCommand, +> extends BaseCommand { + 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; + protected args!: ArgsType; + + public async init(): Promise { + 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; + this.args = args as ArgsType; + } + + 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, + ), + ); + } +} diff --git a/cli/commands/emoji/add.ts b/cli/commands/emoji/add.ts index 5cbb6d6c..78ade9eb 100644 --- a/cli/commands/emoji/add.ts +++ b/cli/commands/emoji/add.ts @@ -1,7 +1,12 @@ import { Args } 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"; export default class EmojiAdd extends BaseCommand { static override args = { @@ -17,7 +22,10 @@ export default class EmojiAdd extends BaseCommand { 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 = {}; @@ -26,7 +34,11 @@ export default class EmojiAdd extends BaseCommand { // Check if emoji already exists 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) { @@ -38,59 +50,98 @@ export default class EmojiAdd extends BaseCommand { 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( - `${chalk.red("✗")} Failed to create user ${chalk.red( - args.username, + `${chalk.red("✗")} Failed to create emoji ${chalk.red( + args.shortcode, )}`, ); 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, - ), + `${chalk.green("✓")} Created emoji ${chalk.green( + args.shortcode, + )} with url ${chalk.blue( + chalk.underline(getUrl(uploaded.path, config)), + )}`, ); - 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); } } diff --git a/cli/commands/emoji/delete.ts b/cli/commands/emoji/delete.ts new file mode 100644 index 00000000..584467a6 --- /dev/null +++ b/cli/commands/emoji/delete.ts @@ -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 { + 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); + } +} diff --git a/cli/commands/emoji/import.ts b/cli/commands/emoji/import.ts new file mode 100644 index 00000000..e59d2782 --- /dev/null +++ b/cli/commands/emoji/import.ts @@ -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 { + 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 { + 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); + } +} diff --git a/cli/commands/emoji/list.ts b/cli/commands/emoji/list.ts new file mode 100644 index 00000000..7a738690 --- /dev/null +++ b/cli/commands/emoji/list.ts @@ -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 { + 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 { + 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); + } +} diff --git a/cli/commands/user/delete.ts b/cli/commands/user/delete.ts index 2e8f851f..b631c028 100644 --- a/cli/commands/user/delete.ts +++ b/cli/commands/user/delete.ts @@ -81,7 +81,7 @@ export default class UserDelete extends UserFinderCommand { await user.delete(); } - spinner.stop(); + spinner.succeed(); this.log(chalk.bold(`${chalk.green("✓")} User(s) deleted`)); diff --git a/cli/index.ts b/cli/index.ts index 98dfce12..2dd3744a 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -4,6 +4,9 @@ import UserCreate from "./commands/user/create"; import UserDelete from "./commands/user/delete"; import UserList from "./commands/user/list"; 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 export const commands = { @@ -12,6 +15,9 @@ export const commands = { "user:create": UserCreate, "user:reset": UserReset, "emoji:add": EmojiAdd, + "emoji:delete": EmojiDelete, + "emoji:list": EmojiList, + "emoji:import": EmojiImport, }; if (import.meta.path === Bun.main) { diff --git a/package.json b/package.json index d575b9db..8ab89d31 100644 --- a/package.json +++ b/package.json @@ -1,143 +1,144 @@ { - "name": "lysand", - "module": "index.ts", - "type": "module", - "version": "0.5.0", - "description": "A project to build a federated social network", - "author": { - "email": "contact@cpluspatch.com", - "name": "CPlusPatch", - "url": "https://cpluspatch.com" - }, - "bugs": { - "url": "https://github.com/lysand-org/lysand/issues" - }, - "icon": "https://github.com/lysand-org/lysand", - "license": "AGPL-3.0-or-later", - "keywords": ["federated", "activitypub", "bun"], - "workspaces": ["packages/*"], - "maintainers": [ - { - "email": "contact@cpluspatch.com", - "name": "CPlusPatch", - "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" + "name": "lysand", + "module": "index.ts", + "type": "module", + "version": "0.5.0", + "description": "A project to build a federated social network", + "author": { + "email": "contact@cpluspatch.com", + "name": "CPlusPatch", + "url": "https://cpluspatch.com" + }, + "bugs": { + "url": "https://github.com/lysand-org/lysand/issues" + }, + "icon": "https://github.com/lysand-org/lysand", + "license": "AGPL-3.0-or-later", + "keywords": ["federated", "activitypub", "bun"], + "workspaces": ["packages/*"], + "maintainers": [ + { + "email": "contact@cpluspatch.com", + "name": "CPlusPatch", + "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": { + "@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" + } }