Skip to content

Commit d573d41

Browse files
authored
Merge pull request #808 from dahlia/cli/discover
Use static command discovery in @fedify/cli
2 parents fbc0062 + f2a541d commit d573d41

20 files changed

Lines changed: 1028 additions & 650 deletions

File tree

deno.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@
5151
"@opentelemetry/sdk-metrics": "npm:@opentelemetry/sdk-metrics@2.7.1",
5252
"@opentelemetry/sdk-trace-base": "npm:@opentelemetry/sdk-trace-base@^2.7.1",
5353
"@opentelemetry/semantic-conventions": "npm:@opentelemetry/semantic-conventions@^1.40.0",
54-
"@optique/config": "jsr:@optique/config@^1.0.2",
55-
"@optique/core": "jsr:@optique/core@^1.0.2",
56-
"@optique/run": "jsr:@optique/run@^1.0.2",
54+
"@optique/config": "jsr:@optique/config@^1.1.0",
55+
"@optique/core": "jsr:@optique/core@^1.1.0",
56+
"@optique/discover": "jsr:@optique/discover@^1.1.0",
57+
"@optique/run": "jsr:@optique/run@^1.1.0",
5758
"@standard-schema/spec": "jsr:@standard-schema/spec@^1.1.0",
5859
"@std/assert": "jsr:@std/assert@^1.0.13",
5960
"@std/async": "jsr:@std/async@^1.0.13",

deno.lock

Lines changed: 22 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"@fxts/core": "catalog:",
8585
"@optique/config": "catalog:",
8686
"@optique/core": "catalog:",
87+
"@optique/discover": "catalog:",
8788
"@optique/run": "catalog:",
8889
"@standard-schema/spec": "catalog:",
8990
"@hongminhee/localtunnel": "^0.3.0",

packages/cli/src/bench/command.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,19 +159,23 @@ tolerance and measured noise band.`,
159159
},
160160
);
161161

162-
export const benchCommand = command(
163-
"bench",
164-
or(compareParser, runParser),
165-
{
166-
brief: message`Benchmark a Fedify federation workload.`,
167-
description: message`Run an ActivityPub-specific load benchmark against a \
162+
export const benchOptions = or(compareParser, runParser);
163+
164+
export const benchMetadata = {
165+
brief: message`Benchmark a Fedify federation workload.`,
166+
description: message`Run an ActivityPub-specific load benchmark against a \
168167
cooperative Fedify target running in benchmark mode.
169168
170169
The suite file declares the target, actors, and scenarios. This version \
171170
executes the \`inbox\`, \`webfinger\`, \`actor\`, \`object\`, \`fanout\`, \
172171
\`failure\`, and \`mixed\` scenario types; \`collection\` remains reserved by \
173172
the suite format.`,
174-
},
173+
};
174+
175+
export const benchCommand = command(
176+
"bench",
177+
benchOptions,
178+
benchMetadata,
175179
);
176180

177181
export type BenchCommand = InferValue<typeof benchCommand>;

