Skip to content
Source
Sign inSign up
Open History to see the last commit

main.ts

237 lines7.5 KB

1/**
2 * CLI entry point for ai-codegen tool.
3 *
4 * @since 4.0.0
5 */
6import * as Command from "effect/cli/Command"
7import * as Flag from "effect/cli/Flag"
8import * as Console from "effect/Console"
9import * as Effect from "effect/Effect"
10import * as FileSystem from "effect/FileSystem"
11import * as FetchHttpClient from "effect/http/FetchHttpClient"
12import * as Layer from "effect/Layer"
13import * as Option from "effect/Option"
14import * as ProviderDiscovery from "./Discovery.ts"
15import * as CodeGenerator from "./Generator.ts"
16import * as Glob from "./Glob.ts"
17import * as PostProcessor from "./PostProcess.ts"
18import * as SpecFetcher from "./SpecFetcher.ts"
19​
20// =============================================================================
21// Flags
22// =============================================================================
23​
24const providerFlag = Flag.String("provider").pipe(
25 Flag.withAlias("p"),
26 Flag.withDescription("Generate for specific provider only"),
27 Flag.optional
29​
30const skipLintFlag = Flag.Boolean("skip-lint").pipe(
31 Flag.withDescription("Skip Oxlint step"),
32 Flag.withDefault(false)
34​
35const skipFormatFlag = Flag.Boolean("skip-format").pipe(
36 Flag.withDescription("Skip Dprint step"),
37 Flag.withDefault(false)
39​
40// =============================================================================
41// ANSI Colors
42// =============================================================================
43​
44const colors = {
45 reset: "\x1b[0m",
46 bold: "\x1b[1m",
47 dim: "\x1b[2m",
48 cyan: "\x1b[36m",
49 green: "\x1b[32m",
50 yellow: "\x1b[33m",
51 gray: "\x1b[90m",
52 white: "\x1b[37m"
53} as const
54​
55// =============================================================================
56// Generate Command
57// =============================================================================
58​
59interface GenerateOptions {
60 readonly skipLint: boolean
61 readonly skipFormat: boolean
63​
64const formatSpecSource = (source: ProviderDiscovery.DiscoveredProvider["specSource"]): string => {
65 switch (source._tag) {
66 case "Url":
67 return source.url
68 case "File":
69 return source.path
70 }
72​
73const generateProvider = Effect.fn("generateProvider")(function*(
74 provider: ProviderDiscovery.DiscoveredProvider,
75 options: GenerateOptions
76) {
77 const specFetcher = yield* SpecFetcher.SpecFetcher
78 const generator = yield* CodeGenerator.CodeGenerator
79 const postProcessor = yield* PostProcessor.PostProcessor
80 const fs = yield* FileSystem.FileSystem
81​
82 yield* Console.log(`\n${colors.bold}${colors.cyan}${provider.name}${colors.reset}`)
83​
84 // Fetch spec
85 yield* Console.log(` ${colors.dim}Fetching spec...${colors.reset}`)
86 const spec = yield* specFetcher.fetch(provider.specSource, provider.name)
87​
88 // Generate code
89 yield* Console.log(` ${colors.dim}Generating code...${colors.reset}`)
90 let code = yield* generator.generate(provider, spec)
91​
92 // Apply replacements
93 const replacements = provider.config.replacementList
94 if (replacements.length > 0) {
95 yield* Console.log(` ${colors.dim}Applying ${replacements.length} replacement(s)...${colors.reset}`)
96 for (const replacement of replacements) {
97 code = code.replaceAll(replacement.from, replacement.to)
98 }
99 }
100​
101 // Prepend header if configured
102 const header = provider.config.headerContent
103 if (header !== undefined) {
104 code = `${header}\n${code}`
105 }
106​
107 // Write output
108 yield* Console.log(` ${colors.dim}Writing output...${colors.reset}`)
109 yield* fs.writeFileString(provider.outputPath, code)
110​
111 // Post-process
112 if (!options.skipLint) {
113 yield* Console.log(` ${colors.dim}Linting...${colors.reset}`)
114 yield* postProcessor.lint(provider.outputPath)
115 }
116​
117 if (!options.skipFormat) {
118 yield* Console.log(` ${colors.dim}Formatting...${colors.reset}`)
119 yield* postProcessor.format(provider.outputPath)
120 }
121​
122 yield* Console.log(
123 ` ${colors.green}✓${colors.reset} ${colors.gray}${provider.config.output}${colors.reset}`
124 )
125 return provider.outputPath
126})
127​
128const generate = Command.make("generate", {
129 provider: providerFlag,
130 skipLint: skipLintFlag,
131 skipFormat: skipFormatFlag
132}).pipe(
133 Command.withHandler(Effect.fnUntraced(function*({ provider, skipLint, skipFormat }) {
134 const discovery = yield* ProviderDiscovery.ProviderDiscovery
135​
136 const providers = yield* Option.match(provider, {
137 onNone: () => discovery.discover(),
138 onSome: (name) => discovery.discoverOne(name).pipe(Effect.map((p) => [p]))
139 })
140​
141 if (providers.length === 0) {
142 yield* Console.log(`${colors.yellow}No providers found.${colors.reset}`)
143 return
144 }
145​
146 yield* Console.log(
147 `${colors.dim}Generating ${providers.length} provider(s)...${colors.reset}`
148 )
149​
150 for (const p of providers) {
151 yield* generateProvider(p, { skipLint, skipFormat })
152 }
153​
154 yield* Console.log(`\n${colors.green}✓ All providers generated successfully!${colors.reset}`)
155 }))
157​
158// =============================================================================
159// List Command
160// =============================================================================
161​
162const list = Command.make("list").pipe(
163 Command.withHandler(Effect.fnUntraced(function*() {
164 const discovery = yield* ProviderDiscovery.ProviderDiscovery
165 const providers = yield* discovery.discover()
166​
167 if (providers.length === 0) {
168 yield* Console.log(`${colors.yellow}No providers found.${colors.reset}`)
169 return
170 }
171​
172 yield* Console.log(`${colors.green}Found ${providers.length} provider(s):${colors.reset}\n`)
173​
174 for (const p of providers) {
175 yield* Console.log(`${colors.bold}${colors.cyan}${p.name}${colors.reset}`)
176 yield* Console.log(` ${colors.gray}spec:${colors.reset} ${formatSpecSource(p.specSource)}`)
177 yield* Console.log(` ${colors.gray}output:${colors.reset} ${p.config.output}`)
178 yield* Console.log(
179 ` ${colors.gray}client:${colors.reset} ${colors.white}${p.config.clientName}${colors.reset}`
180 )
181 yield* Console.log(` ${colors.gray}typeOnly:${colors.reset} ${p.config.isTypeOnly}`)
182 if (p.config.patchList.length > 0) {
183 yield* Console.log(
184 ` ${colors.gray}patches:${colors.reset} ${colors.yellow}${p.config.patchList.length} file(s)${colors.reset}`
185 )
186 }
187 yield* Console.log("")
188 }
189 }))
191​
192// =============================================================================
193// Root Command
194// =============================================================================
195​
196const root = Command.make("effect-ai-codegen").pipe(
197 Command.withSubcommands([generate, list])
199​
200// =============================================================================
201// Layer Composition
202// =============================================================================
203​
204// ProviderDiscovery depends on Glob, FileSystem, and Path
205// SpecFetcher depends on FileSystem and HttpClient
206// CodeGenerator depends on OpenApiGenerator
207// PostProcessor depends on ChildProcessSpawner
208​
209// Build up layers with dependencies
210const DiscoveryLayer = ProviderDiscovery.layer.pipe(
211 Layer.provide(Glob.layer)
213​
214const SpecFetcherLayer = SpecFetcher.layer.pipe(
215 Layer.provide(FetchHttpClient.layer)
217​
218const ServicesLayer = Layer.mergeAll(
219 DiscoveryLayer,
220 SpecFetcherLayer,
221 CodeGenerator.layerSchema,
222 PostProcessor.layer
224​
225// =============================================================================
226// Export
227// =============================================================================
228​
229/**
230 * Run the CLI.
231 *
232 * @category running
233 * @since 4.0.0
234 */
235export const run = Command.run(root, { version: "0.0.0" }).pipe(
236 Effect.provide(ServicesLayer)