@fedify/cli 2.0.0-pr.479.1922 → 2.0.0

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 (160) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -3
  3. package/dist/cache.js +17 -3
  4. package/dist/config.js +105 -0
  5. package/dist/deno.js +18 -8
  6. package/dist/generate-vocab/action.js +1 -1
  7. package/dist/imagerenderer.js +1 -1
  8. package/dist/inbox/rendercode.js +11 -21
  9. package/dist/inbox.js +162 -132
  10. package/dist/init/mod.js +3 -3
  11. package/dist/log.js +35 -1
  12. package/dist/lookup.js +55 -23
  13. package/dist/mod.js +95 -18
  14. package/dist/nodeinfo.js +39 -22
  15. package/dist/options.js +84 -0
  16. package/dist/relay.js +136 -0
  17. package/dist/tempserver.js +15 -8
  18. package/dist/tunnel.js +6 -10
  19. package/dist/utils.js +19 -108
  20. package/dist/webfinger/action.js +1 -1
  21. package/dist/webfinger/command.js +17 -9
  22. package/dist/webfinger/lib.js +3 -3
  23. package/package.json +50 -28
  24. package/deno.json +0 -71
  25. package/dist/globals.js +0 -49
  26. package/dist/init/action/configs.js +0 -91
  27. package/dist/init/action/const.js +0 -10
  28. package/dist/init/action/deps.js +0 -50
  29. package/dist/init/action/dir.js +0 -16
  30. package/dist/init/action/env.js +0 -13
  31. package/dist/init/action/install.js +0 -20
  32. package/dist/init/action/mod.js +0 -39
  33. package/dist/init/action/notice.js +0 -55
  34. package/dist/init/action/patch.js +0 -147
  35. package/dist/init/action/precommand.js +0 -28
  36. package/dist/init/action/recommend.js +0 -24
  37. package/dist/init/action/set.js +0 -31
  38. package/dist/init/action/templates.js +0 -58
  39. package/dist/init/action/utils.js +0 -50
  40. package/dist/init/ask/dir.js +0 -82
  41. package/dist/init/ask/kv.js +0 -44
  42. package/dist/init/ask/mod.js +0 -16
  43. package/dist/init/ask/mq.js +0 -46
  44. package/dist/init/ask/pm.js +0 -49
  45. package/dist/init/ask/wf.js +0 -29
  46. package/dist/init/command.js +0 -50
  47. package/dist/init/const.js +0 -31
  48. package/dist/init/json/biome.js +0 -24
  49. package/dist/init/json/kv.js +0 -53
  50. package/dist/init/json/mq.js +0 -72
  51. package/dist/init/json/pm.js +0 -44
  52. package/dist/init/json/rt.js +0 -39
  53. package/dist/init/json/vscode-settings-for-deno.js +0 -53
  54. package/dist/init/json/vscode-settings.js +0 -49
  55. package/dist/init/lib.js +0 -136
  56. package/dist/init/templates/defaults/federation.ts.tpl +0 -23
  57. package/dist/init/templates/defaults/logging.ts.tpl +0 -23
  58. package/dist/init/templates/express/app.ts.tpl +0 -16
  59. package/dist/init/templates/express/index.ts.tpl +0 -6
  60. package/dist/init/templates/hono/app.tsx.tpl +0 -14
  61. package/dist/init/templates/hono/index/bun.ts.tpl +0 -10
  62. package/dist/init/templates/hono/index/deno.ts.tpl +0 -13
  63. package/dist/init/templates/hono/index/node.ts.tpl +0 -14
  64. package/dist/init/templates/next/middleware.ts.tpl +0 -45
  65. package/dist/init/templates/nitro/.env.test.tpl +0 -1
  66. package/dist/init/templates/nitro/nitro.config.ts.tpl +0 -14
  67. package/dist/init/templates/nitro/server/error.ts.tpl +0 -3
  68. package/dist/init/templates/nitro/server/middleware/federation.ts.tpl +0 -8
  69. package/dist/init/test/action.js +0 -17
  70. package/dist/init/test/create.js +0 -100
  71. package/dist/init/test/fill.js +0 -32
  72. package/dist/init/test/lookup.js +0 -190
  73. package/dist/init/test/run.js +0 -25
  74. package/dist/init/test/utils.js +0 -17
  75. package/dist/init/webframeworks.js +0 -136
  76. package/scripts/pack.ts +0 -71
  77. package/src/cache.ts +0 -17
  78. package/src/docloader.ts +0 -67
  79. package/src/generate-vocab/action.ts +0 -17
  80. package/src/generate-vocab/command.ts +0 -44
  81. package/src/generate-vocab/mod.ts +0 -2
  82. package/src/globals.ts +0 -43
  83. package/src/imagerenderer.ts +0 -149
  84. package/src/inbox/entry.ts +0 -10
  85. package/src/inbox/rendercode.ts +0 -68
  86. package/src/inbox/view.tsx +0 -598
  87. package/src/inbox.tsx +0 -536
  88. package/src/init/action/configs.ts +0 -133
  89. package/src/init/action/const.ts +0 -9
  90. package/src/init/action/deps.ts +0 -161
  91. package/src/init/action/dir.ts +0 -11
  92. package/src/init/action/env.ts +0 -14
  93. package/src/init/action/install.ts +0 -24
  94. package/src/init/action/mod.ts +0 -66
  95. package/src/init/action/notice.ts +0 -103
  96. package/src/init/action/patch.ts +0 -233
  97. package/src/init/action/precommand.ts +0 -29
  98. package/src/init/action/recommend.ts +0 -38
  99. package/src/init/action/set.ts +0 -65
  100. package/src/init/action/templates.ts +0 -96
  101. package/src/init/action/utils.ts +0 -64
  102. package/src/init/ask/dir.ts +0 -98
  103. package/src/init/ask/kv.ts +0 -82
  104. package/src/init/ask/mod.ts +0 -23
  105. package/src/init/ask/mq.ts +0 -86
  106. package/src/init/ask/pm.ts +0 -58
  107. package/src/init/ask/wf.ts +0 -27
  108. package/src/init/command.ts +0 -135
  109. package/src/init/const.ts +0 -4
  110. package/src/init/json/biome.json +0 -17
  111. package/src/init/json/kv.json +0 -39
  112. package/src/init/json/mq.json +0 -95
  113. package/src/init/json/pm.json +0 -47
  114. package/src/init/json/rt.json +0 -42
  115. package/src/init/json/vscode-settings-for-deno.json +0 -43
  116. package/src/init/json/vscode-settings.json +0 -41
  117. package/src/init/lib.ts +0 -223
  118. package/src/init/mod.ts +0 -3
  119. package/src/init/templates/defaults/federation.ts.tpl +0 -23
  120. package/src/init/templates/defaults/logging.ts.tpl +0 -23
  121. package/src/init/templates/express/app.ts.tpl +0 -16
  122. package/src/init/templates/express/index.ts.tpl +0 -6
  123. package/src/init/templates/hono/app.tsx.tpl +0 -14
  124. package/src/init/templates/hono/index/bun.ts.tpl +0 -10
  125. package/src/init/templates/hono/index/deno.ts.tpl +0 -13
  126. package/src/init/templates/hono/index/node.ts.tpl +0 -14
  127. package/src/init/templates/next/middleware.ts.tpl +0 -45
  128. package/src/init/templates/nitro/.env.test.tpl +0 -1
  129. package/src/init/templates/nitro/nitro.config.ts.tpl +0 -14
  130. package/src/init/templates/nitro/server/error.ts.tpl +0 -3
  131. package/src/init/templates/nitro/server/middleware/federation.ts.tpl +0 -8
  132. package/src/init/test/action.ts +0 -28
  133. package/src/init/test/create.ts +0 -137
  134. package/src/init/test/fill.ts +0 -67
  135. package/src/init/test/lookup.ts +0 -254
  136. package/src/init/test/run.ts +0 -39
  137. package/src/init/test/types.ts +0 -27
  138. package/src/init/test/utils.ts +0 -21
  139. package/src/init/types.ts +0 -89
  140. package/src/init/webframeworks.ts +0 -168
  141. package/src/kv.bun.ts +0 -12
  142. package/src/kv.node.ts +0 -11
  143. package/src/log.ts +0 -64
  144. package/src/lookup.test.ts +0 -182
  145. package/src/lookup.ts +0 -563
  146. package/src/mod.ts +0 -62
  147. package/src/nodeinfo.test.ts +0 -229
  148. package/src/nodeinfo.ts +0 -454
  149. package/src/table.ts +0 -17
  150. package/src/tempserver.ts +0 -87
  151. package/src/tunnel.test.ts +0 -157
  152. package/src/tunnel.ts +0 -94
  153. package/src/utils.ts +0 -254
  154. package/src/webfinger/action.ts +0 -50
  155. package/src/webfinger/command.ts +0 -64
  156. package/src/webfinger/error.ts +0 -47
  157. package/src/webfinger/lib.ts +0 -37
  158. package/src/webfinger/mod.test.ts +0 -79
  159. package/src/webfinger/mod.ts +0 -2
  160. package/tsdown.config.ts +0 -35
