cli-builder.ts 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import * as Effect from "effect/Effect"
  2. import * as Command from "effect/unstable/cli/Command"
  3. import { CliApi } from "./cli-api"
  4. export type Input<Value> =
  5. Value extends CliApi.Node<infer _Name, infer Spec, infer _Commands>
  6. ? Input<Spec>
  7. : Value extends Command.Command<infer _Name, infer Input, infer _Context, infer _Error, infer _Requirements>
  8. ? Input
  9. : never
  10. type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown>
  11. type Loader<Node extends CliApi.Any> = () => Promise<{ default: (input: Input<Node>) => Effect.Effect<void, any> }>
  12. type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, never>
  13. export type Handlers<Node extends CliApi.Any> = keyof Node["commands"] extends never
  14. ? Loader<Node>
  15. : { readonly $?: Loader<Node> } & { readonly [Key in keyof Node["commands"]]: Handlers<Node["commands"][Key]> }
  16. interface LazyHandler {
  17. readonly spec: Command.Command.Any
  18. readonly load: () => Promise<{ default: RuntimeHandler }>
  19. }
  20. type RuntimeHandlers =
  21. | (() => Promise<{ default: RuntimeHandler }>)
  22. | {
  23. readonly $?: () => Promise<{ default: RuntimeHandler }>
  24. readonly [key: string]: RuntimeHandlers | (() => Promise<{ default: RuntimeHandler }>) | undefined
  25. }
  26. export function handler<const Node extends CliApi.Any, Error>(
  27. _node: Node,
  28. run: (input: Input<Node>) => Effect.Effect<void, Error>,
  29. ) {
  30. return run
  31. }
  32. export function handlers<const Root extends CliApi.Any>(root: Root, handlers: Handlers<Root>) {
  33. const result: LazyHandler[] = []
  34. function add(node: CliApi.Any, value: RuntimeHandlers) {
  35. if (typeof value === "function") {
  36. result.push({ spec: node.spec, load: value as () => Promise<{ default: RuntimeHandler }> })
  37. return
  38. }
  39. if (value.$) result.push({ spec: node.spec, load: value.$ as () => Promise<{ default: RuntimeHandler }> })
  40. for (const [name, child] of Object.entries(node.commands)) add(child, value[name] as RuntimeHandlers)
  41. }
  42. add(root, handlers as RuntimeHandlers)
  43. return result
  44. }
  45. export function run(api: CliApi.Any, handlers: ReadonlyArray<LazyHandler>, options: { readonly version: string }) {
  46. return Command.run(provide(api, handlers), options) as Effect.Effect<void, unknown, Command.Environment>
  47. }
  48. function provide(node: CliApi.Any, handlers: ReadonlyArray<LazyHandler>): ProvidedCommand {
  49. const spec: Command.Command.Any = Object.keys(node.commands).length
  50. ? (node.spec as Command.Command<string, unknown>).pipe(
  51. Command.withSubcommands(Object.values(node.commands).map((child) => provide(child, handlers))),
  52. )
  53. : node.spec
  54. const handler = handlers.find((handler) => handler.spec === node.spec)
  55. if (!handler) return spec as ProvidedCommand
  56. return spec.pipe(
  57. Command.withHandler((input) => Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))),
  58. ) as ProvidedCommand
  59. }
  60. export * as CliBuilder from "./cli-builder"