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
28)
29
30const skipLintFlag = Flag.Boolean("skip-lint").pipe(
31 Flag.withDescription("Skip Oxlint step"),
32 Flag.withDefault(false)
33)
34
35const skipFormatFlag = Flag.Boolean("skip-format").pipe(
36 Flag.withDescription("Skip Dprint step"),
37 Flag.withDefault(false)
38)
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
62}
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 case "StainlessStats":
71 return `stainless-stats(${source.statsUrl})`
72 }
73}
74
75const generateProvider = Effect.fn("generateProvider")(function*(
76 provider: ProviderDiscovery.DiscoveredProvider,
77 options: GenerateOptions
78) {
79 const specFetcher = yield* SpecFetcher.SpecFetcher
80 const generator = yield* CodeGenerator.CodeGenerator
81 const postProcessor = yield* PostProcessor.PostProcessor
82 const fs = yield* FileSystem.FileSystem
83
84 yield* Console.log(`\n${colors.bold}${colors.cyan}${provider.name}${colors.reset}`)
85
86 // Fetch spec
87 yield* Console.log(` ${colors.dim}Fetching spec...${colors.reset}`)
88 const spec = yield* specFetcher.fetch(provider.specSource, provider.name)
89
90 // Generate code
91 yield* Console.log(` ${colors.dim}Generating code...${colors.reset}`)
92 let code = yield* generator.generate(provider, spec)
93
94 // Apply replacements
95 const replacements = provider.config.replacementList
96 if (replacements.length > 0) {
97 yield* Console.log(` ${colors.dim}Applying ${replacements.length} replacement(s)...${colors.reset}`)
98 for (const replacement of replacements) {
99 code = code.replaceAll(replacement.from, replacement.to)
100 }
101 }
102
103 // Prepend header if configured
104 const header = provider.config.headerContent
105 if (header !== undefined) {
106 code = `${header}\n${code}`
107 }
108
109 // Write output
110 yield* Console.log(` ${colors.dim}Writing output...${colors.reset}`)
111 yield* fs.writeFileString(provider.outputPath, code)
112
113 // Post-process
114 if (!options.skipLint) {
115 yield* Console.log(` ${colors.dim}Linting...${colors.reset}`)
116 yield* postProcessor.lint(provider.outputPath)
117 }
118
119 if (!options.skipFormat) {
120 yield* Console.log(` ${colors.dim}Formatting...${colors.reset}`)
121 yield* postProcessor.format(provider.outputPath)
122 }
123
124 yield* Console.log(
125 ` ${colors.green}✓${colors.reset} ${colors.gray}${provider.config.output}${colors.reset}`
126 )
127 return provider.outputPath
128})
129
130const generate = Command.make("generate", {
131 provider: providerFlag,
132 skipLint: skipLintFlag,
133 skipFormat: skipFormatFlag
134}).pipe(
135 Command.withHandler(Effect.fnUntraced(function*({ provider, skipLint, skipFormat }) {
136 const discovery = yield* ProviderDiscovery.ProviderDiscovery
137
138 const providers = yield* Option.match(provider, {
139 onNone: () => discovery.discover(),
140 onSome: (name) => discovery.discoverOne(name).pipe(Effect.map((p) => [p]))
141 })
142
143 if (providers.length === 0) {
144 yield* Console.log(`${colors.yellow}No providers found.${colors.reset}`)
145 return
146 }
147
148 yield* Console.log(
149 `${colors.dim}Generating ${providers.length} provider(s)...${colors.reset}`
150 )
151
152 for (const p of providers) {
153 yield* generateProvider(p, { skipLint, skipFormat })
154 }
155
156 yield* Console.log(`\n${colors.green}✓ All providers generated successfully!${colors.reset}`)
157 }))
158)
159
160// =============================================================================
161// List Command
162// =============================================================================
163
164const list = Command.make("list").pipe(
165 Command.withHandler(Effect.fnUntraced(function*() {
166 const discovery = yield* ProviderDiscovery.ProviderDiscovery
167 const providers = yield* discovery.discover()
168
169 if (providers.length === 0) {
170 yield* Console.log(`${colors.yellow}No providers found.${colors.reset}`)
171 return
172 }
173
174 yield* Console.log(`${colors.green}Found ${providers.length} provider(s):${colors.reset}\n`)
175
176 for (const p of providers) {
177 yield* Console.log(`${colors.bold}${colors.cyan}${p.name}${colors.reset}`)
178 yield* Console.log(` ${colors.gray}spec:${colors.reset} ${formatSpecSource(p.specSource)}`)
179 yield* Console.log(` ${colors.gray}output:${colors.reset} ${p.config.output}`)
180 yield* Console.log(
181 ` ${colors.gray}client:${colors.reset} ${colors.white}${p.config.clientName}${colors.reset}`
182 )
183 yield* Console.log(` ${colors.gray}typeOnly:${colors.reset} ${p.config.isTypeOnly}`)
184 if (p.config.patchList.length > 0) {
185 yield* Console.log(
186 ` ${colors.gray}patches:${colors.reset} ${colors.yellow}${p.config.patchList.length} file(s)${colors.reset}`
187 )
188 }
189 yield* Console.log("")
190 }
191 }))
192)
193
194// =============================================================================
195// Root Command
196// =============================================================================
197
198const root = Command.make("effect-ai-codegen").pipe(
199 Command.withSubcommands([generate, list])
200)
201
202// =============================================================================
203// Layer Composition
204// =============================================================================
205
206// ProviderDiscovery depends on Glob, FileSystem, and Path
207// SpecFetcher depends on FileSystem and HttpClient
208// CodeGenerator depends on OpenApiGenerator
209// PostProcessor depends on ChildProcessSpawner
210
211// Build up layers with dependencies
212const DiscoveryLayer = ProviderDiscovery.layer.pipe(
213 Layer.provide(Glob.layer)
214)
215
216const SpecFetcherLayer = SpecFetcher.layer.pipe(
217 Layer.provide(FetchHttpClient.layer)
218)
219
220const ServicesLayer = Layer.mergeAll(
221 DiscoveryLayer,
222 SpecFetcherLayer,
223 CodeGenerator.layerSchema,
224 PostProcessor.layer
225)
226
227// =============================================================================
228// Export
229// =============================================================================
230
231/**
232 * Run the CLI.
233 *
234 * @category running
235 * @since 4.0.0
236 */
237export const run = Command.run(root, { version: "0.0.0" }).pipe(
238 Effect.provide(ServicesLayer)
239)