@fedify/cli 2.0.0-dev.1795 → 2.0.0-dev.1801

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/deno.json +1 -1
  2. package/dist/deno.js +1 -1
  3. package/dist/generate-vocab/action.js +20 -0
  4. package/dist/generate-vocab/command.js +26 -0
  5. package/dist/generate-vocab/mod.js +5 -0
  6. package/dist/globals.js +1 -1
  7. package/dist/inbox.js +6 -3
  8. package/dist/init/ask/dir.js +1 -1
  9. package/dist/init/ask/pm.js +1 -1
  10. package/dist/init/command.js +18 -10
  11. package/dist/init/lib.js +1 -1
  12. package/dist/init/templates/defaults/federation.ts.tpl +23 -0
  13. package/dist/init/templates/defaults/logging.ts.tpl +23 -0
  14. package/dist/init/templates/express/app.ts.tpl +16 -0
  15. package/dist/init/templates/express/index.ts.tpl +6 -0
  16. package/dist/init/templates/hono/app.tsx.tpl +14 -0
  17. package/dist/init/templates/hono/index/bun.ts.tpl +10 -0
  18. package/dist/init/templates/hono/index/deno.ts.tpl +13 -0
  19. package/dist/init/templates/hono/index/node.ts.tpl +14 -0
  20. package/dist/init/templates/next/middleware.ts.tpl +45 -0
  21. package/dist/init/templates/nitro/nitro.config.ts.tpl +5 -0
  22. package/dist/init/templates/nitro/server/error.ts.tpl +3 -0
  23. package/dist/init/templates/nitro/server/middleware/federation.ts.tpl +8 -0
  24. package/dist/log.js +1 -1
  25. package/dist/lookup.js +9 -4
  26. package/dist/mod.js +5 -1
  27. package/dist/nodeinfo.js +11 -6
  28. package/dist/tunnel.js +7 -2
  29. package/dist/webfinger/action.js +1 -1
  30. package/dist/webfinger/command.js +7 -2
  31. package/package.json +7 -6
  32. package/scripts/pack.ts +3 -1
  33. package/src/generate-vocab/action.ts +17 -0
  34. package/src/generate-vocab/command.ts +44 -0
  35. package/src/generate-vocab/mod.ts +2 -0
  36. package/src/inbox.tsx +2 -1
  37. package/src/init/command.ts +39 -15
  38. package/src/lookup.ts +8 -3
  39. package/src/mod.ts +8 -0
  40. package/src/nodeinfo.ts +11 -5
  41. package/src/tunnel.ts +9 -5
  42. package/src/webfinger/command.ts +10 -5
  43. package/tsdown.config.ts +11 -0
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/cli",
3
- "version": "2.0.0-dev.1795+b6806c18",
3
+ "version": "2.0.0-dev.1801+bca8ab87",
4
4
  "license": "MIT",
5
5
  "exports": "./src/mod.ts",