package/dist/log.js CHANGED
@@ -47,6 +47,40 @@ await configure({
47
47
  contextLocalStorage: new AsyncLocalStorage(),
48
48
  reset: true
49
49
  });
50
+ async function configureLogging() {
51
+ const logFile$1 = process.env["FEDIFY_LOG_FILE"];
52
+ await configure({
53
+ sinks: {
54
+ console: getConsoleSink(),
55
+ recording: recordingSink,
56
+ file: logFile$1 == null ? () => void 0 : getFileSink(logFile$1)
57
+ },
58
+ filters: {},
59
+ loggers: [
60
+ {
61
+ category: "fedify",
62
+ lowestLevel: "debug",
63
+ sinks: [
64
+ "console",
65
+ "recording",
66
+ "file"
67
+ ]
68
+ },
69
+ {
70
+ category: "localtunnel",
71
+ lowestLevel: "debug",
72
+ sinks: ["console", "file"]
73
+ },
74
+ {
75
+ category: ["logtape", "meta"],
76
+ lowestLevel: "warning",
77
+ sinks: ["console", "file"]
78
+ }
79
+ ],
80
+ reset: true,
81
+ contextLocalStorage: new AsyncLocalStorage()
82
+ });
83
+ }
50
84
 
51
85
  //#endregion
52
- export { recordingSink };
86
+ export { configureLogging, recordingSink };
package/dist/lookup.js CHANGED
@@ -1,18 +1,22 @@
1
1
 
2
2
  import { Temporal } from "@js-temporal/polyfill";
3
3
 
4
+ import { configContext } from "./config.js";
4
5
  import { getContextLoader, getDocumentLoader } from "./docloader.js";
