diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore deleted file mode 100644 index 30b4a857c..000000000 --- a/packages/cli/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -bin/ -fedify-cli-*.tar.xz -fedify-cli-*.tgz -fedify-cli-*.zip diff --git a/packages/cli/deno.json b/packages/cli/deno.json index bdd4dcd7e..64e1b8997 100644 --- a/packages/cli/deno.json +++ b/packages/cli/deno.json @@ -4,38 +4,21 @@ "license": "MIT", "exports": "./src/mod.ts", "imports": { - "@cliffy/ansi": "jsr:@cliffy/ansi@1.0.0-rc.4", - "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.4", - "@cliffy/prompt": "jsr:@cliffy/prompt@1.0.0-rc.4", - "@cliffy/table": "jsr:@cliffy/table@1.0.0-rc.4", "@cross/dir": "jsr:@cross/dir@^1.1.0", - "@david/dax": "jsr:@david/dax@^0.41.0", - "@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.2.0", - "@jimp/core": "npm:@jimp/core@^1.6.0", + "@david/dax": "jsr:@david/dax@^0.43.2", + "@optique/core": "jsr:@optique/core@^0.3.0", + "@optique/run": "jsr:@optique/run@^0.3.0", + "@std/fmt": "jsr:@std/fmt@^1.0.8", + "ora": "npm:ora@^8.2.0", + "@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.3.0", "@jimp/wasm-webp": "npm:@jimp/wasm-webp@^1.6.0", - "@poppanator/http-constants": "npm:@poppanator/http-constants@^1.1.1", - "@std/assert": "jsr:@std/assert@^1.0.13", - "@std/fmt/colors": "jsr:@std/fmt@^0.224.0/colors", - "@std/testing": "jsr:@std/testing@^1.0.8", - "@std/dotenv": "jsr:@std/dotenv@^0.225.2", - "@std/semver": "jsr:@std/semver@^1.0.5", - "cli-highlight": "npm:cli-highlight@^2.1.11", - "fetch-mock": "npm:fetch-mock@^12.5.2", - "hono": "jsr:@hono/hono@^4.8.3", - "icojs": "npm:icojs@^0.19.4", - "jimp": "npm:jimp@^1.6.0", - "ora": "npm:ora@^8.0.1", - "shiki": "npm:shiki@^1.6.4", - "sharp": "npm:sharp@^0.34.3" + "@jimp/core": "npm:@jimp/core@^1.6.0", + "@std/fmt/colors": "jsr:@std/fmt@^0.224.0/colors" }, "exclude": [ - ".vscode", "fedify-cli-*.tar.xz", "fedify-cli-*.tgz", - "fedify-cli-*.zip", - "package.json", - "src/install.mjs", - "src/run.mjs" + "fedify-cli-*.zip" ], "tasks": { "codegen": "deno task -f @fedify/fedify codegen", @@ -51,13 +34,7 @@ "codegen" ] }, - "publish": { - "command": "deno publish", - "dependencies": [ - "codegen" - ] - }, - "publish-dry-run": "deno task publish --dry-run --allow-dirty", + "runi": "deno run --allow-all src/mod.ts", "pack": { "command": "deno run -A scripts/pack.ts", "dependencies": [ diff --git a/packages/cli/package.json b/packages/cli/package.json index e1bbaa3b1..179e74f20 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,31 +1,7 @@ { "name": "@fedify/cli", "version": "2.0.0", - "type": "module", - "files": [ - "README.md", - "package.json", - "src/install.mjs", - "src/run.mjs" - ], - "bin": { - "fedify": "./src/run.mjs" - }, - "scripts": { - "postinstall": "node src/install.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "os": [ - "darwin", - "linux", - "win32" - ], - "cpu": [ - "x64", - "arm64" - ], + "private": true, "description": "CLI toolchain for Fedify and debugging ActivityPub", "keywords": [ "fedify", @@ -51,5 +27,25 @@ "type": "git", "url": "git+https://github.com/fedify-dev/fedify.git", "directory": "packages/cli" + }, + "type": "module", + "engines": { + "node": ">=20.0.0", + "bun": ">=1.2.0", + "denp": ">=2.0.0" + }, + "dependencies": { + "@fedify/fedify": "workspace:", + "@optique/core": "^0.3.0", + "@optique/run": "^0.3.0", + "cli-highlight": "^2.1.11", + "jimp": "^1.6.0", + "@jimp/core": "^1.6.0", + "@jimp/wasm-webp": "^1.6.0", + "ora": "^8.2.0", + "@cross/dir": "jsr:@cross/dir@^1.1.0", + "@hongminhee/localtunnel": "^0.3.0", + "@std/fmt/colors": "jsr:@std/fmt@^0.224.0/colors", + "@david/dax": "jsr:@david/dax@^0.43.2" } } diff --git a/packages/cli/src/globals.ts b/packages/cli/src/globals.ts new file mode 100644 index 000000000..33bfd5183 --- /dev/null +++ b/packages/cli/src/globals.ts @@ -0,0 +1,43 @@ +import { message, object, option } from "@optique/core"; +import { configure, getConsoleSink } from "@logtape/logtape"; +import { getFileSink } from "@logtape/file"; +import { recordingSink } from "./log.ts"; +import { AsyncLocalStorage } from "node:async_hooks"; +import process from "node:process"; + +export const debugOption = object("Global options", { + debug: option("-d", "--debug", { + description: message`Enable debug mode.`, + }), +}); + +export async function configureLogging() { + const logFile = process.env["FEDIFY_LOG_FILE"]; + await configure({ + sinks: { + console: getConsoleSink(), + recording: recordingSink, + file: logFile == null ? () => undefined : getFileSink(logFile), + }, + filters: {}, + loggers: [ + { + category: "fedify", + lowestLevel: "debug", + sinks: ["console", "recording", "file"], + }, + { + category: "localtunnel", + lowestLevel: "debug", + sinks: ["console", "file"], + }, + { + category: ["logtape", "meta"], + lowestLevel: "warning", + sinks: ["console", "file"], + }, + ], + reset: true, + contextLocalStorage: new AsyncLocalStorage(), + }); +} diff --git a/packages/cli/src/inbox.test.ts b/packages/cli/src/inbox.test.ts deleted file mode 100644 index 6a2bc2a06..000000000 --- a/packages/cli/src/inbox.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { type InboxOptions, TunnelConfig } from "./inbox.tsx"; - -Deno.test("handles --no-tunnel flag correctly", () => { - const optionsWithNoTunnel: InboxOptions = { - tunnel: false, - follow: undefined, - acceptFollow: undefined, - }; - assertEquals( - TunnelConfig.shouldDisableTunnel(optionsWithNoTunnel), - true, - "--no-tunnel flag should disable tunnel", - ); -}); - -Deno.test("set noTunnel true, should handles -T flag correctly", () => { - const optionsWithT: InboxOptions = { - tunnel: true, - noTunnel: true, - follow: undefined, - acceptFollow: undefined, - }; - assertEquals( - TunnelConfig.shouldDisableTunnel(optionsWithT), - true, - "-T flag should disable tunnel", - ); -}); - -Deno.test("handles default behavior (no flags)", () => { - const optionsDefault: InboxOptions = { - tunnel: true, - follow: undefined, - acceptFollow: undefined, - }; - assertEquals( - TunnelConfig.shouldDisableTunnel(optionsDefault), - false, - "Default behavior should enable tunnel", - ); -}); - -Deno.test("handles both flags together", () => { - const optionsBothFlags: InboxOptions = { - tunnel: false, - noTunnel: true, - follow: undefined, - acceptFollow: undefined, - }; - assertEquals( - TunnelConfig.shouldDisableTunnel(optionsBothFlags), - true, - "Both flags should disable tunnel", - ); -}); - -Deno.test("Various InboxOptions combinations", () => { - const validOptions: InboxOptions[] = [ - { tunnel: true }, - { tunnel: false }, - { tunnel: true, noTunnel: true }, - { tunnel: false, noTunnel: false }, - { tunnel: true, follow: ["@user@example.com"] }, - { tunnel: true, acceptFollow: ["*"] }, - { tunnel: true, follow: [], acceptFollow: [] }, - ]; - - for (const options of validOptions) { - const result = TunnelConfig.shouldDisableTunnel(options); - assertEquals( - typeof result, - "boolean", - "shouldDisableTunnel must return boolean", - ); - } -}); diff --git a/packages/cli/src/inbox.ts b/packages/cli/src/inbox.ts new file mode 100644 index 000000000..e58a21071 --- /dev/null +++ b/packages/cli/src/inbox.ts @@ -0,0 +1,29 @@ +import { + command, + constant, + type InferValue, + merge, + message, + object, +} from "@optique/core"; +import { debugOption } from "./globals.ts"; + +export const inboxCommand = command( + "inbox", + merge( + object({ + command: constant("inbox"), + }), + debugOption, + ), + { + description: + message`Spins up an ephemeral server that serves the ActivityPub inbox with an one-time actor, through a short-lived public DNS with HTTPS. You can monitor the incoming activities in real-time.`, + }, +); + +export function runInbox( + command: InferValue, +) { + console.debug(command); +} diff --git a/packages/cli/src/inbox.tsx b/packages/cli/src/inbox.tsx deleted file mode 100644 index e3b7fbbba..000000000 --- a/packages/cli/src/inbox.tsx +++ /dev/null @@ -1,521 +0,0 @@ -/** @jsx react-jsx */ -/** @jsxImportSource hono/jsx */ -import { Command } from "@cliffy/command"; -import { Cell, Table } from "@cliffy/table"; -import { - Accept, - Activity, - type Actor, - Application, - type Context, - createFederation, - Delete, - Endpoints, - Follow, - generateCryptoKeyPair, - getActorHandle, - Image, - isActor, - lookupObject, - MemoryKvStore, - PUBLIC_COLLECTION, - type Recipient, -} from "@fedify/fedify"; -import { getLogger } from "@logtape/logtape"; -import * as colors from "@std/fmt/colors"; -import { parse } from "@std/semver"; -import { type Context as HonoContext, Hono } from "hono"; -import type { BlankEnv, BlankInput } from "hono/types"; -import ora from "ora"; -import metadata from "../deno.json" with { type: "json" }; -import { getDocumentLoader } from "./docloader.ts"; -import type { ActivityEntry } from "./inbox/entry.ts"; -import { ActivityEntryPage, ActivityListPage } from "./inbox/view.tsx"; -import { recordingSink } from "./log.ts"; -import { tableStyle } from "./table.ts"; -import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts"; - -/** - * Context data for the ephemeral ActivityPub inbox server. - * - * This interface defines the shape of context data passed to federation - * handlers during inbox command execution. - */ -interface ContextData { - activityIndex: number; - actorName: string; - actorSummary: string; -} - -/** - * Options for actor customization. - */ -interface ActorOptions { - actorName: string; - actorSummary: string; -} - -/** - * Options for the inbox command. - */ -export interface InboxOptions { - follow?: string[]; - acceptFollow?: string[]; - tunnel: boolean; - noTunnel?: boolean; // for -T shorthand support -} - -export const TunnelConfig = { - shouldDisableTunnel: (opts: InboxOptions): boolean => { - return opts.tunnel === false || opts.noTunnel === true; - }, -} as const; - -const logger = getLogger(["fedify", "cli", "inbox"]); - -export const command = new Command() - .description( - "Spins up an ephemeral server that serves the ActivityPub inbox with " + - "an one-time actor, through a short-lived public DNS with HTTPS. " + - "You can monitor the incoming activities in real-time.", - ) - .option( - "-f, --follow=", - "Follow the given actor. The argument can be either an actor URI or " + - "a handle. Can be specified multiple times.", - { collect: true }, - ) - .option( - "-a, --accept-follow=", - "Accept follow requests from the given actor. The argument can be " + - "either an actor URI or a handle, or a wildcard (*). Can be " + - "specified multiple times. If a wildcard is specified, all follow " + - "requests will be accepted.", - { collect: true }, - ) - .option( - "-T, --no-tunnel", - "Do not tunnel the ephemeral ActivityPub server to the public Internet.", - ) - .option( - "--actor-name=", - "Customize the actor display name.", - { default: "Fedify Ephemeral Inbox" }, - ) - .option( - "--actor-summary=", - "Customize the actor description.", - { default: "An ephemeral ActivityPub inbox for testing purposes." }, - ) - .action(async (options: InboxOptions & ActorOptions) => { - const fetch = createFetchHandler(options); - const sendDeleteToPeers = createSendDeleteToPeers(options); - - const spinner = ora({ - text: "Spinning up an ephemeral ActivityPub server...", - discardStdin: false, - }).start(); - const server = await spawnTemporaryServer(fetch, { - noTunnel: TunnelConfig.shouldDisableTunnel(options), - }); - spinner.succeed( - `The ephemeral ActivityPub server is up and running: ${ - colors.green( - server.url.href, - ) - }`, - ); - Deno.addSignalListener("SIGINT", () => { - spinner.stop(); - const peersCnt = Object.keys(peers).length; - spinner.start( - `Sending Delete(Application) activities to the ${peersCnt} ${ - peersCnt === 1 ? "peer" : "peers" - }...`, - ); - sendDeleteToPeers(server).then(() => { - spinner.text = "Stopping server..."; - server.close().then(() => { - spinner.succeed("Server stopped."); - Deno.exit(0); - }); - }); - }); - spinner.start(); - - const fedCtx = federation.createContext(server.url, { - activityIndex: -1, - actorName: options.actorName, - actorSummary: options.actorSummary, - }); - - if (options.acceptFollow != null && options.acceptFollow.length > 0) { - acceptFollows.push(...(options.acceptFollow ?? [])); - } - if (options.follow != null && options.follow.length > 0) { - spinner.text = "Following actors..."; - const documentLoader = await fedCtx.getDocumentLoader({ - identifier: "i", - }); - for (const uri of options.follow) { - spinner.text = `Following ${colors.green(uri)}...`; - const actor = await lookupObject(uri, { documentLoader }); - if (!isActor(actor)) { - spinner.fail(`Not an actor: ${colors.red(uri)}`); - spinner.start(); - continue; - } - if (actor.id != null) peers[actor.id?.href] = actor; - await fedCtx.sendActivity( - { identifier: "i" }, - actor, - new Follow({ - id: new URL(`#follows/${actor.id?.href}`, fedCtx.getActorUri("i")), - actor: fedCtx.getActorUri("i"), - object: actor.id, - }), - ); - spinner.succeed(`Sent follow request to ${colors.green(uri)}.`); - spinner.start(); - } - } - spinner.stop(); - printServerInfo(fedCtx); - }); - -const cliDocumentLoader = await getDocumentLoader(); -const federation = createFederation({ - kv: new MemoryKvStore(), - documentLoaderFactory: () => cliDocumentLoader, - contextLoaderFactory: () => cliDocumentLoader, -}); - -const time = Temporal.Now.instant(); -let actorKeyPairs: CryptoKeyPair[] | undefined = undefined; - -federation - .setActorDispatcher("/{identifier}", async (ctx, identifier) => { - if (identifier !== "i") return null; - return new Application({ - id: ctx.getActorUri(identifier), - preferredUsername: identifier, - name: ctx.data.actorName, - summary: ctx.data.actorSummary, - inbox: ctx.getInboxUri(identifier), - endpoints: new Endpoints({ - sharedInbox: ctx.getInboxUri(), - }), - followers: ctx.getFollowersUri(identifier), - following: ctx.getFollowingUri(identifier), - outbox: ctx.getOutboxUri(identifier), - manuallyApprovesFollowers: true, - published: time, - icon: new Image({ - url: new URL("https://fedify.dev/logo.png"), - mediaType: "image/png", - }), - publicKey: (await ctx.getActorKeyPairs(identifier))[0].cryptographicKey, - assertionMethods: (await ctx.getActorKeyPairs(identifier)) - .map((pair) => pair.multikey), - url: ctx.getActorUri(identifier), - }); - }) - .setKeyPairsDispatcher(async (_ctxData, identifier) => { - if (identifier !== "i") return []; - if (actorKeyPairs == null) { - actorKeyPairs = [ - await generateCryptoKeyPair("RSASSA-PKCS1-v1_5"), - await generateCryptoKeyPair("Ed25519"), - ]; - } - return actorKeyPairs; - }); - -const activities: ActivityEntry[] = []; - -const acceptFollows: string[] = []; - -async function acceptsFollowFrom(actor: Actor): Promise { - const actorUri = actor.id; - let actorHandle: string | undefined = undefined; - if (actorUri == null) return false; - for (let uri of acceptFollows) { - if (uri === "*") return true; - if (uri.startsWith("http:") || uri.startsWith("https:")) { - uri = new URL(uri).href; // normalize - if (uri === actorUri.href) return true; - } - if (actorHandle == null) actorHandle = await getActorHandle(actor); - if (actorHandle === uri) return true; - } - return false; -} - -const peers: Record = {}; - -function createSendDeleteToPeers( - actorOptions: ActorOptions, -): (server: TemporaryServer) => Promise { - return async function sendDeleteToPeers( - server: TemporaryServer, - ): Promise { - const ctx = federation.createContext(new Request(server.url), { - activityIndex: -1, - actorName: actorOptions.actorName, - actorSummary: actorOptions.actorSummary, - }); - - const actor = (await ctx.getActor("i"))!; - try { - await ctx.sendActivity( - { identifier: "i" }, - Object.values(peers), - new Delete({ - id: new URL(`#delete`, actor.id!), - actor: actor.id!, - to: PUBLIC_COLLECTION, - object: actor, - }), - ); - } catch (error) { - logger.error( - "Failed to send Delete(Application) activities to peers:\n{error}", - { error }, - ); - } - }; -} - -const followers: Record = {}; - -federation - .setInboxListeners("/{identifier}/inbox", "/inbox") - .setSharedKeyDispatcher((_) => ({ identifier: "i" })) - .on(Activity, async (ctx, activity) => { - activities[ctx.data.activityIndex].activity = activity; - for await (const actor of activity.getActors()) { - if (actor.id != null) peers[actor.id.href] = actor; - } - for await (const actor of activity.getAttributions()) { - if (actor.id != null) peers[actor.id.href] = actor; - } - if (activity instanceof Follow) { - if (acceptFollows.length < 1) return; - const objectId = activity.objectId; - if (objectId == null) return; - const parsed = ctx.parseUri(objectId); - if (parsed?.type !== "actor" || parsed.identifier !== "i") return; - const { identifier } = parsed; - const follower = await activity.getActor(); - if (!isActor(follower)) return; - const accepts = await acceptsFollowFrom(follower); - if (!accepts || activity.id == null) { - logger.debug("Does not accept follow from {actor}.", { - actor: follower.id?.href, - }); - return; - } - logger.debug("Accepting follow from {actor}.", { - actor: follower.id?.href, - }); - followers[activity.id.href] = follower; - await ctx.sendActivity( - { identifier }, - follower, - new Accept({ - id: new URL(`#accepts/${follower.id?.href}`, ctx.getActorUri("i")), - actor: ctx.getActorUri(identifier), - object: activity.id, - }), - ); - } - }); - -federation - .setFollowersDispatcher("/{identifier}/followers", (_ctx, identifier) => { - if (identifier !== "i") return null; - const items: Recipient[] = []; - for (const follower of Object.values(followers)) { - if (follower.id == null) continue; - items.push(follower); - } - return { items }; - }) - .setCounter((_ctx, identifier) => { - if (identifier !== "i") return null; - return Object.keys(followers).length; - }); - -federation - .setFollowingDispatcher( - "/{identifier}/following", - (_ctx, _identifier) => null, - ) - .setCounter((_ctx, _identifier) => 0); - -federation - .setOutboxDispatcher("/{identifier}/outbox", (_ctx, _identifier) => null) - .setCounter((_ctx, _identifier) => 0); - -federation.setNodeInfoDispatcher("/nodeinfo/2.1", (_ctx) => { - return { - software: { - name: "fedify-cli", - version: parse(metadata.version), - repository: new URL("https://github.com/fedify-dev/fedify"), - }, - protocols: ["activitypub"], - usage: { - users: { - total: 1, - activeMonth: 1, - activeHalfyear: 1, - }, - localComments: 0, - localPosts: 0, - }, - }; -}); - -function printServerInfo(fedCtx: Context): void { - new Table( - [ - new Cell("Actor handle:").align("right"), - colors.green(`i@${fedCtx.getActorUri("i").host}`), - ], - [ - new Cell("Actor URI:").align("right"), - colors.green(fedCtx.getActorUri("i").href), - ], - [ - new Cell("Actor inbox:").align("right"), - colors.green(fedCtx.getInboxUri("i").href), - ], - [ - new Cell("Shared inbox:").align("right"), - colors.green(fedCtx.getInboxUri().href), - ], - ) - .chars(tableStyle) - .border() - .render(); -} - -async function printActivityEntry( - idx: number, - entry: ActivityEntry, -): Promise { - const request = entry.request.clone(); - const response = entry.response?.clone(); - const url = new URL(request.url); - const activity = entry.activity; - const object = await activity?.getObject(); - new Table( - [new Cell("Request #:").align("right"), colors.bold(idx.toString())], - [ - new Cell("Activity type:").align("right"), - activity == null ? colors.red("failed to parse") : colors.green( - `${activity.constructor.name}(${object?.constructor.name})`, - ), - ], - [ - new Cell("HTTP request:").align("right"), - `${ - request.method === "POST" - ? colors.green("POST") - : colors.red(request.method) - } ${url.pathname + url.search}`, - ], - ...(response == null ? [] : [ - [ - new Cell("HTTP response:").align("right"), - `${ - response.ok - ? colors.green(response.status.toString()) - : colors.red(response.status.toString()) - } ${response.statusText}`, - ], - ]), - [new Cell("Details").align("right"), new URL(`/r/${idx}`, url).href], - ) - .chars(tableStyle) - .border() - .render(); -} - -function getHandle( - c: HonoContext, -): string { - const url = new URL(c.req.url); - return `@i@${url.host}`; -} - -const app = new Hono(); - -app.get("/", (c) => c.redirect("/r")); - -app.get( - "/r", - (c) => - c.html( - , - ), -); - -app.get("/r/:idx{[0-9]+}", (c) => { - const idx = parseInt(c.req.param("idx")); - const tab = c.req.query("tab") ?? "request"; - const activity = activities[idx]; - if (activity == null) return c.notFound(); - if ( - tab !== "request" && tab !== "response" && tab !== "raw-activity" && - tab !== "compact-activity" && tab !== "expanded-activity" && tab !== "logs" - ) { - return c.notFound(); - } - return c.html( - , - ); -}); - -function createFetchHandler( - actorOptions: ActorOptions, -): (request: Request) => Promise { - return async function fetch(request: Request): Promise { - const timestamp = Temporal.Now.instant(); - const idx = activities.length; - const pathname = new URL(request.url).pathname; - if (pathname === "/r" || pathname.startsWith("/r/")) { - return app.fetch(request); - } - const inboxRequest = pathname === "/inbox" || - pathname.startsWith("/i/inbox"); - if (inboxRequest) { - recordingSink.startRecording(); - // @ts-ignore: Work around `deno publish --dry-run` bug - activities.push({ timestamp, request: request.clone(), logs: [] }); - } - const response = await federation.fetch(request, { - contextData: { - activityIndex: inboxRequest ? idx : -1, - actorName: actorOptions.actorName, - actorSummary: actorOptions.actorSummary, - }, - onNotAcceptable: app.fetch.bind(app), - onNotFound: app.fetch.bind(app), - onUnauthorized: app.fetch.bind(app), - }); - if (inboxRequest) { - recordingSink.stopRecording(); - activities[idx].response = response.clone(); - activities[idx].logs = recordingSink.getRecords(); - await printActivityEntry(idx, activities[idx]); - } - return response; - }; -} diff --git a/packages/cli/src/inbox/entry.ts b/packages/cli/src/inbox/entry.ts deleted file mode 100644 index 466891926..000000000 --- a/packages/cli/src/inbox/entry.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Activity } from "@fedify/fedify"; -import type { LogRecord } from "@logtape/logtape"; - -export interface ActivityEntry { - timestamp: Temporal.Instant; - request: Request; - response?: Response; - activity?: Activity; - logs: LogRecord[]; -} diff --git a/packages/cli/src/inbox/rendercode.ts b/packages/cli/src/inbox/rendercode.ts deleted file mode 100644 index 09146529c..000000000 --- a/packages/cli/src/inbox/rendercode.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Activity } from "@fedify/fedify"; -import { getStatusText } from "@poppanator/http-constants"; -import { getContextLoader } from "../docloader.ts"; - -export async function renderRequest(request: Request): Promise { - // @ts-ignore: Work around `deno publish --dry-run` bug - request = request.clone(); - const url = new URL(request.url); - let code = `${request.method} ${url.pathname + url.search}\n`; - for (const [key, value] of request.headers.entries()) { - code += `${capitalize(key)}: ${value}\n`; - } - let body: string; - try { - body = await request.text(); - } catch (_) { - body = "[Failed to decode body; it may be binary.]"; - } - code += `\n${body}`; - return code; -} - -export async function renderResponse(response: Response): Promise { - response = response.clone(); - let code = `${response.status} ${ - response.statusText === "" - ? getStatusText(response.status) - : response.statusText - }\n`; - for (const [key, value] of response.headers.entries()) { - code += `${capitalize(key)}: ${value}\n`; - } - let body: string; - try { - body = await response.text(); - } catch (_) { - body = "[Failed to decode body; it may be binary.]"; - } - code += `\n${body}`; - return code; -} - -export async function renderRawActivity(request: Request): Promise { - // @ts-ignore: Work around `deno publish --dry-run` bug - request = request.clone(); - try { - const activity = await request.json(); - return JSON.stringify(activity, null, 2); - } catch { - return "[Failed to decode body; it may not be JSON.]"; - } -} - -export async function renderActivity( - activity: Activity, - expand: boolean = false, -): Promise { - const contextLoader = await getContextLoader(); - const jsonLd = await activity.toJsonLd({ - contextLoader, - format: expand ? "expand" : "compact", - }); - return JSON.stringify(jsonLd, null, 2); -} - -function capitalize(name: string): string { - return name.replace(/(^|-)./g, (match) => match.toUpperCase()); -} diff --git a/packages/cli/src/inbox/view.tsx b/packages/cli/src/inbox/view.tsx deleted file mode 100644 index 365ec65b9..000000000 --- a/packages/cli/src/inbox/view.tsx +++ /dev/null @@ -1,597 +0,0 @@ -/** @jsx react-jsx */ -/** @jsxImportSource hono/jsx */ -import type { LogRecord } from "@logtape/logtape"; -import { getStatusText } from "@poppanator/http-constants"; -import { type FC, Fragment, type PropsWithChildren } from "hono/jsx"; -import { getSingletonHighlighter } from "shiki"; -import type { ActivityEntry } from "./entry.ts"; -import { - renderActivity, - renderRawActivity, - renderRequest, - renderResponse, -} from "./rendercode.ts"; - -interface LayoutProps { - title?: string; - handle: string; -} - -const Layout: FC> = ( - props: PropsWithChildren, -) => { - return ( - - - - - - {props.title == null - ? "" - // deno-lint-ignore jsx-curly-braces - : <Fragment>{props.title} —{" "}</Fragment>}Fedify Ephemeral - Inbox ({props.handle}) - - - - -
- - - - - - - Fedify - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Fedify Ephemeral Inbox

-

- {props.handle} -

-
-
- {props.children} -
- - - ); -}; - -interface TabProps { - active?: boolean; - disabled?: boolean; - label: string; - badge?: string | number; - href: string; -} - -const Tab: FC = ( - { active, disabled, label, badge, href }: TabProps, -) => { - return ( - - ); -}; - -// deno-lint-ignore no-empty-interface -interface TabListProps { -} - -const TabList: FC> = ( - { children }: PropsWithChildren, -) => { - return ( - - ); -}; - -interface CodeBlockProps { - language: string; - code: string; -} - -const highlighter = await getSingletonHighlighter(); -await highlighter.loadTheme("github-light"); -await highlighter.loadLanguage("http"); -await highlighter.loadLanguage("json"); - -const CodeBlock: FC = ({ language, code }: CodeBlockProps) => { - const result = highlighter.codeToHtml(code, { - lang: language, - theme: "github-light", - }); - return
; -}; - -interface LogProps { - log: LogRecord; -} - -const Log: FC = ( - { log: { timestamp, category, level, message } }: LogProps, -) => { - const listClass = level === "debug" - ? "list-group-item-light" - : level === "info" - ? "" - : level === "warning" - ? "list-group-item-warning" - : "list-group-item-danger"; - const time = Temporal.Instant.fromEpochMilliseconds(timestamp); - return ( -
  • -
    -

    - {message.map((m, i) => - i % 2 == 0 ? m : ( - - {typeof m === "string" ? m : Deno.inspect(m)} - - ) - )} -

    - -
    - - {category.map((c, i) => - // deno-lint-ignore jsx-curly-braces - i < 1 ? c : {" "}/ {c} - )} - -
  • - ); -}; - -interface LogListProps { - logs: LogRecord[]; -} - -const LogList: FC = ({ logs }: LogListProps) => { - return ( -
      - {logs.map((log) => )} -
    - ); -}; - -type ActivityEntryTabPage = - | "request" - | "response" - | "raw-activity" - | "compact-activity" - | "expanded-activity" - | "logs"; - -interface ActivityEntryViewProps { - entry: ActivityEntry; - tabPage: ActivityEntryTabPage; -} - -const ActivityEntryView: FC = async ( - { tabPage, entry: { activity, request, response, logs } }: - ActivityEntryViewProps, -) => { - return ( -
    - - - - - - - - - {tabPage === "request" && ( -
    - -
    - )} - {tabPage === "response" && response != null && ( -
    - -
    - )} - {tabPage === "raw-activity" && ( -
    - -
    - )} - {tabPage === "compact-activity" && activity != null && ( -
    - -
    - )} - {tabPage === "expanded-activity" && activity != null && ( -
    - -
    - )} - {tabPage === "logs" && ( -
    - -
    - )} -
    - ); -}; - -export interface ActivityEntryPageProps extends ActivityEntryViewProps { - handle: string; - idx: number; -} - -export const ActivityEntryPage: FC = ( - { handle, idx, entry, tabPage }: ActivityEntryPageProps, -) => { - return ( - - - - - - ); -}; - -export interface ActivityListProps { - entries: ActivityEntry[]; -} - -const ActivityList: FC = ( - { entries }: ActivityListProps, -) => { - return ( -
    - {entries.map((entry, i) => { - const failed = entry.activity == null || entry.response == null || - !entry.response.ok || entry.request.method !== "POST"; - const itemClass = failed ? "list-group-item-danger" : ""; - const url = new URL(entry.request.url); - return ( - - - Request #{i}:{" "} - {entry.request.method} {url.pathname + url.search} - {entry.activity == null ? "" : ( - - {} · {entry.activity.constructor.name} - - )} - {entry.response == null ? "" : ( - - {} →{" "} - - {entry.response.status} {entry.response.statusText === "" - ? getStatusText(entry.response.status) - : entry.response.statusText} - - - )} - - - - ); - }).reverse()} -
    - ); -}; - -export interface ActivityListPageProps extends ActivityListProps { - handle: string; -} - -export const ActivityListPage: FC = ( - { handle, entries }: ActivityListPageProps, -) => { - return ( - - - - - - ); -}; diff --git a/packages/cli/src/init.test.ts b/packages/cli/src/init.test.ts deleted file mode 100644 index b1bfe63f0..000000000 --- a/packages/cli/src/init.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { assertEquals, assertStringIncludes } from "@std/assert"; -import { join } from "@std/path"; -import { exists } from "@std/fs"; - -const CLI_PATH = join(import.meta.dirname!, "mod.ts"); - -async function runInit( - args: string[], -): Promise<{ output: string; success: boolean }> { - const cmd = new Deno.Command("deno", { - args: ["run", "-A", CLI_PATH, "init", ...args], - stdout: "piped", - stderr: "piped", - stdin: "null", - }); - - const process = cmd.spawn(); - const output = await process.output(); - const decoder = new TextDecoder(); - const stdout = decoder.decode(output.stdout); - const stderr = decoder.decode(output.stderr); - - return { - output: stdout + stderr, - success: output.success, - }; -} - -Deno.test("init --dry-run shows preview without creating files", async () => { - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-project"); - - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "deno", - ]); - - // Check that dry-run mode is indicated - assertStringIncludes(result.output, "🔍 DRY RUN MODE"); - assertStringIncludes(result.output, "Would create files:"); - assertStringIncludes(result.output, "Would install dependencies:"); - - assertStringIncludes(result.output, "federation.ts"); - assertStringIncludes(result.output, "logging.ts"); - assertStringIncludes(result.output, "main.ts"); - assertStringIncludes(result.output, "deno.json"); - assertStringIncludes(result.output, ".env"); - - // Verify no files were actually created - assertEquals( - await exists(projectDir), - false, - "Project directory should not be created", - ); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init --dry-run with web framework shows correct files", async () => { - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-hono-project"); - - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "deno", - "--web-framework", - "hono", - ]); - - // Check Hono-specific files - assertStringIncludes(result.output, "src/federation.ts"); - assertStringIncludes(result.output, "src/app.tsx"); - assertStringIncludes(result.output, "src/index.ts"); - assertStringIncludes(result.output, "@hono/hono"); - - // Verify no files were created - assertEquals(await exists(projectDir), false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init --dry-run with external stores shows dependencies", async () => { - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-redis-project"); - - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "deno", - "--kv-store", - "redis", - "--message-queue", - "redis", - ]); - - // Check Redis dependencies - assertStringIncludes(result.output, "@fedify/redis"); - assertStringIncludes(result.output, "ioredis"); - assertStringIncludes(result.output, "REDIS_URL"); - - // Check Redis imports in federation.ts - assertStringIncludes(result.output, "RedisKvStore"); - assertStringIncludes(result.output, "RedisMessageQueue"); - - // Verify no files were created - assertEquals(await exists(projectDir), false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init --dry-run shows command for framework initialization", async () => { - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-nitro-project"); - - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "node", - "--package-manager", - "npm", - "--web-framework", - "nitro", - ]); - - // Check that initialization command is shown - assertStringIncludes(result.output, "Would run command:"); - assertStringIncludes(result.output, "giget@latest nitro"); - - // Check Node.js specific files - assertStringIncludes(result.output, "package.json"); - assertStringIncludes(result.output, "biome.json"); - - // Verify no files were created - assertEquals(await exists(projectDir), false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init --dry-run fails on non-empty directory", async () => { - const testDir = await Deno.makeTempDir(); - - try { - // Create a file in the directory - await Deno.writeTextFile(join(testDir, "existing.txt"), "content"); - - const result = await runInit([ - testDir, - "--dry-run", - "--runtime", - "deno", - ]); - - assertStringIncludes(result.output, "The directory is not empty"); - assertEquals(result.success, false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init --dry-run shows prepend files for Fresh", async () => { - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-fresh-project"); - - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "deno", - "--web-framework", - "fresh", - ]); - - // Check that prepend files are shown - assertStringIncludes(result.output, "Would prepend to files:"); - assertStringIncludes(result.output, "fresh.config.ts"); - - // Verify no files were created - assertEquals(await exists(projectDir), false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init --dry-run shows dev dependencies for Node.js", async () => { - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-node-project"); - - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "node", - "--package-manager", - "npm", - ]); - - // Check dev dependencies - assertStringIncludes(result.output, "Would install dev dependencies:"); - assertStringIncludes(result.output, "@biomejs/biome"); - - // Verify no files were created - assertEquals(await exists(projectDir), false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init - check version for AMQP package", async () => { - const amqpData = await Deno.readTextFile( - join(import.meta.dirname!, "../../amqp/deno.json"), - ); - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-amqp-project"); - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "deno", - "--message-queue", - "amqp", - ]); - - assertStringIncludes( - result.output, - `@fedify/amqp@${JSON.parse(amqpData).version.trim()}`, - ); - assertEquals(await exists(projectDir), false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init - check version for Redis package", async () => { - const redisData = await Deno.readTextFile( - join(import.meta.dirname!, "../../redis/deno.json"), - ); - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-redis-project"); - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "deno", - "--kv-store", - "redis", - ]); - - assertStringIncludes( - result.output, - `@fedify/redis@${JSON.parse(redisData).version.trim()}`, - ); - assertEquals(await exists(projectDir), false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); - -Deno.test("init - check version for Postgres package", async () => { - const postgresData = await Deno.readTextFile( - join(import.meta.dirname!, "../../postgres/deno.json"), - ); - const testDir = await Deno.makeTempDir(); - const projectDir = join(testDir, "test-postgres-project"); - try { - const result = await runInit([ - projectDir, - "--dry-run", - "--runtime", - "deno", - "--kv-store", - "postgres", - ]); - - assertStringIncludes( - result.output, - `@fedify/postgres@${JSON.parse(postgresData).version.trim()}`, - ); - assertEquals(await exists(projectDir), false); - } finally { - await Deno.remove(testDir, { recursive: true }); - } -}); diff --git a/packages/cli/src/init.ts b/packages/cli/src/init.ts index 1aafbb42b..1b93e1c73 100644 --- a/packages/cli/src/init.ts +++ b/packages/cli/src/init.ts @@ -1,1636 +1,31 @@ -import { Command, EnumType } from "@cliffy/command"; -import { Select } from "@cliffy/prompt"; -import { getLogger } from "@logtape/logtape"; -import { stringify } from "@std/dotenv/stringify"; -import * as colors from "@std/fmt/colors"; -import { exists } from "@std/fs"; -import { basename, dirname, join, normalize } from "@std/path"; -import metadata from "../deno.json" with { type: "json" }; - -const packagesMetaData: Record<`@fedify/${string}`, string> = { - "@fedify/fedify": metadata.version, - "@fedify/redis": metadata.version, - "@fedify/postgres": metadata.version, - "@fedify/amqp": metadata.version, - "@fedify/express": metadata.version, - "@fedify/h3": metadata.version, - "@fedify/next": metadata.version, -}; - -const logger = getLogger(["fedify", "cli", "init"]); - -type Runtime = "deno" | "bun" | "node"; - -interface RuntimeDescription { - label: string; - checkCommand: [string, ...string[]]; - outputPattern: RegExp; -} - -const runtimes: Record = { - deno: { - label: "Deno", - checkCommand: ["deno", "--version"], - outputPattern: /^deno\s+\d+\.\d+\.\d+\b/, - }, - bun: { - label: "Bun", - checkCommand: ["bun", "--version"], - outputPattern: /^\d+\.\d+\.\d+$/, - }, - node: { - label: "Node.js", - checkCommand: ["node", "--version"], - outputPattern: /^v\d+\.\d+\.\d+$/, - }, -}; - -const runtimeAvailabilities: Record = Object.fromEntries( - await Promise.all( - (Object.keys(runtimes) as Runtime[]) - .map(async (r) => [r, await isRuntimeAvailable(r)]), - ), -); - -type PackageManager = "npm" | "yarn" | "pnpm"; - -interface PackageManagerDescription { - label: string; - checkCommand: [string, ...string[]]; - outputPattern: RegExp; -} - -const packageManagers: Record = { - npm: { - label: "npm", - checkCommand: ["npm", "--version"], - outputPattern: /^\d+\.\d+\.\d+$/, - }, - yarn: { - label: "Yarn", - checkCommand: ["yarn", "--version"], - outputPattern: /^\d+\.\d+\.\d+$/, - }, - pnpm: { - label: "pnpm", - checkCommand: ["pnpm", "--version"], - outputPattern: /^\d+\.\d+\.\d+$/, - }, -}; - -const packageManagerLocations: Record = - Object.fromEntries( - await Promise.all( - (Object.keys(packageManagers) as PackageManager[]) - .map(async (pm) => [pm, await locatePackageManager(pm)]), - ), - ); - -type WebFramework = "fresh" | "hono" | "express" | "nitro" | "next"; - -interface WebFrameworkInitializer { - command?: [string, ...string[]] | [...string[], string]; - dependencies?: Record; - devDependencies?: Record; - federationFile: string; - loggingFile: string; - files?: Record; - prependFiles?: Record; - compilerOptions?: Record; - tasks?: Record; - instruction: string; -} - -interface WebFrameworkDescription { - label: string; - runtimes: Runtime[] | null; - init( - projectName: string, - runtime: Runtime, - pm: PackageManager, - ): WebFrameworkInitializer; -} - -const webFrameworks: Record = { - fresh: { - label: "Fresh", - runtimes: ["deno"], - init: (_, __, ___) => ({ - command: [ - "deno", - "run", - "-A", - "https://deno.land/x/fresh@1.6.8/init.ts", - ".", - ], - dependencies: { "@hongminhee/x-forwarded-fetch": "^0.2.0" }, - federationFile: "federation/mod.ts", - loggingFile: "logging.ts", - files: { - "routes/_middleware.ts": `\ -import { Handler } from "$fresh/server.ts"; -import federation from "../federation/mod.ts"; -import { integrateHandler } from "@fedify/fedify/x/fresh"; - -// This is the entry point to the Fedify middleware from the Fresh framework: -export const handler: Handler = integrateHandler(federation, () => undefined); -`, - "dev.ts": `\ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -import "$std/dotenv/load.ts"; - -await dev(import.meta.url, "./main.ts"); -`, - "main.ts": `\ -/// -/// -/// -/// -/// - -import "$std/dotenv/load.ts"; - -import { ServerContext } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; -import { behindProxy } from "@hongminhee/x-forwarded-fetch"; - -const ctx = await ServerContext.fromManifest(manifest, { - ...config, - dev: false, -}); -const handler = behindProxy(ctx.handler()); - -Deno.serve({ handler, ...config.server }); -`, - }, - prependFiles: { - "fresh.config.ts": 'import "./logging.ts";\n', - }, - instruction: ` -To start the server, run the following command: - - ${colors.bold(colors.green("deno task start"))} - -Then, try look up an actor from your server: - - ${colors.bold(colors.green("fedify lookup http://localhost:8000/users/john"))} -`, - }), - }, - hono: { - label: "Hono", - runtimes: null, - init: (projectName, runtime, pm) => ({ - dependencies: runtime === "deno" - ? { - "@std/dotenv": "^0.225.2", - "@hono/hono": "^4.5.0", - "@hongminhee/x-forwarded-fetch": "^0.2.0", - } as Record - : runtime === "node" - ? { - "@dotenvx/dotenvx": "^1.14.1", - hono: "^4.5.0", - "@hono/node-server": "^1.12.0", - tsx: "^4.17.0", - "x-forwarded-fetch": "^0.2.0", - } - : { hono: "^4.5.0", "x-forwarded-fetch": "^0.2.0" }, - devDependencies: runtime === "bun" - ? { "@types/bun": "^1.1.6" } as Record - : {}, - federationFile: "src/federation.ts", - loggingFile: "src/logging.ts", - files: { - "src/app.tsx": `\ -import { Hono } from "${runtime === "deno" ? "@hono/hono" : "hono"}"; -import { federation } from "@fedify/fedify/x/hono"; -import { getLogger } from "@logtape/logtape"; -import fedi from "./federation.ts"; - -const logger = getLogger(${JSON.stringify(projectName)}); - -const app = new Hono(); -app.use(federation(fedi, () => undefined)) - -app.get("/", (c) => c.text("Hello, Fedify!")); - -export default app; -`, - "src/index.ts": runtime === "node" - ? `\ -import { serve } from "@hono/node-server"; -import { behindProxy } from "x-forwarded-fetch"; -import app from "./app.tsx"; -import "./logging.ts"; - -serve( - { - port: 8000, - fetch: behindProxy(app.fetch.bind(app)), - }, - (info) => - console.log("Server started at http://" + info.address + ":" + info.port) -); -` - : runtime === "bun" - ? `\ -import { behindProxy } from "x-forwarded-fetch"; -import app from "./app.tsx"; -import "./logging.ts"; - -const server = Bun.serve({ - port: 8000, - fetch: behindProxy(app.fetch.bind(app)), -}); - -console.log("Server started at", server.url.href); -` - : `\ -import "@std/dotenv/load"; -import { behindProxy } from "@hongminhee/x-forwarded-fetch"; -import app from "./app.tsx"; -import "./logging.ts"; - -Deno.serve( - { - port: 8000, - onListen: ({ port, hostname }) => - console.log("Server started at http://" + hostname + ":" + port) - }, - behindProxy(app.fetch.bind(app)), -); -`, - }, - compilerOptions: runtime === "deno" ? undefined : { - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "strict": true, - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", - }, - tasks: { - "dev": runtime === "deno" - ? "deno run -A --watch ./src/index.ts" - : runtime === "bun" - ? "bun run --hot ./src/index.ts" - : "dotenvx run -- tsx watch ./src/index.ts", - "prod": runtime === "deno" - ? "deno run -A ./src/index.ts" - : runtime === "bun" - ? "bun run ./src/index.ts" - : "dotenvx run -- node --import tsx ./src/index.ts", - }, - instruction: ` -To start the server, run the following command: - - ${ - colors.bold(colors.green( - runtime === "deno" - ? "deno task dev" - : runtime === "bun" - ? "bun dev" - : `${pm} run dev`, - )) - } - -Then, try look up an actor from your server: - - ${colors.bold(colors.green("fedify lookup http://localhost:8000/users/john"))} -`, - }), - }, - express: { - label: "Express", - runtimes: ["bun", "node"], - init: (projectName, runtime, pm) => ({ - dependencies: { - express: "^4.19.2", - "@fedify/express": getLatestVersion("@fedify/express"), - ...(runtime === "node" - ? { - "@dotenvx/dotenvx": "^1.14.1", - tsx: "^4.17.0", - } - : {}), - }, - devDependencies: { - "@types/express": "^4.17.21", - ...(runtime === "bun" ? { "@types/bun": "^1.1.6" } : {}), - }, - federationFile: "src/federation.ts", - loggingFile: "src/logging.ts", - files: { - "src/app.ts": `\ -import express from "express"; -import { integrateFederation } from "@fedify/express"; -import { getLogger } from "@logtape/logtape"; -import federation from "./federation.ts"; - -const logger = getLogger(${JSON.stringify(projectName)}); - -export const app = express(); - -app.set("trust proxy", true); - -app.use(integrateFederation(federation, (req) => undefined)); - -app.get("/", (req, res) => res.send("Hello, Fedify!")); - -export default app; -`, - "src/index.ts": `\ -import app from "./app.ts"; -import "./logging.ts"; - -app.listen(8000, () => { - console.log("Server started at http://localhost:8000"); -}); -`, - }, - compilerOptions: runtime === "deno" ? undefined : { - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "strict": true, - }, - tasks: { - "dev": runtime === "bun" - ? "bun run --hot ./src/index.ts" - : "dotenvx run -- tsx watch ./src/index.ts", - "prod": runtime === "bun" - ? "bun run ./src/index.ts" - : "dotenvx run -- node --import tsx ./src/index.ts", - }, - instruction: ` -To start the server, run the following command: - - ${colors.bold(colors.green(runtime === "bun" ? "bun dev" : `${pm} run dev`))} - -Then, try look up an actor from your server: - - ${colors.bold(colors.green("fedify lookup http://localhost:8000/users/john"))} -`, +import { + argument, + command, + constant, + type InferValue, + merge, + message, + object, +} from "@optique/core"; +import { path } from "@optique/run"; +import { debugOption } from "./globals.ts"; + +export const initCommand = command( + "init", + merge( + debugOption, + object({ + command: constant("init"), + resources: argument(path({ metavar: "DIR" })), }), - }, - nitro: { - label: "Nitro", - runtimes: ["bun", "node"], - init: (_, runtime, pm) => ({ - command: [ - ...(runtime === "bun" - ? ["bunx"] - : pm === "npm" || pm === "yarn" - ? ["npx", "--yes"] - : [pm, "dlx"]), - "giget@latest", - "nitro", - ".", - ], - dependencies: { - "@fedify/h3": getLatestVersion("@fedify/h3"), - }, - federationFile: "server/federation.ts", - loggingFile: "server/logging.ts", - files: { - "server/middleware/federation.ts": `\ -import { integrateFederation } from "@fedify/h3"; -import federation from "../federation" - -export default integrateFederation( - federation, - (event, request) => undefined, -); -`, - "server/error.ts": `\ -import { onError } from "@fedify/h3"; - -export default onError; -`, - "nitro.config.ts": `\ -//https://nitro.unjs.io/config -export default defineNitroConfig({ - srcDir: "server", - errorHandler: "~/error" -}); -`, - }, - instruction: ` -To start the server, run the following command: - - ${colors.bold(colors.green(runtime === "bun" ? "bun dev" : `${pm} run dev`))} - -Then, try look up an actor from your server: - - ${colors.bold(colors.green("fedify lookup http://localhost:3000/users/john"))} -`, - }), - }, - next: { - label: "Next.js", - runtimes: ["node"], - init: (_, __, packageManager) => ({ - label: "Next.js", - runtimes: ["node"], - command: [ - ...(packageManager === "npm" ? ["npx"] : [packageManager, "dlx"]), - "create-next-app@canary", - ".", - "--ts", - "--tailwind", - "--eslint", - "--app", - "--turbopack", - "--skip-install", - ], - dependencies: { - "@fedify/next": getLatestVersion("@fedify/next"), - }, - devDependencies: { - "@types/node": "^20.11.2", - }, - federationFile: "federation/index.ts", - loggingFile: "logging.ts", - files: { - "middleware.ts": ` -import { fedifyWith } from "@fedify/next"; -import federation from "./federation"; - -export default fedifyWith(federation)( -/* - function (request: Request) { - // If you need to handle other requests besides federation - // requests in middleware, you can do it here. - // If you handle only federation requests in middleware, - // you don't need this function. - return NextResponse.next(); - }, -*/ -) - -// This config needs because middleware process only requests with the -// "Accept" header matching the federation accept regex. -// More details: https://nextjs.org/docs/app/api-reference/file-conventions/middleware#config-object-optional -export const config = { - runtime: "nodejs", - matcher: [ - { - source: "/:path*", - has: [ - { - type: "header", - key: "Accept", - value: ".*application\\/((jrd|activity|ld)\\+json|xrd\\+xml).*", - }, - ], - }, - { - source: "/:path*", - has: [ - { - type: "header", - key: "content-type", - value: ".*application\\/((jrd|activity|ld)\\+json|xrd\\+xml).*", - }, - ], - }, - { source: "/.well-known/nodeinfo" }, - { source: "/.well-known/x-nodeinfo2" }, - ], -}; -`, - }, - instruction: ` -To start the server, run the following command: - - ${colors.bold(colors.green(packageManager + " run dev"))} -Then, try look up an actor from your server: - ${colors.bold(colors.green("fedify lookup @john@localhost:3000"))} -`, - }), - }, -} as const; - -type KvStore = "redis" | "postgres" | "denokv"; - -interface KvStoreDescription { - label: string; - runtimes?: Runtime[]; - dependencies?: Record; - devDependencies?: Record; - imports?: Record; - object: string | Record; - denoUnstable?: string[]; - env?: Record; -} - -const kvStores: Record = { - redis: { - label: "Redis", - dependencies: { - "@fedify/redis": getLatestVersion("@fedify/redis"), - "npm:ioredis": "^5.4.1", - }, - imports: { "@fedify/redis": ["RedisKvStore"], ioredis: ["Redis"] }, - object: { - deno: 'new RedisKvStore(new Redis(Deno.env.get("REDIS_URL")))', - node: "new RedisKvStore(new Redis(process.env.REDIS_URL))", - bun: "new RedisKvStore(new Redis(process.env.REDIS_URL))", - }, - env: { - REDIS_URL: "redis://localhost:6379", - }, - }, - postgres: { - label: "PostgreSQL", - dependencies: { - "@fedify/postgres": getLatestVersion("@fedify/postgres"), - "npm:postgres": "^3.4.5", - }, - imports: { "@fedify/postgres": ["PostgresKvStore"], postgres: "postgres" }, - object: { - deno: 'new PostgresKvStore(postgres(Deno.env.get("DATABASE_URL")))', - node: "new PostgresKvStore(postgres(process.env.DATABASE_URL))", - bun: "new PostgresKvStore(postgres(process.env.DATABASE_URL))", - }, - env: { - DATABASE_URL: "postgres://postgres@localhost:5432/postgres", - }, - }, - denokv: { - label: "Deno KV", - runtimes: ["deno"], - imports: { "@fedify/fedify/x/denokv": ["DenoKvStore"] }, - object: "new DenoKvStore(await Deno.openKv())", - denoUnstable: ["kv"], - }, -} as const; - -type MessageQueue = "redis" | "postgres" | "amqp" | "denokv"; - -interface MessageQueueDescription { - label: string; - runtimes?: Runtime[]; - dependencies?: Record; - devDependencies?: Record; - imports?: Record; - object: string | Record; - denoUnstable?: string[]; - env?: Record; -} - -const messageQueues: Record = { - redis: { - label: "Redis", - dependencies: { - "@fedify/redis": getLatestVersion("@fedify/redis"), - "npm:ioredis": "^5.4.1", - }, - imports: { "@fedify/redis": ["RedisMessageQueue"], ioredis: ["Redis"] }, - object: { - deno: 'new RedisMessageQueue(() => new Redis(Deno.env.get("REDIS_URL")))', - node: "new RedisMessageQueue(() => new Redis(process.env.REDIS_URL))", - bun: "new RedisMessageQueue(() => new Redis(process.env.REDIS_URL))", - }, - env: { - REDIS_URL: "redis://localhost:6379", - }, - }, - postgres: { - label: "PostgreSQL", - dependencies: { - "@fedify/postgres": getLatestVersion("@fedify/postgres"), - "npm:postgres": "^3.4.5", - }, - imports: { - "@fedify/postgres": ["PostgresMessageQueue"], - postgres: "postgres", - }, - object: { - deno: 'new PostgresMessageQueue(postgres(Deno.env.get("DATABASE_URL")))', - node: "new PostgresMessageQueue(postgres(process.env.DATABASE_URL))", - bun: "new PostgresMessageQueue(postgres(process.env.DATABASE_URL))", - }, - env: { - DATABASE_URL: "postgres://postgres@localhost:5432/postgres", - }, - }, - amqp: { - label: "AMQP (e.g., RabbitMQ)", - dependencies: { - "@fedify/amqp": getLatestVersion("@fedify/amqp"), - "npm:amqplib": "^0.10.4", - }, - devDependencies: { - "npm:@types/amqplib": "^0.10.5", - }, - imports: { - "@fedify/amqp": ["AmqpMessageQueue"], - amqplib: ["connect"], - }, - object: { - deno: 'new AmqpMessageQueue(await connect(Deno.env.get("AMQP_URL")))', - node: "new AmqpMessageQueue(await connect(process.env.AMQP_URL))", - bun: "new AmqpMessageQueue(await connect(process.env.AMQP_URL))", - }, - env: { - AMQP_URL: "amqp://localhost", - }, - }, - denokv: { - label: "Deno KV", - runtimes: ["deno"], - imports: { "@fedify/fedify/x/denokv": ["DenoKvMessageQueue"] }, - object: "new DenoKvMessageQueue(await Deno.openKv())", - denoUnstable: ["kv"], - }, -} as const; - -export const command = new Command() - .type( - "runtime", - new EnumType( - (Object.keys(runtimes) as Runtime[]).filter((r) => - runtimeAvailabilities[r] - ), - ), - ) - .type( - "package-manager", - new EnumType( - (Object.keys(packageManagers) as PackageManager[]).filter((pm) => - packageManagerLocations[pm] - ), - ), - ) - .type( - "web-framework", - new EnumType(Object.keys(webFrameworks) as WebFramework[]), - ) - .type("kv-store", new EnumType(Object.keys(kvStores) as KvStore[])) - .type( - "message-queue", - new EnumType(Object.keys(messageQueues) as MessageQueue[]), - ) - .arguments("") - .description("Initialize a new Fedify project directory.") - .option( - "-r, --runtime ", - "Choose the JavaScript runtime to use.", - ) - .option( - "-p, --package-manager ", - "Choose the package manager to use. Only applicable to -r/--runtime=node.", - ) - .option( - "-w, --web-framework ", - "Choose the web framework to integrate Fedify with.", - ) - .option( - "-k, --kv-store ", - "Choose the key–value store to use for caching.", - ) - .option( - "-q, --message-queue ", - "Choose the message queue to use for background jobs.", - ) - .option( - "--dry-run", - "Show what would be created without actually creating files.", - ) - .action(async (options, dir: string) => { - const dryRun = options.dryRun ?? false; - const projectName = basename( - await exists(dir) ? await Deno.realPath(dir) : normalize(dir), - ); - let dinosaurDrawn = false; - let runtime = options.runtime; - if (runtime == null) { - drawDinosaur(); - dinosaurDrawn = true; - runtime = await Select.prompt({ - message: "Choose the JavaScript runtime to use", - options: Object.entries(runtimes).map(([value, { label }]) => ({ - name: label, - value, - disabled: !runtimeAvailabilities[value as Runtime], - })), - }) as unknown as Runtime; - } - let packageManager = options.packageManager; - if (runtime === "node" && packageManager == null) { - if (!dinosaurDrawn) { - drawDinosaur(); - dinosaurDrawn = true; - } - packageManager = await Select.prompt({ - message: "Choose the package manager to use", - options: Object.entries(packageManagers).map(([value, { label }]) => ({ - name: label, - value, - })), - }) as unknown as PackageManager; - } else if (packageManager == null) packageManager = "npm"; - let webFramework = options.webFramework; - if (webFramework == null && options.runtime == null) { - if (!dinosaurDrawn) { - drawDinosaur(); - dinosaurDrawn = true; - } - webFramework = await Select.prompt({ - message: "Choose the web framework to integrate Fedify with", - options: [ - { name: "Bare-bones", value: null }, - ...Object.entries(webFrameworks).map(( - [value, { label, runtimes }], - ) => ({ - name: label, - value, - disabled: runtimes != null && !runtimes.includes(runtime), - })), - ], - }) as unknown as WebFramework; - } - if ( - webFramework != null && webFrameworks[webFramework].runtimes != null && - !webFrameworks[webFramework].runtimes!.includes(runtime) - ) { - console.error( - `The ${ - webFrameworks[webFramework].label - } framework is not available on the ${ - runtimes[runtime].label - } runtime.`, - ); - Deno.exit(1); - } - let kvStore = options.kvStore; - if (kvStore == null && options.runtime == null) { - if (!dinosaurDrawn) { - drawDinosaur(); - dinosaurDrawn = true; - } - kvStore = await Select.prompt({ - message: "Choose the key–value store to use for caching", - options: [ - { name: "In-memory", value: null }, - ...Object.entries(kvStores).map(([value, { label, runtimes }]) => ({ - name: label, - value, - disabled: runtimes != null && !runtimes.includes(runtime), - })), - ], - }) as unknown as KvStore; - } - if ( - kvStore != null && kvStores[kvStore].runtimes != null && - !kvStores[kvStore].runtimes!.includes(runtime) - ) { - console.error( - `The ${kvStores[kvStore].label} store is not available on the ${ - runtimes[runtime].label - } runtime.`, - ); - Deno.exit(1); - } - let messageQueue = options.messageQueue; - if (messageQueue == null && options.runtime == null) { - if (!dinosaurDrawn) { - drawDinosaur(); - dinosaurDrawn = true; - } - messageQueue = await Select.prompt({ - message: "Choose the message queue to use for background jobs", - options: [ - { name: "In-process", value: null }, - ...Object.entries(messageQueues).map(( - [value, { label, runtimes }], - ) => ({ - name: label, - value, - disabled: runtimes != null && !runtimes.includes(runtime), - })), - ], - }) as unknown as MessageQueue; - } - if ( - messageQueue != null && messageQueues[messageQueue].runtimes != null && - !messageQueues[messageQueue].runtimes!.includes(runtime) - ) { - console.error( - `The ${ - messageQueues[messageQueue].label - } message queue is not available on the ${ - runtimes[runtime].label - } runtime.`, - ); - Deno.exit(1); - } - logger.debug( - "Runtime: {runtime}; package manager: {packageManager}; " + - "web framework: {webFramework}; key–value store: {kvStore}; " + - "message queue: {messageQueue}", - { - runtime, - packageManager, - webFramework, - kvStore, - messageQueue, - }, - ); - if (!runtimeAvailabilities[runtime]) { - console.error( - `The ${ - runtimes[runtime].label - } runtime is not available on this system.`, - ); - Deno.exit(1); - } - if (runtime === "node" && !packageManagerLocations[packageManager]) { - console.error(`The ${packageManager} is not available on this system.`); - Deno.exit(1); - } - let initializer: WebFrameworkInitializer; - if (webFramework == null) { - initializer = { - federationFile: "federation.ts", - loggingFile: "logging.ts", - dependencies: runtime === "deno" - ? { - "@std/dotenv": "^0.225.2", - "@hongminhee/x-forwarded-fetch": "^0.2.0", - } - : runtime === "node" - ? { - "@dotenvx/dotenvx": "^1.14.1", - "@hono/node-server": "^1.12.0", - tsx: "^4.17.0", - "x-forwarded-fetch": "^0.2.0", - } - : { "x-forwarded-fetch": "^0.2.0" }, - devDependencies: runtime === "bun" ? { "@types/bun": "^1.1.6" } : {}, - files: { - "main.ts": runtime === "node" - ? `\ -import { serve } from "@hono/node-server"; -import { behindProxy } from "x-forwarded-fetch"; -import federation from "./federation.ts"; -import "./logging.ts"; - -serve( - { - port: 8000, - fetch: behindProxy( - (req) => federation.fetch(req, { contextData: undefined }), - ), - }, - (info) => - console.log("Server started at http://" + info.address + ":" + info.port) -); -` - : runtime === "bun" - ? `\ -import { behindProxy } from "x-forwarded-fetch"; -import federation from "./federation.ts"; -import "./logging.ts"; - -const server = Bun.serve({ - port: 8000, - fetch: behindProxy( - (req) => federation.fetch(req, { contextData: undefined }), ), -}); - -console.log("Server started at", server.url.href); -` - : `\ -import "@std/dotenv/load"; -import { behindProxy } from "@hongminhee/x-forwarded-fetch"; -import federation from "./federation.ts"; -import "./logging.ts"; - -Deno.serve( { - port: 8000, - onListen: ({ port, hostname }) => - console.log("Server started at http://" + hostname + ":" + port) + description: message`Initialize a new Fedify project directory.`, }, - behindProxy((req) => federation.fetch(req, { contextData: undefined })), ); -`, - }, - compilerOptions: runtime === "deno" - ? { - "jsx": "precompile", - "jsxImportSource": "hono/jsx", - } - : { - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "strict": true, - }, - tasks: { - "dev": runtime === "deno" - ? "deno run -A --watch ./main.ts" - : runtime === "bun" - ? "bun run --hot ./main.ts" - : "dotenvx run -- tsx watch ./main.ts", - "prod": runtime === "deno" - ? "deno run -A ./main.ts" - : runtime === "bun" - ? "bun run ./main.ts" - : "dotenvx run -- node --import tsx ./main.ts", - }, - instruction: ` -To start the server, run the following command: - - ${ - colors.bold(colors.green( - runtime === "deno" - ? "deno task dev" - : runtime === "bun" - ? "bun dev" - : `${packageManager} run dev`, - )) - } - -Then, try look up an actor from your server: - - ${colors.bold(colors.green("fedify lookup http://localhost:8000/users/john"))} -`, - }; - } else { - const desc = webFrameworks[webFramework]; - if (desc.runtimes != null && !desc.runtimes.includes(runtime)) { - console.error( - `The ${desc.label} framework is not available on the ${ - runtimes[runtime].label - } runtime.`, - ); - Deno.exit(1); - } - initializer = desc.init(projectName, runtime, packageManager); - } - const kvStoreDesc: KvStoreDescription = kvStore != null - ? kvStores[kvStore] - : { - label: "", - imports: { "@fedify/fedify": ["MemoryKvStore"] }, - object: "new MemoryKvStore()", - }; - const mqDesc: MessageQueueDescription = messageQueue != null - ? messageQueues[messageQueue] - : { - label: "", - imports: { "@fedify/fedify": ["InProcessMessageQueue"] }, - object: "new InProcessMessageQueue()", - }; - const imports: Record = {}; - for ( - const [module, symbols] of [ - ...Object.entries(kvStoreDesc.imports ?? {}), - ...Object.entries(mqDesc.imports ?? {}), - ] - ) { - if (module in imports) { - if (Array.isArray(symbols)) { - for (const symbol of symbols) { - if (imports[module].names.includes(symbol)) continue; - imports[module].names.push(symbol); - } - } else if (imports[module].$ == null) { - imports[module].$ = symbols; - } else if (symbols !== imports[module].$) { - throw new Error( - "Multiple default imports from the same module; report this as a bug.", - ); - } - } else { - if (Array.isArray(symbols)) { - imports[module] = { $: null, names: symbols }; - } else { - imports[module] = { $: symbols, names: [] }; - } - } - } - const importStatements = Object.entries(imports) - .map(( - [module, { $, names }], - ) => - [ - $ == null ? null : `import ${$} from ${JSON.stringify(module)};`, - names.length > 0 - ? `import { ${names.join(", ")} } from ${JSON.stringify(module)};` - : null, - ].filter((s) => s != null).join("\n") - ) - .join("\n"); - const federation = `\ -import { createFederation, Person } from "@fedify/fedify"; -import { getLogger } from "@logtape/logtape"; -${importStatements} - -const logger = getLogger(${JSON.stringify(projectName)}); - -const federation = createFederation({ - kv: ${ - typeof kvStoreDesc.object === "string" - ? kvStoreDesc.object - : kvStoreDesc.object[runtime] - }, - queue: ${ - typeof mqDesc.object === "string" ? mqDesc.object : mqDesc.object[runtime] - }, -}); - -federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { - return new Person({ - id: ctx.getActorUri(identifier), - preferredUsername: identifier, - name: identifier, - }); -}); - -export default federation; -`; - const logging = `\ -import { configure, getConsoleSink } from "@logtape/logtape"; -import { AsyncLocalStorage } from "node:async_hooks"; - -await configure({ - contextLocalStorage: new AsyncLocalStorage(), - sinks: { - console: getConsoleSink(), - }, - filters: {}, - loggers: [ - { category: ${ - JSON.stringify(projectName) - }, lowestLevel: "debug", sinks: ["console"] }, - { category: "fedify", lowestLevel: "info", sinks: ["console"] }, - { category: ["logtape", "meta"], lowestLevel: "warning", sinks: ["console"] }, - ], -}); -`; - const env = { ...kvStoreDesc.env, ...mqDesc.env }; - const files = { - [initializer.federationFile]: federation, - [initializer.loggingFile]: logging, - ".env": stringify(env), - ...initializer.files, - }; - const { prependFiles } = initializer; - - if (dryRun) { - console.log( - colors.bold( - colors.yellow("🔍 DRY RUN MODE - No files will be created\n"), - ), - ); - } - - // Check if directory is empty - const checkDirectoryEmpty = async (path: string) => { - try { - for await (const _ of Deno.readDir(path)) { - console.error("The directory is not empty. Aborting."); - Deno.exit(1); - } - } catch (e) { - if (!(e instanceof Deno.errors.NotFound)) { - throw e; - } - } - }; - - if (dryRun) { - await checkDirectoryEmpty(dir); - } else { - await Deno.mkdir(dir, { recursive: true }); - await checkDirectoryEmpty(dir); - } - if (initializer.command != null) { - if (dryRun) { - console.log(colors.bold(colors.cyan("📦 Would run command:"))); - console.log( - ` ${ - [initializer.command[0], ...initializer.command.slice(1)].join(" ") - }\n`, - ); - } else { - const cmd = new Deno.Command(initializer.command[0], { - args: initializer.command.slice(1), - cwd: dir, - stdin: "inherit", - stdout: "inherit", - stderr: "inherit", - }); - const result = await cmd.output(); - if (!result.success) { - console.error("Failed to initialize the project."); - Deno.exit(1); - } - } - } - if (runtime !== "deno") { - const packageJsonPath = join(dir, "package.json"); - if (!dryRun) { - try { - await Deno.stat(packageJsonPath); - } catch (e) { - if (e instanceof Deno.errors.NotFound) { - await Deno.writeTextFile(packageJsonPath, "{}"); - } else throw e; - } - } - } - const dependencies: Record = { - "@fedify/fedify": getLatestVersion("@fedify/fedify"), - "@logtape/logtape": "^0.8.2", - ...initializer.dependencies, - ...kvStoreDesc?.dependencies, - ...mqDesc?.dependencies, - }; - if (dryRun) { - const deps = Object.entries(dependencies) - .map(([name, version]) => `${name}@${version}`) - .join("\n"); - if (deps) { - console.log(colors.bold(colors.cyan("📦 Would install dependencies:"))); - console.log(`${deps}\n`); - } - } else { - await addDependencies( - runtime, - packageManager, - dir, - dependencies, - ); - } - if (runtime !== "deno") { - const devDependencies: Record = { - "@biomejs/biome": "^1.8.3", - ...initializer.devDependencies, - ...kvStoreDesc?.devDependencies, - ...mqDesc?.devDependencies, - }; - if (dryRun) { - const devDeps = Object.entries(devDependencies) - .map(([name, version]) => `${name}@${version}`) - .join("\n"); - if (devDeps) { - console.log( - colors.bold(colors.cyan("📦 Would install dev dependencies:")), - ); - console.log(`${devDeps}\n`); - } - } else { - await addDependencies( - runtime, - packageManager, - dir, - devDependencies, - true, - ); - } - } - if (dryRun) { - console.log(colors.bold(colors.green("📄 Would create files:\n"))); - for (const [filename, content] of Object.entries(files)) { - const path = join(dir, filename); - displayFileContent(path, content); - } - } else { - for (const [filename, content] of Object.entries(files)) { - const path = join(dir, filename); - const dirName = dirname(path); - await Deno.mkdir(dirName, { recursive: true }); - await Deno.writeTextFile(path, content); - } - } - if (prependFiles != null) { - if (dryRun) { - console.log(colors.bold(colors.blue("Would prepend to files:\n"))); - for (const [filename, prefix] of Object.entries(prependFiles)) { - const path = join(dir, filename); - console.log(colors.blue(`${path}`)); - console.log(colors.gray("─".repeat(60))); - console.log(colors.gray("Prepending:")); - console.log(prefix); - console.log(colors.gray("─".repeat(60)) + "\n"); - } - } else { - for (const [filename, prefix] of Object.entries(prependFiles)) { - const path = join(dir, filename); - const dirName = dirname(path); - await Deno.mkdir(dirName, { recursive: true }); - await Deno.writeTextFile( - path, - `${prefix}${await Deno.readTextFile(path)}`, - ); - } - } - } - if (runtime === "deno") { - if (dryRun) { - console.log( - colors.bold(colors.green("Would create/update JSON files:\n")), - ); - } - await rewriteJsonFile( - join(dir, "deno.json"), - {}, - (cfg) => ({ - ...cfg, - ...initializer.compilerOptions == null ? {} : { - compilerOptions: { - ...cfg?.compilerOptions, - ...initializer.compilerOptions, - }, - }, - unstable: [ - "temporal", - ...kvStoreDesc.denoUnstable ?? [], - ...mqDesc.denoUnstable ?? [], - ], - tasks: { ...cfg.tasks, ...initializer.tasks }, - }), - dryRun, - ); - await rewriteJsonFile( - join(dir, ".vscode", "settings.json"), - {}, - (vsCodeSettings) => ({ - "deno.enable": true, - "deno.unstable": true, - "editor.detectIndentation": false, - "editor.indentSize": 2, - "editor.insertSpaces": true, - "[javascript]": { - "editor.defaultFormatter": "denoland.vscode-deno", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.sortImports": "always", - }, - }, - "[javascriptreact]": { - "editor.defaultFormatter": "denoland.vscode-deno", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.sortImports": "always", - }, - }, - "[json]": { - "editor.defaultFormatter": "vscode.json-language-features", - "editor.formatOnSave": true, - }, - "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features", - "editor.formatOnSave": true, - }, - "[typescript]": { - "editor.defaultFormatter": "denoland.vscode-deno", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.sortImports": "always", - }, - }, - "[typescriptreact]": { - "editor.defaultFormatter": "denoland.vscode-deno", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.sortImports": "always", - }, - }, - ...vsCodeSettings, - }), - dryRun, - ); - await rewriteJsonFile( - join(dir, ".vscode", "extensions.json"), - {}, - (vsCodeExtensions) => ({ - recommendations: uniqueArray([ - "denoland.vscode-deno", - ...vsCodeExtensions.recommendations ?? [], - ]), - ...vsCodeExtensions, - }), - dryRun, - ); - } else { - if (dryRun) { - console.log( - colors.bold(colors.green("Would create/update JSON files:\n")), - ); - } - await rewriteJsonFile( - join(dir, "package.json"), - {}, - (cfg) => ({ - type: "module", - ...cfg, - scripts: { ...cfg.scripts, ...initializer.tasks }, - }), - dryRun, - ); - if (initializer.compilerOptions != null) { - await rewriteJsonFile( - join(dir, "tsconfig.json"), - {}, - (cfg) => ({ - ...cfg, - compilerOptions: { - ...cfg?.compilerOptions, - ...initializer.compilerOptions, - }, - }), - dryRun, - ); - } - await rewriteJsonFile( - join(dir, ".vscode", "settings.json"), - {}, - (vsCodeSettings) => ({ - "editor.detectIndentation": false, - "editor.indentSize": 2, - "editor.insertSpaces": true, - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports.biome": "always", - }, - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports.biome": "always", - }, - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true, - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true, - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports.biome": "always", - }, - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports.biome": "always", - }, - }, - ...vsCodeSettings, - }), - dryRun, - ); - await rewriteJsonFile( - join(dir, ".vscode", "extensions.json"), - {}, - (vsCodeExtensions) => ({ - recommendations: uniqueArray([ - "biomejs.biome", - ...vsCodeExtensions.recommendations ?? [], - ]), - ...vsCodeExtensions, - }), - dryRun, - ); - await rewriteJsonFile( - join(dir, "biome.json"), - {}, - (cfg) => ({ - "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", - ...cfg, - organizeImports: { - ...cfg.organizeImports, - enabled: true, - }, - formatter: { - ...cfg.formatter, - enabled: true, - indentStyle: "space", - indentWidth: 2, - }, - linter: { - ...cfg.linter, - enabled: true, - rules: { recommended: true }, - }, - }), - dryRun, - ); - } - console.error(initializer.instruction); - if (Object.keys(env).length > 0) { - console.error( - `Note that you probably want to edit the ${ - colors.bold(colors.blue(".env")) - } file. It currently contains the following values:\n`, - ); - for (const key in env) { - const value = stringify({ _: env[key] }).substring(2); - console.error( - ` ${colors.bold(colors.green(key))}${colors.gray("=")}${value}`, - ); - } - console.error(); - } - console.error(`\ -Start by editing the ${colors.bold(colors.blue(initializer.federationFile))} \ -file to define your federation! -`); - }); - -function drawDinosaur() { - const d = (text: string) => colors.bgBlue(colors.black(text)); - const f = colors.blue; - console.error(`\ -${d(" ___ ")} ${f(" _____ _ _ __")} -${d(" /'_') ")} ${f("| ___|__ __| (_)/ _|_ _")} -${d(" .-^^^-/ / ")} ${f("| |_ / _ \\/ _` | | |_| | | |")} -${d(" __/ / ")} ${f("| _| __/ (_| | | _| |_| |")} -${d(" <__.|_|-|_| ")} ${f("|_| \\___|\\__,_|_|_| \\__, |")} -${d(" ")} ${f(" |___/")} -`); -} -function displayFileContent( - path: string, - content: string, - emoji: string = "📄", - pathColor: (text: string) => string = colors.green, +export function runInit( + command: InferValue, ) { - console.log(pathColor(`${emoji} ${path}`)); - console.error(colors.gray("─".repeat(60))); - console.log(content); - console.error(colors.gray("─".repeat(60)) + "\n"); + console.debug(command); } - -async function isCommandAvailable( - { checkCommand, outputPattern }: { - checkCommand: [string, ...string[]]; - outputPattern: RegExp; - }, -): Promise { - const cmd = new Deno.Command(checkCommand[0], { - args: checkCommand.slice(1), - stdin: "null", - stdout: "piped", - stderr: "null", - }); - try { - const output = await cmd.output(); - const stdout = new TextDecoder().decode(output.stdout); - logger.debug( - "The stdout of the command {command} is: {stdout}", - { command: checkCommand, stdout }, - ); - return outputPattern.exec(stdout.trim()) ? true : false; - } catch (error) { - logger.debug( - "The command {command} failed with the error: {error}", - { command: checkCommand, error }, - ); - if (error instanceof Deno.errors.NotFound) return false; - throw error; - } -} - -function isRuntimeAvailable(runtime: Runtime): Promise { - return isCommandAvailable(runtimes[runtime]); -} - -async function locatePackageManager( - pm: PackageManager, -): Promise { - if (await isCommandAvailable(packageManagers[pm])) { - return packageManagers[pm].checkCommand[0]; - } - if (Deno.build.os !== "windows") return undefined; - const cmd: [string, ...string[]] = [ - packageManagers[pm].checkCommand[0] + ".cmd", - ...packageManagers[pm].checkCommand.slice(1), - ]; - if (await isCommandAvailable({ ...packageManagers[pm], checkCommand: cmd })) { - return cmd[0]; - } - return undefined; -} - -async function addDependencies( - runtime: Runtime, - pm: PackageManager, - dir: string, - dependencies: Record, - dev: boolean = false, -): Promise { - const deps = Object.entries(dependencies) - .map(([name, version]) => - `${ - runtime !== "deno" && name.startsWith("npm:") - ? name.substring(4) - : runtime === "deno" && !name.startsWith("npm:") - ? `jsr:${name}` - : name - }@${ - runtime !== "deno" && version.includes("+") - ? version.substring(0, version.indexOf("+")) - : version - }` - ); - if (deps.length < 1) return; - const cmd = new Deno.Command( - runtime === "node" ? (packageManagerLocations[pm] ?? pm) : runtime, - { - args: [ - "add", - ...(dev - ? [runtime === "bun" || pm === "yarn" ? "--dev" : "--save-dev"] - : []), - ...uniqueArray(deps), - ], - cwd: dir, - stdin: "inherit", - stdout: "inherit", - stderr: "inherit", - }, - ); - const result = await cmd.output(); - if (!result.success) { - throw new Error("Failed to add dependencies."); - } -} - -function getLatestVersion(packageName: `@fedify/${string}`): string { - const version = packagesMetaData[packageName]; - if (!version) { - throw new Error( - `Version for package "${packageName}" not found in local metadata.`, - ); - } - return version; -} -async function rewriteJsonFile( - path: string, - // deno-lint-ignore no-explicit-any - empty: any, - // deno-lint-ignore no-explicit-any - rewriter: (json: any) => any, - dryRun: boolean = false, -): Promise { - let jsonText: string | null = null; - try { - jsonText = await Deno.readTextFile(path); - } catch (e) { - if (!(e instanceof Deno.errors.NotFound)) throw e; - } - let json = jsonText == null ? empty : JSON.parse(jsonText); - json = rewriter(json); - - if (dryRun) { - displayFileContent(path, JSON.stringify(json, null, 2)); - } else { - await Deno.mkdir(dirname(path), { recursive: true }); - await Deno.writeTextFile(path, JSON.stringify(json, null, 2) + "\n"); - } -} - -function uniqueArray(a: T[]): T[] { - const result: T[] = []; - for (const v of a) { - if (!result.includes(v)) result.push(v); - } - return result; -} - -// cSpell: ignore asynciterable bunx giget denoland biomejs dotenvx diff --git a/packages/cli/src/install.mjs b/packages/cli/src/install.mjs deleted file mode 100644 index 91460fe25..000000000 --- a/packages/cli/src/install.mjs +++ /dev/null @@ -1,189 +0,0 @@ -import { execFile } from "node:child_process"; -import { createWriteStream, existsSync } from "node:fs"; -import { - access, - chmod, - constants, - copyFile, - mkdir, - mkdtemp, - readFile, - realpath, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; -import process from "node:process"; -import { Readable } from "node:stream"; -import { fileURLToPath } from "node:url"; - -const platforms = { - darwin: { - arm64: "macos-aarch64.tar.xz", - x64: "macos-x86_64.tar.xz", - }, - linux: { - arm64: "linux-aarch64.tar.xz", - x64: "linux-x86_64.tar.xz", - }, - win32: { - arm64: "windows-x86_64.zip", - x64: "windows-x86_64.zip", - }, -}; - -export async function main(version) { - const filename = fileURLToPath(import.meta.url); - const dirName = dirname(dirname(filename)); - const packageJson = await readFile(join(dirName, "package.json"), { - encoding: "utf8", - }); - const pkg = JSON.parse(packageJson); - const binDir = join(dirName, "bin"); - await mkdir(binDir, { recursive: true }); - await install(version ?? pkg.version, binDir); -} - -async function install(version, targetDir) { - const downloadUrl = getDownloadUrl(version); - const downloadPath = await download(downloadUrl); - if (downloadPath == null) { - throw new Error(`Failed to download from ${downloadUrl}`); - } - let extractPath; - if (downloadPath.endsWith(".zip")) { - extractPath = await extractZip(downloadPath); - } else { - extractPath = await extractTar(downloadPath); - } - const exePath = join(extractPath, "fedify.exe"); - if (await isFile(exePath)) { - const targetPath = join(targetDir, "fedify.exe"); - await copyFile(exePath, targetPath); - return targetPath; - } - const binPath = join(extractPath, "fedify"); - if (await isFile(binPath)) { - const targetPath = join(targetDir, "fedify"); - await copyFile(binPath, targetPath); - await chmod(targetPath, 0o755); - return targetPath; - } - throw new Error("Executable not found in the archive"); -} - -function getDownloadUrl(version) { - const platform = platforms[process.platform]; - if (!platform) { - console.error("Unsupported platform:", process.platform); - return; - } - const suffix = platform[process.arch]; - if (!suffix) { - console.error("Unsupported architecture:", process.arch); - return; - } - const filename = `fedify-cli-${version}-${suffix}`; - const url = - `https://github.com/fedify-dev/fedify/releases/download/${version}/${filename}`; - return url; -} - -async function download(url) { - const response = await fetch(url, { redirect: "follow" }); - if (!response.ok) { - console.error("Download failed:", response.statusText); - return; - } - const tmpDir = await mkdtemp(join(tmpdir(), `fedify-`)); - const filename = url.substring(url.lastIndexOf("/") + 1); - const downloadPath = join(tmpDir, filename); - const fileStream = createWriteStream(downloadPath); - const readable = Readable.fromWeb(response.body); - await new Promise((resolve, reject) => { - readable.pipe(fileStream); - readable.on("error", reject); - fileStream.on("finish", resolve); - }); - return downloadPath; -} - -async function extractZip(path) { - const dir = await mkdtemp(join(tmpdir(), "fedify-")); - await new Promise((resolve, reject) => { - execFile("powershell", [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - `Import-Module Microsoft.PowerShell.Archive;\ - Expand-Archive -LiteralPath '${path}' -DestinationPath '${dir}'`, - ], (error, _, stderr) => { - if (error) { - console.error("Extraction failed:", error); - reject(error); - return; - } - if (stderr) console.warn(stderr); - resolve(); - }); - }); - return dir; -} - -async function extractTar(path) { - const switches = { - ".tar": "", - ".tar.gz": "z", - ".tgz": "z", - ".tar.bz2": "j", - ".tbz2": "j", - ".tar.xz": "J", - ".txz": "J", - }; - let switch_ = ""; - for (const ext in switches) { - if (path.endsWith(ext)) { - switch_ = switches[ext]; - break; - } - } - path = await realpath(path); - const dir = await mkdtemp(join(tmpdir(), "fedify-")); - await execTar(`xvf${switch_}`, dir, path); - return dir; -} - -function execTar(switch_, dir, path) { - return new Promise((resolve, reject) => { - execFile("tar", [switch_, path], { cwd: dir }, (error, _, stderr) => { - if (error) { - console.error("Extraction failed:", error); - reject(error); - return; - } - if (stderr) console.warn(stderr); - resolve(); - }); - }); -} - -export async function isFile(path) { - try { - await access(path, constants.R_OK); - return true; - } catch (error) { - if (error.code === "ENOENT") return false; - throw error; - } -} - -if (fileURLToPath(import.meta.url) === process.argv[1]) { - if (existsSync(join(dirname(dirname(fileURLToPath(import.meta.url))), "deno.json"))) { - console.error( - "This post-install script is not intended to be run within a workspace; " + - "skipping installation.", - ); - } else { - await main(process.argv[2]); - } -} diff --git a/packages/cli/src/log.ts b/packages/cli/src/log.ts index 03f3b20ab..971c3b3e9 100644 --- a/packages/cli/src/log.ts +++ b/packages/cli/src/log.ts @@ -5,8 +5,10 @@ import { type LogRecord, type Sink, } from "@logtape/logtape"; -import { dirname } from "@std/path"; +import { dirname } from "node:path"; +import process from "node:process"; import { AsyncLocalStorage } from "node:async_hooks"; +import { mkdir } from "node:fs/promises"; export interface RecordingSink extends Sink { startRecording(): void; @@ -33,9 +35,9 @@ export function getRecordingSink(): RecordingSink { export const recordingSink = getRecordingSink(); -export const logFile = Deno.env.get("FEDIFY_LOG_FILE"); +export const logFile = process.env["FEDIFY_LOG_FILE"]; if (logFile != null) { - await Deno.mkdir(dirname(logFile), { recursive: true }); + await mkdir(dirname(logFile), { recursive: true }); } await configure({ diff --git a/packages/cli/src/lookup.test.ts b/packages/cli/src/lookup.test.ts index cfcc9724f..eb980a830 100644 --- a/packages/cli/src/lookup.test.ts +++ b/packages/cli/src/lookup.test.ts @@ -1,321 +1,152 @@ import { Activity, Note } from "@fedify/fedify"; import { assertEquals, assertExists } from "@std/assert"; +import test from "node:test"; +import { mkdir, readFile, rm } from "node:fs/promises"; import { getContextLoader } from "./docloader.ts"; import { clearTimeoutSignal, - createFileStream, createTimeoutSignal, + TimeoutError, writeObjectToStream, } from "./lookup.ts"; -Deno.test("createFileStream - creates file stream with proper directory creation", async () => { - const testDir = "./test_output"; - const testFile = `${testDir}/test.json`; - - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } - - const stream = await createFileStream(testFile); - assertExists(stream); - - const stat = await Deno.stat(testDir); - assertEquals(stat.isDirectory, true); - - stream.close(); - - await Deno.remove(testDir, { recursive: true }); -}); - -Deno.test("createFileStream - works with absolute paths", async () => { - const testDir = `${Deno.cwd()}/test_output_absolute`; - const testFile = `${testDir}/test.json`; - - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } - - const stream = await createFileStream(testFile); - assertExists(stream); - - const stat = await Deno.stat(testDir); - assertEquals(stat.isDirectory, true); - - stream.close(); - - await Deno.remove(testDir, { recursive: true }); -}); - -Deno.test("createFileStream - creates nested directories", async () => { - const testDir = "./test_output_nested/deep/path"; - const testFile = `${testDir}/test.json`; - - try { - await Deno.remove("./test_output_nested", { recursive: true }); - } catch { - // Ignore if doesn't exist - } - - const stream = await createFileStream(testFile); - assertExists(stream); - - // Verify nested directories were created - const stat = await Deno.stat(testDir); - assertEquals(stat.isDirectory, true); - stream.close(); - - await Deno.remove("./test_output_nested", { recursive: true }); -}); - -Deno.test("createFileStream - writes data correctly", async () => { - const testDir = "./test_output_write"; - const testFile = `${testDir}/test.txt`; - - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } - - const stream = await createFileStream(testFile); - const writer = stream.getWriter(); - - const testData = new TextEncoder().encode("Hello, World!"); - await writer.write(testData); - await writer.close(); - - const content = await Deno.readTextFile(testFile); - assertEquals(content, "Hello, World!"); - - await Deno.remove(testDir, { recursive: true }); -}); - -Deno.test("createFileStream - truncates existing file", async () => { - const testDir = "./test_output_truncate"; - const testFile = `${testDir}/test.txt`; - - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } - - await Deno.mkdir(testDir, { recursive: true }); - await Deno.writeTextFile(testFile, "Old content"); - - const stream = await createFileStream(testFile); - const writer = stream.getWriter(); - - const testData = new TextEncoder().encode("New content"); - await writer.write(testData); - await writer.close(); - - // Verify file was truncated and new content written - const content = await Deno.readTextFile(testFile); - assertEquals(content, "New content"); - - await Deno.remove(testDir, { recursive: true }); -}); - -Deno.test("writeObjectToStream - writes Note object with default options", { - sanitizeResources: false, -}, async () => { +test("writeObjectToStream - writes Note object with default options", async () => { const testDir = "./test_output_note"; const testFile = `${testDir}/note.txt`; - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } + await mkdir(testDir, { recursive: true }); const note = new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, fediverse!", }); - const options = { - firstKnock: "rfc9421" as const, - separator: "----", - output: testFile, - }; - const contextLoader = await getContextLoader({}); + await writeObjectToStream(note, testFile, undefined, contextLoader); + await new Promise((resolve) => setTimeout(resolve, 100)); - await writeObjectToStream(note, options, contextLoader); + const content = await readFile(testFile, { encoding: "utf8" }); - const content = await Deno.readTextFile(testFile); assertExists(content); assertEquals(content.includes("Hello, fediverse!"), true); - assertEquals(content.includes("Note"), true); + assertEquals(content.includes("id"), true); - await Deno.remove(testDir, { recursive: true }); + await rm(testDir, { recursive: true }); }); -Deno.test("writeObjectToStream - writes Activity object in raw JSON-LD format", async () => { +test("writeObjectToStream - writes Activity object in raw JSON-LD format", async () => { const testDir = "./test_output_activity"; - const testFile = `${testDir}/activity.json`; + const testFile = `${testDir}/raw.json`; - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } + await mkdir(testDir, { recursive: true }); const activity = new Activity({ id: new URL("https://example.com/activities/1"), }); - const options = { - firstKnock: "rfc9421" as const, - separator: "----", - output: testFile, - raw: true, - }; - const contextLoader = await getContextLoader({}); - - await writeObjectToStream(activity, options, contextLoader); + await writeObjectToStream(activity, testFile, "raw", contextLoader); + await new Promise((resolve) => setTimeout(resolve, 100)); // Verify file exists and contains JSON-LD - const content = await Deno.readTextFile(testFile); + const content = await readFile(testFile); assertExists(content); assertEquals(content.includes("@context"), true); assertEquals(content.includes("id"), true); - await Deno.remove(testDir, { recursive: true }); + await rm(testDir, { recursive: true }); }); -Deno.test("writeObjectToStream - writes object in compact JSON-LD format", async () => { +test("writeObjectToStream - writes object in compact JSON-LD format", async () => { const testDir = "./test_output_compact"; const testFile = `${testDir}/compact.json`; - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } + await mkdir(testDir, { recursive: true }); const note = new Note({ id: new URL("https://example.com/notes/1"), content: "Test note", }); - const options = { - firstKnock: "rfc9421" as const, - separator: "----", - output: testFile, - compact: true, - }; - const contextLoader = await getContextLoader({}); - - await writeObjectToStream(note, options, contextLoader); + await writeObjectToStream(note, testFile, "compact", contextLoader); + await new Promise((resolve) => setTimeout(resolve, 100)); // Verify file exists and contains compacted JSON-LD - const content = await Deno.readTextFile(testFile); + const content = await readFile(testFile); assertExists(content); assertEquals(content.includes("Test note"), true); - await Deno.remove(testDir, { recursive: true }); + await rm(testDir, { recursive: true }); }); -Deno.test("writeObjectToStream - writes object in expanded JSON-LD format", async () => { +test("writeObjectToStream - writes object in expanded JSON-LD format", async () => { const testDir = "./test_output_expand"; const testFile = `${testDir}/expand.json`; - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } + await mkdir(testDir, { recursive: true }); const note = new Note({ id: new URL("https://example.com/notes/1"), content: "Test note for expansion", }); - const options = { - firstKnock: "rfc9421" as const, - separator: "----", - output: testFile, - expand: true, - }; - const contextLoader = await getContextLoader({}); + await writeObjectToStream(note, testFile, "expand", contextLoader); + await new Promise((resolve) => setTimeout(resolve, 100)); - await writeObjectToStream(note, options, contextLoader); - - const content = await Deno.readTextFile(testFile); + const content = await readFile(testFile); assertExists(content); assertEquals(content.includes("Test note for expansion"), true); - await Deno.remove(testDir, { recursive: true }); + await rm(testDir, { recursive: true }); }); -Deno.test("writeObjectToStream - writes to stdout when no output file specified", async () => { +test("writeObjectToStream - writes to stdout when no output file specified", async () => { const note = new Note({ id: new URL("https://example.com/notes/1"), content: "Test stdout note", }); - const options = { - firstKnock: "rfc9421" as const, - separator: "----", - }; - const contextLoader = await getContextLoader({}); - await writeObjectToStream(note, options, contextLoader); + await writeObjectToStream(note, undefined, undefined, contextLoader); }); -Deno.test("writeObjectToStream - handles empty content properly", async () => { +test("writeObjectToStream - handles empty content properly", async () => { const testDir = "./test_output_empty"; const testFile = `${testDir}/empty.txt`; - try { - await Deno.remove(testDir, { recursive: true }); - } catch { - // Ignore if doesn't exist - } + await mkdir(testDir, { recursive: true }); const note = new Note({ id: new URL("https://example.com/notes/1"), }); - const options = { - firstKnock: "rfc9421" as const, - separator: "----", - output: testFile, - }; - const contextLoader = await getContextLoader({}); - await writeObjectToStream(note, options, contextLoader); + await writeObjectToStream(note, testFile, undefined, contextLoader); + await new Promise((resolve) => setTimeout(resolve, 100)); - const content = await Deno.readTextFile(testFile); + const content = await readFile(testFile); assertExists(content); assertEquals(content.includes("Note"), true); - await Deno.remove(testDir, { recursive: true }); + await rm(testDir, { recursive: true }); }); -Deno.test("createTimeoutSignal - returns undefined when no timeout specified", () => { +test("createTimeoutSignal - returns undefined when no timeout specified", () => { const signal = createTimeoutSignal(); assertEquals(signal, undefined); }); -Deno.test("createTimeoutSignal - returns undefined when timeout is null", () => { +test("createTimeoutSignal - returns undefined when timeout is null", () => { const signal = createTimeoutSignal(undefined); assertEquals(signal, undefined); }); -Deno.test("createTimeoutSignal - creates AbortSignal that aborts after timeout", async () => { +test("createTimeoutSignal - creates AbortSignal that aborts after timeout", async () => { const signal = createTimeoutSignal(0.1); assertExists(signal); assertEquals(signal.aborted, false); @@ -323,14 +154,14 @@ Deno.test("createTimeoutSignal - creates AbortSignal that aborts after timeout", await new Promise((resolve) => setTimeout(resolve, 150)); assertEquals(signal.aborted, true); - assertEquals(signal.reason instanceof Deno.errors.TimedOut, true); + assertEquals(signal.reason instanceof TimeoutError, true); assertEquals( - (signal.reason as Deno.errors.TimedOut).message, + (signal.reason as TimeoutError).message, "Request timed out after 0.1 seconds", ); }); -Deno.test("createTimeoutSignal - signal is not aborted before timeout", () => { +test("createTimeoutSignal - signal is not aborted before timeout", () => { const signal = createTimeoutSignal(1); // 1 second timeout assertExists(signal); assertEquals(signal.aborted, false); @@ -338,7 +169,7 @@ Deno.test("createTimeoutSignal - signal is not aborted before timeout", () => { clearTimeoutSignal(signal); }); -Deno.test("clearTimeoutSignal - cleans up timer properly", async () => { +test("clearTimeoutSignal - cleans up timer properly", async () => { const signal = createTimeoutSignal(0.05); // 50ms timeout assertExists(signal); assertEquals(signal.aborted, false); diff --git a/packages/cli/src/lookup.ts b/packages/cli/src/lookup.ts index 2c48a41e6..abd8b36a0 100644 --- a/packages/cli/src/lookup.ts +++ b/packages/cli/src/lookup.ts @@ -1,4 +1,23 @@ -import { Command, EnumType } from "@cliffy/command"; +import { + argument, + choice, + command, + constant, + flag, + float, + type InferValue, + map, + merge, + message, + multiple, + object, + option, + optional, + or, + string, + withDefault, +} from "@optique/core"; +import { path, print, printError } from "@optique/run"; import { Application, Collection, @@ -14,75 +33,130 @@ import { traverseCollection, } from "@fedify/fedify"; import { getLogger } from "@logtape/logtape"; -import * as colors from "@std/fmt/colors"; -import { dirname, isAbsolute, resolve } from "@std/path"; import ora from "ora"; +import * as colors from "@std/fmt/colors"; +import process from "node:process"; +import { createWriteStream, type WriteStream } from "node:fs"; import { getContextLoader, getDocumentLoader } from "./docloader.ts"; -import { renderImages } from "./imagerenderer.ts"; import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts"; import { colorEnabled, formatObject } from "./utils.ts"; +import { renderImages } from "./imagerenderer.ts"; +import { configureLogging, debugOption } from "./globals.ts"; const logger = getLogger(["fedify", "cli", "lookup"]); -const sigSpec = new EnumType(["draft-cavage-http-signatures-12", "rfc9421"]); - -interface CommandOptions { - authorizedFetch?: boolean; - firstKnock: "draft-cavage-http-signatures-12" | "rfc9421"; - traverse?: boolean; - suppressErrors?: boolean; - raw?: boolean; - compact?: boolean; - expand?: boolean; - userAgent?: string; - separator: string; - output?: string; - timeout?: number; -} - -export async function createFileStream( - outputPath: string, -): Promise { - try { - const filepath = isAbsolute(outputPath) - ? outputPath - : resolve(Deno.env.get("PWD") || Deno.cwd(), outputPath); - - const parentDir = dirname(filepath); - await Deno.mkdir(parentDir, { recursive: true }); - - const file = await Deno.open(filepath, { - write: true, - create: true, - truncate: true, - }); - - return new WritableStream({ - write: (chunk) => file.write(chunk).then(() => {}), - close: () => file.close(), - abort: (reason) => { - file.close(); - throw reason; - }, - }); - } catch (err) { - const spinner = ora({ - text: `Failed to write output to ${colors.red(outputPath)}.`, - discardStdin: false, - }); - spinner.fail(); - console.error(`Error: ${String(err)}`); - - if (err instanceof Deno.errors.PermissionDenied) { - console.error( - "Permission denied. Try running with proper permissions.", - ); - } else if (err instanceof Deno.errors.NotFound) { - console.error("Path does not exist or is invalid."); - } else if (err instanceof Deno.errors.IsADirectory) { - console.error("The specified path is a directory, not a file."); - } - Deno.exit(1); +export const authorizedFetchOption = withDefault( + object({ + authorizedFetch: flag("-a", "--authorized-fetch", { + description: message`Sign the request with an one-time key.`, + }), + firstKnock: withDefault( + option( + "--first-knock", + choice(["draft-cavage-http-signatures-12", "rfc9421"]), + { + description: + message`The first-knock spec for -a/--authorized-fetch. It is used for the double-knocking technique.`, + }, + ), + "draft-cavage-http-signatures-12" as const, + ), + }), + { authorizedFetch: false } as const, +); + +const traverseOption = withDefault( + object({ + traverse: flag("-t", "--traverse", { + description: + message`Traverse the given collection to fetch all items. If it is turned on, the argument cannot be multiple.`, + }), + suppressErrors: option("-S", "--suppress-errors", { + description: + message`Suppress partial errors while traversing the collection.`, + }), + }), + { traverse: false } as const, +); + +export const lookupCommand = command( + "lookup", + merge( + object({ command: constant("lookup") }), + traverseOption, + authorizedFetchOption, + debugOption, + object("Looking up options", { + format: withDefault( + or( + map( + option("-r", "--raw", { + description: message`Print the fetched JSON-LD document as is.`, + }), + () => "raw" as const, + ), + map( + option("-C", "--compact", { + description: message`Compact the fetched JSON-LD document.`, + }), + () => "compact" as const, + ), + map( + option("-e", "--expand", { + description: message`Expand the fetched JSON-LD document.`, + }), + () => "expand" as const, + ), + ), + "default" as const, + ), + userAgent: optional( + option("-u", "--user-agent", string({ metavar: "USER_AGENT" }), { + description: message`The custom User-Agent header value.`, + }), + ), + separator: withDefault( + option("-s", "--separator", string({ metavar: "SEPARATOR" }), { + description: + message`Specify the separator between adjacent output objects or collection items.`, + }), + "----", + ), + output: optional(option( + "-o", + "--output", + path({ + metavar: "OUTPUT_PATH", + type: "file", + allowCreate: true, + }), + { description: message`Specify the output file path.` }, + )), + timeout: optional(option( + "-T", + "--timeout", + float({ min: 0, metavar: "SECONDS" }), + { description: message`Set timeout for network requests in seconds.` }, + )), + urls: multiple( + argument(string({ metavar: "URL_OR_HANDLE" }), { + description: message`One or more URLs or handles to look up.`, + }), + { min: 1 }, + ), + }), + ), + { + description: + message`Lookup an Activity Streams object by URL or the actor handle. The argument can be either a URL or an actor handle (e.g., @username@domain), and it can be multiple.`, + }, +); + +export class TimeoutError extends Error { + override name = "TimeoutError"; + constructor(message: string) { + super(message); + this.name = "TimeoutError"; } } @@ -103,50 +177,47 @@ async function findAllImages(obj: APObject): Promise { export async function writeObjectToStream( object: APObject | Link, - options: CommandOptions, + outputPath: string | undefined, + format: string | undefined, contextLoader: DocumentLoader, ): Promise { - const stream = options.output - ? await createFileStream(options.output) - : Deno.stdout.writable; + const stream: WriteStream | NodeJS.WritableStream = outputPath + ? createWriteStream(outputPath) + : process.stdout; - const writer = stream.getWriter(); + let content; + let json = true; + let imageUrls: URL[] = []; - try { - let content; - let json = true; - let imageUrls: URL[] = []; - - if (options.raw) { + if (format) { + if (format === "raw") { content = await object.toJsonLd({ contextLoader }); - } else if (options.compact) { + } else if (format === "compact") { content = await object.toJsonLd({ format: "compact", contextLoader }); - } else if (options.expand) { + } else if (format === "expand") { content = await object.toJsonLd({ format: "expand", contextLoader }); } else { content = object; json = false; } + } else { + content = object; + json = false; + } - const enableColors = colorEnabled && options.output === undefined; - content = formatObject(content, enableColors, json); + const enableColors = colorEnabled && outputPath === undefined; + content = formatObject(content, enableColors, json); - const encoder = new TextEncoder(); - const bytes = encoder.encode(content + "\n"); + const encoder = new TextEncoder(); + const bytes = encoder.encode(content + "\n"); - await writer.write(bytes); + stream.write(bytes); - if (object instanceof APObject) { - imageUrls = await findAllImages(object); - } - if (!options.output && imageUrls.length > 0) { - await renderImages(imageUrls); - } - } finally { - writer.releaseLock(); - if (options.output) { - await stream.close(); - } + if (object instanceof APObject) { + imageUrls = await findAllImages(object); + } + if (!outputPath && imageUrls.length > 0) { + await renderImages(imageUrls); } } @@ -159,9 +230,7 @@ export function createTimeoutSignal( const controller = new AbortController(); const timerId = setTimeout(() => { controller.abort( - new Deno.errors.TimedOut( - `Request timed out after ${timeoutSeconds} seconds`, - ), + new TimeoutError(`Request timed out after ${timeoutSeconds} seconds`), ); }, timeoutSeconds * 1000); @@ -179,18 +248,6 @@ export function clearTimeoutSignal(signal?: AbortSignal): void { } } -function handleTimeoutError( - spinner: { fail: (text: string) => void }, - timeoutSeconds?: number, - url?: string, -): void { - const urlText = url ? ` for: ${colors.red(url)}` : ""; - spinner.fail(`Request timed out after ${timeoutSeconds} seconds${urlText}.`); - console.error( - "Try increasing the timeout with -T/--timeout option or check network connectivity.", - ); -} - function wrapDocumentLoaderWithTimeout( loader: DocumentLoader, timeoutSeconds?: number, @@ -205,299 +262,285 @@ function wrapDocumentLoaderWithTimeout( }; } -export const command = new Command() - .type("sig-spec", sigSpec) - .arguments("<...urls:string>") - .description( - "Lookup an Activity Streams object by URL or the actor handle. " + - "The argument can be either a URL or an actor handle " + - "(e.g., @username@domain), and it can be multiple.", - ) - .option("-a, --authorized-fetch", "Sign the request with an one-time key.") - .option( - "--first-knock ", - "The first-knock spec for -a/--authorized-fetch. It is used for " + - "the double-knocking technique.", - { depends: ["authorized-fetch"], default: "rfc9421" }, - ) - .option( - "-t, --traverse", - "Traverse the given collection to fetch all items. If it is turned on, " + - "the argument cannot be multiple.", - ) - .option( - "-S, --suppress-errors", - "Suppress partial errors while traversing the collection.", - { depends: ["traverse"] }, - ) - .option("-r, --raw", "Print the fetched JSON-LD document as is.", { - conflicts: ["compact", "expand"], - }) - .option("-C, --compact", "Compact the fetched JSON-LD document.", { - conflicts: ["raw", "expand"], - }) - .option("-e, --expand", "Expand the fetched JSON-LD document.", { - conflicts: ["raw", "compact"], - }) - .option("-u, --user-agent ", "The custom User-Agent header value.") - .option( - "-s, --separator ", - "Specify the separator between adjacent output objects or " + - "collection items.", - { default: "----" }, - ) - .option( - "-o, --output ", - "Specify the output file path.", - ) - .option( - "-T, --timeout ", - "Set timeout for network requests in seconds.", - ) - .action(async (options, ...urls: string[]) => { - if (urls.length < 1) { - console.error("At least one URL or actor handle must be provided."); - Deno.exit(1); - } else if (options.traverse && urls.length > 1) { - console.error( - "The -t/--traverse option cannot be used with multiple arguments.", - ); - Deno.exit(1); - } +function handleTimeoutError( + spinner: { fail: (text: string) => void }, + timeoutSeconds?: number, + url?: string, +): void { + const urlText = url ? ` for: ${colors.red(url)}` : ""; + spinner.fail(`Request timed out after ${timeoutSeconds} seconds${urlText}.`); + printError( + message`Try increasing the timeout with -T/--timeout option or check network connectivity.`, + ); +} - const spinner = ora({ - text: `Looking up the ${ - options.traverse ? "collection" : urls.length > 1 ? "objects" : "object" - }...`, - discardStdin: false, - }).start(); - let server: TemporaryServer | undefined = undefined; - const baseDocumentLoader = await getDocumentLoader({ - userAgent: options.userAgent, - }); - const documentLoader = wrapDocumentLoaderWithTimeout( - baseDocumentLoader, - options.timeout, - ); - const baseContextLoader = await getContextLoader({ - userAgent: options.userAgent, - }); - const contextLoader = wrapDocumentLoaderWithTimeout( - baseContextLoader, - options.timeout, +export async function runLookup(command: InferValue) { + // FIXME: Implement -t, --traverse when multiple URLs are provided + if (command.urls.length < 1) { + printError(message`At least one URL or actor handle must be provided.`); + process.exit(1); + } else if (command.traverse && command.urls.length > 1) { + printError( + message`The -t/--traverse option cannot be used with multiple arguments.`, ); - let authLoader: DocumentLoader | undefined = undefined; - if (options.authorizedFetch) { - spinner.text = "Generating a one-time key pair..."; - const key = await generateCryptoKeyPair(); - spinner.text = "Spinning up a temporary ActivityPub server..."; - server = await spawnTemporaryServer((req) => { - const serverUrl = server?.url ?? new URL("http://localhost/"); - if (new URL(req.url).pathname == "/.well-known/webfinger") { - const jrd: ResourceDescriptor = { - subject: `acct:${serverUrl.hostname}@${serverUrl.hostname}`, - aliases: [serverUrl.href], - links: [ - { - rel: "self", - href: serverUrl.href, - type: "application/activity+json", - }, - ], - }; - return new Response(JSON.stringify(jrd), { - headers: { "Content-Type": "application/jrd+json" }, - }); - } - return respondWithObject( - new Application({ - id: serverUrl, - preferredUsername: serverUrl?.hostname, - publicKey: new CryptographicKey({ - id: new URL("#main-key", serverUrl), - owner: serverUrl, - publicKey: key.publicKey, - }), - manuallyApprovesFollowers: true, - inbox: new URL("/inbox", serverUrl), - outbox: new URL("/outbox", serverUrl), - }), - { contextLoader }, - ); - }); - const baseAuthLoader = getAuthenticatedDocumentLoader( - { - keyId: new URL("#main-key", server.url), - privateKey: key.privateKey, - }, - { - specDeterminer: { - determineSpec() { - return options.firstKnock; - }, - rememberSpec() { - }, - }, - }, - ); - authLoader = wrapDocumentLoaderWithTimeout( - baseAuthLoader, - options.timeout, - ); - } + process.exit(1); + } - spinner.text = `Looking up the ${ - options.traverse ? "collection" : urls.length > 1 ? "objects" : "object" - }...`; + // Enable Debug mode if requested + if (command.debug) { + await configureLogging(); + } - if (options.traverse) { - const url = urls[0]; - let collection: APObject | null; - try { - collection = await lookupObject(url, { - documentLoader: authLoader ?? documentLoader, - contextLoader, - userAgent: options.userAgent, + const spinner = ora({ + text: `Looking up the ${ + command.traverse + ? "collection" + : command.urls.length > 1 + ? "objects" + : "object" + }...`, + discardStdin: false, + }).start(); + + let server: TemporaryServer | undefined = undefined; + const baseDocumentLoader = await getDocumentLoader({ + userAgent: command.userAgent, + }); + const documentLoader = wrapDocumentLoaderWithTimeout( + baseDocumentLoader, + command.timeout, + ); + const baseContextLoader = await getContextLoader({ + userAgent: command.userAgent, + }); + const contextLoader = wrapDocumentLoaderWithTimeout( + baseContextLoader, + command.timeout, + ); + + let authLoader: DocumentLoader | undefined = undefined; + + if (command.authorizedFetch) { + spinner.text = "Generating a one-time key pair..."; + const key = await generateCryptoKeyPair(); + spinner.text = "Spinning up a temporary ActivityPub server..."; + server = await spawnTemporaryServer((req) => { + const serverUrl = server?.url ?? new URL("http://localhost/"); + if (new URL(req.url).pathname == "/.well-known/webfinger") { + const jrd: ResourceDescriptor = { + subject: `acct:${serverUrl.hostname}@${serverUrl.hostname}`, + aliases: [serverUrl.href], + links: [ + { + rel: "self", + href: serverUrl.href, + type: "application/activity+json", + }, + ], + }; + return new Response(JSON.stringify(jrd), { + headers: { "Content-Type": "application/jrd+json" }, }); - } catch (error) { - if (error instanceof Deno.errors.TimedOut) { - handleTimeoutError(spinner, options.timeout, url); - } else { - spinner.fail(`Failed to fetch object: ${colors.red(url)}.`); - if (authLoader == null) { - console.error( - "It may be a private object. Try with -a/--authorized-fetch.", - ); - } - } - await server?.close(); - Deno.exit(1); } - if (collection == null) { + return respondWithObject( + new Application({ + id: serverUrl, + preferredUsername: serverUrl?.hostname, + publicKey: new CryptographicKey({ + id: new URL("#main-key", serverUrl), + owner: serverUrl, + publicKey: key.publicKey, + }), + manuallyApprovesFollowers: true, + inbox: new URL("/inbox", serverUrl), + outbox: new URL("/outbox", serverUrl), + }), + { contextLoader }, + ); + }); + const baseAuthLoader = getAuthenticatedDocumentLoader( + { + keyId: new URL("#main-key", server.url), + privateKey: key.privateKey, + }, + { + specDeterminer: { + determineSpec() { + return command.firstKnock; + }, + rememberSpec() { + }, + }, + }, + ); + authLoader = wrapDocumentLoaderWithTimeout( + baseAuthLoader, + command.timeout, + ); + } + + spinner.text = `Looking up the ${ + command.traverse + ? "collection" + : command.urls.length > 1 + ? "objects" + : "object" + }...`; + + if (command.traverse) { + const url = command.urls[0]; + let collection: APObject | null; + try { + collection = await lookupObject(url, { + documentLoader: authLoader ?? documentLoader, + contextLoader, + userAgent: command.userAgent, + }); + } catch (error) { + if (error instanceof TimeoutError) { + handleTimeoutError(spinner, command.timeout, url); + } else { spinner.fail(`Failed to fetch object: ${colors.red(url)}.`); if (authLoader == null) { - console.error( - "It may be a private object. Try with -a/--authorized-fetch.", + printError( + message`It may be a private object. Try with -a/--authorized-fetch.`, ); } - await server?.close(); - Deno.exit(1); } - if (!(collection instanceof Collection)) { - spinner.fail( - `Not a collection: ${colors.red(url)}. ` + - "The -t/--traverse option requires a collection.", + await server?.close(); + process.exit(1); + } + if (collection == null) { + spinner.fail(`Failed to fetch object: ${colors.red(url)}.`); + if (authLoader == null) { + printError( + message`It may be a private object. Try with -a/--authorized-fetch.`, ); - await server?.close(); - Deno.exit(1); - } - spinner.succeed(`Fetched collection: ${colors.green(url)}.`); - try { - let i = 0; - for await ( - const item of traverseCollection(collection, { - documentLoader: authLoader ?? documentLoader, - contextLoader, - suppressError: options.suppressErrors, - }) - ) { - if (!options.output && i > 0) console.log(options.separator); - await writeObjectToStream(item, options, contextLoader); - i++; - } - } catch (error) { - logger.error("Failed to complete the traversal: {error}", { error }); - if (error instanceof Deno.errors.TimedOut) { - handleTimeoutError(spinner, options.timeout); - } else { - spinner.fail("Failed to complete the traversal."); - if (authLoader == null) { - console.error( - "It may be a private object. Try with -a/--authorized-fetch.", - ); - } else { - console.error( - "Use the -S/--suppress-errors option to suppress partial errors.", - ); - } - } - await server?.close(); - Deno.exit(1); } - spinner.succeed("Successfully fetched all items in the collection."); - await server?.close(); - Deno.exit(0); + process.exit(1); } - - const promises: Promise[] = []; - for (const url of urls) { - promises.push( - lookupObject( - url, - { - documentLoader: authLoader ?? documentLoader, - contextLoader, - userAgent: options.userAgent, - }, - ).catch((error) => { - if (error instanceof Deno.errors.TimedOut) { - handleTimeoutError(spinner, options.timeout, url); - } - throw error; - }), + if (!(collection instanceof Collection)) { + spinner.fail( + `Not a collection: ${colors.red(url)}. ` + + "The -t/--traverse option requires a collection.", ); + await server?.close(); + process.exit(1); } + spinner.succeed(`Fetched collection: ${colors.green(url)}.`); - let objects: (APObject | null)[]; try { - objects = await Promise.all(promises); - } catch (_error) { - await server?.close(); - Deno.exit(1); - } - let success = true; - let i = 0; - for (const object of objects) { - const url = urls[i]; - if (i > 0) console.log(options.separator); - i++; - try { - if (object == null) { - spinner.fail(`Failed to fetch object: ${colors.red(url)}.`); - if (authLoader == null) { - console.error( - "It may be a private object. Try with -a/--authorized-fetch.", - ); - } - success = false; + let i = 0; + for await ( + const item of traverseCollection(collection, { + documentLoader: authLoader ?? documentLoader, + contextLoader, + suppressError: command.suppressErrors, + }) + ) { + if (!command.output && i > 0) print(message`${command.separator}`); + await writeObjectToStream( + item, + command.output, + command.format, + contextLoader, + ); + i++; + } + } catch (error) { + logger.error("Failed to complete the traversal: {error}", { error }); + if (error instanceof TimeoutError) { + handleTimeoutError(spinner, command.timeout); + } else { + spinner.fail("Failed to complete the traversal."); + if (authLoader == null) { + printError( + message`It may be a private object. Try with -a/--authorized-fetch.`, + ); } else { - spinner.succeed(`Fetched object: ${colors.green(url)}.`); - await writeObjectToStream(object, options, contextLoader); - if (i < urls.length - 1) { - console.log(options.separator); - } + printError( + message`Use the -S/--suppress-errors option to suppress partial errors.`, + ); } - } catch (_) { - success = false; } + await server?.close(); + process.exit(1); } - if (success) { - spinner.succeed( - urls.length > 1 - ? "Successfully fetched all objects." - : "Successfully fetched the object.", - ); - } + spinner.succeed("Successfully fetched all items in the collection."); + await server?.close(); - if (!success) { - Deno.exit(1); - } - if (success && options.output) { - spinner.succeed( - `Successfully wrote output to ${colors.green(options.output)}.`, + process.exit(0); + } + + const promises: Promise[] = []; + + for (const url of command.urls) { + promises.push( + lookupObject(url, { + documentLoader: authLoader ?? documentLoader, + contextLoader, + userAgent: command.userAgent, + }).catch((error) => { + if (error instanceof TimeoutError) { + handleTimeoutError(spinner, command.timeout, url); + } + throw error; + }), + ); + } + + let objects: (APObject | null)[]; + try { + objects = await Promise.all(promises); + } catch (_error) { + //TODO: implement -a --authorized-fetch + // await server?.close(); + process.exit(1); + } + + spinner.stop(); + let success = true; + let i = 0; + for (const obj of objects) { + const url = command.urls[i]; + if (i > 0) print(message`${command.separator}`); + i++; + if (obj == null) { + spinner.fail(`Failed to fetch ${colors.red(url)}`); + if (authLoader == null) { + printError( + message`It may be a private object. Try with -a/--authorized-fetch.`, + ); + } + success = false; + } else { + spinner.succeed(`Fetched object: ${colors.green(url)}`); + await writeObjectToStream( + obj, + command.output, + command.format, + contextLoader, ); + if (i < command.urls.length - 1) { + print(message`${command.separator}`); + } } - }); - -// cSpell: ignore sigspec + } + if (success) { + spinner.succeed( + command.urls.length > 1 + ? "Successfully fetched all objects." + : "Successfully fetched the object.", + ); + } + await server?.close(); + if (!success) { + process.exit(1); + } + if (success && command.output) { + spinner.succeed( + `Successfully wrote output to ${colors.green(command.output)}.`, + ); + } +} diff --git a/packages/cli/src/mod.ts b/packages/cli/src/mod.ts index 8d9cb0c5d..f952fa360 100644 --- a/packages/cli/src/mod.ts +++ b/packages/cli/src/mod.ts @@ -1,79 +1,44 @@ -import { Command, CompletionsCommand, HelpCommand } from "@cliffy/command"; -import { getFileSink } from "@logtape/file"; -import { configure, getConsoleSink } from "@logtape/logtape"; -import { setColorEnabled } from "@std/fmt/colors"; -import { AsyncLocalStorage } from "node:async_hooks"; -import { DEFAULT_CACHE_DIR, setCacheDir } from "./cache.ts"; -import metadata from "../deno.json" with { type: "json" }; -import { command as inbox } from "./inbox.tsx"; -import { command as init } from "./init.ts"; -import { logFile, recordingSink } from "./log.ts"; -import { command as lookup } from "./lookup.ts"; -import { command as nodeinfo } from "./nodeinfo.ts"; -import { command as tunnel } from "./tunnel.ts"; -import { colorEnabled } from "./utils.ts"; -import { command as webfinger } from "./webfinger.ts"; +import { or } from "@optique/core"; +import { run } from "@optique/run"; +import { lookupCommand, runLookup } from "./lookup.ts"; +import { runWebFinger, webFingerCommand } from "./webfinger.ts"; +import { initCommand, runInit } from "./init.ts"; +import { inboxCommand, runInbox } from "./inbox.ts"; +import { nodeInfoCommand, runNodeInfo } from "./nodeinfo.ts"; +import { runTunnel, tunnelCommand } from "./tunnel.ts"; -setColorEnabled(colorEnabled); +const command = or( + initCommand, + webFingerCommand, + lookupCommand, + inboxCommand, + nodeInfoCommand, + tunnelCommand, +); -const command = new Command() - .name("fedify") - .version(metadata.version) - .help({ colors: colorEnabled }) - .globalEnv( - "FEDIFY_LOG_FILE=", - "An optional file to write logs to. " + - "Regardless of -d/--debug option, " + - "all levels of logs are written to this file. " + - "Note that this does not mute console logs.", - ) - .globalOption("-d, --debug", "Enable debug mode.", { - async action() { - await configure({ - sinks: { - console: getConsoleSink(), - recording: recordingSink, - file: logFile == null ? () => undefined : getFileSink(logFile), - }, - filters: {}, - loggers: [ - { - category: "fedify", - lowestLevel: "debug", - sinks: ["console", "recording", "file"], - }, - { - category: "localtunnel", - lowestLevel: "debug", - sinks: ["console", "file"], - }, - { - category: ["logtape", "meta"], - lowestLevel: "warning", - sinks: ["console", "file"], - }, - ], - reset: true, - contextLocalStorage: new AsyncLocalStorage(), - }); - }, - }) - .globalOption("-c, --cache-dir=", "Set the cache directory.", { - default: DEFAULT_CACHE_DIR, - async action(options) { - await setCacheDir(options.cacheDir); - }, - }) - .default("help") - .command("init", init) - .command("lookup", lookup) - .command("inbox", inbox) - .command("nodeinfo", nodeinfo) - .command("tunnel", tunnel) - .command("completions", new CompletionsCommand()) - .command("webfinger", webfinger) - .command("help", new HelpCommand().global()); - -if (import.meta.main) { - await command.parse(Deno.args); +async function main() { + const result = run(command, { + programName: "fedify", + help: "both", + }); + if (result.command === "init") { + runInit(result); + } + if (result.command === "lookup") { + await runLookup(result); + } + if (result.command === "webfinger") { + runWebFinger(result); + } + if (result.command === "inbox") { + runInbox(result); + } + if (result.command === "nodeinfo") { + runNodeInfo(result); + } + if (result.command === "tunnel") { + runTunnel(result); + } } + +await main(); diff --git a/packages/cli/src/nodeinfo.test.ts b/packages/cli/src/nodeinfo.test.ts deleted file mode 100644 index 14e0d42d0..000000000 --- a/packages/cli/src/nodeinfo.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { assertEquals } from "@std/assert"; -import fetchMock from "fetch-mock"; -import { getAsciiArt, getFaviconUrl, Jimp, rgbTo256Color } from "./nodeinfo.ts"; - -const HTML_WITH_SMALL_ICON = ` - - - - Test Site - - - -Test - -`; - -Deno.test("getFaviconUrl - small favicon.ico and apple-touch-icon.png", async () => { - fetchMock.spyGlobal(); - - fetchMock.get("https://example.com/", { - body: HTML_WITH_SMALL_ICON, - headers: { "Content-Type": "text/html" }, - }); - - const result = await getFaviconUrl("https://example.com/"); - assertEquals(result.href, "https://example.com/apple-touch-icon.png"); - - fetchMock.hardReset(); -}); - -const HTML_WITH_ICON = ` - - - - Test Site - - - -Test - -`; - -Deno.test("getFaviconUrl - favicon.ico and apple-touch-icon.png", async () => { - fetchMock.spyGlobal(); - - fetchMock.get("https://example.com/", { - body: HTML_WITH_ICON, - headers: { "Content-Type": "text/html" }, - }); - - const result = await getFaviconUrl("https://example.com/"); - assertEquals(result.href, "https://example.com/favicon.ico"); - - fetchMock.hardReset(); -}); - -const HTML_WITH_SVG_ONLY = ` - - - - Test Site - - -Test - -`; - -Deno.test("getFaviconUrl - svg icons only falls back to /favicon.ico", async () => { - fetchMock.spyGlobal(); - - fetchMock.get("https://example.com/", { - body: HTML_WITH_SVG_ONLY, - headers: { "Content-Type": "text/html" }, - }); - - const result = await getFaviconUrl("https://example.com/"); - assertEquals(result.href, "https://example.com/favicon.ico"); - - fetchMock.hardReset(); -}); - -const HTML_WITHOUT_ICON = ` - - - - Test Site - -Test - -`; - -Deno.test("getFaviconUrl - falls back to /favicon.ico", async () => { - fetchMock.spyGlobal(); - - fetchMock.get("https://example.com/", { - body: HTML_WITHOUT_ICON, - headers: { "Content-Type": "text/html" }, - }); - - const result = await getFaviconUrl("https://example.com/"); - assertEquals(result.href, "https://example.com/favicon.ico"); - - fetchMock.hardReset(); -}); - -Deno.test("rgbTo256Color - check RGB cube", () => { - const CUBE_VALUES = [0, 95, 135, 175, 215, 255]; - const colors: Array<{ r: number; g: number; b: number }> = []; - - for (let r = 0; r < 6; r++) { - for (let g = 0; g < 6; g++) { - for (let b = 0; b < 6; b++) { - colors.push({ - r: CUBE_VALUES[r], - g: CUBE_VALUES[g], - b: CUBE_VALUES[b], - }); - } - } - } - - // Expected color indices for the above colors (16-231) - // RGB cube: 6x6x6 = 216 colors, indices 16-231 - const expected_color_idx = Array.from( - { length: colors.length }, - (_, i) => 16 + i, - ); - - const results = colors.map((color) => - rgbTo256Color(color.r, color.g, color.b) - ); - assertEquals(results, expected_color_idx); -}); - -Deno.test("rgbTo256Color - check grayscale", () => { - const grayscale = Array.from({ length: 24 }).map( - (_, idx) => ({ - r: 8 + idx * 10, - g: 8 + idx * 10, - b: 8 + idx * 10, - }), - ); - - const expected_gray_idx = Array.from( - { length: grayscale.length }, - (_, i) => 232 + i, - ); - - const results = grayscale.map((GRAY) => - rgbTo256Color(GRAY.r, GRAY.g, GRAY.b) - ); - assertEquals(results, expected_gray_idx); -}); - -async function createTestImage( - color: number, -): Promise>> { - const image = new Jimp({ width: 1, height: 1, color }); - const imageBuffer = await image.getBuffer("image/webp"); - return Jimp.read(imageBuffer); -} - -Deno.test("getAsciiArt - Darkest Letter without color support", async () => { - const blackResult = getAsciiArt( - await createTestImage(0x000000ff), - 1, - "none", - ); - - assertEquals(blackResult, "█"); -}); - -Deno.test("getAsciiArt - Brightest Letter without color support", async () => { - const whiteResult = getAsciiArt( - await createTestImage(0xffffffff), - 1, - "none", - ); - - assertEquals(whiteResult, " "); -}); - -Deno.test("getAsciiArt - Darkest Letter with 256 color support", async () => { - const blackResult = getAsciiArt( - await createTestImage(0x000000ff), - 1, - "256color", - ); - - assertEquals(blackResult, "\u001b[38;5;16m█\u001b[39m"); -}); - -Deno.test("getAsciiArt - Brightest Letter with 256 color support", async () => { - const whiteResult = getAsciiArt( - await createTestImage(0xffffffff), - 1, - "256color", - ); - - assertEquals(whiteResult, "\u001b[38;5;231m \u001b[39m"); -}); - -Deno.test("getAsciiArt - Darkest Letter with true color support", async () => { - const blackResult = getAsciiArt( - await createTestImage(0x000000ff), - 1, - "truecolor", - ); - - assertEquals(blackResult, "\u001b[38;2;0;0;0m█\u001b[39m"); -}); - -Deno.test("getAsciiArt - Brightest Letter with true color support", async () => { - const whiteResult = getAsciiArt( - await createTestImage(0xffffffff), - 1, - "truecolor", - ); - - assertEquals(whiteResult, "\u001b[38;2;255;255;255m \u001b[39m"); -}); diff --git a/packages/cli/src/nodeinfo.ts b/packages/cli/src/nodeinfo.ts index 33771b48f..2dc11c38a 100644 --- a/packages/cli/src/nodeinfo.ts +++ b/packages/cli/src/nodeinfo.ts @@ -1,403 +1,37 @@ -import { Command } from "@cliffy/command"; -import { formatSemVer, getNodeInfo, getUserAgent } from "@fedify/fedify"; +import { + command as Command, + constant, + type InferValue, + merge, + message, + object, +} from "@optique/core"; import { createJimp } from "@jimp/core"; import webp from "@jimp/wasm-webp"; -import { getLogger } from "@logtape/logtape"; -import * as colors from "@std/fmt/colors"; -import { isICO, parseICO } from "icojs"; -import { defaultFormats, defaultPlugins, intToRGBA } from "jimp"; -import ora from "ora"; -import { formatObject } from "./utils.ts"; - -const logger = getLogger(["fedify", "cli", "nodeinfo"]); - -export const command = new Command() - .alias("node") - .arguments("") - .description( - "Get information about a remote node using the NodeInfo protocol. " + - "The argument is the hostname of the remote node, or the URL of the " + - "remote node.", - ) - .option("-r, --raw", "Print the fetched NodeInfo document as is.") - .option( - "-b, --best-effort", - "Try to parse the NodeInfo document even if it is invalid.", - { conflicts: ["raw"] }, - ) - .option( - "--no-favicon", - "Do not display the favicon of the node.", - { conflicts: ["raw"] }, - ) - .option( - "-m, --metadata", - "Print metadata fields of the NodeInfo document.", - { conflicts: ["raw"] }, - ) - .option("-u, --user-agent ", "The custom User-Agent header value.") - .action(async (options, host: string) => { - const command = Deno.args.find((arg) => - arg === "node" || arg === "nodeinfo" - ); - if (command === "node") { - console.warn( - "Warning: `fedify node` will be deprecated in Fedify 2.0.0. Use `fedify nodeinfo` instead.", - ); - } - - const spinner = ora({ - text: "Fetching a NodeInfo document...", - discardStdin: false, - }).start(); - const url = new URL(URL.canParse(host) ? host : `https://${host}/`); - if (options.raw) { - const nodeInfo = await getNodeInfo(url, { - parse: "none", - userAgent: options.userAgent, - }); - if (nodeInfo === undefined) { - spinner.fail("No NodeInfo document found."); - console.error("No NodeInfo document found."); - Deno.exit(1); - } - spinner.succeed("NodeInfo document fetched."); - console.log(formatObject(nodeInfo, undefined, true)); - return; - } - const nodeInfo = await getNodeInfo(url, { - parse: options.bestEffort ? "best-effort" : "strict", - userAgent: options.userAgent, - }); - logger.debug("NodeInfo document: {nodeInfo}", { nodeInfo }); - if (nodeInfo == undefined) { - spinner.fail("No NodeInfo document found or it is invalid."); - console.error("No NodeInfo document found or it is invalid."); - if (!options.bestEffort) { - console.error( - "Use the -b/--best-effort option to try to parse the document anyway.", - ); - } - Deno.exit(1); - } - let layout: string[]; - let defaultWidth = 0; - if (options.favicon) { - spinner.text = "Fetching the favicon..."; - try { - const faviconUrl = await getFaviconUrl(url, options.userAgent); - const response = await fetch(faviconUrl, { - headers: { - "User-Agent": options.userAgent == null - ? getUserAgent() - : options.userAgent, - }, - }); - if (response.ok) { - const contentType = response.headers.get("Content-Type"); - let buffer: ArrayBuffer = await response.arrayBuffer(); - if ( - contentType === "image/vnd.microsoft.icon" || - contentType === "image/x-icon" || - isICO(buffer) - ) { - const images = await parseICO(buffer); - if (images.length < 1) { - throw new Error("No images found in the ICO file."); - } - buffer = images[0].buffer; - } - const image = await Jimp.read(buffer); - const colorSupport = checkTerminalColorSupport(); - layout = getAsciiArt(image, DEFAULT_IMAGE_WIDTH, colorSupport) - .split("\n").map((line) => ` ${line} `); - defaultWidth = 41; - } else { - logger.error( - "Failed to fetch the favicon: {status} {statusText}", - { status: response.status, statusText: response.statusText }, - ); - layout = [""]; - } - } catch (error) { - logger.error( - "Failed to fetch or render the favicon: {error}", - { error }, - ); - layout = [""]; - } - } else { - layout = [""]; - } - spinner.succeed("NodeInfo document fetched."); - console.log(); - let i = 0; - const next = () => { - i++; - if (i >= layout.length) layout.push(" ".repeat(defaultWidth)); - return i; - }; - layout[i] += colors.bold(url.host); - layout[next()] += colors.dim("=".repeat(url.host.length)); - layout[next()] += colors.bold(colors.dim("Software:")); - layout[next()] += ` ${nodeInfo.software.name} v${ - formatSemVer(nodeInfo.software.version) - }`; - if (nodeInfo.software.homepage != null) { - layout[next()] += ` ${nodeInfo.software.homepage.href}`; - } - if (nodeInfo.software.repository != null) { - layout[next()] += " " + - colors.dim(nodeInfo.software.repository.href); - } - if (nodeInfo.protocols.length > 0) { - layout[next()] += colors.bold(colors.dim("Protocols:")); - for (const protocol of nodeInfo.protocols) { - layout[next()] += ` ${protocol}`; - } - } - if (nodeInfo.services?.inbound?.length ?? 0 > 0) { - layout[next()] += colors.bold(colors.dim("Inbound services:")); - for (const service of nodeInfo.services?.inbound ?? []) { - layout[next()] += ` ${service}`; - } - } - if (nodeInfo.services?.outbound?.length ?? 0 > 0) { - layout[next()] += colors.bold(colors.dim("Outbound services:")); - for (const service of nodeInfo.services?.outbound ?? []) { - layout[next()] += ` ${service}`; - } - } - if ( - nodeInfo.usage?.users != null && (nodeInfo.usage.users.total != null || - nodeInfo.usage.users.activeHalfyear != null || - nodeInfo.usage.users.activeMonth != null) - ) { - layout[next()] += colors.bold(colors.dim("Users:")); - if (nodeInfo.usage.users.total != null) { - layout[next()] += - ` ${nodeInfo.usage.users.total.toLocaleString("en-US")} ` + - colors.dim("(total)"); - } - if (nodeInfo.usage.users.activeHalfyear != null) { - layout[next()] += - ` ${nodeInfo.usage.users.activeHalfyear.toLocaleString("en-US")} ` + - colors.dim("(active half year)"); - } - if (nodeInfo.usage.users.activeMonth != null) { - layout[next()] += - ` ${nodeInfo.usage.users.activeMonth.toLocaleString("en-US")} ` + - colors.dim("(active month)"); - } - } - if (nodeInfo.usage?.localPosts != null) { - layout[next()] += colors.bold(colors.dim("Local posts: ")); - layout[next()] += " " + - nodeInfo.usage.localPosts.toLocaleString("en-US"); - } - if (nodeInfo.usage?.localComments != null) { - layout[next()] += colors.bold(colors.dim("Local comments:")); - layout[next()] += " " + - nodeInfo.usage.localComments.toLocaleString("en-US"); - } - if (nodeInfo.openRegistrations != null) { - layout[next()] += colors.bold(colors.dim("Open registrations:")); - layout[next()] += " " + (nodeInfo.openRegistrations ? "Yes" : "No"); - } - if ( - options.metadata && - nodeInfo.metadata != null && Object.keys(nodeInfo.metadata).length > 0 - ) { - layout[next()] += colors.bold(colors.dim("Metadata:")); - for (const [key, value] of Object.entries(nodeInfo.metadata)) { - layout[next()] += ` ${colors.dim(key + ":")} ${ - indent( - typeof value === "string" ? value : formatObject(value), - defaultWidth + 4 + key.length, - ) - }`; - } - } - console.log(layout.join("\n")); - }); - -function indent(text: string, depth: number) { - return text.replace(/\n/g, "\n" + " ".repeat(depth)); -} - -const LINK_REGEXP = - //ig; -const LINK_ATTRS_REGEXP = /(?:\s+([-a-z]+)=("[^"]*"|'[^']*'|[^\s]+))/ig; - -export async function getFaviconUrl( - url: string | URL, - userAgent?: string, -): Promise { - const response = await fetch(url, { - headers: { - "User-Agent": userAgent == null ? getUserAgent() : userAgent, - }, - }); - const text = await response.text(); - for (const match of text.matchAll(LINK_REGEXP)) { - const attrs: Record = {}; - for (const attrMatch of match[1].matchAll(LINK_ATTRS_REGEXP)) { - const [, key, value] = attrMatch; - attrs[key] = value.startsWith('"') || value.startsWith("'") - ? value.slice(1, -1) - : value; - } - const rel = attrs.rel?.toLowerCase()?.trim()?.split(/\s+/) ?? []; - if (!rel.includes("icon") && !rel.includes("apple-touch-icon")) continue; - if ("sizes" in attrs && attrs.sizes.match(/\d+x\d+/)) { - const [w, h] = attrs.sizes.split("x").map((v) => Number.parseInt(v)); - if (w < 38 || h < 19) continue; - } - if ("href" in attrs) { - if (attrs.href.endsWith(".svg")) continue; - return new URL(attrs.href, response.url); - } - } - return new URL("/favicon.ico", response.url); -} +import { defaultFormats, defaultPlugins } from "jimp"; +import { debugOption } from "./globals.ts"; export const Jimp = createJimp({ formats: [...defaultFormats, webp], plugins: defaultPlugins, }); -function checkTerminalColorSupport(): "truecolor" | "256color" | "none" { - // Check if colors are explicitly disabled - const noColor = Deno.env.get("NO_COLOR"); - if (noColor != null && noColor !== "") { - return "none"; - } - - // Check for true color (24-bit) support - const colorTerm = Deno.env.get("COLORTERM"); - if ( - colorTerm != null && - (colorTerm.includes("24bit") || colorTerm.includes("truecolor")) - ) { - return "truecolor"; - } - - // Check for xterm 256-color support - const term = Deno.env.get("TERM"); - if ( - term != null && - (term.includes("256color") || - term.includes("xterm") || - term === "screen" || - term === "tmux") - ) { - return "256color"; - } - - // Fallback: assume basic color support if TERM is set - if (term != null && term !== "dumb") { - return "256color"; - } - - // Check for Windows Terminal support - // FIXME: WT_SESSION is not a reliable way to check for Windows Terminal support - const isWindows = Deno.build.os === "windows"; - const isWT = Deno.env.get("WT_SESSION"); - if (isWindows && isWT != null && isWT !== "") { - return "truecolor"; - } - - return "none"; -} - -const DEFAULT_IMAGE_WIDTH = 38; - -const ASCII_CHARS = - // cSpell: disable - "█▓▒░@#B8&WM%*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "; -// cSpell: enable - -const CUBE_VALUES = [0, 95, 135, 175, 215, 255]; - -const findClosestIndex = (value: number): number => { - let minDiff = Infinity; - let closestIndex = 0; - for (let idx = 0; idx < CUBE_VALUES.length; idx++) { - const diff = Math.abs(value - CUBE_VALUES[idx]); - if (diff < minDiff) { - minDiff = diff; - closestIndex = idx; - } - } - return closestIndex; -}; - -export function rgbTo256Color(r: number, g: number, b: number): number { - // Check if it's a grayscale color first (when all RGB values are very close) - const gray = Math.round((r + g + b) / 3); - const isGrayscale = Math.abs(r - gray) <= 5 && Math.abs(g - gray) <= 5 && - Math.abs(b - gray) <= 5; - - // Handle grayscale colors (colors 232-255) - but exclude exact cube values - if (isGrayscale) { - const isExactCubeValue = CUBE_VALUES.includes(r) && r === g && g === b; - - if (!isExactCubeValue) { - if (gray < 8) return 232; // Darkest grayscale - if (gray > 238) return 255; // Brightest grayscale - - // Map to grayscale range 232-255 (24 levels) - // XTerm grayscale: 8, 18, 28, ..., 238 maps to 232, 233, 234, ..., 255 - const grayIndex = Math.round((gray - 8) / 10); - return Math.max(232, Math.min(255, 232 + grayIndex)); - } - } - - // Handle RGB colors (colors 16-231) - // XTerm 256 color cube values: [0, 95, 135, 175, 215, 255] - - const r6 = findClosestIndex(r); - const g6 = findClosestIndex(g); - const b6 = findClosestIndex(b); - - return 16 + (36 * r6) + (6 * g6) + b6; -} - -export function getAsciiArt( - image: Awaited>, - width = DEFAULT_IMAGE_WIDTH, - colorSupport: "truecolor" | "256color" | "none", -): string { - const ratio = image.width / image.height; - const height = Math.round( - width / ratio * 0.5, // Multiply by 0.5 because characters are taller than they are wide. - ); - image.resize({ w: width, h: height }); - let art = ""; - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const pixel = image.getPixelColor(x, y); - const color = intToRGBA(pixel); - if (color.a < 1) { - art += " "; - continue; - } - const brightness = (color.r + color.g + color.b) / 3; - const charIndex = Math.round( - (brightness / 255) * (ASCII_CHARS.length - 1), - ); - const char = ASCII_CHARS[charIndex]; - - if (colorSupport === "truecolor") { - art += colors.rgb24(char, color); - } else if (colorSupport === "256color") { - const colorIndex = rgbTo256Color(color.r, color.g, color.b); - art += colors.rgb8(char, colorIndex); - } else { - art += char; - } - } - if (y < height - 1) art += "\n"; - } - return art; +export const nodeInfoCommand = Command( + "nodeinfo", + merge( + object({ + command: constant("nodeinfo"), + }), + debugOption, + ), + { + description: + message`Get information about a remote node using the NodeInfo protocol. The argument is the hostname of the remote node, or the URL of the remote node.`, + }, +); + +export function runNodeInfo( + command: InferValue, +) { + console.debug(command); } diff --git a/packages/cli/src/run.mjs b/packages/cli/src/run.mjs deleted file mode 100644 index dee1f7f58..000000000 --- a/packages/cli/src/run.mjs +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node -import { spawnSync } from "node:child_process"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { main as install, isFile } from "./install.mjs"; - -async function main() { - const filename = fileURLToPath(import.meta.url); - const dirName = dirname(dirname(filename)); - const binPath = join( - dirName, - "bin", - process.platform === "win32" ? "fedify.exe" : "fedify", - ); - if (!await isFile(binPath)) await install(); - const result = spawnSync(binPath, process.argv.slice(2), { - stdio: "inherit", - }); - process.exit(result.status); -} - -await main(); diff --git a/packages/cli/src/table.ts b/packages/cli/src/table.ts deleted file mode 100644 index 945d3f670..000000000 --- a/packages/cli/src/table.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const tableStyle = { - top: "─", - topMid: "┬", - topLeft: "╭", - topRight: "╮", - bottom: "─", - bottomMid: "┴", - bottomLeft: "╰", - bottomRight: "╯", - left: "│", - leftMid: "├", - mid: "─", - midMid: "┼", - right: "│", - rightMid: "┤", - middle: "│", -}; diff --git a/packages/cli/src/tunnel.test.ts b/packages/cli/src/tunnel.test.ts deleted file mode 100644 index e9a9856cc..000000000 --- a/packages/cli/src/tunnel.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { Tunnel, TunnelOptions } from "@hongminhee/localtunnel"; -import { assert, assertEquals, assertFalse, assertRejects } from "@std/assert"; -import { assertSpyCall, stub } from "@std/testing/mock"; -import type { Ora } from "ora"; -import { command, tunnelAction } from "./tunnel.ts"; - -Deno.test("tunnel description", () => { - // Test that the command is properly configured - assert( - command.getDescription().includes( - "Expose a local HTTP server to the public internet using a secure tunnel.\n\n" + - "Note that the HTTP requests through the tunnel have X-Forwarded-* headers.", - ), - ); -}); - -Deno.test("tunnel command validates port argument", async () => { - const exitStub = stub(Deno, "exit", () => { - throw new Error("Process would exit"); - }); - - try { - await assertRejects( - () => command.parse(["invalid-port"]), - Error, - "Process would exit", - ); - assertSpyCall(exitStub, 0, { args: [2] }); - } finally { - exitStub.restore(); - } -}); - -Deno.test("tunnel successfully creates and manages tunnel", async () => { - // Track function calls - let openTunnelCalled = false; - let openTunnelArgs: TunnelOptions[] = []; - let startCalled = false; - let succeedCalled = false; - let succeedArgs: string[] = []; - let logArgs: string[] = []; - let errorArgs: string[] = []; - let addSignalListenerCalled = false; - let exitCalled = false; - - // Create a mock tunnel object - const mockTunnel = { - url: new URL("https://abc123.localhost.run"), - localPort: 3000, - pid: 12345, - close: () => Promise.resolve(), - }; - - // Create mock dependencies - const mockDeps = { - openTunnel: (args: TunnelOptions) => { - openTunnelCalled = true; - openTunnelArgs = [args]; - return Promise.resolve(mockTunnel as Tunnel); - }, - ora: () => ({ - start() { - startCalled = true; - return this; - }, - succeed(...args: string[]) { - succeedCalled = true; - succeedArgs = args; - return this; - }, - fail() { - return this; - }, - } as unknown as Ora), - console: { - log: (...args: string[]) => { - logArgs = args; - }, - error: (...args: string[]) => { - errorArgs = args; - }, - } as Console, - addSignalListener: (() => { - addSignalListenerCalled = true; - }) as typeof Deno.addSignalListener, - exit: (() => { - exitCalled = true; - }) as typeof Deno.exit, - }; - - await tunnelAction({ service: undefined }, 3000, mockDeps); - - // Verify all the expected interactions occurred - assert(openTunnelCalled); - assertEquals(openTunnelArgs, [{ port: 3000, service: undefined }]); - assert(startCalled); - assert(succeedCalled); - assertEquals(succeedArgs, [ - "Your local server at 3000 is now publicly accessible:\n", - ]); - assertEquals(logArgs, ["https://abc123.localhost.run/"]); - assertEquals(errorArgs, ["\nPress ^C to close the tunnel."]); - assert(addSignalListenerCalled); - assertFalse(exitCalled); -}); - -Deno.test("tunnel fails to create a secure tunnel and handles error", async () => { - const exitStub = stub(Deno, "exit", () => { - throw new Error("Process would exit"); - }); - - // Track function calls - let openTunnelCalled = false; - let openTunnelArgs: TunnelOptions[] = []; - let startCalled = false; - let failCalled = false; - let failArgs: string[] = []; - let addSignalListenerCalled = false; - - const tunnelError = new Error("Failed to create a secure tunnel."); - - // Create mock dependencies that simulate failure - const mockDeps = { - openTunnel: (args: TunnelOptions) => { - openTunnelCalled = true; - openTunnelArgs = [args]; - return Promise.reject(tunnelError); - }, - ora: () => ({ - start() { - startCalled = true; - return this; - }, - succeed() { - return this; - }, - fail(...args: string[]) { - failCalled = true; - failArgs = args; - return this; - }, - } as unknown as Ora), - console: { - log: () => {}, - error: () => {}, - } as Console, - addSignalListener: (() => { - addSignalListenerCalled = true; - }) as typeof Deno.addSignalListener, - exit: (() => { - throw new Error("Process would exit"); - }) as typeof Deno.exit, - }; - - try { - await assertRejects( - () => tunnelAction({ service: undefined }, 3000, mockDeps), - Error, - "Process would exit", - ); - } finally { - exitStub.restore(); - } - - // Verify error handling interactions - assert(openTunnelCalled); - assertEquals(openTunnelArgs, [{ port: 3000, service: undefined }]); - assert(startCalled); - assert(failCalled); - assertEquals(failArgs, ["Failed to create a secure tunnel."]); - assertFalse(addSignalListenerCalled); -}); diff --git a/packages/cli/src/tunnel.ts b/packages/cli/src/tunnel.ts index 502acbea3..2872cb8a9 100644 --- a/packages/cli/src/tunnel.ts +++ b/packages/cli/src/tunnel.ts @@ -1,53 +1,32 @@ -import { Command, EnumType } from "@cliffy/command"; -import { openTunnel, type Tunnel } from "@hongminhee/localtunnel"; -import ora from "ora"; +import { + argument, + command, + constant, + type InferValue, + integer, + merge, + message, + object, +} from "@optique/core"; +import { debugOption } from "./globals.ts"; -const service = new EnumType(["localhost.run", "serveo.net"]); - -export async function tunnelAction( - options: { service?: "localhost.run" | "serveo.net" }, - port: number, - deps: { - openTunnel: typeof openTunnel; - ora: typeof ora; - console: typeof console; - addSignalListener: typeof Deno.addSignalListener; - exit: typeof Deno.exit; - } = { - openTunnel, - ora, - console, - addSignalListener: Deno.addSignalListener, - exit: Deno.exit, +export const tunnelCommand = command( + "tunnel", + merge( + object({ + command: constant("tunnel"), + port: argument(integer({ metavar: "PORT", min: 0, max: 65_535 })), + }), + debugOption, + ), + { + description: + message`Expose a local HTTP server to the public internet using a secure tunnel.`, }, +); + +export function runTunnel( + command: InferValue, ) { - const spinner = deps.ora({ - text: "Creating a secure tunnel...", - discardStdin: false, - }).start(); - let tunnel: Tunnel; - try { - tunnel = await deps.openTunnel({ port, service: options.service }); - } catch { - spinner.fail("Failed to create a secure tunnel."); - deps.exit(1); - } - spinner.succeed( - `Your local server at ${port} is now publicly accessible:\n`, - ); - deps.console.log(tunnel.url.href); - deps.console.error("\nPress ^C to close the tunnel."); - deps.addSignalListener("SIGINT", async () => { - await tunnel.close(); - }); + console.debug(command); } - -export const command = new Command() - .type("service", service) - .arguments("") - .description( - "Expose a local HTTP server to the public internet using a secure tunnel.\n\n" + - "Note that the HTTP requests through the tunnel have X-Forwarded-* headers.", - ) - .option("-s, --service ", "The localtunnel service to use.") - .action(tunnelAction); diff --git a/packages/cli/src/webfinger.ts b/packages/cli/src/webfinger.ts index c118cc9d3..2333ada1e 100644 --- a/packages/cli/src/webfinger.ts +++ b/packages/cli/src/webfinger.ts @@ -1,118 +1,34 @@ -import { Command, ValidationError } from "@cliffy/command"; -import { toAcctUrl } from "@fedify/fedify/vocab"; -import { lookupWebFinger } from "@fedify/fedify/webfinger"; -import ora from "ora"; -import { formatObject } from "./utils.ts"; +import { + argument, + command, + constant, + type InferValue, + merge, + message, + multiple, + object, + string, +} from "@optique/core"; +import { debugOption } from "./globals.ts"; -export const command = new Command() - .arguments("<...resources:string>") - .description( - "Look up a WebFinger resource by resource. The argument can be multiple.", - ) - .option( - "-a, --user-agent ", - "The user agent to use for the request.", - ) - .option( - "-p, --allow-private-address", - "Allow private IP addresses in the URL.", - ) - .option( - "--max-redirection ", - "Maximum number of redirections to follow.", - { default: 5 }, - ) - .action(async (options, ...resources: string[]) => { - if (options.maxRedirection < 0) { // Validate maxRedirection option - throw new ValidationError( - `Option --max-redirection must be greater than or equal to 0, but got ${options.maxRedirection}.`, - ); - } +export const webFingerCommand = command( + "webfinger", + merge( + object({ + command: constant("webfinger"), + resources: multiple(argument(string({ metavar: "RESOURCE" }), { + description: message`WebFinger resource(s) to look up.`, + })), + }), + debugOption, + ), + { + description: message`Look up WebFinger resources.`, + }, +); - for (const resource of resources) { - const spinner = ora({ // Create a spinner for the lookup process - text: `Looking up WebFinger for ${resource}`, - discardStdin: false, - }).start(); - try { - const url = convertUrlIfHandle(resource); // Convert resource to URL - const webFinger = await lookupWebFinger(url, options) ?? // Look up WebFinger - new NotFoundError(resource).throw(); // throw NotFoundError if not found - - spinner.succeed(`WebFinger found for ${resource}:`); // Succeed the spinner - console.log(formatObject(webFinger, undefined, true)); // Print the WebFinger - } catch (error) { - if (error instanceof InvalidHandleError) { // If the handle format is invalid, - spinner.fail(`Invalid handle format: ${error.handle}`); // log error message with handle - } else if (error instanceof NotFoundError) { // If the resource is not found, - spinner.fail(`Resource not found: ${error.resource}`); // log not found message - } else if (error instanceof Error) { - spinner.fail( // For other errors, log the error message - `Error looking up WebFinger for ${resource}: ${error}`, - ); - } - } - } - }); - -/** - * Converts a handle or URL to a URL object. - * If the input is a valid URL, it returns the URL object. - * If the input is a handle in the format `@username@domain`, it converts it to a URL. - * @param handleOrUrl The handle or URL to convert. - * @returns A URL object representing the handle or URL. - */ -function convertUrlIfHandle(handleOrUrl: string): URL { - try { - return new URL(handleOrUrl); // Try to convert the input to a URL - } catch { - return convertHandleToUrl(handleOrUrl); // If it fails, treat it as a handle - } -} - -/** - * Custom error class for invalid handle formats. - * @param {string} handle The invalid handle that caused the error. - * @extends {Error} - */ -class InvalidHandleError extends Error { - constructor(public handle: string) { - super(`Invalid handle format: ${handle}`); - this.name = "InvalidHandleError"; - } - throw(): never { - throw this; - } -} - -/** - * Custom error class for not found resources. - * @param {string} resource The resource that was not found. - * @extends {Error} - */ -class NotFoundError extends Error { - constructor(public resource: string) { - super(`Resource not found: ${resource}`); - this.name = "NotFoundError"; - } - throw(): never { - throw this; - } -} - -/** - * Converts a handle in the format `@username@domain` to a URL. - * The resulting URL will be in the format `https://domain/@username`. - * @param handle The handle to convert, in the format `@username@domain`. - * @returns A URL object representing the handle. - * @throws {Error} If the handle format is invalid. - * @example - * ```ts - * const url = convertHandleToUrl("@username@domain.com"); - * console.log(url.toString()); // "https://domain.com/@username" - * ``` - */ -export function convertHandleToUrl(handle: string): URL { - return toAcctUrl(handle) ?? // Convert the handle to a URL - new InvalidHandleError(handle).throw(); // or throw an error if invalid +export function runWebFinger( + command: InferValue, +) { + console.debug(command); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50b379995..ad5a4a110 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ catalogs: version: 1.0.1 '@logtape/logtape': specifier: ^1.0.0 - version: 1.0.0 + version: 1.0.4 '@nestjs/common': specifier: ^11.0.1 version: 11.1.4 @@ -29,7 +29,7 @@ catalogs: version: 1.0.13 '@std/path': specifier: jsr:^1.0.6 - version: 1.1.0 + version: 1.1.2 '@sveltejs/kit': specifier: ^2.0.0 version: 2.36.2 @@ -140,10 +140,10 @@ importers: version: 0.5.1 '@logtape/file': specifier: 'catalog:' - version: 1.0.1(@logtape/logtape@1.0.0) + version: 1.0.1(@logtape/logtape@1.0.4) '@logtape/logtape': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.4 '@nestjs/common': specifier: 'catalog:' version: 11.1.4(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -227,7 +227,7 @@ importers: version: 1.6.3(patch_hash=56bd737eca4c1ba581d00bedd4fe307f6e48f782af59933eac54bb7d43206b99)(@algolia/client-search@5.29.0)(@types/node@22.16.0)(@types/react@18.3.23)(lightningcss@1.30.1)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.2) vitepress-plugin-group-icons: specifier: ^1.3.5 - version: 1.6.1(markdown-it@14.1.0)(vite@5.4.19(@types/node@22.16.0)(lightningcss@1.30.1)) + version: 1.6.1(markdown-it@14.1.0)(vite@7.1.3(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0)) vitepress-plugin-llms: specifier: ^1.1.0 version: 1.6.0 @@ -277,7 +277,7 @@ importers: version: link:../../packages/fedify '@logtape/logtape': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.4 express: specifier: 'catalog:' version: 4.21.2 @@ -351,7 +351,7 @@ importers: version: link:../../packages/fedify '@logtape/logtape': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.4 next: specifier: ^14.0.0 version: 14.2.30(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-7771d3a7-20240827(react@19.0.0-rc-7771d3a7-20240827))(react@19.0.0-rc-7771d3a7-20240827) @@ -553,7 +553,44 @@ importers: specifier: 'catalog:' version: 5.9.2 - packages/cli: {} + packages/cli: + dependencies: + '@cross/dir': + specifier: jsr:@cross/dir@^1.1.0 + version: '@jsr/cross__dir@1.1.0' + '@david/dax': + specifier: jsr:@david/dax@^0.43.2 + version: '@jsr/david__dax@0.43.2' + '@fedify/fedify': + specifier: 'workspace:' + version: link:../fedify + '@hongminhee/localtunnel': + specifier: ^0.3.0 + version: 0.3.0 + '@jimp/core': + specifier: ^1.6.0 + version: 1.6.0 + '@jimp/wasm-webp': + specifier: ^1.6.0 + version: 1.6.0 + '@optique/core': + specifier: ^0.3.0 + version: 0.3.0 + '@optique/run': + specifier: ^0.3.0 + version: 0.3.0 + '@std/fmt/colors': + specifier: jsr:@std/fmt@^0.224.0/colors + version: link:jsr:@std/fmt@^0.224.0/colors + cli-highlight: + specifier: ^2.1.11 + version: 2.1.11 + jimp: + specifier: ^1.6.0 + version: 1.6.0 + ora: + specifier: ^8.2.0 + version: 8.2.0 packages/elysia: dependencies: @@ -609,7 +646,7 @@ importers: version: 0.5.1 '@logtape/logtape': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.4 '@multiformats/base-x': specifier: ^4.0.1 version: 4.0.1 @@ -661,7 +698,7 @@ importers: version: '@jsr/std__assert@0.226.0' '@std/path': specifier: 'catalog:' - version: '@jsr/std__path@1.1.0' + version: '@jsr/std__path@1.1.2' '@std/url': specifier: jsr:1.0.0-rc.3 version: '@jsr/std__url@1.0.0-rc.3' @@ -776,7 +813,7 @@ importers: version: 0.5.1 '@logtape/logtape': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.4 postgres: specifier: 'catalog:' version: 3.4.7 @@ -801,7 +838,7 @@ importers: version: 0.5.1 '@logtape/logtape': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.4 ioredis: specifier: 'catalog:' version: 5.6.1 @@ -826,7 +863,7 @@ importers: version: link:../fedify '@logtape/logtape': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.4 es-toolkit: specifier: ^1.31.0 version: 1.39.5 @@ -1655,6 +1692,10 @@ packages: engines: {node: '>=6'} hasBin: true + '@hongminhee/localtunnel@0.3.0': + resolution: {integrity: sha512-GRn02MyJIal6DjgDiaLDQhaiTvYlqTLq97bpTNqp2qGnwy4c77pHm6kWPBdSOjmLGPCf2bb/USFKQBgUhafbuA==} + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} + '@hono/node-server@1.14.4': resolution: {integrity: sha512-DnxpshhYewr2q9ZN8ez/M5mmc3sucr8CT1sIgIy1bkeUXut9XWDkqHoFHRhWIQgkYnKpVRxunyhK7WzpJeJ6qQ==} engines: {node: '>=18.14.1'} @@ -1958,6 +1999,122 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@jimp/core@1.6.0': + resolution: {integrity: sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==} + engines: {node: '>=18'} + + '@jimp/diff@1.6.0': + resolution: {integrity: sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw==} + engines: {node: '>=18'} + + '@jimp/file-ops@1.6.0': + resolution: {integrity: sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ==} + engines: {node: '>=18'} + + '@jimp/js-bmp@1.6.0': + resolution: {integrity: sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw==} + engines: {node: '>=18'} + + '@jimp/js-gif@1.6.0': + resolution: {integrity: sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g==} + engines: {node: '>=18'} + + '@jimp/js-jpeg@1.6.0': + resolution: {integrity: sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA==} + engines: {node: '>=18'} + + '@jimp/js-png@1.6.0': + resolution: {integrity: sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg==} + engines: {node: '>=18'} + + '@jimp/js-tiff@1.6.0': + resolution: {integrity: sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw==} + engines: {node: '>=18'} + + '@jimp/plugin-blit@1.6.0': + resolution: {integrity: sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA==} + engines: {node: '>=18'} + + '@jimp/plugin-blur@1.6.0': + resolution: {integrity: sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw==} + engines: {node: '>=18'} + + '@jimp/plugin-circle@1.6.0': + resolution: {integrity: sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw==} + engines: {node: '>=18'} + + '@jimp/plugin-color@1.6.0': + resolution: {integrity: sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA==} + engines: {node: '>=18'} + + '@jimp/plugin-contain@1.6.0': + resolution: {integrity: sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ==} + engines: {node: '>=18'} + + '@jimp/plugin-cover@1.6.0': + resolution: {integrity: sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA==} + engines: {node: '>=18'} + + '@jimp/plugin-crop@1.6.0': + resolution: {integrity: sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang==} + engines: {node: '>=18'} + + '@jimp/plugin-displace@1.6.0': + resolution: {integrity: sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q==} + engines: {node: '>=18'} + + '@jimp/plugin-dither@1.6.0': + resolution: {integrity: sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ==} + engines: {node: '>=18'} + + '@jimp/plugin-fisheye@1.6.0': + resolution: {integrity: sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA==} + engines: {node: '>=18'} + + '@jimp/plugin-flip@1.6.0': + resolution: {integrity: sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg==} + engines: {node: '>=18'} + + '@jimp/plugin-hash@1.6.0': + resolution: {integrity: sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q==} + engines: {node: '>=18'} + + '@jimp/plugin-mask@1.6.0': + resolution: {integrity: sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA==} + engines: {node: '>=18'} + + '@jimp/plugin-print@1.6.0': + resolution: {integrity: sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A==} + engines: {node: '>=18'} + + '@jimp/plugin-quantize@1.6.0': + resolution: {integrity: sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg==} + engines: {node: '>=18'} + + '@jimp/plugin-resize@1.6.0': + resolution: {integrity: sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==} + engines: {node: '>=18'} + + '@jimp/plugin-rotate@1.6.0': + resolution: {integrity: sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw==} + engines: {node: '>=18'} + + '@jimp/plugin-threshold@1.6.0': + resolution: {integrity: sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w==} + engines: {node: '>=18'} + + '@jimp/types@1.6.0': + resolution: {integrity: sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg==} + engines: {node: '>=18'} + + '@jimp/utils@1.6.0': + resolution: {integrity: sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==} + engines: {node: '>=18'} + + '@jimp/wasm-webp@1.6.0': + resolution: {integrity: sha512-P0zUpK6n2XIAn8bt0F6rhSn1+FgteBTrL+TBb6Oqw8v5qEDJoNYkd6LlfZYN8YwtRBTBdZ8GFnWsg2Sar+qOkA==} + engines: {node: '>=18'} + '@jridgewell/gen-mapping@0.3.11': resolution: {integrity: sha512-C512c1ytBTio4MrpWKlJpyFHT6+qfFL8SZ58zBzJ1OOzUEjHeF1BtjY2fH7n4x/g2OV/KiiMLAivOp1DXmiMMw==} @@ -1984,6 +2141,36 @@ packages: resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} engines: {node: '>=12'} + '@jsquash/webp@1.5.0': + resolution: {integrity: sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw==} + + '@jsr/cross__deepmerge@1.0.0': + resolution: {integrity: sha512-AojyLcLUUfKRy+ZuNYD1c5W1EWVhAHqxwJs5OXxPBUgzerk5RsV0GGSxp31cEb4xOedD+Cz4KEdNNCtpRMekfw==, tarball: https://npm.jsr.io/~/11/@jsr/cross__deepmerge/1.0.0.tgz} + + '@jsr/cross__dir@1.1.0': + resolution: {integrity: sha512-AcsOTWumdnZ/oFohrBUhVRmH+s6UzCCKtXyYBhsJDJ8kDpC8M3uZlPlhXMCi/d6TIcog2CXo8Mh6EUcnerkZMA==, tarball: https://npm.jsr.io/~/11/@jsr/cross__dir/1.1.0.tgz} + + '@jsr/cross__env@1.0.3': + resolution: {integrity: sha512-tp28OiCalQDungF2MC7wiwN+Qgr4B9/gGN7o40zIt+ZVCilm6xM9ITnLr6oB3X+MY1ppVYFeOwmsBe3RfZC2rw==, tarball: https://npm.jsr.io/~/11/@jsr/cross__env/1.0.3.tgz} + + '@jsr/cross__runtime@1.2.1': + resolution: {integrity: sha512-/11gi9jNGA38w3vbRLqrjfz6LBU711+NZ9gYLxUFPsrGGwjOHwcJ1TSC2+fjIZbzJ21e733IEbvKX2sz8jKvmg==, tarball: https://npm.jsr.io/~/11/@jsr/cross__runtime/1.2.1.tgz} + + '@jsr/cross__utils@0.7.1': + resolution: {integrity: sha512-eR7g6g9DDd4Uuy+9+4LOgdwi6KOeMRQbMSEIwTQFFpZ5hh5SY1MxZWkUZyX5CuKW6FrOMV9UHNopC+ujEHn1yA==, tarball: https://npm.jsr.io/~/11/@jsr/cross__utils/0.7.1.tgz} + + '@jsr/david__console-static-text@0.3.0': + resolution: {integrity: sha512-5IkJzn/i5UBw/3Dxgk5OvcPwAbIrdcOxz52BYKgpmOdAcUUlElkWoC2eVWKf8a1AD2fJWxVj37JQ1BQpxzr/9g==, tarball: https://npm.jsr.io/~/11/@jsr/david__console-static-text/0.3.0.tgz} + + '@jsr/david__dax@0.43.2': + resolution: {integrity: sha512-RVY8x8FbcKRd1rF5FBIgB+c6vbf9c2hss7PsfdNp0Bc5F+duI1oVkgz62Ynfb9NHQ/DKprBXKCi1qfdWOz8kBQ==, tarball: https://npm.jsr.io/~/11/@jsr/david__dax/0.43.2.tgz} + + '@jsr/david__path@0.2.0': + resolution: {integrity: sha512-EkTLcM3E5O9biZyKomwn/P/YcvYYYDM60gWijIaACP+mxQz9uR7ThljXAVdmdiXnA8bYKIOH+aD4GV4mgin7LA==, tarball: https://npm.jsr.io/~/11/@jsr/david__path/0.2.0.tgz} + + '@jsr/david__which@0.4.1': + resolution: {integrity: sha512-cNHC/eAq33lrp2xIGOjx2OMnK7ad5naUq4HXqIDjSVN8Ak8j8bpfL6WCw4ruacIVe0WqFRFPKLnwrvv7KoLR8Q==, tarball: https://npm.jsr.io/~/11/@jsr/david__which/0.4.1.tgz} + '@jsr/std__assert@0.226.0': resolution: {integrity: sha512-xCuUFDfHkIZd96glKgjZbnYFqu6blu8Y53SyvDMlFDJm1Y/j+/FcW6xq7TzGFIaF5B9QecIlDfamfhzA8ZdVbg==, tarball: https://npm.jsr.io/~/11/@jsr/std__assert/0.226.0.tgz} @@ -1993,12 +2180,30 @@ packages: '@jsr/std__async@1.0.13': resolution: {integrity: sha512-GEApyNtzauJ0kEZ/GxebSkdEN0t29qJtkw+WEvzYTwkL6fHX8cq3YWzRjCqHu+4jMl+rpHiwyr/lfitNInntzA==, tarball: https://npm.jsr.io/~/11/@jsr/std__async/1.0.13.tgz} + '@jsr/std__bytes@1.0.6': + resolution: {integrity: sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==, tarball: https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz} + + '@jsr/std__fmt@1.0.8': + resolution: {integrity: sha512-miZHzj9OgjuajrcMKzpqNVwFb9O71UHZzV/FHVq0E0Uwmv/1JqXgmXAoBNPrn+MP0fHT3mMgaZ6XvQO7dam67Q==, tarball: https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.8.tgz} + + '@jsr/std__fs@1.0.19': + resolution: {integrity: sha512-TEjyE8g+46jPlu7dJHLrwc8NMGl8zfG+JjWxyNQyDbxP0RtqZ4JmYZfR9vy4RWYWJQbLpw6Kbt2n+K/2zAO/JA==, tarball: https://npm.jsr.io/~/11/@jsr/std__fs/1.0.19.tgz} + + '@jsr/std__internal@1.0.10': + resolution: {integrity: sha512-fmD6yKep/sMnB2yPQU/REZG7Z4N9SZwcUBNnceo4QkXk67l3JEfxHoROQ/YHeVSOmq6x55Ra6nuMjz2ib3nj3g==, tarball: https://npm.jsr.io/~/11/@jsr/std__internal/1.0.10.tgz} + '@jsr/std__internal@1.0.8': resolution: {integrity: sha512-C0HSkIOA4gZPhLT2YXBXYF51YTpWXWHYUgzs3PhRCVbeVpbJ3Iomghu/ylIG+SsIDGDwxUkSCvfOOveRNS/mKg==, tarball: https://npm.jsr.io/~/11/@jsr/std__internal/1.0.8.tgz} + '@jsr/std__io@0.225.2': + resolution: {integrity: sha512-QNImMbao6pKXvV4xpFkY2zviZi1r+1KpYzgMqaa2gHDPZhXQqlia/Og+VqMxxfAr8Pw6BF3tw/hSw3LrWWTRmA==, tarball: https://npm.jsr.io/~/11/@jsr/std__io/0.225.2.tgz} + '@jsr/std__path@1.1.0': resolution: {integrity: sha512-rnxGg/nJGfDbJO+xIJ9YEzLD7dCzjr3NHShf4dbnlt44WEYNwMjg+TcDO6F2NPHBnn/6iUFwbnNzysrZvyD1Og==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.1.0.tgz} + '@jsr/std__path@1.1.2': + resolution: {integrity: sha512-5hkOR1s5M7am02Bn9KS+SNMNwUSivz7t7/w2HBhFIfO7Eh8+mWilaZ+1tdanV9aaSHr4c99Zo4Da+cCSuzUOdA==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.1.2.tgz} + '@jsr/std__url@1.0.0-rc.3': resolution: {integrity: sha512-LdgttuY43p/yqvnwN6U5OZsnG4uTpu+xH4mzuH2qrHW6Jn1L+ToRL7WlCHSK2cp4of3Oit8QIwGyx1k1ay1LJQ==, tarball: https://npm.jsr.io/~/11/@jsr/std__url/1.0.0-rc.3.tgz} @@ -2010,8 +2215,8 @@ packages: peerDependencies: '@logtape/logtape': 1.0.1 - '@logtape/logtape@1.0.0': - resolution: {integrity: sha512-GOOiaJcHSJQfFt+khrtoxfQ29klDiG8UxrgC+lPt7K6HhwEMQnB47j7V0GQ6F8bnS7JIZgYQmbXb+yGUF2pKhA==} + '@logtape/logtape@1.0.4': + resolution: {integrity: sha512-YvNVrXIxVpnY528zoiEjX8PqTfr0UCtKXyssvaWL8AE+OByFTCooKuKMdPlm6g65YUI9fPXrHn4UnogSskABnA==} '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} @@ -2574,6 +2779,14 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@optique/core@0.3.0': + resolution: {integrity: sha512-URh4/zzVpjRbS/5D6ryBeW5sdQf4pAJbqo3dC6BAwpFRX4aEsQhngCLMWS19ily0YCdbEW7YPvwEoRFEAX8U8g==} + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} + + '@optique/run@0.3.0': + resolution: {integrity: sha512-FbbUzaTuPNfrcYmuV3Kqwi7q142Pmahg4K60z343Ud8Kwq68no37B4LXTXw03b2Fbb/z6q/TyH8i44bcQ8l4fQ==} + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} + '@oxc-project/runtime@0.75.0': resolution: {integrity: sha512-gzRmVI/vorsPmbDXt7GD4Uh2lD3rCOku/1xWPB4Yx48k0EP4TZmzQudWapjN4+7Vv+rgXr0RqCHQadeaMvdBuw==} engines: {node: '>=6.9.0'} @@ -3276,6 +3489,9 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + '@types/node@16.9.1': + resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} + '@types/node@20.19.11': resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==} @@ -3794,6 +4010,9 @@ packages: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} + any-base@1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -3875,6 +4094,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + await-to-js@3.0.0: + resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==} + engines: {node: '>=6.0.0'} + axe-core@4.10.3: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} @@ -3889,6 +4112,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -3899,6 +4125,9 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + bmp-ts@1.0.9: + resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==} + body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -3916,6 +4145,9 @@ packages: buffer-more-ints@1.0.0: resolution: {integrity: sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bun-types@1.2.17: resolution: {integrity: sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ==} @@ -3985,6 +4217,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.0: + resolution: {integrity: sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -4017,9 +4253,25 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -4441,6 +4693,9 @@ packages: emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@10.5.0: + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4728,6 +4983,10 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + exact-mirror@0.1.3: resolution: {integrity: sha512-yI62LpSby0ItzPJF05C4DRycVAoknRiCIDOLOCCs9zaEKylOXQtOFM3flX54S44swpRz584vk3P70yWQodsLlg==} peerDependencies: @@ -4736,6 +4995,9 @@ packages: '@sinclair/typebox': optional: true + exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -4812,6 +5074,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + file-type@21.0.0: resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} engines: {node: '>=20'} @@ -4900,6 +5166,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4918,6 +5188,9 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + gifwrap@0.10.1: + resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5018,6 +5291,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hono@4.8.3: resolution: {integrity: sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ==} engines: {node: '>=16.9.0'} @@ -5051,6 +5327,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + image-q@4.0.0: + resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -5157,6 +5436,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -5208,6 +5491,14 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -5245,6 +5536,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jimp@1.6.0: + resolution: {integrity: sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==} + engines: {node: '>=18'} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -5257,6 +5552,9 @@ packages: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5467,6 +5765,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -5674,6 +5976,10 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + miniflare@4.20250617.4: resolution: {integrity: sha512-IAoApFKxOJlaaFkym5ETstVX3qWzVt3xyqCDj6vSSTgEH3zxZJ5417jZGg8iQfMHosKCcQH1doPPqqnOZm/yrw==} engines: {node: '>=18.0.0'} @@ -5884,6 +6190,9 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -5891,6 +6200,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + oniguruma-to-es@2.3.0: resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} @@ -5904,6 +6217,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -5922,10 +6239,31 @@ packages: package-manager-detector@1.3.0: resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + + parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + + parse-bmfont-xml@1.1.6: + resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -5972,6 +6310,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -6005,6 +6347,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pixelmatch@5.3.0: + resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} + hasBin: true + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -6015,6 +6361,14 @@ packages: resolution: {integrity: sha512-WX0la7n7CbnguuaIQoT4Fc0IJckPDOUldzOwlZ0nwpOcySS+Six/tXBdc0RX17J5o1To0SAr3xDJjDLsOfDFQA==} engines: {node: '>=12.0.0'} + pngjs@6.0.0: + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} + engines: {node: '>=12.13.0'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} @@ -6200,6 +6554,10 @@ packages: printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -6284,6 +6642,14 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -6369,6 +6735,10 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -6443,6 +6813,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + scheduler@0.25.0-rc-7771d3a7-20240827: resolution: {integrity: sha512-n4nHmAoerbIOSrH24w0+fcdCUwQ4Npm7yXfsrn09FL01OWIaxpuo4P0rj3qPyLFgsJDbn18sWvLVB/e/KPnR+A==} @@ -6542,6 +6915,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-xml-to-json@1.2.3: + resolution: {integrity: sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==} + engines: {node: '>=20.12.2'} + sirv@3.0.1: resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} @@ -6581,6 +6958,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -6601,6 +6982,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -6624,6 +7009,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -6651,6 +7039,10 @@ packages: resolution: {integrity: sha512-or9w505RhhY66+uoe5YOC5QO/bRuATaoim3XTh+pGKx5VMWi/HDhMKuCjDLsLJouU2zg9Hf1nLPcNW7IHv80kQ==} engines: {node: '>=18'} + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + structured-field-values@2.0.4: resolution: {integrity: sha512-5zpJXYLPwW3WYUD/D58tQjIBs10l3Yx64jZfcKGs/RH79E2t9Xm/b9+ydwdMNVSksnsIY+HR/2IlQmgo0AcTAg==} @@ -6753,6 +7145,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} @@ -6768,6 +7163,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + token-types@6.0.3: resolution: {integrity: sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==} engines: {node: '>=14.16'} @@ -6987,6 +7386,9 @@ packages: urlpattern-polyfill@10.1.0: resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + utif2@4.1.0: + resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -7150,6 +7552,9 @@ packages: typescript: optional: true + wasm-feature-detect@1.8.0: + resolution: {integrity: sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -7225,6 +7630,17 @@ packages: x-forwarded-fetch@0.2.0: resolution: {integrity: sha512-2qsguQrLHFvP3/rnvxSSaw3DlK9eOOx9iyL+Qv+1YH8jzq6nbRk+P6gyFFNdWanyusyCHuE3/CnX3+gmLeYEeg==} + xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7249,10 +7665,18 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -7270,6 +7694,9 @@ packages: zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -7859,6 +8286,10 @@ snapshots: protobufjs: 7.5.3 yargs: 17.7.2 + '@hongminhee/localtunnel@0.3.0': + dependencies: + '@logtape/logtape': 1.0.4 + '@hono/node-server@1.14.4(hono@4.8.3)': dependencies: hono: 4.8.3 @@ -8101,6 +8532,200 @@ snapshots: dependencies: minipass: 7.1.2 + '@jimp/core@1.6.0': + dependencies: + '@jimp/file-ops': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + await-to-js: 3.0.0 + exif-parser: 0.1.12 + file-type: 16.5.4 + mime: 3.0.0 + + '@jimp/diff@1.6.0': + dependencies: + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + pixelmatch: 5.3.0 + + '@jimp/file-ops@1.6.0': {} + + '@jimp/js-bmp@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + bmp-ts: 1.0.9 + + '@jimp/js-gif@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + gifwrap: 0.10.1 + omggif: 1.0.10 + + '@jimp/js-jpeg@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + jpeg-js: 0.4.4 + + '@jimp/js-png@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + pngjs: 7.0.0 + + '@jimp/js-tiff@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + utif2: 4.1.0 + + '@jimp/plugin-blit@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-blur@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/utils': 1.6.0 + + '@jimp/plugin-circle@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-color@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + tinycolor2: 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-contain@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-cover@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-crop@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-displace@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-dither@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + + '@jimp/plugin-fisheye@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-flip@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-hash@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/js-bmp': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/js-tiff': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + any-base: 1.1.0 + + '@jimp/plugin-mask@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-print@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/types': 1.6.0 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.6 + simple-xml-to-json: 1.2.3 + zod: 3.25.76 + + '@jimp/plugin-quantize@1.6.0': + dependencies: + image-q: 4.0.0 + zod: 3.25.76 + + '@jimp/plugin-resize@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/types': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-rotate@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/plugin-threshold@1.6.0': + dependencies: + '@jimp/core': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-hash': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + zod: 3.25.76 + + '@jimp/types@1.6.0': + dependencies: + zod: 3.25.76 + + '@jimp/utils@1.6.0': + dependencies: + '@jimp/types': 1.6.0 + tinycolor2: 1.6.0 + + '@jimp/wasm-webp@1.6.0': + dependencies: + '@jsquash/webp': 1.5.0 + zod: 3.25.76 + '@jridgewell/gen-mapping@0.3.11': dependencies: '@jridgewell/sourcemap-codec': 1.5.3 @@ -8131,31 +8756,92 @@ snapshots: dependencies: jsbi: 4.3.2 + '@jsquash/webp@1.5.0': + dependencies: + wasm-feature-detect: 1.8.0 + + '@jsr/cross__deepmerge@1.0.0': {} + + '@jsr/cross__dir@1.1.0': + dependencies: + '@jsr/cross__env': 1.0.3 + '@jsr/cross__runtime': 1.2.1 + '@jsr/cross__utils': 0.7.1 + + '@jsr/cross__env@1.0.3': + dependencies: + '@jsr/cross__deepmerge': 1.0.0 + '@jsr/cross__runtime': 1.2.1 + + '@jsr/cross__runtime@1.2.1': {} + + '@jsr/cross__utils@0.7.1': + dependencies: + '@jsr/cross__runtime': 1.2.1 + + '@jsr/david__console-static-text@0.3.0': {} + + '@jsr/david__dax@0.43.2': + dependencies: + '@jsr/david__console-static-text': 0.3.0 + '@jsr/david__path': 0.2.0 + '@jsr/david__which': 0.4.1 + '@jsr/std__fmt': 1.0.8 + '@jsr/std__fs': 1.0.19 + '@jsr/std__io': 0.225.2 + '@jsr/std__path': 1.1.0 + + '@jsr/david__path@0.2.0': + dependencies: + '@jsr/std__fs': 1.0.19 + '@jsr/std__path': 1.1.0 + + '@jsr/david__which@0.4.1': {} + '@jsr/std__assert@0.226.0': dependencies: '@jsr/std__internal': 1.0.8 '@jsr/std__assert@1.0.13': dependencies: - '@jsr/std__internal': 1.0.8 + '@jsr/std__internal': 1.0.10 '@jsr/std__async@1.0.13': {} + '@jsr/std__bytes@1.0.6': {} + + '@jsr/std__fmt@1.0.8': {} + + '@jsr/std__fs@1.0.19': + dependencies: + '@jsr/std__internal': 1.0.10 + '@jsr/std__path': 1.1.2 + + '@jsr/std__internal@1.0.10': {} + '@jsr/std__internal@1.0.8': {} + '@jsr/std__io@0.225.2': + dependencies: + '@jsr/std__bytes': 1.0.6 + '@jsr/std__path@1.1.0': {} + '@jsr/std__path@1.1.2': + dependencies: + '@jsr/std__internal': 1.0.10 + '@jsr/std__url@1.0.0-rc.3': dependencies: '@jsr/std__path': 1.1.0 '@jsr/std__yaml@1.0.8': {} - '@logtape/file@1.0.1(@logtape/logtape@1.0.0)': + '@logtape/file@1.0.1(@logtape/logtape@1.0.4)': dependencies: - '@logtape/logtape': 1.0.0 + '@logtape/logtape': 1.0.4 - '@logtape/logtape@1.0.0': {} + '@logtape/logtape@1.0.4': {} '@lukeed/csprng@1.1.0': {} @@ -8786,6 +9472,12 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@optique/core@0.3.0': {} + + '@optique/run@0.3.0': + dependencies: + '@optique/core': 0.3.0 + '@oxc-project/runtime@0.75.0': {} '@oxc-project/types@0.75.0': {} @@ -9518,6 +10210,8 @@ snapshots: dependencies: '@types/node': 24.3.0 + '@types/node@16.9.1': {} + '@types/node@20.19.11': dependencies: undici-types: 6.21.0 @@ -10115,6 +10809,8 @@ snapshots: ansis@4.1.0: {} + any-base@1.1.0: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -10226,6 +10922,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + await-to-js@3.0.0: {} + axe-core@4.10.3: {} axobject-query@4.1.0: {} @@ -10234,12 +10932,16 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + binary-extensions@2.3.0: {} birpc@2.4.0: {} blake3-wasm@2.1.5: {} + bmp-ts@1.0.9: {} + body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -10272,6 +10974,11 @@ snapshots: buffer-more-ints@1.0.0: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bun-types@1.2.17: dependencies: '@types/node': 24.3.0 @@ -10327,6 +11034,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.0: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -10367,8 +11076,29 @@ snapshots: cjs-module-lexer@1.4.3: {} + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + + cli-spinners@2.9.2: {} + client-only@0.0.1: {} + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -10768,6 +11498,8 @@ snapshots: emoji-regex-xs@1.0.0: {} + emoji-regex@10.5.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -10991,7 +11723,7 @@ snapshots: '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) @@ -11055,7 +11787,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -11085,18 +11817,18 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.5.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.5.1)))(eslint@9.32.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -11118,7 +11850,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11147,7 +11879,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.32.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.32.0(jiti@2.5.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.32.0(jiti@2.5.1)))(eslint@9.32.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11410,10 +12142,14 @@ snapshots: event-target-shim@5.0.1: {} + events@3.3.0: {} + exact-mirror@0.1.3(@sinclair/typebox@0.34.38): optionalDependencies: '@sinclair/typebox': 0.34.38 + exif-parser@0.1.12: {} + exit-hook@2.2.1: {} express@4.21.2: @@ -11522,6 +12258,12 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 6.3.0 + token-types: 4.2.1 + file-type@21.0.0: dependencies: '@tokenizer/inflate': 0.2.7 @@ -11616,6 +12358,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.3.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -11649,6 +12393,11 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + gifwrap@0.10.1: + dependencies: + image-q: 4.0.0 + omggif: 1.0.10 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -11770,6 +12519,8 @@ snapshots: he@1.2.0: {} + highlight.js@10.7.3: {} + hono@4.8.3: {} hookable@5.5.3: {} @@ -11798,6 +12549,10 @@ snapshots: ignore@7.0.5: {} + image-q@4.0.0: + dependencies: + '@types/node': 16.9.1 + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -11918,6 +12673,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-interactive@2.0.0: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -11965,6 +12722,10 @@ snapshots: dependencies: which-typed-array: 1.1.19 + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -12001,12 +12762,44 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jimp@1.6.0: + dependencies: + '@jimp/core': 1.6.0 + '@jimp/diff': 1.6.0 + '@jimp/js-bmp': 1.6.0 + '@jimp/js-gif': 1.6.0 + '@jimp/js-jpeg': 1.6.0 + '@jimp/js-png': 1.6.0 + '@jimp/js-tiff': 1.6.0 + '@jimp/plugin-blit': 1.6.0 + '@jimp/plugin-blur': 1.6.0 + '@jimp/plugin-circle': 1.6.0 + '@jimp/plugin-color': 1.6.0 + '@jimp/plugin-contain': 1.6.0 + '@jimp/plugin-cover': 1.6.0 + '@jimp/plugin-crop': 1.6.0 + '@jimp/plugin-displace': 1.6.0 + '@jimp/plugin-dither': 1.6.0 + '@jimp/plugin-fisheye': 1.6.0 + '@jimp/plugin-flip': 1.6.0 + '@jimp/plugin-hash': 1.6.0 + '@jimp/plugin-mask': 1.6.0 + '@jimp/plugin-print': 1.6.0 + '@jimp/plugin-quantize': 1.6.0 + '@jimp/plugin-resize': 1.6.0 + '@jimp/plugin-rotate': 1.6.0 + '@jimp/plugin-threshold': 1.6.0 + '@jimp/types': 1.6.0 + '@jimp/utils': 1.6.0 + jiti@1.21.7: {} jiti@2.4.2: {} jiti@2.5.1: {} + jpeg-js@0.4.4: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -12180,6 +12973,11 @@ snapshots: lodash.merge@4.6.2: {} + log-symbols@6.0.0: + dependencies: + chalk: 5.6.0 + is-unicode-supported: 1.3.0 + long@5.3.2: {} longest-streak@3.1.0: {} @@ -12547,6 +13345,8 @@ snapshots: mime@3.0.0: {} + mimic-function@5.0.1: {} + miniflare@4.20250617.4: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -12770,6 +13570,8 @@ snapshots: ohash@2.0.11: {} + omggif@1.0.10: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -12778,6 +13580,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + oniguruma-to-es@2.3.0: dependencies: emoji-regex-xs: 1.0.0 @@ -12802,6 +13608,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@8.2.0: + dependencies: + chalk: 5.6.0 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -12820,10 +13638,29 @@ snapshots: package-manager-detector@1.3.0: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-bmfont-ascii@1.0.6: {} + + parse-bmfont-binary@1.0.6: {} + + parse-bmfont-xml@1.1.6: + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.5.0 + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -12853,6 +13690,8 @@ snapshots: pathe@2.0.3: {} + peek-readable@4.1.0: {} + perfect-debounce@1.0.0: {} pg-int8@1.0.1: {} @@ -12877,6 +13716,10 @@ snapshots: pirates@4.0.7: {} + pixelmatch@5.3.0: + dependencies: + pngjs: 6.0.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -12898,6 +13741,10 @@ snapshots: pvutils: 1.1.3 tslib: 2.8.1 + pngjs@6.0.0: {} + + pngjs@7.0.0: {} + points-on-curve@0.2.0: {} points-on-path@0.2.1: @@ -13001,6 +13848,8 @@ snapshots: printable-characters@1.0.42: {} + process@0.11.10: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -13086,6 +13935,18 @@ snapshots: dependencies: pify: 2.3.0 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -13202,6 +14063,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -13319,6 +14185,8 @@ snapshots: safer-buffer@2.1.2: {} + sax@1.4.1: {} + scheduler@0.25.0-rc-7771d3a7-20240827: {} scheduler@0.26.0: {} @@ -13509,6 +14377,8 @@ snapshots: dependencies: is-arrayish: 0.3.2 + simple-xml-to-json@1.2.3: {} + sirv@3.0.1: dependencies: '@polka/url': 1.0.0-next.29 @@ -13538,6 +14408,8 @@ snapshots: statuses@2.0.1: {} + stdin-discarder@0.2.2: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -13559,6 +14431,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.5.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 @@ -13609,6 +14487,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -13632,6 +14514,11 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + structured-field-values@2.0.4: {} styled-jsx@5.1.1(react@19.0.0-rc-7771d3a7-20240827): @@ -13760,6 +14647,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tinycolor2@1.6.0: {} + tinyexec@1.0.1: {} tinyglobby@0.2.14: @@ -13773,6 +14662,11 @@ snapshots: toidentifier@1.0.1: {} + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + token-types@6.0.3: dependencies: '@tokenizer/token': 0.3.0 @@ -14051,6 +14945,10 @@ snapshots: urlpattern-polyfill@10.1.0: {} + utif2@4.1.0: + dependencies: + pako: 1.0.11 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -14081,6 +14979,22 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.30.1 + vite@7.1.3(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0): + dependencies: + esbuild: 0.25.5 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.44.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 22.16.0 + fsevents: 2.3.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + tsx: 4.20.3 + yaml: 2.8.0 + vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 @@ -14101,13 +15015,13 @@ snapshots: optionalDependencies: vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0) - vitepress-plugin-group-icons@1.6.1(markdown-it@14.1.0)(vite@5.4.19(@types/node@22.16.0)(lightningcss@1.30.1)): + vitepress-plugin-group-icons@1.6.1(markdown-it@14.1.0)(vite@7.1.3(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0)): dependencies: '@iconify-json/logos': 1.2.4 '@iconify-json/vscode-icons': 1.2.23 '@iconify/utils': 2.3.0 markdown-it: 14.1.0 - vite: 5.4.19(@types/node@22.16.0)(lightningcss@1.30.1) + vite: 7.1.3(@types/node@22.16.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -14217,6 +15131,8 @@ snapshots: optionalDependencies: typescript: 5.9.2 + wasm-feature-detect@1.8.0: {} + web-streams-polyfill@3.3.3: {} which-boxed-primitive@1.1.1: @@ -14313,6 +15229,15 @@ snapshots: x-forwarded-fetch@0.2.0: {} + xml-parse-from-string@1.0.1: {} + + xml2js@0.5.0: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -14325,8 +15250,20 @@ snapshots: yaml@2.8.0: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -14349,4 +15286,6 @@ snapshots: zod@3.22.3: {} + zod@3.25.76: {} + zwitch@2.0.4: {}