6
6
  "imports": {
package/dist/deno.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  //#region deno.json
5
5
  var name = "@fedify/cli";
6
- var version = "2.0.0-dev.1795+b6806c18";
6
+ var version = "2.0.0-dev.1801+bca8ab87";
7
7
  var license = "MIT";
8
8
  var exports = "./src/mod.ts";
9
9
  var imports = {
@@ -0,0 +1,20 @@
1
+
2
+ import { Temporal } from "@js-temporal/polyfill";
3
+
4
+ import { stat } from "node:fs/promises";
5
+ import { printError } from "@optique/run";
6
+ import { generateVocab } from "@fedify/vocab-tools";
7
+ import { message } from "@optique/core/message";
8
+ import process from "node:process";
9
+
10
+ //#region src/generate-vocab/action.ts
11
+ async function runGenerateVocab({ schemaDir, generatedPath }) {
12
+ if (!(await stat(schemaDir)).isDirectory()) {
13
+ printError(message`${schemaDir} is not a directory.`);
14
+ process.exit(1);
15
+ }
16
+ await generateVocab(schemaDir, generatedPath);
17
+ }
18
+
19
+ //#endregion
20
+ export { runGenerateVocab };
@@ -0,0 +1,26 @@
1
+
2
+ import { Temporal } from "@js-temporal/polyfill";
3
+
4
+ import { argument, command, constant, message, object, option, withDefault } from "@optique/core";
5
+ import { path } from "@optique/run";
6
+
7
+ //#region src/generate-vocab/command.ts
8
+ const schemaDir = withDefault(option("-i", "--input", path({
9
+ metavar: "DIR",
10
+ type: "directory",
11
+ mustExist: true
12
+ }), { description: message`Directory containing schema files.` }), ".");
13
+ const generatedPath = argument(path({
14
+ metavar: "PATH",
15
+ type: "file",
16
+ allowCreate: true
17
+ }), { description: message`Path to output the generated vocabulary classes. Should end with ${".ts"} suffix.` });
18
+ const generateVocabCommand = command("generate-vocab", object("Generation options", {
19
+ command: constant("generate-vocab"),
20
+ schemaDir,
21
+ generatedPath
22
+ }), { description: message`Generate vocabulary classes from schema files.` });
23
+ var command_default = generateVocabCommand;
24
+
25
+ //#endregion
26
+ export { command_default as default };
@@ -0,0 +1,5 @@
1
+
2
+ import { Temporal } from "@js-temporal/polyfill";
3
+
4
+ import { runGenerateVocab } from "./action.js";
5
+ import command_default from "./command.js";
package/dist/globals.js CHANGED
@@ -3,8 +3,8 @@
3
3
 
4
4
  import { recordingSink } from "./log.js";
5
5
  import { message, object, option } from "@optique/core";
6
- import { configure, getConsoleSink } from "@logtape/logtape";
7
6
  import process from "node:process";
7
+ import { configure, getConsoleSink } from "@logtape/logtape";
8
8
  import { getFileSink } from "@logtape/file";
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
10
 
package/dist/inbox.js CHANGED
@@ -10,11 +10,11 @@ import { tableStyle } from "./table.js";
10
10
  import { spawnTemporaryServer } from "./tempserver.js";
11
11
  import { colors } from "./utils.js";
12
12
  import { command, constant, merge, message, multiple, object, option, optional, string, withDefault } from "@optique/core";
13
+ import process from "node:process";
13
14
  import { Accept, Activity, Application, Delete, Endpoints, Follow, Image, MemoryKvStore, PUBLIC_COLLECTION, createFederation, generateCryptoKeyPair, getActorHandle, isActor, lookupObject } from "@fedify/fedify";
14
15
  import { getLogger } from "@logtape/logtape";
15
16
  import Table from "cli-table3";
16
17
  import { Hono } from "hono";
17
- import process from "node:process";
18
18
  import ora from "ora";
19
19
  import { jsx } from "hono/jsx/jsx-runtime";
20
20
 
@@ -27,11 +27,14 @@ const logger = getLogger([
27
27
  const inboxCommand = command("inbox", merge(object("Inbox options", {
28
28
  command: constant("inbox"),
29
29
  follow: optional(multiple(option("-f", "--follow", string({ metavar: "URI" }), { description: message`Follow the given actor. The argument can be either an actor URI or a handle. Can be specified multiple times.` }))),
30
- acceptFollow: optional(multiple(option("-a", "--accept-follow", string({ metavar: "URI" }), { description: message`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.` }))),
30
+ acceptFollow: optional(multiple(option("-a", "--accept-follow", string({ metavar: "URI" }), { description: message`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.` }))),
31
31
  noTunnel: option("-T", "--no-tunnel", { description: message`Do not tunnel the ephemeral ActivityPub server to the public Internet.` }),
32
32
  actorName: withDefault(option("--actor-name", string({ metavar: "NAME" }), { description: message`Customize the actor display name.` }), "Fedify Ephemeral Inbox"),
33
33
  actorSummary: withDefault(option("--actor-summary", string({ metavar: "SUMMARY" }), { description: message`Customize the actor description.` }), "An ephemeral ActivityPub inbox for testing purposes.")
34
- }), 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.` });
34
+ }), debugOption), {
35
+ brief: message`Run an ephemeral ActivityPub inbox server.`,
36
+ 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.`
37
+ });
35
38
  async function runInbox(command$1) {
36
39
  const fetch = createFetchHandler({
37
40
  actorName: command$1.actorName,
@@ -4,9 +4,9 @@
4
4
  import { getCwd, getOsType, runSubCommand } from "../../utils.js";
5
5
  import { isDirectoryEmpty, logger } from "../lib.js";
6
6
  import { printError } from "@optique/run";
7
+ import { message } from "@optique/core/message";
7
8
  import { identity, pipe, when } from "@fxts/core";
8
9
  import { input } from "@inquirer/prompts";
9
- import { message } from "@optique/core/message";
10
10
  import toggle from "inquirer-toggle";
11
11
 
12
12
  //#region src/init/ask/dir.ts
@@ -5,8 +5,8 @@ import { PACKAGE_MANAGER } from "../const.js";
5
5
  import webframeworks_default from "../webframeworks.js";
6
6
  import { getInstallUrl, getLabel, isPackageManagerAvailable } from "../lib.js";
7
7
  import { print } from "@optique/run";
8
- import { select } from "@inquirer/prompts";
9
8
  import { message } from "@optique/core/message";
9
+ import { select } from "@inquirer/prompts";
10
10
 
11
11
  //#region src/init/ask/pm.ts
12
12
  /**
@@ -1,25 +1,33 @@
1
1
 
2
2
  import { Temporal } from "@js-temporal/polyfill";
3
3
 
4
+ import { debugOption } from "../globals.js";
4
5
  import { KV_STORE, MESSAGE_QUEUE, PACKAGE_MANAGER, WEB_FRAMEWORK } from "./const.js";
5
- import { argument, choice, command, constant, message, object, option, optional } from "@optique/core";
6
+ import { argument, choice, command, constant, message, object, option, optionNames, optional } from "@optique/core";
6
7
  import { path } from "@optique/run";
7
8
 
8
9
  //#region src/init/command.ts
9
- const joinSep = (str) => str.join(" | ");
10
- const webFramework = optional(option("-w", "--web-framework", choice(WEB_FRAMEWORK, { metavar: `WEB_FRAMEWORK: ${joinSep(WEB_FRAMEWORK)}` })));
11
- const packageManager = optional(option("-p", "--package-manager", choice(PACKAGE_MANAGER, { metavar: `PACKAGE_MANAGER: ${joinSep(PACKAGE_MANAGER)}` })));
12
- const kvStore = optional(option("-k", "--kv-store", choice(KV_STORE, { metavar: `KV_STORE: ${joinSep(KV_STORE)}` })));
13
- const messageQueue = optional(option("-m", "--message-queue", choice(MESSAGE_QUEUE, { metavar: `MESSAGE_QUEUE: ${joinSep(MESSAGE_QUEUE)}` })));
14
- const initCommand = command("init", object({
10
+ const webFramework = optional(option("-w", "--web-framework", choice(WEB_FRAMEWORK, { metavar: "WEB_FRAMEWORK" }), { description: message`The web framework to integrate Fedify with.` }));
11
+ const packageManager = optional(option("-p", "--package-manager", choice(PACKAGE_MANAGER, { metavar: "PACKAGE_MANAGER" }), { description: message`The package manager to use for installing dependencies.` }));
12
+ const kvStore = optional(option("-k", "--kv-store", choice(KV_STORE, { metavar: "KV_STORE" }), { description: message`The key-value store to use for caching and some other features.` }));
13
+ const messageQueue = optional(option("-m", "--message-queue", choice(MESSAGE_QUEUE, { metavar: "MESSAGE_QUEUE" }), { description: message`The message queue to use for background tasks.` }));
14
+ const initCommand = command("init", object("Initialization options", {
15
15
  command: constant("init"),
16
- dir: optional(argument(path({ metavar: "DIRECTORY" }))),
16
+ dir: optional(argument(path({ metavar: "DIR" }), { description: message`The project directory to initialize. If a specified directory does not exist, it will be created.` })),
17
17
  webFramework,
18
18
  packageManager,
19
19
  kvStore,
20
20
  messageQueue,
21
- dryRun: option("-d", "--dry-run")
22
- }), { description: message`Initialize a new Fedify project directory.` });
21
+ dryRun: option("-d", "--dry-run", { description: message`Perform a trial run with no changes made.` }),
22
+ debugOption
23
+ }), {
24
+ brief: message`Initialize a new Fedify project directory.`,
25
+ description: message`Initialize a new Fedify project directory.
26
+
27
+ By default, it initializes the current directory. You can specify a different directory as an argument.
28
+
29
+ Unless you specify all options (${optionNames(["-w", "--web-framework"])}, ${optionNames(["-p", "--package-manager"])}, ${optionNames(["-k", "--kv-store"])}, and ${optionNames(["-m", "--message-queue"])}), it will prompt you to select the options interactively.`
30
+ });
23
31
 
24
32
  //#endregion
25
33
  export { initCommand };
package/dist/init/lib.js CHANGED
@@ -10,8 +10,8 @@ import rt_default from "./json/rt.js";
10
10
  import webframeworks_default from "./webframeworks.js";
11
11
  import { dirname, join } from "node:path";
12
12
  import { mkdir, readdir, writeFile } from "node:fs/promises";
13
- import { getLogger } from "@logtape/logtape";
14
13
  import process from "node:process";
14
+ import { getLogger } from "@logtape/logtape";
15
15
  import { entries, evolve, fromEntries, isObject, map, negate, pipe, throwIf, when } from "@fxts/core";
16
16
  import { toMerged } from "es-toolkit";
17
17
  import { readFileSync } from "node:fs";
@@ -0,0 +1,23 @@
1
+ import { createFederation, Person } from "@fedify/fedify";
2
+ import { getLogger } from "@logtape/logtape";
3
+ /* imports */
4
+
5
+ const logger = getLogger(/* logger */);
6
+
7
+ const federation = createFederation({
8
+ kv: /* kv */,
9
+ queue: /* queue */,
10
+ });
11
+
12
+ federation.setActorDispatcher(
13
+ "/users/{identifier}",
14
+ async (ctx, identifier) => {
15
+ return new Person({
16
+ id: ctx.getActorUri(identifier),
17
+ preferredUsername: identifier,
18
+ name: identifier,
19
+ });
20
+ },
21
+ );
22
+
23
+ export default federation;
@@ -0,0 +1,23 @@
1
+ import { configure, getConsoleSink } from "@logtape/logtape";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+
4
+ await configure({
5
+ contextLocalStorage: new AsyncLocalStorage(),
6
+ sinks: {
7
+ console: getConsoleSink(),
8
+ },
9
+ filters: {},
10
+ loggers: [
11
+ {
12
+ category: /* project name */,
13
+ lowestLevel: "debug",
14
+ sinks: ["console"],
15
+ },
16
+ { category: "fedify", lowestLevel: "info", sinks: ["console"] },
17
+ {
18
+ category: ["logtape", "meta"],
19
+ lowestLevel: "warning",
20
+ sinks: ["console"],
21
+ },
22
+ ],
23
+ });
@@ -0,0 +1,16 @@
1
+ import { integrateFederation } from "@fedify/express";
2
+ import { getLogger } from "@logtape/logtape";
3
+ import express from "express";
4
+ import federation from "./federation.ts";
5
+
6
+ const logger = getLogger("/* logger */");
7
+
8
+ export const app = express();
9
+
10
+ app.set("trust proxy", true);
11
+
12
+ app.use(integrateFederation(federation, () => undefined));
13
+
14
+ app.get("/", (_, res) => res.send("Hello, Fedify!"));
15
+
16
+ export default app;
@@ -0,0 +1,6 @@
1
+ import app from "./app.ts";
2
+ import "./logging.ts";
3
+
4
+ app.listen(8000, () => {
5
+ console.log("Server started at http://localhost:8000");
6
+ });
@@ -0,0 +1,14 @@
1
+ // @ts-nocheck this file is just a template
2
+ import { Hono } from "/* hono */";
3
+ import { federation } from "@fedify/hono";
4
+ import { getLogger } from "@logtape/logtape";
5
+ import fedi from "./federation.ts";
6
+
7
+ const logger = getLogger("/* logger */");
8
+
9
+ const app = new Hono();
10
+ app.use(federation(fedi, () => undefined));
11
+
12
+ app.get("/", (c) => c.text("Hello, Fedify!"));
13
+
14
+ export default app;
@@ -0,0 +1,10 @@
1
+ import { behindProxy } from "x-forwarded-fetch";
2
+ import app from "./app.tsx";
3
+ import "./logging.ts";
4
+
5
+ const server = Bun.serve({
6
+ port: 8000,
7
+ fetch: behindProxy(app.fetch.bind(app)),
8
+ });
9
+
10
+ console.log("Server started at", server.url.href);
@@ -0,0 +1,13 @@
1
+ import { behindProxy } from "@hongminhee/x-forwarded-fetch";
2
+ import "@std/dotenv/load";
3
+ import app from "./app.tsx";
4
+ import "./logging.ts";
5
+
6
+ Deno.serve(
7
+ {
8
+ port: 8000,
9
+ onListen: ({ port, hostname }) =>
10
+ console.log("Server started at http://" + hostname + ":" + port),
11
+ },
12
+ behindProxy(app.fetch.bind(app)),
13
+ );
@@ -0,0 +1,14 @@
1
+ // @ts-nocheck this file is just a template
2
+ import { serve } from "@hono/node-server";
3
+ import { behindProxy } from "x-forwarded-fetch";
4
+ import app from "./app.tsx";
5
+ import "./logging.ts";
6
+
7
+ serve(
8
+ {
9
+ port: 8000,
10
+ fetch: behindProxy(app.fetch.bind(app)),
11
+ },
12
+ (info) =>
13
+ console.log("Server started at http://" + info.address + ":" + info.port),
14
+ );
@@ -0,0 +1,45 @@
1
+ import { fedifyWith } from "@fedify/next";
2
+ import federation from "./federation";
3
+
4
+ export default fedifyWith(federation)(
5
+ /*
6
+ function (request: Request) {
7
+ // If you need to handle other requests besides federation
8
+ // requests in middleware, you can do it here.
9
+ // If you handle only federation requests in middleware,
10
+ // you don't need this function.
11
+ return NextResponse.next();
12
+ },
13
+ */
14
+ )
15
+
16
+ // This config needs because middleware process only requests with the
17
+ // "Accept" header matching the federation accept regex.
18
+ // More details: https://nextjs.org/docs/app/api-reference/file-conventions/middleware#config-object-optional
19
+ export const config = {
20
+ runtime: "nodejs",
21
+ matcher: [
22
+ {
23
+ source: "/:path*",
24
+ has: [
25
+ {
26
+ type: "header",
27
+ key: "Accept",
28
+ value: ".*application\\/((jrd|activity|ld)\\+json|xrd\\+xml).*",
29
+ },
30
+ ],
31
+ },
32
+ {
33
+ source: "/:path*",
34
+ has: [
35
+ {
36
+ type: "header",
37
+ key: "content-type",
38
+ value: ".*application\\/((jrd|activity|ld)\\+json|xrd\\+xml).*",
39
+ },
40
+ ],
41
+ },
42
+ { source: "/.well-known/nodeinfo" },
43
+ { source: "/.well-known/x-nodeinfo2" },
44
+ ],
45
+ };
@@ -0,0 +1,5 @@
1
+ // https://nitro.unjs.io/config
2
+ export default defineNitroConfig({
3
+ srcDir: "server",
4
+ errorHandler: "~/error"
5
+ });
@@ -0,0 +1,3 @@
1
+ import { onError } from "@fedify/h3";
2
+
3
+ export default onError;
@@ -0,0 +1,8 @@
1
+
2
+ import { integrateFederation } from "@fedify/h3";
3
+ import federation from "../federation"
4
+
5
+ export default integrateFederation(
6
+ federation,
7
+ (event, request) => undefined,
8
+ );
package/dist/log.js CHANGED
@@ -3,8 +3,8 @@
3
3
 
4
4
  import { dirname } from "node:path";
5
5
  import { mkdir } from "node:fs/promises";
6
- import { configure, getConsoleSink } from "@logtape/logtape";
7
6
  import process from "node:process";
7
+ import { configure, getConsoleSink } from "@logtape/logtape";
8
8
  import { getFileSink } from "@logtape/file";
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
10
 
package/dist/lookup.js CHANGED
@@ -6,11 +6,11 @@ import { configureLogging, debugOption } from "./globals.js";
6
6
  import { spawnTemporaryServer } from "./tempserver.js";
7
7
  import { colorEnabled, colors, formatObject } from "./utils.js";
8
8
  import { renderImages } from "./imagerenderer.js";
9
- import { argument, choice, command, constant, flag, float, map, merge, message, multiple, object, option, optional, or, string, withDefault } from "@optique/core";
9
+ import { argument, choice, command, constant, flag, float, map, merge, message, multiple, object, option, optionNames, optional, or, string, withDefault } from "@optique/core";
10
10
  import { path, print, printError } from "@optique/run";
11
+ import process from "node:process";
11
12
  import { Application, Collection, CryptographicKey, Object as Object$1, generateCryptoKeyPair, getAuthenticatedDocumentLoader, lookupObject, respondWithObject, traverseCollection } from "@fedify/fedify";
12
13
  import { getLogger } from "@logtape/logtape";
13
- import process from "node:process";
14
14
  import ora from "ora";
15
15
  import { createWriteStream } from "node:fs";
16
16
 
@@ -22,7 +22,7 @@ const logger = getLogger([
22
22
  ]);
23
23
  const authorizedFetchOption = withDefault(object({
24
24
  authorizedFetch: flag("-a", "--authorized-fetch", { description: message`Sign the request with an one-time key.` }),
25
- 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")
25
+ firstKnock: withDefault(option("--first-knock", choice(["draft-cavage-http-signatures-12", "rfc9421"]), { description: message`The first-knock spec for ${optionNames(["-a", "--authorized-fetch"])}. It is used for the double-knocking technique.` }), "draft-cavage-http-signatures-12")
26
26
  }), { authorizedFetch: false });
27
27
  const traverseOption = withDefault(object({
28
28
  traverse: flag("-t", "--traverse", { description: message`Traverse the given collection(s) to fetch all items.` }),
@@ -42,7 +42,12 @@ const lookupCommand = command("lookup", merge("Looking up options", object({ com
42
42
  min: 0,
43
43
  metavar: "SECONDS"
44
44
  }), { description: message`Set timeout for network requests in seconds.` }))
45
- })), { 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.` });
45
+ })), {
46
+ brief: message`Look up Activity Streams objects.`,
47
+ description: message`Look up Activity Streams objects by URL or actor handle.
48
+
49
+ The arguments can be either URLs or actor handles (e.g., ${"@username@domain"}), and they can be multiple.`
50
+ });
46
51
  var TimeoutError = class extends Error {
47
52
  name = "TimeoutError";
48
53
  constructor(message$1) {
package/dist/mod.js CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  import { Temporal } from "@js-temporal/polyfill";
4
4
 
5
+ import { runGenerateVocab } from "./generate-vocab/action.js";
6
+ import command_default from "./generate-vocab/command.js";
7
+ import "./generate-vocab/mod.js";
5
8
  import { inboxCommand, runInbox } from "./inbox.js";
6
9
  import action_default from "./init/action/mod.js";
7
10
  import { initCommand } from "./init/command.js";
@@ -16,7 +19,7 @@ import { or } from "@optique/core";
16
19
  import { run } from "@optique/run";
17
20
 
18
21
  //#region src/mod.ts
19
- const command$1 = or(initCommand, webFingerCommand, lookupCommand, inboxCommand, nodeInfoCommand, tunnelCommand);
22
+ const command$1 = or(initCommand, webFingerCommand, lookupCommand, inboxCommand, nodeInfoCommand, tunnelCommand, command_default);
20
23
  async function main() {
21
24
  const result = run(command$1, {
22
25
  programName: "fedify",
@@ -28,6 +31,7 @@ async function main() {
28
31
  if (result.command === "inbox") runInbox(result);
29
32
  if (result.command === "nodeinfo") runNodeInfo(result);
30
33
  if (result.command === "tunnel") await runTunnel(result);
34
+ if (result.command === "generate-vocab") await runGenerateVocab(result);
31
35
  }
32
36
  await main();
33
37
 
package/dist/nodeinfo.js CHANGED
@@ -5,9 +5,9 @@ import { debugOption } from "./globals.js";
5
5
  import { colors, formatObject } from "./utils.js";
6
6
  import { argument, command, constant, flag, merge, message, object, option, optional, or, string } from "@optique/core";
7
7
  import { print, printError } from "@optique/run";
8
+ import process from "node:process";
8
9
  import { getNodeInfo } from "@fedify/fedify";
9
10
  import { getLogger } from "@logtape/logtape";
10
- import process from "node:process";
11
11
  import ora from "ora";
12
12
  import { getUserAgent } from "@fedify/vocab-runtime";
13
13
  import os from "node:os";
@@ -27,15 +27,20 @@ const Jimp = createJimp({
27
27
  plugins: defaultPlugins
28
28
  });
29
29
  const nodeInfoOption = optional(or(object({ raw: flag("-r", "--raw", { description: message`Show NodeInfo document in the raw JSON format` }) }), object({
30
- bestEffort: optional(flag("-b", "--best-effort", { description: message`Parse the NodeInfo document with best effort. If the NodeInfo document is not well-formed, the option will try to parse it as much as possible.` })),
30
+ bestEffort: optional(flag("-b", "--best-effort", { description: message`Parse the NodeInfo document with best effort. If the NodeInfo document is not well-formed, the option will try to parse it as much as possible.` })),
31
31
  noFavicon: optional(flag("--no-favicon", { description: message`Disable fetching the favicon of the instance` })),
32
- metadata: optional(flag("-m", "--metadata", { description: message`show the extra metadata of the NodeInfo, i.e., the metadata field of the document.` }))
32
+ metadata: optional(flag("-m", "--metadata", { description: message`Show the extra metadata of the NodeInfo, i.e., the metadata field of the document.` }))
33
33
  })));
34
- const userAgentOption = optional(object({ userAgent: option("-u", "--user-agent", string()) }));
34
+ const userAgentOption = optional(object({ userAgent: option("-u", "--user-agent", string(), { description: message`The custom User-Agent header value.` }) }));
35
35
  const nodeInfoCommand = command("nodeinfo", merge(object({
36
36
  command: constant("nodeinfo"),
37
- host: argument(string({ metavar: "hostname or URL" }), { description: message`Bare hostname or a full URL of the instance` })
38
- }), debugOption, nodeInfoOption, userAgentOption), { 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.` });
37
+ host: argument(string({ metavar: "HOST" }), { description: message`Bare hostname or a full URL of the instance` })
38
+ }), debugOption, nodeInfoOption, userAgentOption), {
39
+ brief: message`Get information about a remote node using the NodeInfo protocol`,
40
+ description: message`Get information about a remote node using the NodeInfo protocol.
41
+
42
+ The argument is the hostname of the remote node, or the URL of the remote node.`
43
+ });
39
44
  async function runNodeInfo(command$1) {
40
45
  const spinner = ora({
41
46
  text: "Fetching a NodeInfo document...",
package/dist/tunnel.js CHANGED
@@ -20,8 +20,13 @@ const tunnelCommand = command("tunnel", merge("Tunnel options", object({ command
20
20
  "localhost.run",
21
21
  "serveo.net",
22
22
  "pinggy.io"
23
- ]), { description: message`The localtunnel service to use.` }))
24
- }), debugOption), { description: message`Expose a local HTTP server to the public internet using a secure tunnel.\nNote that the HTTP requests through the tunnel have X-Forwarded-* headers.` });
23
+ ], { metavar: "SERVICE" }), { description: message`The tunneling service to use.` }))
24
+ }), debugOption), {
25
+ brief: message`Expose a local HTTP server to the public internet using a secure tunnel.`,
26
+ description: message`Expose a local HTTP server to the public internet using a secure tunnel.
27
+
28
+ Note that the HTTP requests through the tunnel have X-Forwarded-* headers.`
29
+ });
25
30
  async function runTunnel(command$1, deps = {
26
31
  openTunnel,
27
32
  ora,
@@ -5,8 +5,8 @@ import { formatObject } from "../utils.js";
5
5
  import { NotFoundError, getErrorMessage } from "./error.js";
6
6
  import { convertUrlIfHandle } from "./lib.js";
7
7
  import { print } from "@optique/run";
8
- import ora from "ora";
9
8
  import { formatMessage, message } from "@optique/core/message";
9
+ import ora from "ora";
10
10
  import { lookupWebFinger } from "@fedify/fedify/webfinger";
11
11
 
12
12
  //#region src/webfinger/action.ts
@@ -10,11 +10,16 @@ const allowPrivateAddresses = optional(flag("-p", "--allow-private-address", { d
10
10
  const maxRedirection = withDefault(option("--max-redirection", integer({ min: 0 }), { description: message`Maximum number of redirections to follow.` }), 5);
11
11
  const webFingerCommand = command("webfinger", merge(object({
12
12
  command: constant("webfinger"),
13
- resources: multiple(argument(string({ metavar: "RESOURCE" }), { description: message`WebFinger resource(s) to look up.` })),
13
+ resources: multiple(argument(string({ metavar: "RESOURCE" }), { description: message`WebFinger resource(s) to look up.` }), { min: 1 }),
14
14
  userAgent,
15
15
  allowPrivateAddresses,
16
16
  maxRedirection
17
- }), debugOption), { description: message`Look up WebFinger resources. The argument can be multiple.` });
17
+ }), debugOption), {
18
+ brief: message`Look up WebFinger resources.`,
19
+ description: message`Look up WebFinger resources.
20
+
21
+ The argument can be multiple.`
22
+ });
18
23
 
19
24
  //#endregion
20
25
  export { webFingerCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/cli",
3
- "version": "2.0.0-dev.1795+b6806c18",
3
+ "version": "2.0.0-dev.1801+bca8ab87",
4
4
  "description": "CLI toolchain for Fedify and debugging ActivityPub",
5
5
  "keywords": [
6
6
  "fedify",
@@ -61,7 +61,7 @@
61
61
  "cli-table3": "^0.6.5",
62
62
  "enquirer": "^2.4.1",
63
63
  "env-paths": "^3.0.0",
64
- "es-toolkit": "1.39.5",
64
+ "es-toolkit": "1.39.10",
65
65
  "fetch-mock": "^12.5.4",
66
66
  "hono": "^4.8.3",
67
67
  "icojs": "^0.19.5",
@@ -71,13 +71,14 @@
71
71
  "ora": "^8.2.0",
72
72
  "shiki": "^1.6.4",
73
73
  "srvx": "^0.8.7",
74
- "@fedify/sqlite": "2.0.0-dev.1795+b6806c18",
75
- "@fedify/fedify": "2.0.0-dev.1795+b6806c18",
76
- "@fedify/vocab-runtime": "2.0.0-dev.1795+b6806c18"
74
+ "@fedify/sqlite": "2.0.0-dev.1801+bca8ab87",
75
+ "@fedify/vocab-tools": "2.0.0-dev.1801+bca8ab87",
76
+ "@fedify/vocab-runtime": "2.0.0-dev.1801+bca8ab87",
77
+ "@fedify/fedify": "2.0.0-dev.1801+bca8ab87"
77
78
  },
78
79
  "devDependencies": {
79
80
  "@types/bun": "^1.2.23",
80
- "@types/node": "^22.16.0",
81
+ "@types/node": "^22.17.0",
81
82
  "tsdown": "^0.12.9",
82
83
  "typescript": "^5.9.2"
83
84
  },
package/scripts/pack.ts CHANGED
@@ -24,7 +24,9 @@ async function compile(os: OS, arch: Arch, into: string): Promise<void> {
24
24
  if (!target) {
25
25
  throw new Error(`Unsupported os/arch: ${os}/${arch}`);
26
26
  }
27
- await $`deno compile --allow-all --target=${target} --output=${into} ${
27
+ await $`deno compile --allow-all --include=${
28
+ join("src", "init", "templates")
29
+ } --target=${target} --output=${into} ${
28
30
  join(dirname(import.meta.dirname!), "src", "mod.ts")
29
31
  }`;
30
32
  }
@@ -0,0 +1,17 @@
1
+ import { generateVocab } from "@fedify/vocab-tools";
2
+ import { message } from "@optique/core/message";
3
+ import { printError } from "@optique/run";
4
+ import { stat } from "node:fs/promises";
5
+ import process from "node:process";
6
+ import type { GenerateVocabCommand } from "./command.ts";
7
+
8
+ export default async function runGenerateVocab(
9
+ { schemaDir, generatedPath }: GenerateVocabCommand,
10
+ ) {
11
+ if (!(await stat(schemaDir)).isDirectory()) {
12
+ printError(message`${schemaDir} is not a directory.`);
13
+ process.exit(1);
14
+ }
15
+
16
+ await generateVocab(schemaDir, generatedPath);
17
+ }
@@ -0,0 +1,44 @@
1
+ import {
2
+ argument,
3
+ command,
4
+ constant,
5
+ type InferValue,
6
+ message,
7
+ object,
8
+ option,
9
+ withDefault,
10
+ } from "@optique/core";
11
+ import { path } from "@optique/run";
12
+
13
+ const schemaDir = withDefault(
14
+ option(
15
+ "-i",
16
+ "--input",
17
+ path({ metavar: "DIR", type: "directory", mustExist: true }),
18
+ { description: message`Directory containing schema files.` },
19
+ ),
20
+ ".",
21
+ );
22
+ const generatedPath = argument(
23
+ path({ metavar: "PATH", type: "file", allowCreate: true }),
24
+ {
25
+ description:
26
+ message`Path to output the generated vocabulary classes. Should end with ${".ts"} suffix.`,
27
+ },
28
+ );
29
+
30
+ const generateVocabCommand = command(
31
+ "generate-vocab",
32
+ object("Generation options", {
33
+ command: constant("generate-vocab"),
34
+ schemaDir,
35
+ generatedPath,
36
+ }),
37
+ {
38
+ description: message`Generate vocabulary classes from schema files.`,
39
+ },
40
+ );
41
+
42
+ export default generateVocabCommand;
43
+
44
+ export type GenerateVocabCommand = InferValue<typeof generateVocabCommand>;
@@ -0,0 +1,2 @@
1
+ export { default as runGenerateVocab } from "./action.ts";
2
+ export { default as generateVocabCommand } from "./command.ts";
package/src/inbox.tsx CHANGED
@@ -79,7 +79,7 @@ export const inboxCommand = command(
79
79
  multiple(
80
80
  option("-a", "--accept-follow", string({ metavar: "URI" }), {
81
81
  description:
82
- message`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.`,
82
+ message`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.`,
83
83
  }),
84
84
  ),
85
85
  ),
@@ -107,6 +107,7 @@ export const inboxCommand = command(
107
107
  debugOption,
108
108
  ),
109
109
  {
110
+ brief: message`Run an ephemeral ActivityPub inbox server.`,
110
111
  description:
111
112
  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.`,
112
113
  },
@@ -8,6 +8,7 @@ import {
8
8
  object,
9
9
  option,
10
10
  optional,
11
+ optionNames,
11
12
  } from "@optique/core";
12
13
  import { path } from "@optique/run";
13
14
  import {
@@ -16,48 +17,71 @@ import {
16
17
  PACKAGE_MANAGER,
17
18
  WEB_FRAMEWORK,
18
19
  } from "./const.ts";
20
+ import { debugOption } from "../globals.ts";
19
21
 
20
- const joinSep = (str: readonly string[]) => str.join(" | ");
21
22
  const webFramework = optional(option(
22
23
  "-w",
23
24
  "--web-framework",
24
- choice(WEB_FRAMEWORK, {
25
- metavar: `WEB_FRAMEWORK: ${joinSep(WEB_FRAMEWORK)}`,
26
- }),
25
+ choice(WEB_FRAMEWORK, { metavar: "WEB_FRAMEWORK" }),
26
+ {
27
+ description: message`The web framework to integrate Fedify with.`,
28
+ },
27
29
  ));
28
30
  const packageManager = optional(option(
29
31
  "-p",
30
32
  "--package-manager",
31
- choice(PACKAGE_MANAGER, {
32
- metavar: `PACKAGE_MANAGER: ${joinSep(PACKAGE_MANAGER)}`,
33
- }),
33
+ choice(PACKAGE_MANAGER, { metavar: "PACKAGE_MANAGER" }),
34
+ {
35
+ description:
36
+ message`The package manager to use for installing dependencies.`,
37
+ },
34
38
  ));
35
39
  const kvStore = optional(option(
36
40
  "-k",
37
41
  "--kv-store",
38
- choice(KV_STORE, { metavar: `KV_STORE: ${joinSep(KV_STORE)}` }),
42
+ choice(KV_STORE, { metavar: "KV_STORE" }),
43
+ {
44
+ description:
45
+ message`The key-value store to use for caching and some other features.`,
46
+ },
39
47
  ));
40
48
  const messageQueue = optional(option(
41
49
  "-m",
42
50
  "--message-queue",
43
- choice(MESSAGE_QUEUE, {
44
- metavar: `MESSAGE_QUEUE: ${joinSep(MESSAGE_QUEUE)}`,
45
- }),
51
+ choice(MESSAGE_QUEUE, { metavar: "MESSAGE_QUEUE" }),
52
+ {
53
+ description: message`The message queue to use for background tasks.`,
54
+ },
46
55
  ));
47
56
 
48
57
  export const initCommand = command(
49
58
  "init",
50
- object({
59
+ object("Initialization options", {
51
60
  command: constant("init"),
52
- dir: optional(argument(path({ metavar: "DIRECTORY" }))),
61
+ dir: optional(argument(path({ metavar: "DIR" }), {
62
+ description:
63
+ message`The project directory to initialize. If a specified directory does not exist, it will be created.`,
64
+ })),
53
65
  webFramework,
54
66
  packageManager,
55
67
  kvStore,
56
68
  messageQueue,
57
- dryRun: option("-d", "--dry-run"),
69
+ dryRun: option("-d", "--dry-run", {
70
+ description: message`Perform a trial run with no changes made.`,
71
+ }),
72
+ debugOption,
58
73
  }),
59
74
  {
60
- description: message`Initialize a new Fedify project directory.`,
75
+ brief: message`Initialize a new Fedify project directory.`,
76
+ description: message`Initialize a new Fedify project directory.
77
+
78
+ By default, it initializes the current directory. You can specify a different directory as an argument.
79
+
80
+ Unless you specify all options (${optionNames(["-w", "--web-framework"])}, ${
81
+ optionNames(["-p", "--package-manager"])
82
+ }, ${optionNames(["-k", "--kv-store"])}, and ${
83
+ optionNames(["-m", "--message-queue"])
84
+ }), it will prompt you to select the options interactively.`,
61
85
  },
62
86
  );
63
87
 
package/src/lookup.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  object,
29
29
  option,
30
30
  optional,
31
+ optionNames,
31
32
  or,
32
33
  string,
33
34
  withDefault,
@@ -54,8 +55,9 @@ export const authorizedFetchOption = withDefault(
54
55
  "--first-knock",
55
56
  choice(["draft-cavage-http-signatures-12", "rfc9421"]),
56
57
  {
57
- description:
58
- message`The first-knock spec for -a/--authorized-fetch. It is used for the double-knocking technique.`,
58
+ description: message`The first-knock spec for ${
59
+ optionNames(["-a", "--authorized-fetch"])
60
+ }. It is used for the double-knocking technique.`,
59
61
  },
60
62
  ),
61
63
  "draft-cavage-http-signatures-12" as const,
@@ -147,8 +149,11 @@ export const lookupCommand = command(
147
149
  }),
148
150
  ),
149
151
  {
152
+ brief: message`Look up Activity Streams objects.`,
150
153
  description:
151
- 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.`,
154
+ message`Look up Activity Streams objects by URL or actor handle.
155
+
156
+ The arguments can be either URLs or actor handles (e.g., ${"@username@domain"}), and they can be multiple.`,
152
157
  },
153
158
  );
154
159
 
package/src/mod.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { or } from "@optique/core";
3
3
  import { run } from "@optique/run";
4
+ import {
5
+ generateVocabCommand,
6
+ runGenerateVocab,
7
+ } from "./generate-vocab/mod.ts";
4
8
  import { inboxCommand, runInbox } from "./inbox.tsx";
5
9
  import { initCommand, runInit } from "./init/mod.ts";
6
10
  import { lookupCommand, runLookup } from "./lookup.ts";
@@ -15,6 +19,7 @@ const command = or(
15
19
  inboxCommand,
16
20
  nodeInfoCommand,
17
21
  tunnelCommand,
22
+ generateVocabCommand,
18
23
  );
19
24
 
20
25
  async function main() {
@@ -40,6 +45,9 @@ async function main() {
40
45
  if (result.command === "tunnel") {
41
46
  await runTunnel(result);
42
47
  }
48
+ if (result.command === "generate-vocab") {
49
+ await runGenerateVocab(result);
50
+ }
43
51
  }
44
52
 
45
53
  await main();
package/src/nodeinfo.ts CHANGED
@@ -44,21 +44,23 @@ const nodeInfoOption = optional(
44
44
  object({
45
45
  bestEffort: optional(flag("-b", "--best-effort", {
46
46
  description:
47
- message`Parse the NodeInfo document with best effort. If the NodeInfo document is not well-formed, the option will try to parse it as much as possible.`,
47
+ message`Parse the NodeInfo document with best effort. If the NodeInfo document is not well-formed, the option will try to parse it as much as possible.`,
48
48
  })),
49
49
  noFavicon: optional(flag("--no-favicon", {
50
50
  description: message`Disable fetching the favicon of the instance`,
51
51
  })),
52
52
  metadata: optional(flag("-m", "--metadata", {
53
53
  description:
54
- message`show the extra metadata of the NodeInfo, i.e., the metadata field of the document.`,
54
+ message`Show the extra metadata of the NodeInfo, i.e., the metadata field of the document.`,
55
55
  })),
56
56
  }),
57
57
  ),
58
58
  );
59
59
 
60
60
  const userAgentOption = optional(object({
61
- userAgent: option("-u", "--user-agent", string()),
61
+ userAgent: option("-u", "--user-agent", string(), {
62
+ description: message`The custom User-Agent header value.`,
63
+ }),
62
64
  }));
63
65
 
64
66
  export const nodeInfoCommand = Command(
@@ -66,7 +68,7 @@ export const nodeInfoCommand = Command(
66
68
  merge(
67
69
  object({
68
70
  command: constant("nodeinfo"),
69
- host: argument(string({ metavar: "hostname or URL" }), {
71
+ host: argument(string({ metavar: "HOST" }), {
70
72
  description: message`Bare hostname or a full URL of the instance`,
71
73
  }),
72
74
  }),
@@ -75,8 +77,12 @@ export const nodeInfoCommand = Command(
75
77
  userAgentOption,
76
78
  ),
77
79
  {
80
+ brief:
81
+ message`Get information about a remote node using the NodeInfo protocol`,
78
82
  description:
79
- 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.`,
83
+ message`Get information about a remote node using the NodeInfo protocol.
84
+
85
+ The argument is the hostname of the remote node, or the URL of the remote node.`,
80
86
  },
81
87
  );
82
88
 
package/src/tunnel.ts CHANGED
@@ -32,18 +32,22 @@ export const tunnelCommand = command(
32
32
  option(
33
33
  "-s",
34
34
  "--service",
35
- choice(["localhost.run", "serveo.net", "pinggy.io"]),
36
- {
37
- description: message`The localtunnel service to use.`,
38
- },
35
+ choice(["localhost.run", "serveo.net", "pinggy.io"], {
36
+ metavar: "SERVICE",
37
+ }),
38
+ { description: message`The tunneling service to use.` },
39
39
  ),
40
40
  ),
41
41
  }),