packages/cli/src/commands.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { initOptions, runInit } from "@fedify/init";
2+
import {
3+
constant,
4+
merge,
5+
message,
6+
object,
7+
optionNames,
8+
type Parser,
9+
} from "@optique/core";
10+
import {
11+
type CommandMetadata,
12+
type CommandPath,
13+
defineCommand,
14+
type StaticCommand,
15+
} from "@optique/discover";
16+
import { benchMetadata, benchOptions } from "./bench/command.ts";
17+
import {
18+
generateVocabMetadata,
19+
generateVocabOptions,
20+
} from "./generate-vocab/command.ts";
21+
import { inboxMetadata, inboxOptions } from "./inbox/command.ts";
22+
import { lookupMetadata, lookupOptions } from "./lookup/command.ts";
23+
import { nodeInfoMetadata, nodeInfoOptions, runNodeInfo } from "./nodeinfo.ts";
24+
import type { GlobalOptions } from "./options.ts";
25+
import { relayMetadata, relayOptions } from "./relay/command.ts";
26+
import { runTunnel, tunnelMetadata, tunnelOptions } from "./tunnel.ts";
27+
import { webFingerMetadata, webFingerOptions } from "./webfinger/command.ts";
28+
29+
type CliCommandHandler<TValue extends object> = (
30+
value: TValue & GlobalOptions,
31+
) => unknown | Promise<unknown>;
32+
33+
export type CliStaticCommand<TValue extends object> =
34+
& Omit<StaticCommand<"sync", TValue>, "handler">
35+
& {
36+
readonly handler: (value: never) => unknown | Promise<unknown>;
37+
readonly run: CliCommandHandler<TValue>;
38+
};
39+
40+
export type AnyCliStaticCommand =
41+
& Omit<StaticCommand<"sync", never>, "handler" | "parser">
42+
& {
43+
readonly parser: Parser<"sync", unknown, unknown>;
44+
readonly handler: (value: never) => unknown | Promise<unknown>;
45+
readonly run: (value: never) => unknown | Promise<unknown>;
46+
};
47+
48+
export type CliCommandValue<TCommand extends AnyCliStaticCommand> =
49+
TCommand extends CliStaticCommand<infer TValue> ? TValue : never;
50+
51+
function defineCliCommand<const TValue extends object>(
52+
command: {
53+
readonly path: CommandPath;
54+
readonly parser: Parser<"sync", TValue, unknown>;
55+
readonly metadata?: CommandMetadata;
56+
readonly run: CliCommandHandler<NoInfer<TValue>>;
57+
},
58+
): CliStaticCommand<TValue> {
59+
const { run, ...definition } = command;
60+
return {
61+
...defineCommand({
62+
...definition,
63+
handler: (_value: TValue) => {},
64+
}),
65+
run,
66+
};
67+
}
68+
69+
const initParser = merge(
70+
initOptions,
71+
object({ command: constant("init") }),
72+
);
73+
74+
const initMetadata = {
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.`,
85+
};
86+
87+
export const generatingCommands = [
88+
defineCliCommand({
89+
path: ["init"],
90+
parser: initParser,
91+
metadata: initMetadata,
92+
run: runInit,
93+
}),
94+
defineCliCommand({
95+
path: ["generate-vocab"],
96+
parser: generateVocabOptions,
97+
metadata: generateVocabMetadata,
98+
run: async (value) => {
99+
const { default: runGenerateVocab } = await import(
100+
"./generate-vocab/action.ts"
101+
);
102+
return await runGenerateVocab(value);
103+
},
104+
}),
105+
] satisfies readonly AnyCliStaticCommand[];
106+
107+
export const activityPubCommands = [
108+
defineCliCommand({
109+
path: ["webfinger"],
110+
parser: webFingerOptions,
111+
metadata: webFingerMetadata,
112+
run: async (value) => {
113+
const { default: runWebFinger } = await import("./webfinger/action.ts");
114+
return await runWebFinger(value);
115+
},
116+
}),
117+
defineCliCommand({
118+
path: ["lookup"],
119+
parser: lookupOptions,
120+
metadata: lookupMetadata,
121+
run: async (value) => {
122+
const { runLookup } = await import("./lookup.ts");
123+
return await runLookup(value);
124+
},
125+
}),
126+
defineCliCommand({
127+
path: ["inbox"],
128+
parser: inboxOptions,
129+
metadata: inboxMetadata,
130+
run: async (value) => {
131+
const { runInbox } = await import("./inbox.tsx");
132+
return await runInbox(value);
133+
},
134+
}),
135+
defineCliCommand({
136+
path: ["nodeinfo"],
137+
parser: nodeInfoOptions,
138+
metadata: nodeInfoMetadata,
139+
run: runNodeInfo,
140+
}),
141+
defineCliCommand({
142+
path: ["relay"],
143+
parser: relayOptions,
144+
metadata: relayMetadata,
145+
run: async (value) => {
146+
const { runRelay } = await import("./relay.ts");
147+
return await runRelay(value);
148+
},
149+
}),
150+
defineCliCommand({
151+
path: ["bench"],
152+
parser: benchOptions,
153+
metadata: benchMetadata,
154+
run: async (value) => {
155+
const { runBench } = await import("./bench/mod.ts");
156+
return await runBench(value);
157+
},
158+
}),
159+
] satisfies readonly AnyCliStaticCommand[];
160+
161+
export const networkCommands = [
162+
defineCliCommand({
163+
path: ["tunnel"],
164+
parser: tunnelOptions,
165+
metadata: tunnelMetadata,
166+
run: runTunnel,
167+
}),
168+
] satisfies readonly AnyCliStaticCommand[];
169+
170+
export const cliCommands = [
171+
...generatingCommands,
172+
...activityPubCommands,
173+
...networkCommands,
174+
] as const satisfies readonly AnyCliStaticCommand[];
175+
176+
export type CliCommand = typeof cliCommands[number];

packages/cli/src/generate-vocab/command.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,20 @@ const generatedPath = argument(
2727
},
2828
);
2929

30+
export const generateVocabOptions = object("Generation options", {
31+
command: constant("generate-vocab"),
32+
schemaDir,
33+
generatedPath,
34+
});
35+
36+
export const generateVocabMetadata = {
37+
description: message`Generate vocabulary classes from schema files.`,
38+
};
39+
3040
const generateVocabCommand = command(
3141
"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-
},
42+
generateVocabOptions,
43+
generateVocabMetadata,
4044
);
4145

4246
export default generateVocabCommand;

0 commit comments

Comments
 (0)