From c8143f9d8d7bc3a12eb80e406b3fe7a67d6b31d5 Mon Sep 17 00:00:00 2001 From: Jesse Kelly Date: Mon, 24 Jun 2024 20:37:24 +0200 Subject: [PATCH 1/6] added cli --- packages/effect-http/package.json | 7 +- packages/effect-http/src/CliConfig.ts | 9 +++ packages/effect-http/src/bin.ts | 72 +++++++++++++++++++ .../effect-http/src/effect-http.config.ts | 17 +++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 packages/effect-http/src/CliConfig.ts create mode 100644 packages/effect-http/src/bin.ts create mode 100644 packages/effect-http/src/effect-http.config.ts diff --git a/packages/effect-http/package.json b/packages/effect-http/package.json index 187822d52..8c82fafed 100644 --- a/packages/effect-http/package.json +++ b/packages/effect-http/package.json @@ -41,8 +41,13 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "^10.1.0", + "@effect/cli": "^0.36.62", "@effect/platform": "^0.58.4", + "@effect/platform-node": "^0.53.3", "@effect/schema": "^0.68.6", - "effect": "^3.4.0" + "effect": "^3.4.0", + "effect-http-node": "^0.15.3", + "effect-log": "^0.31.5", + "importx": "^0.3.7" } } diff --git a/packages/effect-http/src/CliConfig.ts b/packages/effect-http/src/CliConfig.ts new file mode 100644 index 000000000..854aede1e --- /dev/null +++ b/packages/effect-http/src/CliConfig.ts @@ -0,0 +1,9 @@ +import { Data } from "effect"; +import { Api } from "effect-http"; + +export class CliConfig extends Data.TaggedClass("CliConfig")<{ + api: Api.Api.Any, + server?: Partial<{ + port: number + }> +}> {} diff --git a/packages/effect-http/src/bin.ts b/packages/effect-http/src/bin.ts new file mode 100644 index 000000000..fdcad2126 --- /dev/null +++ b/packages/effect-http/src/bin.ts @@ -0,0 +1,72 @@ +import { Command, Options } from "@effect/cli"; +import { NodeContext, NodeRuntime } from "@effect/platform-node"; +import * as Path from "@effect/platform/Path"; +import { Data, Effect, Option } from "effect"; +import { NodeServer } from "effect-http-node"; +import * as PrettyLogger from "effect-log/PrettyLogger"; +import * as importx from "importx"; +import pkg from "../package.json"; +import * as CliConfig from "./CliConfig.js"; +import * as ExampleServer from "./ExampleServer.js"; +import * as RouterBuilder from "./RouterBuilder.js"; + + +/** + * An error that occurs when loading the config file. + */ +class ConfigError extends Data.TaggedError("ConfigError")<{ + message: string; +}> {} + +const configArg = Options.file("config").pipe( + Options.withAlias("c"), + Options.withDescription("Path to the config file"), + Options.withDefault("./effect-http.config.ts"), +); + +const portArg = Options.integer("port").pipe( + Options.withAlias("p"), + Options.withDescription("Port to run the server on"), + Options.optional +); + +const loadConfig = (relativePath: string) => + Effect.flatMap(Path.Path, (path) => + Effect.tryPromise(() => + importx.import(path.join(process.cwd(), relativePath), import.meta.url), + ).pipe( + Effect.mapError( + () => new ConfigError({ message: `Failed to find config at ${path}` }), + ), + Effect.flatMap((module) => + module?.default + ? Effect.succeed(module.default) + : new ConfigError({ message: `No default export found in ${path}` }), + ), + Effect.flatMap((defaultExport) => + defaultExport instanceof CliConfig.CliConfig + ? Effect.succeed(defaultExport) + : new ConfigError({ message: `Invalid config found in ${path}` }), + ), + Effect.withSpan("loadConfig", { attributes: { path } }) + ), + ); + +const command = Command.make("serve", { config: configArg, port: portArg }, (args) => + Effect.gen(function* () { + const config = yield* loadConfig(args.config); + const port = Option.getOrUndefined(args.port) ?? config.server?.port ?? 11779; + yield* ExampleServer.make(config.api).pipe(RouterBuilder.buildPartial, NodeServer.listen({ port })) + }), +); + +const cli = Command.run(command, { + name: "Effect Http Cli", + version: `v${pkg.version}`, +}); + +cli(process.argv).pipe( + Effect.provide(NodeContext.layer), + Effect.provide(PrettyLogger.layer({ showFiberId: false })), + NodeRuntime.runMain, +); diff --git a/packages/effect-http/src/effect-http.config.ts b/packages/effect-http/src/effect-http.config.ts new file mode 100644 index 000000000..3e108cea8 --- /dev/null +++ b/packages/effect-http/src/effect-http.config.ts @@ -0,0 +1,17 @@ +import { Schema } from "@effect/schema"; +import { pipe } from "effect"; +import * as Api from "./Api.js"; +import { CliConfig } from "./CliConfig.js"; + +const api = pipe( + Api.make({ title: "Users API" }), + Api.addEndpoint( + Api.get("getUser", "/user").pipe( + Api.setResponseBody(Schema.Number), + ) + ) +) + +export default new CliConfig({ + api, +}) From aa041864e9d9dbc4f7dfa72ec53f0a04a3d76efd Mon Sep 17 00:00:00 2001 From: Jesse Kelly Date: Mon, 24 Jun 2024 22:45:55 +0200 Subject: [PATCH 2/6] added TypeId to CliConfig --- packages/effect-http/src/Api.ts | 8 +++---- packages/effect-http/src/CliConfig.ts | 24 +++++++++++++++++-- packages/effect-http/src/bin.ts | 2 +- .../effect-http/src/effect-http.config.ts | 4 ++-- .../effect-http/src/internal/cliConfig.ts | 8 +++++++ 5 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 packages/effect-http/src/internal/cliConfig.ts diff --git a/packages/effect-http/src/Api.ts b/packages/effect-http/src/Api.ts index aeeeb2f4d..ba20b9235 100644 --- a/packages/effect-http/src/Api.ts +++ b/packages/effect-http/src/Api.ts @@ -3,7 +3,7 @@ * * @since 1.0.0 */ -import type * as Schema from "@effect/schema/Schema" +import type * as CliConfig from "@effect/schema/Schema" import type * as Pipeable from "effect/Pipeable" import type * as Types from "effect/Types" @@ -154,12 +154,12 @@ export { * @category constructors * @since 1.0.0 */ - get, + make as endpoint, /** * @category constructors * @since 1.0.0 */ - make as endpoint, + get, /** * @category constructors * @since 1.0.0 @@ -251,7 +251,7 @@ export const setOptions: ( * @category schemas * @since 1.0.0 */ -export const FormData: Schema.Schema = ApiSchema.FormData +export const FormData: CliConfig.Schema = ApiSchema.FormData /** * @category refinements diff --git a/packages/effect-http/src/CliConfig.ts b/packages/effect-http/src/CliConfig.ts index 854aede1e..09328421b 100644 --- a/packages/effect-http/src/CliConfig.ts +++ b/packages/effect-http/src/CliConfig.ts @@ -1,9 +1,29 @@ import { Data } from "effect"; import { Api } from "effect-http"; +import * as internal from "./internal/cliConfig.js"; -export class CliConfig extends Data.TaggedClass("CliConfig")<{ - api: Api.Api.Any, +/** + * @since 1.0.0 + * @category type id + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type id + */ +export type TypeId = typeof TypeId + +export class CliConfig extends Data.Class<{ + [TypeId]: typeof TypeId; + api: Api.Api.Any; server?: Partial<{ port: number }> }> {} + +export const make = (config: Omit) => + new CliConfig({ ...config, [TypeId]: TypeId }); + +export const isCliConfig = (u: unknown): u is CliConfig => + internal.isCliConfig(u); diff --git a/packages/effect-http/src/bin.ts b/packages/effect-http/src/bin.ts index fdcad2126..991e89ef9 100644 --- a/packages/effect-http/src/bin.ts +++ b/packages/effect-http/src/bin.ts @@ -44,7 +44,7 @@ const loadConfig = (relativePath: string) => : new ConfigError({ message: `No default export found in ${path}` }), ), Effect.flatMap((defaultExport) => - defaultExport instanceof CliConfig.CliConfig + CliConfig.isCliConfig(defaultExport) ? Effect.succeed(defaultExport) : new ConfigError({ message: `Invalid config found in ${path}` }), ), diff --git a/packages/effect-http/src/effect-http.config.ts b/packages/effect-http/src/effect-http.config.ts index 3e108cea8..57688de67 100644 --- a/packages/effect-http/src/effect-http.config.ts +++ b/packages/effect-http/src/effect-http.config.ts @@ -1,7 +1,7 @@ import { Schema } from "@effect/schema"; import { pipe } from "effect"; import * as Api from "./Api.js"; -import { CliConfig } from "./CliConfig.js"; +import * as CliConfig from "./CliConfig.js"; const api = pipe( Api.make({ title: "Users API" }), @@ -12,6 +12,6 @@ const api = pipe( ) ) -export default new CliConfig({ +export default CliConfig.make({ api, }) diff --git a/packages/effect-http/src/internal/cliConfig.ts b/packages/effect-http/src/internal/cliConfig.ts new file mode 100644 index 000000000..52c383404 --- /dev/null +++ b/packages/effect-http/src/internal/cliConfig.ts @@ -0,0 +1,8 @@ +import type * as CliConfig from "../CliConfig.js" + +export const TypeId: CliConfig.TypeId = Symbol.for( + "effect-http/CliConfig/TypeId" +) as CliConfig.TypeId + +export const isCliConfig = (u: unknown): u is CliConfig.CliConfig => + typeof u === "object" && u !== null && TypeId in u From e0df9830a69273b84e1f7c4668e9353f8fc20ddd Mon Sep 17 00:00:00 2001 From: Jesse Kelly Date: Mon, 24 Jun 2024 23:27:57 +0200 Subject: [PATCH 3/6] added logging middleware --- packages/effect-http/src/bin.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/effect-http/src/bin.ts b/packages/effect-http/src/bin.ts index 991e89ef9..ddf3e413e 100644 --- a/packages/effect-http/src/bin.ts +++ b/packages/effect-http/src/bin.ts @@ -1,5 +1,6 @@ import { Command, Options } from "@effect/cli"; import { NodeContext, NodeRuntime } from "@effect/platform-node"; +import * as HttpMiddleware from "@effect/platform/HttpMiddleware"; import * as Path from "@effect/platform/Path"; import { Data, Effect, Option } from "effect"; import { NodeServer } from "effect-http-node"; @@ -56,7 +57,11 @@ const command = Command.make("serve", { config: configArg, port: portArg }, (arg Effect.gen(function* () { const config = yield* loadConfig(args.config); const port = Option.getOrUndefined(args.port) ?? config.server?.port ?? 11779; - yield* ExampleServer.make(config.api).pipe(RouterBuilder.buildPartial, NodeServer.listen({ port })) + yield* ExampleServer.make(config.api).pipe( + RouterBuilder.buildPartial, + HttpMiddleware.logger, + NodeServer.listen({ port }) + ) }), ); From afee2fe7d785a93ea4ce211976bec66b322c62b4 Mon Sep 17 00:00:00 2001 From: Jesse Kelly Date: Tue, 25 Jun 2024 13:06:42 +0200 Subject: [PATCH 4/6] moved -c flag to root command --- packages/effect-http/src/CliConfig.ts | 3 + packages/effect-http/src/bin.ts | 124 +++++++++++++----- .../effect-http/src/effect-http.config.ts | 5 +- 3 files changed, 101 insertions(+), 31 deletions(-) diff --git a/packages/effect-http/src/CliConfig.ts b/packages/effect-http/src/CliConfig.ts index 09328421b..bc207b332 100644 --- a/packages/effect-http/src/CliConfig.ts +++ b/packages/effect-http/src/CliConfig.ts @@ -19,6 +19,9 @@ export class CliConfig extends Data.Class<{ api: Api.Api.Any; server?: Partial<{ port: number + }>; + client?: Partial<{ + baseUrl: string }> }> {} diff --git a/packages/effect-http/src/bin.ts b/packages/effect-http/src/bin.ts index ddf3e413e..7e7cf4ecf 100644 --- a/packages/effect-http/src/bin.ts +++ b/packages/effect-http/src/bin.ts @@ -1,8 +1,9 @@ -import { Command, Options } from "@effect/cli"; +import { Command, HelpDoc, Options, ValidationError } from "@effect/cli"; import { NodeContext, NodeRuntime } from "@effect/platform-node"; import * as HttpMiddleware from "@effect/platform/HttpMiddleware"; import * as Path from "@effect/platform/Path"; -import { Data, Effect, Option } from "effect"; +import { Schema } from "@effect/schema"; +import { Console, Data, Effect, Option, pipe } from "effect"; import { NodeServer } from "effect-http-node"; import * as PrettyLogger from "effect-log/PrettyLogger"; import * as importx from "importx"; @@ -10,7 +11,7 @@ import pkg from "../package.json"; import * as CliConfig from "./CliConfig.js"; import * as ExampleServer from "./ExampleServer.js"; import * as RouterBuilder from "./RouterBuilder.js"; - +import { Api, ApiEndpoint } from "./index.js"; /** * An error that occurs when loading the config file. @@ -19,18 +20,6 @@ class ConfigError extends Data.TaggedError("ConfigError")<{ message: string; }> {} -const configArg = Options.file("config").pipe( - Options.withAlias("c"), - Options.withDescription("Path to the config file"), - Options.withDefault("./effect-http.config.ts"), -); - -const portArg = Options.integer("port").pipe( - Options.withAlias("p"), - Options.withDescription("Port to run the server on"), - Options.optional -); - const loadConfig = (relativePath: string) => Effect.flatMap(Path.Path, (path) => Effect.tryPromise(() => @@ -49,29 +38,104 @@ const loadConfig = (relativePath: string) => ? Effect.succeed(defaultExport) : new ConfigError({ message: `Invalid config found in ${path}` }), ), - Effect.withSpan("loadConfig", { attributes: { path } }) + Effect.withSpan("loadConfig", { attributes: { path } }), ), ); -const command = Command.make("serve", { config: configArg, port: portArg }, (args) => - Effect.gen(function* () { - const config = yield* loadConfig(args.config); - const port = Option.getOrUndefined(args.port) ?? config.server?.port ?? 11779; - yield* ExampleServer.make(config.api).pipe( - RouterBuilder.buildPartial, - HttpMiddleware.logger, - NodeServer.listen({ port }) - ) - }), +const urlArg = Options.text("url").pipe( + Options.withDescription("URL to make the request to"), + Options.optional, +); + +const configArg = Options.file("config").pipe( + Options.withAlias("c"), + Options.withDescription("Path to the config file"), + Options.withDefault("./effect-http.config.ts"), + Options.mapEffect((s) => + loadConfig(s).pipe( + Effect.mapError((e) => + ValidationError.invalidArgument(HelpDoc.h1(e.message)), + ), + ), + ), +); + +const portArg = Options.integer("port").pipe( + Options.withAlias("p"), + Options.withDescription("Port to run the server on"), + Options.optional, ); -const cli = Command.run(command, { - name: "Effect Http Cli", - version: `v${pkg.version}`, -}); +const api = pipe( + Api.make({ title: "Users API" }), + Api.addEndpoint( + Api.get("getUser", "/user", { description: "Get a user by their id" }).pipe( + Api.setResponseBody(Schema.Number), + ), + ), +); + +export const genClientCli = (api: Api.Api.Any) => { + return api.groups + .map((group) => group.endpoints) + .flat() + .map((endpoint) => { + return Command.make( + ApiEndpoint.getId(endpoint), + { url: urlArg }, + (args) => Effect.log(`Making request to ${args.url}`), + ).pipe( + Command.withDescription( + ApiEndpoint.getOptions(endpoint).description || "", + ), + ); + }); +}; + +const serveCommand = Command.make( + "serve", + { port: portArg }, + (args) => + Effect.gen(function* () { + const { config } = yield* rootCommand + const port = + Option.getOrUndefined(args.port) ?? config.server?.port ?? 11779; + yield* ExampleServer.make(config.api).pipe( + RouterBuilder.buildPartial, + HttpMiddleware.logger, + NodeServer.listen({ port }), + ); + }), +).pipe(Command.withDescription("Start an example server")); + +const clientCommand = Command.make( + "client", + { url: urlArg }, + (args) => + Effect.gen(function* () { + // const { config } = yield* rootCommand + const endpoints = api.groups.map((group) => group.endpoints).flat(); + yield* Console.log(endpoints[0].pipe(ApiEndpoint.getId)); + }), +); + +const rootCommand = Command.make("effect-http", { + config: configArg, +}) + +const cli = Command.run( + rootCommand.pipe( + Command.withSubcommands([serveCommand, clientCommand]), + ), + { + name: "Effect Http Cli", + version: `v${pkg.version}`, + }, +); cli(process.argv).pipe( Effect.provide(NodeContext.layer), + Effect.catchAll(Effect.logError), Effect.provide(PrettyLogger.layer({ showFiberId: false })), NodeRuntime.runMain, ); diff --git a/packages/effect-http/src/effect-http.config.ts b/packages/effect-http/src/effect-http.config.ts index 57688de67..6949651a7 100644 --- a/packages/effect-http/src/effect-http.config.ts +++ b/packages/effect-http/src/effect-http.config.ts @@ -6,7 +6,7 @@ import * as CliConfig from "./CliConfig.js"; const api = pipe( Api.make({ title: "Users API" }), Api.addEndpoint( - Api.get("getUser", "/user").pipe( + Api.get("getUser", "/user", { "description": "Get a user by their id" }).pipe( Api.setResponseBody(Schema.Number), ) ) @@ -14,4 +14,7 @@ const api = pipe( export default CliConfig.make({ api, + client: { + baseUrl: "http://localhost:3000" + } }) From 6591452ba9b3fcbc6c3077c4ffdf3cfdc83451a7 Mon Sep 17 00:00:00 2001 From: Jesse Kelly Date: Tue, 25 Jun 2024 13:11:20 +0200 Subject: [PATCH 5/6] removed inline api def --- packages/effect-http/src/bin.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/effect-http/src/bin.ts b/packages/effect-http/src/bin.ts index 7e7cf4ecf..0880a3afa 100644 --- a/packages/effect-http/src/bin.ts +++ b/packages/effect-http/src/bin.ts @@ -2,8 +2,7 @@ import { Command, HelpDoc, Options, ValidationError } from "@effect/cli"; import { NodeContext, NodeRuntime } from "@effect/platform-node"; import * as HttpMiddleware from "@effect/platform/HttpMiddleware"; import * as Path from "@effect/platform/Path"; -import { Schema } from "@effect/schema"; -import { Console, Data, Effect, Option, pipe } from "effect"; +import { Console, Data, Effect, Option } from "effect"; import { NodeServer } from "effect-http-node"; import * as PrettyLogger from "effect-log/PrettyLogger"; import * as importx from "importx"; @@ -66,15 +65,6 @@ const portArg = Options.integer("port").pipe( Options.optional, ); -const api = pipe( - Api.make({ title: "Users API" }), - Api.addEndpoint( - Api.get("getUser", "/user", { description: "Get a user by their id" }).pipe( - Api.setResponseBody(Schema.Number), - ), - ), -); - export const genClientCli = (api: Api.Api.Any) => { return api.groups .map((group) => group.endpoints) @@ -113,8 +103,8 @@ const clientCommand = Command.make( { url: urlArg }, (args) => Effect.gen(function* () { - // const { config } = yield* rootCommand - const endpoints = api.groups.map((group) => group.endpoints).flat(); + const { config } = yield* rootCommand + const endpoints = config.api.groups.map((group) => group.endpoints).flat(); yield* Console.log(endpoints[0].pipe(ApiEndpoint.getId)); }), ); From 4883c0fc040d23bb4d787dc861db861cc4c9c8b8 Mon Sep 17 00:00:00 2001 From: Jesse Kelly Date: Tue, 25 Jun 2024 14:51:10 +0200 Subject: [PATCH 6/6] added list command --- packages/effect-http/src/bin.ts | 102 +++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/packages/effect-http/src/bin.ts b/packages/effect-http/src/bin.ts index 0880a3afa..6ebd6f2f2 100644 --- a/packages/effect-http/src/bin.ts +++ b/packages/effect-http/src/bin.ts @@ -1,4 +1,10 @@ -import { Command, HelpDoc, Options, ValidationError } from "@effect/cli"; +import { + Command, + HelpDoc, + Options, + Prompt, + ValidationError, +} from "@effect/cli"; import { NodeContext, NodeRuntime } from "@effect/platform-node"; import * as HttpMiddleware from "@effect/platform/HttpMiddleware"; import * as Path from "@effect/platform/Path"; @@ -20,26 +26,30 @@ class ConfigError extends Data.TaggedError("ConfigError")<{ }> {} const loadConfig = (relativePath: string) => - Effect.flatMap(Path.Path, (path) => - Effect.tryPromise(() => - importx.import(path.join(process.cwd(), relativePath), import.meta.url), + Effect.flatMap(Path.Path, (path) => { + const fullPath = path.join(process.cwd(), relativePath); + return Effect.tryPromise(() => + importx.import(fullPath, import.meta.url), ).pipe( Effect.mapError( - () => new ConfigError({ message: `Failed to find config at ${path}` }), + () => + new ConfigError({ message: `Failed to find config at ${fullPath}` }), ), Effect.flatMap((module) => module?.default ? Effect.succeed(module.default) - : new ConfigError({ message: `No default export found in ${path}` }), + : new ConfigError({ + message: `No default export found in ${fullPath}`, + }), ), Effect.flatMap((defaultExport) => CliConfig.isCliConfig(defaultExport) ? Effect.succeed(defaultExport) - : new ConfigError({ message: `Invalid config found in ${path}` }), + : new ConfigError({ message: `Invalid config found in ${fullPath}` }), ), - Effect.withSpan("loadConfig", { attributes: { path } }), - ), - ); + Effect.withSpan("loadConfig", { attributes: { fullPath } }), + ); + }); const urlArg = Options.text("url").pipe( Options.withDescription("URL to make the request to"), @@ -82,40 +92,62 @@ export const genClientCli = (api: Api.Api.Any) => { }); }; -const serveCommand = Command.make( - "serve", - { port: portArg }, - (args) => - Effect.gen(function* () { - const { config } = yield* rootCommand - const port = - Option.getOrUndefined(args.port) ?? config.server?.port ?? 11779; - yield* ExampleServer.make(config.api).pipe( - RouterBuilder.buildPartial, - HttpMiddleware.logger, - NodeServer.listen({ port }), - ); - }), +const serveCommand = Command.make("serve", { port: portArg }, (args) => + Effect.gen(function* () { + const { config } = yield* rootCommand; + const port = + Option.getOrUndefined(args.port) ?? config.server?.port ?? 11779; + yield* ExampleServer.make(config.api).pipe( + RouterBuilder.buildPartial, + HttpMiddleware.logger, + NodeServer.listen({ port }), + ); + }), ).pipe(Command.withDescription("Start an example server")); -const clientCommand = Command.make( - "client", - { url: urlArg }, - (args) => - Effect.gen(function* () { - const { config } = yield* rootCommand - const endpoints = config.api.groups.map((group) => group.endpoints).flat(); - yield* Console.log(endpoints[0].pipe(ApiEndpoint.getId)); - }), +const clientCommand = Command.make("client", { url: urlArg }, (args) => + Effect.gen(function* () { + const { config } = yield* rootCommand; + const endpoints = config.api.groups.map((group) => group.endpoints).flat(); + + const selectedEndpoint = yield* Prompt.select({ + message: "Select an endpoint", + choices: endpoints.map((endpoint) => ({ + value: endpoint, + title: ApiEndpoint.getId(endpoint), + describe: ApiEndpoint.getOptions(endpoint).description, + })), + }); + + yield* Effect.log(selectedEndpoint); + }), ); const rootCommand = Command.make("effect-http", { config: configArg, -}) +}); + +/** + * List endpoints + */ +const listCommand = Command.make("list", {}, (args) => + Effect.gen(function* () { + const { config } = yield* rootCommand; + const endpoints = config.api.groups.map((group) => group.endpoints).flat(); + yield* Console.log( + endpoints + .map( + (e) => + `${ApiEndpoint.getId(e)}(${ApiEndpoint.getMethod(e)} ${ApiEndpoint.getPath(e)}): ${ApiEndpoint.getOptions(e).description}`, + ) + .join("\n"), + ); + }), +).pipe(Command.withDescription("List all endpoints")); const cli = Command.run( rootCommand.pipe( - Command.withSubcommands([serveCommand, clientCommand]), + Command.withSubcommands([listCommand, serveCommand, clientCommand]), ), { name: "Effect Http Cli",