5
- import { configureLogging, debugOption } from "./globals.js";
6
+ import { configureLogging } from "./log.js";
7
+ import { createTunnelServiceOption, userAgentOption } from "./options.js";
6
8
  import { spawnTemporaryServer } from "./tempserver.js";
7
9
  import { colorEnabled, colors, formatObject } from "./utils.js";
8
10
  import { renderImages } from "./imagerenderer.js";
11
+ import process from "node:process";
9
12
  import { argument, choice, command, constant, flag, float, map, merge, message, multiple, object, option, optionNames, optional, or, string, withDefault } from "@optique/core";
10
13
  import { path, print, printError } from "@optique/run";
11
- import process from "node:process";
12
- import { Application, Collection, CryptographicKey, Object as Object$1, generateCryptoKeyPair, getAuthenticatedDocumentLoader, lookupObject, respondWithObject, traverseCollection } from "@fedify/fedify";
14
+ import { createWriteStream } from "node:fs";
15
+ import { bindConfig } from "@optique/config";
16
+ import { generateCryptoKeyPair, getAuthenticatedDocumentLoader, respondWithObject } from "@fedify/fedify";
17
+ import { Application, Collection, CryptographicKey, Object as Object$1, lookupObject, traverseCollection } from "@fedify/vocab";
13
18
  import { getLogger } from "@logtape/logtape";
14
19
  import ora from "ora";
15
- import { createWriteStream } from "node:fs";
16
20
 
17
21
  //#region src/lookup.ts
