@fedify/cli 1.8.11 → 2.0.0-dev.1757

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 (130) hide show
  1. package/deno.json +72 -0
  2. package/dist/cache.js +17 -0
  3. package/dist/deno.js +72 -0
  4. package/dist/docloader.js +52 -0
  5. package/dist/globals.js +49 -0
  6. package/dist/imagerenderer.js +105 -0
  7. package/dist/inbox/rendercode.js +57 -0
  8. package/dist/inbox/view.js +508 -0
  9. package/dist/inbox.js +315 -0
  10. package/dist/init/action/configs.js +81 -0
  11. package/dist/init/action/deps.js +52 -0
  12. package/dist/init/action/dir.js +16 -0
  13. package/dist/init/action/env.js +13 -0
  14. package/dist/init/action/install.js +22 -0
  15. package/dist/init/action/mod.js +39 -0
  16. package/dist/init/action/notice.js +62 -0
  17. package/dist/init/action/patch.js +141 -0
  18. package/dist/init/action/precommand.js +23 -0
  19. package/dist/init/action/recommend.js +24 -0
  20. package/dist/init/action/set.js +31 -0
  21. package/dist/init/action/templates.js +57 -0
  22. package/dist/init/action/utils.js +50 -0
  23. package/dist/init/ask/dir.js +82 -0
  24. package/dist/init/ask/kv.js +33 -0
  25. package/dist/init/ask/mod.js +16 -0
  26. package/dist/init/ask/mq.js +33 -0
  27. package/dist/init/ask/pm.js +49 -0
  28. package/dist/init/ask/wf.js +29 -0
  29. package/dist/init/command.js +25 -0
  30. package/dist/init/const.js +31 -0
  31. package/dist/init/json/biome.js +24 -0
  32. package/dist/init/json/kv.js +53 -0
  33. package/dist/init/json/mq.js +72 -0
  34. package/dist/init/json/pm.js +44 -0
  35. package/dist/init/json/rt.js +39 -0
  36. package/dist/init/json/vscode-settings-for-deno.js +53 -0
  37. package/dist/init/json/vscode-settings.js +49 -0
  38. package/dist/init/lib.js +129 -0
  39. package/dist/init/mod.js +5 -0
  40. package/dist/init/webframeworks.js +133 -0
  41. package/dist/kv.bun.js +17 -0
  42. package/dist/kv.node.js +17 -0
  43. package/dist/log.js +52 -0
  44. package/dist/lookup.js +287 -0
  45. package/dist/mod.js +34 -0
  46. package/dist/nodeinfo.js +261 -0
  47. package/dist/table.js +24 -0
  48. package/dist/tempserver.js +71 -0
  49. package/dist/tunnel.js +21 -0
  50. package/dist/utils.js +67 -0
  51. package/dist/webfinger/action.js +44 -0
  52. package/dist/webfinger/command.js +20 -0
  53. package/dist/webfinger/error.js +47 -0
  54. package/dist/webfinger/lib.js +45 -0
  55. package/dist/webfinger/mod.js +5 -0
  56. package/package.json +64 -24
  57. package/scripts/pack.ts +64 -0
  58. package/src/cache.ts +17 -0
  59. package/src/docloader.ts +67 -0
  60. package/src/globals.ts +43 -0
  61. package/src/imagerenderer.ts +149 -0
  62. package/src/inbox/entry.ts +10 -0
  63. package/src/inbox/rendercode.ts +68 -0
  64. package/src/inbox/view.tsx +598 -0
  65. package/src/inbox.tsx +535 -0
  66. package/src/init/action/configs.ts +88 -0
  67. package/src/init/action/deps.ts +93 -0
  68. package/src/init/action/dir.ts +11 -0
  69. package/src/init/action/env.ts +14 -0
  70. package/src/init/action/install.ts +59 -0
  71. package/src/init/action/mod.ts +66 -0
  72. package/src/init/action/notice.ts +101 -0
  73. package/src/init/action/patch.ts +212 -0
  74. package/src/init/action/precommand.ts +22 -0
  75. package/src/init/action/recommend.ts +38 -0
  76. package/src/init/action/set.ts +78 -0
  77. package/src/init/action/templates.ts +95 -0
  78. package/src/init/action/utils.ts +64 -0
  79. package/src/init/ask/dir.ts +98 -0
  80. package/src/init/ask/kv.ts +39 -0
  81. package/src/init/ask/mod.ts +23 -0
  82. package/src/init/ask/mq.ts +37 -0
  83. package/src/init/ask/pm.ts +58 -0
  84. package/src/init/ask/wf.ts +27 -0
  85. package/src/init/command.ts +64 -0
  86. package/src/init/const.ts +4 -0
  87. package/src/init/json/biome.json +17 -0
  88. package/src/init/json/kv.json +39 -0
  89. package/src/init/json/mq.json +95 -0
  90. package/src/init/json/pm.json +47 -0
  91. package/src/init/json/rt.json +42 -0
  92. package/src/init/json/vscode-settings-for-deno.json +43 -0
  93. package/src/init/json/vscode-settings.json +41 -0
  94. package/src/init/lib.ts +220 -0
  95. package/src/init/mod.ts +2 -0
  96. package/src/init/templates/defaults/federation.ts.tpl +23 -0
  97. package/src/init/templates/defaults/logging.ts.tpl +23 -0
  98. package/src/init/templates/express/app.ts.tpl +16 -0
  99. package/src/init/templates/express/index.ts.tpl +6 -0
  100. package/src/init/templates/hono/app.tsx.tpl +14 -0
  101. package/src/init/templates/hono/index/bun.ts.tpl +10 -0
  102. package/src/init/templates/hono/index/deno.ts.tpl +13 -0
  103. package/src/init/templates/hono/index/node.ts.tpl +14 -0
  104. package/src/init/templates/next/middleware.ts.tpl +45 -0
  105. package/src/init/templates/nitro/nitro.config.ts.tpl +5 -0
  106. package/src/init/templates/nitro/server/error.ts.tpl +3 -0
  107. package/src/init/templates/nitro/server/middleware/federation.ts.tpl +8 -0
  108. package/src/init/types.ts +88 -0
  109. package/src/init/webframeworks.ts +151 -0
  110. package/src/kv.bun.ts +12 -0
  111. package/src/kv.node.ts +11 -0
  112. package/src/log.ts +64 -0
  113. package/src/lookup.test.ts +182 -0
  114. package/src/lookup.ts +558 -0
  115. package/src/mod.ts +45 -0
  116. package/src/nodeinfo.test.ts +229 -0
  117. package/src/nodeinfo.ts +447 -0
  118. package/src/table.ts +17 -0
  119. package/src/tempserver.ts +87 -0
  120. package/src/tunnel.ts +32 -0
  121. package/src/utils.ts +136 -0
  122. package/src/webfinger/action.ts +50 -0
  123. package/src/webfinger/command.ts +59 -0
  124. package/src/webfinger/error.ts +47 -0
  125. package/src/webfinger/lib.ts +37 -0
  126. package/src/webfinger/mod.test.ts +79 -0
  127. package/src/webfinger/mod.ts +2 -0
  128. package/tsdown.config.ts +24 -0
  129. package/src/install.mjs +0 -189
  130. package/src/run.mjs +0 -22
