diff --git a/bun.lockb b/bun.lockb index 858989ad..8368daba 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cli/classes.ts b/cli/classes.ts index 13c36c70..a2f32019 100644 --- a/cli/classes.ts +++ b/cli/classes.ts @@ -1,9 +1,10 @@ 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 { and, eq, getTableColumns, like } from "drizzle-orm"; +import { Emojis, Instances, Users } from "~drizzle/schema"; import { User } from "~packages/database-interface/user"; import { BaseCommand } from "./base"; +import { db } from "~drizzle/db"; export type FlagsType = 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" + } }