18
22
  const logger = getLogger([
@@ -20,28 +24,56 @@ const logger = getLogger([
20
24
  "cli",
21
25
  "lookup"
22
26
  ]);
23
- const authorizedFetchOption = withDefault(object({
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 ${optionNames(["-a", "--authorized-fetch"])}. It is used for the double-knocking technique.` }), "draft-cavage-http-signatures-12")
26
- }), { authorizedFetch: false });
27
- const traverseOption = withDefault(object({
28
- traverse: flag("-t", "--traverse", { description: message`Traverse the given collection(s) to fetch all items.` }),
29
- suppressErrors: option("-S", "--suppress-errors", { description: message`Suppress partial errors while traversing the collection.` })
30
- }), { traverse: false });
31
- const lookupCommand = command("lookup", merge("Looking up options", object({ command: constant("lookup") }), traverseOption, authorizedFetchOption, debugOption, object({
32
- urls: multiple(argument(string({ metavar: "URL_OR_HANDLE" }), { description: message`One or more URLs or handles to look up.` }), { min: 1 }),
33
- format: withDefault(or(map(option("-r", "--raw", { description: message`Print the fetched JSON-LD document as is.` }), () => "raw"), map(option("-C", "--compact", { description: message`Compact the fetched JSON-LD document.` }), () => "compact"), map(option("-e", "--expand", { description: message`Expand the fetched JSON-LD document.` }), () => "expand")), "default"),
34
- userAgent: optional(option("-u", "--user-agent", string({ metavar: "USER_AGENT" }), { description: message`The custom User-Agent header value.` })),
35
- separator: withDefault(option("-s", "--separator", string({ metavar: "SEPARATOR" }), { description: message`Specify the separator between adjacent output objects or collection items.` }), "----"),
27
+ const authorizedFetchOption = withDefault(object("Authorized fetch options", {
28
+ authorizedFetch: bindConfig(map(flag("-a", "--authorized-fetch", { description: message`Sign the request with an one-time key.` }), () => true), {
29
+ context: configContext,
30
+ key: (config) => config.lookup?.authorizedFetch ? true : void 0
31
+ }),
32
+ firstKnock: bindConfig(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.` }), {
33
+ context: configContext,
34
+ key: (config) => config.lookup?.firstKnock ?? "draft-cavage-http-signatures-12",
35
+ default: "draft-cavage-http-signatures-12"
36
+ }),
37
+ tunnelService: createTunnelServiceOption()
38
+ }), {
39
+ authorizedFetch: false,
40
+ firstKnock: void 0,
41
+ tunnelService: void 0
42
+ });
43
+ const traverseOption = object("Traverse options", {
44
+ traverse: bindConfig(flag("-t", "--traverse", { description: message`Traverse the given collection(s) to fetch all items.` }), {
45
+ context: configContext,
46
+ key: (config) => config.lookup?.traverse ?? false,
47
+ default: false
48
+ }),
49
+ suppressErrors: bindConfig(flag("-S", "--suppress-errors", { description: message`Suppress partial errors while traversing the collection.` }), {
50
+ context: configContext,
51
+ key: (config) => config.lookup?.suppressErrors ?? false,
52
+ default: false
53
+ })
54
+ });
55
+ const lookupCommand = command("lookup", merge(object({ command: constant("lookup") }), traverseOption, authorizedFetchOption, merge("Network options", userAgentOption, object({ timeout: optional(bindConfig(option("-T", "--timeout", float({
56
+ min: 0,
57
+ metavar: "SECONDS"
58
+ }), { description: message`Set timeout for network requests in seconds.` }), {
59
+ context: configContext,
60
+ key: (config) => config.lookup?.timeout
61
+ })) })), object("Arguments", { urls: multiple(argument(string({ metavar: "URL_OR_HANDLE" }), { description: message`One or more URLs or handles to look up.` }), { min: 1 }) }), object("Output options", {
62
+ format: bindConfig(optional(or(map(flag("-r", "--raw", { description: message`Print the fetched JSON-LD document as is.` }), () => "raw"), map(flag("-C", "--compact", { description: message`Compact the fetched JSON-LD document.` }), () => "compact"), map(flag("-e", "--expand", { description: message`Expand the fetched JSON-LD document.` }), () => "expand"))), {
63
+ context: configContext,
64
+ key: (config) => config.lookup?.defaultFormat ?? "default",
65
+ default: "default"
66
+ }),
67
+ separator: bindConfig(option("-s", "--separator", string({ metavar: "SEPARATOR" }), { description: message`Specify the separator between adjacent output objects or collection items.` }), {
68
+ context: configContext,
69
+ key: (config) => config.lookup?.separator ?? "----",
70
+ default: "----"
71
+ }),
36
72
  output: optional(option("-o", "--output", path({
37
73
  metavar: "OUTPUT_PATH",
38
74
  type: "file",
39
75
  allowCreate: true
40
- }), { description: message`Specify the output file path.` })),
41
- timeout: optional(option("-T", "--timeout", float({
42
- min: 0,
43
- metavar: "SECONDS"
44
- }), { description: message`Set timeout for network requests in seconds.` }))
76
+ }), { description: message`Specify the output file path.` }))
45
77
  })), {
46
78
  brief: message`Look up Activity Streams objects.`,
47
79
  description: message`Look up Activity Streams objects by URL or actor handle.
@@ -172,7 +204,7 @@ async function runLookup(command$1) {
172
204
  inbox: new URL("/inbox", serverUrl),
173
205
  outbox: new URL("/outbox", serverUrl)
174
206
  }), { contextLoader });
175
- });
207
+ }, { service: command$1.tunnelService });
176
208
  const baseAuthLoader = getAuthenticatedDocumentLoader({
177
209
  keyId: new URL("#main-key", server.url),
178
210
  privateKey: key.privateKey
package/dist/mod.js CHANGED
@@ -1,39 +1,116 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node --disable-warning=ExperimentalWarning
2
2
 
3
3
  import { Temporal } from "@js-temporal/polyfill";
4
4
 
5
+ import { configContext, tryLoadToml } from "./config.js";
5
6
  import { runGenerateVocab } from "./generate-vocab/action.js";
6
7
  import command_default from "./generate-vocab/command.js";
7
8
  import "./generate-vocab/mod.js";
9
+ import deno_default from "./deno.js";
10
+ import { globalOptions } from "./options.js";
8
11
  import { inboxCommand, runInbox } from "./inbox.js";
9
- import action_default from "./init/action/mod.js";
10
- import { initCommand, testInitCommand } from "./init/command.js";
11
- import action_default$1 from "./init/test/action.js";
12
- import "./init/mod.js";
12
+ import { initCommand, runInit } from "./init/mod.js";
13
13
  import { nodeInfoCommand, runNodeInfo } from "./nodeinfo.js";
14
14
  import { lookupCommand, runLookup } from "./lookup.js";
15
+ import { relayCommand, runRelay } from "./relay.js";
15
16
  import { runTunnel, tunnelCommand } from "./tunnel.js";
16
17
  import { runWebFinger } from "./webfinger/action.js";
17
18
  import { webFingerCommand } from "./webfinger/command.js";
18
19
  import "./webfinger/mod.js";
19
- import { or } from "@optique/core";
20
- import { run } from "@optique/run";
20
+ import { join } from "node:path";
21
+ import { homedir } from "node:os";
22
+ import process from "node:process";
23
+ import { runWithConfig } from "@optique/config/run";
24
+ import { group, merge, message, or } from "@optique/core";
25
+ import { printError } from "@optique/run";
26
+ import { merge as merge$1 } from "es-toolkit";
27
+ import { readFileSync } from "node:fs";
28
+ import { parse } from "smol-toml";
21
29
 
22
30
  //#region src/mod.ts
23
- const command$1 = or(initCommand, webFingerCommand, lookupCommand, inboxCommand, nodeInfoCommand, tunnelCommand, command_default, testInitCommand);
31
+ /**
32
+ * Returns the system-wide configuration file paths.
33
+ * - Linux/macOS: Searches `$XDG_CONFIG_DIRS` (default: /etc/xdg)
34
+ * - Windows: Uses `%ProgramData%` (default: C:\ProgramData)
35
+ */
36
+ function getSystemConfigPaths() {
37
+ if (process.platform === "win32") {
38
+ const programData = process.env.ProgramData || "C:\\ProgramData";
39
+ return [join(programData, "fedify", "config.toml")];
40
+ }
41
+ return (process.env.XDG_CONFIG_DIRS || "/etc/xdg").split(":").map((dir) => join(dir, "fedify", "config.toml"));
42
+ }
43
+ /**
44
+ * Returns the user-level configuration file path.
45
+ * - Linux/macOS: `$XDG_CONFIG_HOME/fedify/config.toml` (default: ~/.config)
46
+ * - Windows: `%APPDATA%\fedify\config.toml`
47
+ */
48
+ function getUserConfigPath() {
49
+ if (process.platform === "win32") {
50
+ const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
51
+ return join(appData, "fedify", "config.toml");
52
+ }
53
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
54
+ return join(xdgConfigHome, "fedify", "config.toml");
55
+ }
56
+ const command$1 = merge(or(group("Generating code", or(initCommand, command_default)), group("ActivityPub tools", or(webFingerCommand, lookupCommand, inboxCommand, nodeInfoCommand, relayCommand)), group("Network tools", tunnelCommand)), globalOptions);
24
57
  async function main() {
25
- const result = run(command$1, {
58
+ const result = await runWithConfig(command$1, configContext, {
26
59
  programName: "fedify",
27
- help: "both"
60
+ load: (parsed) => {
61
+ if (parsed.ignoreConfig) return {};
62
+ const systemConfigs = getSystemConfigPaths().map(tryLoadToml);
63
+ const system = systemConfigs.reduce((acc, config) => merge$1(acc, config), {});
64
+ const user = tryLoadToml(getUserConfigPath());
65
+ const project = tryLoadToml(join(process.cwd(), ".fedify.toml"));
66
+ let custom = {};
67
+ if (parsed.configPath) try {
68
+ custom = parse(readFileSync(parsed.configPath, "utf-8"));
69
+ } catch (error) {
70
+ printError(message`Could not load config file at ${parsed.configPath}: ${error instanceof Error ? error.message : String(error)}`);
71
+ process.exit(1);
72
+ }
73
+ return [
74
+ system,
75
+ user,
76
+ project,
77
+ custom
78
+ ].reduce((acc, config) => merge$1(acc, config), {});
79
+ },
80
+ args: process.argv.slice(2),
81
+ help: {
82
+ mode: "both",
83
+ onShow: () => process.exit(0),
84
+ group: "Meta commands"
85
+ },
86
+ version: {
87
+ mode: "both",
88
+ value: deno_default.version,
89
+ group: "Meta commands"
90
+ },
91
+ completion: {
92
+ mode: "command",
93
+ name: "both",
94
+ helpVisibility: "plural",
95
+ group: "Meta commands"
96
+ },
97
+ onError: () => process.exit(1),
98
+ colors: process.stdout.isTTY && (process.env.NO_COLOR == null || process.env.NO_COLOR === ""),
99
+ maxWidth: process.stdout.columns,
100
+ showDefault: true,
101
+ showChoices: true
28
102
  });
29
- if (result.command === "init") await action_default(result);
30
- if (result.command === "lookup") await runLookup(result);
31
- if (result.command === "webfinger") await runWebFinger(result);
32
- if (result.command === "inbox") runInbox(result);
33
- if (result.command === "nodeinfo") runNodeInfo(result);
34
- if (result.command === "tunnel") await runTunnel(result);
35
- if (result.command === "generate-vocab") await runGenerateVocab(result);
36
- if (result.command === "test-init") await action_default$1(result);
103
+ if (result.command === "init") await runInit(result);
104
+ else if (result.command === "lookup") await runLookup(result);
105
+ else if (result.command === "webfinger") await runWebFinger(result);
106
+ else if (result.command === "inbox") runInbox(result);
107
+ else if (result.command === "nodeinfo") runNodeInfo(result);
108
+ else if (result.command === "tunnel") await runTunnel(result);
109
+ else if (result.command === "generate-vocab") await runGenerateVocab(result);
110
+ else if (result.command === "relay") await runRelay(result);
111
+ else {
112
+ const _exhaustiveCheck = result;
113
+ }
37
114
  }
38
115
  await main();
39
116
 
package/dist/nodeinfo.js CHANGED
@@ -1,16 +1,18 @@
1
1
 
2
2
  import { Temporal } from "@js-temporal/polyfill";
3
3
 
4
- import { debugOption } from "./globals.js";
4
+ import { configContext } from "./config.js";
5
+ import { userAgentOption } from "./options.js";
5
6
  import { colors, formatObject } from "./utils.js";
6
- import { argument, command, constant, flag, merge, message, object, option, optional, or, string, text } from "@optique/core";
7
- import { print, printError } from "@optique/run";
7
+ import os from "node:os";
8
8
  import process from "node:process";
9
+ import { argument, command, constant, flag, group, merge, message, object, string, text } from "@optique/core";
10
+ import { print, printError } from "@optique/run";
11
+ import { bindConfig } from "@optique/config";
9
12
  import { getNodeInfo } from "@fedify/fedify";
10
13
  import { getLogger } from "@logtape/logtape";
11
14
  import ora from "ora";
12
15
  import { getUserAgent } from "@fedify/vocab-runtime";
13
- import os from "node:os";
14
16
  import { createJimp } from "@jimp/core";
15
17
  import webp from "@jimp/wasm-webp";
16
18
  import { isICO, parseICO } from "icojs";
@@ -26,16 +28,31 @@ const Jimp = createJimp({
26
28
  formats: [...defaultFormats, webp],
27
29
  plugins: defaultPlugins
28
30
  });
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.` })),
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.` }))
33
- })));
34
- const userAgentOption = optional(object({ userAgent: option("-u", "--user-agent", string(), { description: message`The custom User-Agent header value.` }) }));
35
- const nodeInfoCommand = command("nodeinfo", merge(object({
31
+ const nodeInfoOption = merge(object("Display options", {
32
+ raw: bindConfig(flag("-r", "--raw", { description: message`Show NodeInfo document in the raw JSON format` }), {
33
+ context: configContext,
34
+ key: (config) => config.nodeinfo?.raw ?? false,
35
+ default: false
36
+ }),
37
+ noFavicon: bindConfig(flag("--no-favicon", { description: message`Disable fetching the favicon of the instance` }), {
38
+ context: configContext,
39
+ key: (config) => config.nodeinfo?.showFavicon === false,
40
+ default: false
41
+ }),
42
+ metadata: bindConfig(flag("-m", "--metadata", { description: message`Show the extra metadata of the NodeInfo, i.e., the metadata field of the document.` }), {
43
+ context: configContext,
44
+ key: (config) => config.nodeinfo?.showMetadata ?? false,
45
+ default: false
46
+ })
47
+ }), object("Parsing options", { bestEffort: bindConfig(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.` }), {
48
+ context: configContext,
49
+ key: (config) => config.nodeinfo?.bestEffort ?? false,
50
+ default: false
51
+ }) }));
52
+ const nodeInfoCommand = command("nodeinfo", merge(object("Arguments", {
36
53
  command: constant("nodeinfo"),
37
54
  host: argument(string({ metavar: "HOST" }), { description: message`Bare hostname or a full URL of the instance` })
38
- }), debugOption, nodeInfoOption, userAgentOption), {
55
+ }), nodeInfoOption, group("Network options", userAgentOption)), {
39
56
  brief: message`Get information about a remote node using the NodeInfo protocol`,
40
57
  description: message`Get information about a remote node using the NodeInfo protocol.
41
58
 
@@ -47,7 +64,7 @@ async function runNodeInfo(command$1) {
47
64
  discardStdin: false
48
65
  }).start();
49
66
  const url = new URL(URL.canParse(command$1.host) ? command$1.host : `https://${command$1.host}`);
50
- if ("raw" in command$1 && command$1.raw) {
67
+ if (command$1.raw) {
51
68
  const nodeInfo$1 = await getNodeInfo(url, {
52
69
  parse: "none",
53
70
  userAgent: command$1.userAgent
@@ -62,19 +79,19 @@ async function runNodeInfo(command$1) {
62
79
  return;
63
80
  }
64
81
  const nodeInfo = await getNodeInfo(url, {
65
- parse: "bestEffort" in command$1 && command$1.bestEffort ? "best-effort" : "strict",
82
+ parse: command$1.bestEffort ? "best-effort" : "strict",
66
83
  userAgent: command$1.userAgent
67
84
  });
68
85
  logger.debug("NodeInfo document: {nodeInfo}", { nodeInfo });
69
86
  if (nodeInfo == void 0) {
70
87
  spinner.fail("No NodeInfo document found or it is invalid.");
71
88
  printError(message`No NodeInfo document found or it is invalid.`);
72
- if (!("bestEffort" in command$1 && command$1.bestEffort)) printError(message`Use the -b/--best-effort option to try to parse the document anyway.`);
89
+ if (!command$1.bestEffort) printError(message`Use the -b/--best-effort option to try to parse the document anyway.`);
73
90
  process.exit(1);
74
91
  }
75
92
  let layout;
76
93
  let defaultWidth = 0;
77
- if (!("noFavicon" in command$1 && command$1.noFavicon)) {
94
+ if (!command$1.noFavicon) {
78
95
  spinner.text = "Fetching the favicon...";
79
96
  try {
80
97
  const faviconUrl = await getFaviconUrl(url, command$1.userAgent);
@@ -147,9 +164,9 @@ async function runNodeInfo(command$1) {
147
164
  layout[next()] += colors.bold(colors.dim("Open registrations:"));
148
165
  layout[next()] += " " + (nodeInfo.openRegistrations ? "Yes" : "No");
149
166
  }
150
- if ("metadata" in command$1 && command$1.metadata && nodeInfo.metadata != null && Object.keys(nodeInfo.metadata).length > 0) {
167
+ if (command$1.metadata && nodeInfo.metadata != null && Object.keys(nodeInfo.metadata).length > 0) {
151
168
  layout[next()] += colors.bold(colors.dim("Metadata:"));
152
- 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)}`;
169
+ for (const [key, value$1] of Object.entries(nodeInfo.metadata)) layout[next()] += ` ${colors.dim(key + ":")} ${indent(typeof value$1 === "string" ? value$1 : formatObject(value$1), defaultWidth + 4 + key.length)}`;
153
170
  }
154
171
  console.log(layout.join("\n"));
155
172
  }
@@ -164,8 +181,8 @@ async function getFaviconUrl(url, userAgent) {
164
181
  for (const match of text$1.matchAll(LINK_REGEXP)) {
165
182
  const attrs = {};
166
183
  for (const attrMatch of match[1].matchAll(LINK_ATTRS_REGEXP)) {
167
- const [, key, value] = attrMatch;
168
- attrs[key] = value.startsWith("\"") || value.startsWith("'") ? value.slice(1, -1) : value;
184
+ const [, key, value$1] = attrMatch;
185
+ attrs[key] = value$1.startsWith("\"") || value$1.startsWith("'") ? value$1.slice(1, -1) : value$1;
169
186
  }
170
187
  const rel = attrs.rel?.toLowerCase()?.trim()?.split(/\s+/) ?? [];
171
188
  if (!rel.includes("icon") && !rel.includes("apple-touch-icon")) continue;
@@ -203,11 +220,11 @@ const CUBE_VALUES = [
203
220
  215,
204
221
  255
205
222
  ];
206
- const findClosestIndex = (value) => {
223
+ const findClosestIndex = (value$1) => {
207
224
  let minDiff = Infinity;
208
225
  let closestIndex = 0;
209
226
  for (let idx = 0; idx < CUBE_VALUES.length; idx++) {
210
- const diff = Math.abs(value - CUBE_VALUES[idx]);
227
+ const diff = Math.abs(value$1 - CUBE_VALUES[idx]);
211
228
  if (diff < minDiff) {
212
229
  minDiff = diff;
213
230
  closestIndex = idx;
@@ -0,0 +1,84 @@
1
+
2
+ import { Temporal } from "@js-temporal/polyfill";
3
+
4
+ import { configContext } from "./config.js";
5
+ import { choice, constant, flag, map, merge, message, object, option, or, string, withDefault } from "@optique/core";
6
+ import { bindConfig } from "@optique/config";
7
+ import { getUserAgent } from "@fedify/vocab-runtime";
8
+
9
+ //#region src/options.ts
10
+ /**
11
+ * Available tunneling services for exposing local servers to the public internet.
12
+ */
13
+ const TUNNEL_SERVICES = [
14
+ "localhost.run",
15
+ "serveo.net",
16
+ "pinggy.io"
17
+ ];
18
+ /**
19
+ * Creates a tunnel service option with customizable option names.
20
+ */
21
+ function createTunnelServiceOption(optionNames$1 = ["--tunnel-service"]) {
22
+ return withDefault(bindConfig(option(...optionNames$1, choice(TUNNEL_SERVICES, { metavar: "SERVICE" }), { description: message`The tunneling service to use.
23
+ By default, any of the supported tunneling services will be used
24
+ (randomly selected for each tunnel).` }), {
25
+ context: configContext,
26
+ key: (config) => config.tunnelService
27
+ }), void 0);
28
+ }
29
+ /**
30
+ * Creates a tunnel option that binds to a specific config section's noTunnel field.
31
+ * Use this when tunneling can be disabled (e.g., in `inbox` and `relay`).
32
+ *
33
+ * @param section - The config section to read noTunnel from ("inbox" or "relay")
34
+ * @returns An option object with `tunnel` (boolean) and `tunnelService` fields
35
+ */
36
+ function createTunnelOption(section) {
37
+ return object({
38
+ tunnel: bindConfig(withDefault(map(flag("-T", "--no-tunnel", { description: message`Do not tunnel the server to the public Internet.` }), () => false), true), {
39
+ context: configContext,
40
+ key: (config) => !(config[section]?.noTunnel ?? false),
41
+ default: true
42
+ }),
43
+ tunnelService: createTunnelServiceOption()
44
+ });
45
+ }
46
+ const debugOption = object({ debug: bindConfig(option("-d", "--debug", { description: message`Enable debug mode.` }), {
47
+ context: configContext,
48
+ key: (config) => config.debug ?? false,
49
+ default: false
50
+ }) });
51
+ const userAgentOption = object({ userAgent: bindConfig(option("-u", "--user-agent", string({ metavar: "USER_AGENT" }), { description: message`The custom User-Agent header value.` }), {
52
+ context: configContext,
53
+ key: (config) => config.userAgent ?? getUserAgent(),
54
+ default: getUserAgent()
55
+ }) });
56
+ /**
57
+ * Configuration file options.
58
+ *
59
+ * These options are mutually exclusive:
60
+ * - `--config PATH` loads an additional config file on top of standard hierarchy
61
+ * - `--ignore-config` skips all config files (useful for CI reproducibility)
62
+ *
63
+ * Returns either:
64
+ * - `{ ignoreConfig: true }` when `--ignore-config` is specified
65
+ * - `{ ignoreConfig: false, configPath: string }` when `--config` is specified
66
+ * - `{ ignoreConfig: false }` when neither is specified (default)
67
+ */
68
+ const configOption = withDefault(or(object({ ignoreConfig: map(flag("--ignore-config", { description: message`Ignore all configuration files.` }), () => true) }), object({
69
+ ignoreConfig: constant(false),
70
+ configPath: option("--config", string({ metavar: "PATH" }), { description: message`Load an additional configuration file.` })
71
+ })), {
72
+ ignoreConfig: false,
73
+ configPath: void 0
74
+ });
75
+ /**
76
+ * Global options that apply to all commands.
77
+ *
78
+ * Combines debug mode and configuration file options into a single
79
+ * "Global options" group.
80
+ */
81
+ const globalOptions = merge("Global options", debugOption, configOption);
82
+
83
+ //#endregion
84
+ export { createTunnelOption, createTunnelServiceOption, globalOptions, userAgentOption };
package/dist/relay.js ADDED
@@ -0,0 +1,136 @@
1
+
2
+ import { Temporal } from "@js-temporal/polyfill";
3
+
4
+ import { configContext } from "./config.js";
5
+ import { configureLogging } from "./log.js";
6
+ import { createTunnelOption } from "./options.js";
7
+ import { tableStyle } from "./table.js";
8
+ import { spawnTemporaryServer } from "./tempserver.js";
9
+ import { colors, matchesActor } from "./utils.js";
10
+ import { SqliteKvStore } from "@fedify/sqlite";
11
+ import process from "node:process";
12
+ import { DatabaseSync } from "node:sqlite";
13
+ import { command, constant, group, integer, merge, message, multiple, object, option, optionName, optional, string, value } from "@optique/core";
14
+ import { bindConfig } from "@optique/config";
15
+ import { MemoryKvStore } from "@fedify/fedify";
16
+ import { getLogger } from "@logtape/logtape";
17
+ import Table from "cli-table3";
18
+ import ora from "ora";
19
+ import { createRelay } from "@fedify/relay";
20
+ import { choice as choice$1 } from "@optique/core/valueparser";
21
+
22
+ //#region src/relay.ts
23
+ const logger = getLogger([
24
+ "fedify",
25
+ "cli",
26
+ "relay"
27
+ ]);
28
+ const relayCommand = command("relay", merge(object("Relay options", {
29
+ command: constant("relay"),
30
+ protocol: bindConfig(option("-p", "--protocol", choice$1(["mastodon", "litepub"], { metavar: "TYPE" }), { description: message`The relay protocol to use. ${value("mastodon")} for Mastodon-compatible relay, ${value("litepub")} for LitePub-compatible relay.` }), {
31
+ context: configContext,
32
+ key: (config) => config.relay?.protocol ?? "mastodon",
33
+ default: "mastodon"
34
+ }),
35
+ persistent: optional(bindConfig(option("--persistent", string({ metavar: "PATH" }), { description: message`Path to SQLite database file for persistent storage. If not specified, uses in-memory storage which is lost when the server stops.` }), {
36
+ context: configContext,
37
+ key: (config) => config.relay?.persistent
38
+ })),
39
+ port: bindConfig(option("-P", "--port", integer({
40
+ min: 0,
41
+ max: 65535,
42
+ metavar: "PORT"
43
+ }), { description: message`The local port to listen on.` }), {
44
+ context: configContext,
45
+ key: (config) => config.relay?.port ?? 8e3,
46
+ default: 8e3
47
+ }),
48
+ name: bindConfig(option("-n", "--name", string({ metavar: "NAME" }), { description: message`The relay display name.` }), {
49
+ context: configContext,
50
+ key: (config) => config.relay?.name ?? "Fedify Relay",
51
+ default: "Fedify Relay"
52
+ }),
53
+ acceptFollow: bindConfig(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.` })), {
54
+ context: configContext,
55
+ key: (config) => config.relay?.acceptFollow ?? [],
56
+ default: []
57
+ }),
58
+ rejectFollow: bindConfig(multiple(option("-r", "--reject-follow", string({ metavar: "URI" }), { description: message`Reject 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 rejected.` })), {
59
+ context: configContext,
60
+ key: (config) => config.relay?.rejectFollow ?? [],
61
+ default: []
62
+ })
63
+ }), group("Tunnel options", createTunnelOption("relay"))), {
64
+ brief: message`Run an ephemeral ActivityPub relay server.`,
65
+ description: message`Spins up an ActivityPub relay server that forwards activities between federated instances. The server can use either Mastodon or LitePub compatible relay protocol.
66
+
67
+ By default, the server is tunneled to the public internet for external access. Use ${optionName("--no-tunnel")} to run locally only.`
68
+ });
69
+ async function runRelay(command$1) {
70
+ if (command$1.debug) await configureLogging();
71
+ const spinner = ora({
72
+ text: "Starting relay server...",
73
+ discardStdin: false
74
+ }).start();
75
+ let kv;
76
+ if (command$1.persistent) {
77
+ logger.debug("Using SQLite storage at {path}.", { path: command$1.persistent });
78
+ const db = new DatabaseSync(command$1.persistent);
79
+ kv = new SqliteKvStore(db);
80
+ } else {
81
+ logger.debug("Using in-memory storage.");
82
+ kv = new MemoryKvStore();
83
+ }
84
+ let relay;
85
+ let server = null;
86
+ const acceptFollows = [];
87
+ const rejectFollows = [];
88
+ if (command$1.acceptFollow != null && command$1.acceptFollow.length > 0) acceptFollows.push(...command$1.acceptFollow ?? []);
89
+ if (command$1.rejectFollow != null && command$1.rejectFollow.length > 0) rejectFollows.push(...command$1.rejectFollow ?? []);
90
+ server = await spawnTemporaryServer(async (request) => {
91
+ return await relay.fetch(request);
92
+ }, {
93
+ noTunnel: !command$1.tunnel,
94
+ port: command$1.port,
95
+ ...command$1.tunnel && { service: command$1.tunnelService }
96
+ });
97
+ relay = createRelay(command$1.protocol, {
98
+ origin: server?.url.origin,
99
+ name: command$1.name,
100
+ kv,
101
+ subscriptionHandler: async (_ctx, actor) => {
102
+ const isInAcceptList = await matchesActor(actor, acceptFollows);
103
+ const isInRejectList = await matchesActor(actor, rejectFollows);
104
+ return isInAcceptList && !isInRejectList;
105
+ }
106
+ });
107
+ spinner.succeed(`Relay server is running: ${colors.green(server.url.href)}`);
108
+ await printRelayInfo(relay, {
109
+ protocol: command$1.protocol,
110
+ name: command$1.name,
111
+ persistent: command$1.persistent
112
+ });
113
+ process.on("SIGINT", async () => {
114
+ spinner.start("Shutting down relay server...");
115
+ await server.close();
116
+ spinner.succeed("Relay server stopped.");
117
+ process.exit(0);
118
+ });
119
+ }
120
+ async function printRelayInfo(relay, options) {
121
+ const actorUri = await relay.getActorUri();
122
+ const sharedInboxUri = await relay.getSharedInboxUri();
123
+ const table = new Table({
124
+ chars: tableStyle,
125
+ style: {
126
+ head: [],
127
+ border: []
128
+ }
129
+ });
130
+ table.push({ "Actor URI:": colors.green(actorUri.href) }, { "Shared Inbox:": colors.green(sharedInboxUri.href) }, { "Protocol:": colors.green(options.protocol) }, { "Name:": colors.green(options.name) }, { "Storage:": colors.green(options.persistent ?? "in-memory") });
131
+ console.log(table.toString());
132
+ console.log("\nPress ^C to stop the relay server.");
133
+ }
134
+
135
+ //#endregion
136
+ export { relayCommand, runRelay };