package/src/inbox.tsx ADDED
@@ -0,0 +1,535 @@
1
+ /** @jsx react-jsx */
2
+ /** @jsxImportSource hono/jsx */
3
+ import {
4
+ Accept,
5
+ Activity,
6
+ type Actor,
7
+ Application,
8
+ type Context,
9
+ createFederation,
10
+ Delete,
11
+ Endpoints,
12
+ Follow,
13
+ generateCryptoKeyPair,
14
+ getActorHandle,
15
+ Image,
16
+ isActor,
17
+ lookupObject,
18
+ MemoryKvStore,
19
+ PUBLIC_COLLECTION,
20
+ type Recipient,
21
+ } from "@fedify/fedify";
22
+ import { getLogger } from "@logtape/logtape";
23
+ import {
24
+ command,
25
+ constant,
26
+ type InferValue,
27
+ merge,
28
+ message,
29
+ multiple,
30
+ object,
31
+ option,
32
+ optional,
33
+ string,
34
+ withDefault,
35
+ } from "@optique/core";
36
+ import Table from "cli-table3";
37
+ import { type Context as HonoContext, Hono } from "hono";
38
+ import type { BlankEnv, BlankInput } from "hono/types";
39
+ import process from "node:process";
40
+ import ora from "ora";
41
+ import metadata from "../deno.json" with { type: "json" };
42
+ import { getDocumentLoader } from "./docloader.ts";
43
+ import { configureLogging, debugOption } from "./globals.ts";
44
+ import type { ActivityEntry } from "./inbox/entry.ts";
45
+ import { ActivityEntryPage, ActivityListPage } from "./inbox/view.tsx";
46
+ import { recordingSink } from "./log.ts";
47
+ import { tableStyle } from "./table.ts";
48
+ import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts";
49
+ import { colors } from "./utils.ts";
50
+
51
+ /**
52
+ * Context data for the ephemeral ActivityPub inbox server.
53
+ *
54
+ * This interface defines the shape of context data passed to federation
55
+ * handlers during inbox command execution.
56
+ */
57
+ interface ContextData {
58
+ activityIndex: number;
59
+ actorName: string;
60
+ actorSummary: string;
61
+ }
62
+
63
+ const logger = getLogger(["fedify", "cli", "inbox"]);
64
+
65
+ export const inboxCommand = command(
66
+ "inbox",
67
+ merge(
68
+ object("Inbox options", {
69
+ command: constant("inbox"),
70
+ follow: optional(
71
+ multiple(
72
+ option("-f", "--follow", string({ metavar: "URI" }), {
73
+ description:
74
+ message`Follow the given actor. The argument can be either an actor URI or a handle. Can be specified multiple times.`,
75
+ }),
76
+ ),
77
+ ),
78
+ acceptFollow: optional(
79
+ multiple(
80
+ option("-a", "--accept-follow", string({ metavar: "URI" }), {
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.`,
83
+ }),
84
+ ),
85
+ ),
86
+ noTunnel: option(
87
+ "-T",
88
+ "--no-tunnel",
89
+ {
90
+ description:
91
+ message`Do not tunnel the ephemeral ActivityPub server to the public Internet.`,
92
+ },
93
+ ),
94
+ actorName: withDefault(
95
+ option("--actor-name", string({ metavar: "NAME" }), {
96
+ description: message`Customize the actor display name.`,
97
+ }),
98
+ "Fedify Ephemeral Inbox",
99
+ ),
100
+ actorSummary: withDefault(
101
+ option("--actor-summary", string({ metavar: "SUMMARY" }), {
102
+ description: message`Customize the actor description.`,
103
+ }),
104
+ "An ephemeral ActivityPub inbox for testing purposes.",
105
+ ),
106
+ }),
107
+ debugOption,
108
+ ),
109
+ {
110
+ description:
111
+ 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
+ );
114
+
115
+ export async function runInbox(
116
+ command: InferValue<typeof inboxCommand>,
117
+ ) {
118
+ const fetch = createFetchHandler({
119
+ actorName: command.actorName,
120
+ actorSummary: command.actorSummary,
121
+ });
122
+ const sendDeleteToPeers = createSendDeleteToPeers({
123
+ actorName: command.actorName,
124
+ actorSummary: command.actorSummary,
125
+ });
126
+
127
+ // Enable Debug mode if requested
128
+ if (command.debug) {
129
+ await configureLogging();
130
+ }
131
+
132
+ const spinner = ora({
133
+ text: "Spinning up an ephemeral ActivityPub server...",
134
+ discardStdin: false,
135
+ }).start();
136
+ const server = await spawnTemporaryServer(fetch, {
137
+ noTunnel: command.noTunnel,
138
+ });
139
+ spinner.succeed(
140
+ `The ephemeral ActivityPub server is up and running: ${
141
+ colors.green(
142
+ server.url.href,
143
+ )
144
+ }`,
145
+ );
146
+ process.on("SIGINT", () => {
147
+ spinner.stop();
148
+ const peersCnt = Object.keys(peers).length;
149
+ spinner.start(
150
+ `Sending Delete(Application) activities to the ${peersCnt} ${
151
+ peersCnt === 1 ? "peer" : "peers"
152
+ }...`,
153
+ );
154
+ sendDeleteToPeers(server).then(() => {
155
+ spinner.text = "Stopping server...";
156
+ server.close().then(() => {
157
+ spinner.succeed("Server stopped.");
158
+ process.exit(0);
159
+ });
160
+ });
161
+ });
162
+ spinner.start();
163
+
164
+ const fedCtx = federation.createContext(server.url, {
165
+ activityIndex: -1,
166
+ actorName: command.actorName,
167
+ actorSummary: command.actorSummary,
168
+ });
169
+
170
+ if (command.acceptFollow != null && command.acceptFollow.length > 0) {
171
+ acceptFollows.push(...(command.acceptFollow ?? []));
172
+ }
173
+ if (command.follow != null && command.follow.length > 0) {
174
+ spinner.text = "Following actors...";
175
+ const documentLoader = await fedCtx.getDocumentLoader({
176
+ identifier: "i",
177
+ });
178
+ for (const uri of command.follow) {
179
+ spinner.text = `Following ${colors.green(uri)}...`;
180
+ const actor = await lookupObject(uri, { documentLoader });
181
+ if (!isActor(actor)) {
182
+ spinner.fail(`Not an actor: ${colors.red(uri)}`);
183
+ spinner.start();
184
+ continue;
185
+ }
186
+ if (actor.id != null) peers[actor.id?.href] = actor;
187
+ await fedCtx.sendActivity(
188
+ { identifier: "i" },
189
+ actor,
190
+ new Follow({
191
+ id: new URL(`#follows/${actor.id?.href}`, fedCtx.getActorUri("i")),
192
+ actor: fedCtx.getActorUri("i"),
193
+ object: actor.id,
194
+ }),
195
+ );
196
+ spinner.succeed(`Sent follow request to ${colors.green(uri)}.`);
197
+ spinner.start();
198
+ }
199
+ }
200
+ spinner.stop();
201
+ printServerInfo(fedCtx);
202
+ }
203
+
204
+ const federationDocumentLoader = await getDocumentLoader();
205
+
206
+ const federation = createFederation<ContextData>({
207
+ kv: new MemoryKvStore(),
208
+ documentLoaderFactory: () => {
209
+ return federationDocumentLoader;
210
+ },
211
+ });
212
+
213
+ const time = Temporal.Now.instant();
214
+ let actorKeyPairs: CryptoKeyPair[] | undefined = undefined;
215
+
216
+ federation
217
+ .setActorDispatcher("/{identifier}", async (ctx, identifier) => {
218
+ if (identifier !== "i") return null;
219
+ return new Application({
220
+ id: ctx.getActorUri(identifier),
221
+ preferredUsername: identifier,
222
+ name: ctx.data.actorName,
223
+ summary: ctx.data.actorSummary,
224
+ inbox: ctx.getInboxUri(identifier),
225
+ endpoints: new Endpoints({
226
+ sharedInbox: ctx.getInboxUri(),
227
+ }),
228
+ followers: ctx.getFollowersUri(identifier),
229
+ following: ctx.getFollowingUri(identifier),
230
+ outbox: ctx.getOutboxUri(identifier),
231
+ manuallyApprovesFollowers: true,
232
+ published: time,
233
+ icon: new Image({
234
+ url: new URL("https://fedify.dev/logo.png"),
235
+ mediaType: "image/png",
236
+ }),
237
+ publicKey: (await ctx.getActorKeyPairs(identifier))[0].cryptographicKey,
238
+ assertionMethods: (await ctx.getActorKeyPairs(identifier))
239
+ .map((pair) => pair.multikey),
240
+ url: ctx.getActorUri(identifier),
241
+ });
242
+ })
243
+ .setKeyPairsDispatcher(async (_ctxData, identifier) => {
244
+ if (identifier !== "i") return [];
245
+ if (actorKeyPairs == null) {
246
+ actorKeyPairs = [
247
+ await generateCryptoKeyPair("RSASSA-PKCS1-v1_5"),
248
+ await generateCryptoKeyPair("Ed25519"),
249
+ ];
250
+ }
251
+ return actorKeyPairs;
252
+ });
253
+
254
+ const activities: ActivityEntry[] = [];
255
+
256
+ const acceptFollows: string[] = [];
257
+
258
+ async function acceptsFollowFrom(actor: Actor): Promise<boolean> {
259
+ const actorUri = actor.id;
260
+ let actorHandle: string | undefined = undefined;
261
+ if (actorUri == null) return false;
262
+ for (let uri of acceptFollows) {
263
+ if (uri === "*") return true;
264
+ if (uri.startsWith("http:") || uri.startsWith("https:")) {
265
+ uri = new URL(uri).href; // normalize
266
+ if (uri === actorUri.href) return true;
267
+ }
268
+ if (actorHandle == null) actorHandle = await getActorHandle(actor);
269
+ if (actorHandle === uri) return true;
270
+ }
271
+ return false;
272
+ }
273
+
274
+ const peers: Record<string, Actor> = {};
275
+
276
+ function createSendDeleteToPeers(
277
+ actorOptions: { actorName: string; actorSummary: string },
278
+ ): (server: TemporaryServer) => Promise<void> {
279
+ return async function sendDeleteToPeers(
280
+ server: TemporaryServer,
281
+ ): Promise<void> {
282
+ const ctx = federation.createContext(new Request(server.url), {
283
+ activityIndex: -1,
284
+ actorName: actorOptions.actorName,
285
+ actorSummary: actorOptions.actorSummary,
286
+ });
287
+
288
+ const actor = (await ctx.getActor("i"))!;
289
+ try {
290
+ await ctx.sendActivity(
291
+ { identifier: "i" },
292
+ Object.values(peers),
293
+ new Delete({
294
+ id: new URL(`#delete`, actor.id!),
295
+ actor: actor.id!,
296
+ to: PUBLIC_COLLECTION,
297
+ object: actor,
298
+ }),
299
+ );
300
+ } catch (error) {
301
+ logger.error(
302
+ "Failed to send Delete(Application) activities to peers:\n{error}",
303
+ { error },
304
+ );
305
+ }
306
+ };
307
+ }
308
+
309
+ const followers: Record<string, Actor> = {};
310
+
311
+ federation
312
+ .setInboxListeners("/{identifier}/inbox", "/inbox")
313
+ .setSharedKeyDispatcher((_) => ({ identifier: "i" }))
314
+ .on(Activity, async (ctx, activity) => {
315
+ activities[ctx.data.activityIndex].activity = activity;
316
+ for await (const actor of activity.getActors()) {
317
+ if (actor.id != null) peers[actor.id.href] = actor;
318
+ }
319
+ for await (const actor of activity.getAttributions()) {
320
+ if (actor.id != null) peers[actor.id.href] = actor;
321
+ }
322
+ if (activity instanceof Follow) {
323
+ if (acceptFollows.length < 1) return;
324
+ const objectId = activity.objectId;
325
+ if (objectId == null) return;
326
+ const parsed = ctx.parseUri(objectId);
327
+ if (parsed?.type !== "actor" || parsed.identifier !== "i") return;
328
+ const { identifier } = parsed;
329
+ const follower = await activity.getActor();
330
+ if (!isActor(follower)) return;
331
+ const accepts = await acceptsFollowFrom(follower);
332
+ if (!accepts || activity.id == null) {
333
+ logger.debug("Does not accept follow from {actor}.", {
334
+ actor: follower.id?.href,
335
+ });
336
+ return;
337
+ }
338
+ logger.debug("Accepting follow from {actor}.", {
339
+ actor: follower.id?.href,
340
+ });
341
+ followers[activity.id.href] = follower;
342
+ await ctx.sendActivity(
343
+ { identifier },
344
+ follower,
345
+ new Accept({
346
+ id: new URL(`#accepts/${follower.id?.href}`, ctx.getActorUri("i")),
347
+ actor: ctx.getActorUri(identifier),
348
+ object: activity.id,
349
+ }),
350
+ );
351
+ }
352
+ });
353
+
354
+ federation
355
+ .setFollowersDispatcher("/{identifier}/followers", (_ctx, identifier) => {
356
+ if (identifier !== "i") return null;
357
+ const items: Recipient[] = [];
358
+ for (const follower of Object.values(followers)) {
359
+ if (follower.id == null) continue;
360
+ items.push(follower);
361
+ }
362
+ return { items };
363
+ })
364
+ .setCounter((_ctx, identifier) => {
365
+ if (identifier !== "i") return null;
366
+ return Object.keys(followers).length;
367
+ });
368
+
369
+ federation
370
+ .setFollowingDispatcher(
371
+ "/{identifier}/following",
372
+ (_ctx, _identifier) => null,
373
+ )
374
+ .setCounter((_ctx, _identifier) => 0);
375
+
376
+ federation
377
+ .setOutboxDispatcher("/{identifier}/outbox", (_ctx, _identifier) => null)
378
+ .setCounter((_ctx, _identifier) => 0);
379
+
380
+ federation.setNodeInfoDispatcher("/nodeinfo/2.1", (_ctx) => {
381
+ return {
382
+ software: {
383
+ name: "fedify-cli",
384
+ version: metadata.version,
385
+ repository: new URL("https://github.com/fedify-dev/fedify"),
386
+ },
387
+ protocols: ["activitypub"],
388
+ usage: {
389
+ users: {
390
+ total: 1,
391
+ activeMonth: 1,
392
+ activeHalfyear: 1,
393
+ },
394
+ localComments: 0,
395
+ localPosts: 0,
396
+ },
397
+ };
398
+ });
399
+
400
+ function printServerInfo(fedCtx: Context<ContextData>): void {
401
+ const table = new Table({
402
+ chars: tableStyle,
403
+ style: { head: [], border: [] },
404
+ });
405
+
406
+ table.push(
407
+ { "Actor handle:": colors.green(`i@${fedCtx.getActorUri("i").host}`) },
408
+ { "Actor URI:": colors.green(fedCtx.getActorUri("i").href) },
409
+ { "Actor inbox:": colors.green(fedCtx.getInboxUri("i").href) },
410
+ { "Shared inbox:": colors.green(fedCtx.getInboxUri().href) },
411
+ );
412
+
413
+ console.log(table.toString());
414
+ }
415
+
416
+ async function printActivityEntry(
417
+ idx: number,
418
+ entry: ActivityEntry,
419
+ ): Promise<void> {
420
+ const request = entry.request.clone();
421
+ const response = entry.response?.clone();
422
+ const url = new URL(request.url);
423
+ const activity = entry.activity;
424
+ const object = await activity?.getObject();
425
+
426
+ const table = new Table({
427
+ chars: tableStyle,
428
+ style: { head: [], border: [] },
429
+ });
430
+
431
+ table.push(
432
+ { "Request #:": colors.bold(idx.toString()) },
433
+ {
434
+ "Activity type:": activity == null
435
+ ? colors.red("failed to parse")
436
+ : colors.green(
437
+ `${activity.constructor.name}(${object?.constructor.name})`,
438
+ ),
439
+ },
440
+ {
441
+ "HTTP request:": `${
442
+ request.method === "POST"
443
+ ? colors.green("POST")
444
+ : colors.red(request.method)
445
+ } ${url.pathname + url.search}`,
446
+ },
447
+ ...(response == null ? [] : [{
448
+ "HTTP response:": `${
449
+ response.ok
450
+ ? colors.green(response.status.toString())
451
+ : colors.red(response.status.toString())
452
+ } ${response.statusText}`,
453
+ }]),
454
+ { "Details": new URL(`/r/${idx}`, url).href },
455
+ );
456
+
457
+ console.log(table.toString());
458
+ }
459
+
460
+ function getHandle<T extends string>(
461
+ c: HonoContext<BlankEnv, T, BlankInput>,
462
+ ): string {
463
+ const url = new URL(c.req.url);
464
+ return `@i@${url.host}`;
465
+ }
466
+
467
+ const app = new Hono();
468
+
469
+ app.get("/", (c) => c.redirect("/r"));
470
+
471
+ app.get(
472
+ "/r",
473
+ (c) =>
474
+ c.html(
475
+ <ActivityListPage handle={getHandle(c)} entries={activities} />,
476
+ ),
477
+ );
478
+
479
+ app.get("/r/:idx{[0-9]+}", (c) => {
480
+ const idx = parseInt(c.req.param("idx"));
481
+ const tab = c.req.query("tab") ?? "request";
482
+ const activity = activities[idx];
483
+ if (activity == null) return c.notFound();
484
+ if (
485
+ tab !== "request" && tab !== "response" && tab !== "raw-activity" &&
486
+ tab !== "compact-activity" && tab !== "expanded-activity" && tab !== "logs"
487
+ ) {
488
+ return c.notFound();
489
+ }
490
+ return c.html(
491
+ <ActivityEntryPage
492
+ handle={getHandle(c)}
493
+ idx={idx}
494
+ entry={activity}
495
+ tabPage={tab}
496
+ />,
497
+ );
498
+ });
499
+
500
+ function createFetchHandler(
501
+ actorOptions: { actorName: string; actorSummary: string },
502
+ ): (request: Request) => Promise<Response> {
503
+ return async function fetch(request: Request): Promise<Response> {
504
+ const timestamp = Temporal.Now.instant();
505
+ const idx = activities.length;
506
+ const pathname = new URL(request.url).pathname;
507
+ if (pathname === "/r" || pathname.startsWith("/r/")) {
508
+ return app.fetch(request);
509
+ }
510
+ const inboxRequest = pathname === "/inbox" ||
511
+ pathname.startsWith("/i/inbox");
512
+ if (inboxRequest) {
513
+ recordingSink.startRecording();
514
+ // @ts-ignore: Work around `deno publish --dry-run` bug
515
+ activities.push({ timestamp, request: request.clone(), logs: [] });
516
+ }
517
+ const response = await federation.fetch(request, {
518
+ contextData: {
519
+ activityIndex: inboxRequest ? idx : -1,
520
+ actorName: actorOptions.actorName,
521
+ actorSummary: actorOptions.actorSummary,
522
+ },
523
+ onNotAcceptable: app.fetch.bind(app),
524
+ onNotFound: app.fetch.bind(app),
525
+ onUnauthorized: app.fetch.bind(app),
526
+ });
527
+ if (inboxRequest) {
528
+ recordingSink.stopRecording();
529
+ activities[idx].response = response.clone();
530
+ activities[idx].logs = recordingSink.getRecords();
531
+ await printActivityEntry(idx, activities[idx]);
532
+ }
533
+ return response;
534
+ };
535
+ }
@@ -0,0 +1,88 @@
1
+ import { join as joinPath } from "node:path";
2
+ import biome from "../json/biome.json" with { type: "json" };
3
+ import vscodeSettingsForDeno from "../json/vscode-settings-for-deno.json" with {
4
+ type: "json",
5
+ };
6
+ import vscodeSettings from "../json/vscode-settings.json" with {
7
+ type: "json",
8
+ };
9
+ import type { InitCommandData } from "../types.ts";
10
+
11
+ /**
12
+ * Loads Deno configuration object with compiler options, unstable features, and tasks.
13
+ * Combines unstable features required by KV store and message queue with framework-specific options.
14
+ *
15
+ * @param param0 - Destructured initialization data containing KV, MQ, initializer, and directory
16
+ * @returns Configuration object with path and Deno-specific settings
17
+ */
18
+ export const loadDenoConfig = (
19
+ { kv, mq, initializer, dir }: InitCommandData,
20
+ ) => ({
21
+ path: joinPath(dir, "deno.json"),
22
+ data: {
23
+ compilerOptions: initializer.compilerOptions,
24
+ },
25
+ unstable: [
26
+ "temporal",
27
+ ...kv.denoUnstable ?? [],
28
+ ...mq.denoUnstable ?? [],
29
+ ],
30
+ tasks: initializer.tasks,
31
+ });
32
+
33
+ /**
34
+ * Loads TypeScript configuration object for Node.js/Bun projects.
35
+ * Uses compiler options from the framework initializer.
36
+ *
37
+ * @param param0 - Destructured initialization data containing initializer and directory
38
+ * @returns Configuration object with path and TypeScript compiler options
39
+ */
40
+ export const loadTsConfig = ({ initializer, dir }: InitCommandData) => ({
41
+ path: joinPath(dir, "tsconfig.json"),
42
+ data: {
43
+ compilerOptions: initializer.compilerOptions,
44
+ },
45
+ });
46
+
47
+ /**
48
+ * Loads package.json configuration object for Node.js/Bun projects.
49
+ * Sets up ES modules and includes framework-specific npm scripts.
50
+ *
51
+ * @param param0 - Destructured initialization data containing initializer and directory
52
+ * @returns Configuration object with path and package.json settings
53
+ */
54
+ export const loadPackageJson = ({ initializer, dir }: InitCommandData) => ({
55
+ path: joinPath(dir, "package.json"),
56
+ data: {
57
+ type: "module",
58
+ scripts: initializer.tasks,
59
+ },
60
+ });
61
+
62
+ /**
63
+ * Configuration objects for various development tool setup files.
64
+ * Contains predefined configurations for code formatting, VS Code settings, and extensions
65
+ * based on the project type (Node.js/Bun or Deno).
66
+ */
67
+ export const devToolConfigs = {
68
+ biome: {
69
+ path: joinPath("biome.json"),
70
+ data: biome,
71
+ },
72
+ vscExt: {
73
+ path: joinPath(".vscode", "extensions.json"),
74
+ data: { recommendations: ["biomejs.biome"] },
75
+ },
76
+ vscSet: {
77
+ path: joinPath(".vscode", "settings.json"),
78
+ data: vscodeSettings,
79
+ },
80
+ vscSetDeno: {
81
+ path: joinPath(".vscode", "settings.json"),
82
+ data: vscodeSettingsForDeno,
83
+ },
84
+ vscExtDeno: {
85
+ path: joinPath(".vscode", "extensions.json"),
86
+ data: { recommendations: ["denoland.vscode-deno"] },
87
+ },
88
+ } as const;
@@ -0,0 +1,93 @@
1
+ import { entries, map, pipe, toArray } from "@fxts/core";
2
+ import { merge } from "../../utils.ts";
3
+ import { PACKAGE_VERSION } from "../lib.ts";
4
+ import type { InitCommandData, PackageManager } from "../types.ts";
5
+
6
+ type Deps = Record<string, string>;
7
+
8
+ /**
9
+ * Gathers all dependencies required for the project based on the initializer,
10
+ * key-value store, and message queue configurations.
11
+ *
12
+ * @param data - Web Framework initializer, key-value store and message queue descriptions
13
+ * @returns A record of dependencies with their versions
14
+ */
15
+ export const getDependencies = (
16
+ { initializer, kv, mq }: InitCommandData,
17
+ ): Deps =>
18
+ pipe(
19
+ {
20
+ "@fedify/fedify": PACKAGE_VERSION,
21
+ "@logtape/logtape": "^1.1.0",
22
+ },
23
+ merge(initializer.dependencies),
24
+ merge(kv.dependencies),
25
+ merge(mq.dependencies),
26
+ );
27
+
28
+ /** Gathers all devDependencies required for the project based on the initializer,
29
+ * key-value store, and message queue configurations, including Biome for linting/formatting.
30
+ *
31
+ * @param data - Web Framework initializer, key-value store and message queue descriptions
32
+ * @returns A record of devDependencies with their versions
33
+ */
34
+ export const getDevDependencies = (
35
+ { initializer, kv, mq }: InitCommandData,
36
+ ): Deps =>
37
+ pipe(
38
+ {
39
+ "@biomejs/biome": "^2.2.4",
40
+ },
41
+ merge(initializer.devDependencies),
42
+ merge(kv.devDependencies),
43
+ merge(mq.devDependencies),
44
+ );
45
+
46
+ /**
47
+ * Generates the command-line arguments needed to add dependencies or devDependencies
48
+ * using the specified package manager.
49
+ * If it is devDependencies, the '-D' flag is included.
50
+ *
51
+ * @param param0 - Object containing the package manager and a boolean indicating if dev dependencies are to be added
52
+ * @yields The command-line arguments as strings
53
+ */
54
+ export function* getAddDepsArgs<
55
+ T extends { packageManager: PackageManager; dev?: boolean },
56
+ >({ packageManager, dev = false }: T): Generator<string> {
57
+ yield packageManager;
58
+ yield "add";
59
+ if (dev) yield "-D";
60
+ }
61
+
62
+ /**
63
+ * Joins package names with their versions for installation commands.
64
+ * For Deno, it prefixes packages with 'jsr:' unless they already start with 'npm:'.
65
+ *
66
+ * @param data - Package manager and dependencies to be joined with versions
67
+ * @returns `${registry}:${package}@${version}`[] for deno or `${package}@${version}`[] for others
68
+ */
69
+ export const joinDepsVer = <
70
+ T extends { packageManager: PackageManager; dependencies: Deps },
71
+ >({ packageManager: pm, dependencies }: T): string[] =>
72
+ pipe(
73
+ dependencies,
74
+ entries,
75
+ map(([name, version]) =>
76
+ `${getPackageName(pm, name)}@${getPackageVersion(pm, version)}`
77
+ ),
78
+ toArray,
79
+ );
80
+
81
+ const getPackageName = (pm: PackageManager, name: string) =>
82
+ pm !== "deno"
83
+ ? name
84
+ : name.startsWith("npm:")
85
+ ? name.substring(4)
86
+ : !name.startsWith("npm:")
87
+ ? `jsr:${name}`
88
+ : name;
89
+
90
+ const getPackageVersion = (pm: PackageManager, version: string) =>
91
+ pm !== "deno" && version.includes("+")
92
+ ? version.substring(0, version.indexOf("+"))
93
+ : version;