42
42
  debugOption,
43
43
  ),
44
44
  {
45
+ brief:
46
+ message`Expose a local HTTP server to the public internet using a secure tunnel.`,
45
47
  description:
46
- message`Expose a local HTTP server to the public internet using a secure tunnel.\nNote that the HTTP requests through the tunnel have X-Forwarded-* headers.`,
48
+ message`Expose a local HTTP server to the public internet using a secure tunnel.
49
+
50
+ Note that the HTTP requests through the tunnel have X-Forwarded-* headers.`,
47
51
  },
48
52
  );
49
53
 
@@ -41,9 +41,12 @@ export const webFingerCommand = command(
41
41
  merge(
42
42
  object({
43
43
  command: constant("webfinger"),
44
- resources: multiple(argument(string({ metavar: "RESOURCE" }), {
45
- description: message`WebFinger resource(s) to look up.`,
46
- })),
44
+ resources: multiple(
45
+ argument(string({ metavar: "RESOURCE" }), {
46
+ description: message`WebFinger resource(s) to look up.`,
47
+ }),
48
+ { min: 1 },
49
+ ),
47
50
  userAgent,
48
51
  allowPrivateAddresses,
49
52
  maxRedirection,
@@ -51,8 +54,10 @@ export const webFingerCommand = command(
51
54
  debugOption,
52
55
  ),
53
56
  {
54
- description:
55
- message`Look up WebFinger resources. The argument can be multiple.`,
57
+ brief: message`Look up WebFinger resources.`,
58
+ description: message`Look up WebFinger resources.
59
+
60
+ The argument can be multiple.`,
56
61
  },
57
62
  );
58
63
 
package/tsdown.config.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { cp } from "node:fs/promises";
2
+ import { join } from "node:path";
1
3
  import { defineConfig } from "tsdown";
2
4
 
3
5
  export default defineConfig({
@@ -21,4 +23,13 @@ export default defineConfig({
21
23
  `;
22
24
  return outputOptions;
23
25
  },
26
+ hooks: {
27
+ "build:done": async (ctx) => {
28
+ await cp(
29
+ join("src", "init", "templates"),
30
+ join(ctx.options.outDir, "init", "templates"),
31
+ { recursive: true },
32
+ );
33
+ },
34
+ },
24
35
  });