| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 5 | import { make as makeJsonSchemaGenerator } from "@effect/openapi-generator/JsonSchemaGenerator"; |
| 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as FileSystem from "effect/FileSystem"; |
| 8 | import * as Layer from "effect/Layer"; |
| 9 | import * as Logger from "effect/Logger"; |
| 10 | import * as Path from "effect/Path"; |
| 11 | import * as Schema from "effect/Schema"; |
| 12 | import { Command, Flag } from "effect/cli"; |
| 13 | import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/http"; |
| 14 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| 15 | |
| 16 | const V2_SCHEMA_RELEASE = "schema-v2.0.0-alpha.3"; |
| 17 | const V1_SCHEMA_RELEASE = "schema-v1.21.0"; |
| 18 | |
| 19 | interface GenerateCommandError { |
| 20 | readonly _tag: "GenerateCommandError"; |
| 21 | readonly message: string; |
| 22 | } |
| 23 | |
| 24 | interface GeneratedPaths { |
| 25 | readonly generatedDir: string; |
| 26 | readonly upstreamSchemaPath: string; |
| 27 | readonly upstreamMetaPath: string; |
| 28 | readonly upstreamV1SchemaPath: string; |
| 29 | readonly upstreamV1MetaPath: string; |
| 30 | readonly schemaOutputPath: string; |
| 31 | readonly v1SchemaOutputPath: string; |
| 32 | readonly metaOutputPath: string; |
| 33 | } |
| 34 | |
| 35 | const UpstreamJsonSchemaSchema = Schema.Struct({ |
| 36 | $defs: Schema.Record(Schema.String, Schema.Json), |
| 37 | }); |
| 38 | const MetaJsonSchema = Schema.Struct({ |
| 39 | agentMethods: Schema.Record(Schema.String, Schema.String), |
| 40 | clientMethods: Schema.Record(Schema.String, Schema.String), |
| 41 | version: Schema.Union([Schema.Number, Schema.String]), |
| 42 | }); |
| 43 | const encodeAgentMethods = Schema.encodeEffect( |
| 44 | Schema.fromJsonString(MetaJsonSchema.fields.agentMethods), |
| 45 | ); |
| 46 | const encodeClientMethods = Schema.encodeEffect( |
| 47 | Schema.fromJsonString(MetaJsonSchema.fields.clientMethods), |
| 48 | ); |
| 49 | const encodeVersion = Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.version)); |
| 50 | |
| 51 | const decodeUpstreamSchema = Schema.decodeEffect(Schema.fromJsonString(UpstreamJsonSchemaSchema)); |
| 52 | const decodeMetaJson = Schema.decodeEffect(Schema.fromJsonString(MetaJsonSchema)); |
| 53 | |
| 54 | const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () { |
| 55 | const path = yield* Path.Path; |
| 56 | const generatedDir = path.join(import.meta.dirname, "..", "src", "_generated"); |
| 57 | return { |
| 58 | generatedDir, |
| 59 | upstreamSchemaPath: path.join(generatedDir, "upstream-schema.json"), |
| 60 | upstreamMetaPath: path.join(generatedDir, "upstream-meta.json"), |
| 61 | upstreamV1SchemaPath: path.join(generatedDir, "upstream-schema-v1.json"), |
| 62 | upstreamV1MetaPath: path.join(generatedDir, "upstream-meta-v1.json"), |
| 63 | schemaOutputPath: path.join(generatedDir, "schema.gen.ts"), |
| 64 | v1SchemaOutputPath: path.join(generatedDir, "schema-v1.gen.ts"), |
| 65 | metaOutputPath: path.join(generatedDir, "meta.gen.ts"), |
| 66 | } satisfies GeneratedPaths; |
| 67 | }); |
| 68 | |
| 69 | const ensureGeneratedDir = Effect.fn("ensureGeneratedDir")(function* () { |
| 70 | const fs = yield* FileSystem.FileSystem; |
| 71 | const { generatedDir } = yield* getGeneratedPaths(); |
| 72 | |
| 73 | yield* fs.makeDirectory(generatedDir, { recursive: true }); |
| 74 | }); |
| 75 | |
| 76 | const downloadFile = Effect.fn("downloadFile")(function* (url: string, outputPath: string) { |
| 77 | const fs = yield* FileSystem.FileSystem; |
| 78 | const path = yield* Path.Path; |
| 79 | |
| 80 | yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true }); |
| 81 | |
| 82 | const text = yield* HttpClient.get(url).pipe( |
| 83 | Effect.flatMap(HttpClientResponse.filterStatusOk), |
| 84 | Effect.flatMap((response) => response.text), |
| 85 | ); |
| 86 | |
| 87 | yield* fs.writeFileString(outputPath, text); |
| 88 | }); |
| 89 | |
| 90 | const downloadSchemas = Effect.fn("downloadSchemas")(function* ( |
| 91 | tag: string, |
| 92 | paths: { readonly schema: string; readonly meta: string }, |
| 93 | ) { |
| 94 | const fs = yield* FileSystem.FileSystem; |
| 95 | const baseUrl = `https://github.com/agentclientprotocol/agent-client-protocol/releases/download/${tag}`; |
| 96 | |
| 97 | yield* Effect.all( |
| 98 | [ |
| 99 | downloadFile(`${baseUrl}/schema.unstable.json`, paths.schema), |
| 100 | downloadFile(`${baseUrl}/meta.unstable.json`, paths.meta), |
| 101 | ], |
| 102 | { concurrency: 2 }, |
| 103 | ); |
| 104 | |
| 105 | yield* Effect.addFinalizer(() => |
| 106 | Effect.all([fs.remove(paths.schema), fs.remove(paths.meta)]).pipe( |
| 107 | Effect.ignoreCause({ log: true }), |
| 108 | ), |
| 109 | ); |
| 110 | }); |
| 111 | |
| 112 | const readFileString = Effect.fn("readJsonFile")(function* (filePath: string) { |
| 113 | const fs = yield* FileSystem.FileSystem; |
| 114 | return yield* fs.readFileString(filePath); |
| 115 | }); |
| 116 | |
| 117 | const writeGeneratedFiles = Effect.fn("writeGeneratedFiles")(function* ( |
| 118 | schemaOutput: string, |
| 119 | v1SchemaOutput: string, |
| 120 | metaOutput: string, |
| 121 | ) { |
| 122 | const fs = yield* FileSystem.FileSystem; |
| 123 | const { metaOutputPath, schemaOutputPath, v1SchemaOutputPath } = yield* getGeneratedPaths(); |
| 124 | |
| 125 | yield* fs.writeFileString(schemaOutputPath, schemaOutput); |
| 126 | yield* fs.writeFileString(v1SchemaOutputPath, v1SchemaOutput); |
| 127 | yield* fs.writeFileString(metaOutputPath, metaOutput); |
| 128 | }); |
| 129 | |
| 130 | function collectSchemaEntries( |
| 131 | chunk: string, |
| 132 | ): ReadonlyArray<{ readonly name: string; readonly code: string }> { |
| 133 | const lines = chunk |
| 134 | .split("\n") |
| 135 | .map((line) => line.trim()) |
| 136 | .filter((line) => line.length > 0 && !line.startsWith("//")); |
| 137 | const entries: Array<{ name: string; code: string }> = []; |
| 138 | |
| 139 | for (let index = 0; index < lines.length; index += 1) { |
| 140 | const typeLine = lines[index]; |
| 141 | if (!typeLine?.startsWith("export type ")) { |
| 142 | continue; |
| 143 | } |
| 144 | |
| 145 | const constLine = lines[index + 1]; |
| 146 | if (!constLine?.startsWith("export const ")) { |
| 147 | throw new Error(`Malformed generator output near: ${typeLine}`); |
| 148 | } |
| 149 | |
| 150 | const match = /^export type ([A-Za-z0-9_]+)/.exec(typeLine); |
| 151 | if (!match?.[1]) { |
| 152 | throw new Error(`Could not extract schema name from: ${typeLine}`); |
| 153 | } |
| 154 | |
| 155 | entries.push({ |
| 156 | name: match[1], |
| 157 | code: `${typeLine}\n${constLine}`, |
| 158 | }); |
| 159 | index += 1; |
| 160 | } |
| 161 | |
| 162 | return entries; |
| 163 | } |
| 164 | |
| 165 | function isJsonArray(value: Schema.Json | undefined): value is ReadonlyArray<Schema.Json> { |
| 166 | return Array.isArray(value); |
| 167 | } |
| 168 | |
| 169 | function normalizeSchemaKeywords(value: Schema.Json): Schema.Json { |
| 170 | if (isJsonArray(value)) { |
| 171 | return value.map(normalizeSchemaKeywords); |
| 172 | } |
| 173 | if (value === null || typeof value !== "object") { |
| 174 | return value; |
| 175 | } |
| 176 | |
| 177 | const normalizedEntries = Object.entries(value).map(([key, child]) => [ |
| 178 | key, |
| 179 | normalizeSchemaKeywords(child), |
| 180 | ]); |
| 181 | const normalizedObject = Object.fromEntries(normalizedEntries) as Record<string, Schema.Json>; |
| 182 | // ACP's forward-compatible variants exclude known discriminator values. |
| 183 | // The Effect generator does not support `not`; express this exact exclusion |
| 184 | // as a string pattern while preserving validation of the known variants. |
| 185 | if (normalizedObject.not !== undefined) { |
| 186 | const exclusion = normalizedObject.not; |
| 187 | if ( |
| 188 | exclusion === null || |
| 189 | typeof exclusion !== "object" || |
| 190 | isJsonArray(exclusion) || |
| 191 | !isJsonArray(exclusion.anyOf) |
| 192 | ) { |
| 193 | throw new Error("Unsupported ACP schema exclusion"); |
| 194 | } |
| 195 | const excluded = exclusion.anyOf.map((variant) => { |
| 196 | if (variant === null || typeof variant !== "object" || isJsonArray(variant)) |
| 197 | throw new Error("Invalid ACP excluded variant"); |
| 198 | const properties = variant.properties; |
| 199 | if (properties === null || typeof properties !== "object" || isJsonArray(properties)) |
| 200 | throw new Error("Invalid ACP excluded properties"); |
| 201 | const entries = Object.entries(properties); |
| 202 | const entry = entries[0]; |
| 203 | if (entries.length !== 1 || entry === undefined) throw new Error("Invalid ACP discriminator"); |
| 204 | const [key, schema] = entry; |
| 205 | if ( |
| 206 | schema === null || |
| 207 | typeof schema !== "object" || |
| 208 | isJsonArray(schema) || |
| 209 | typeof schema.const !== "string" |
| 210 | ) |
| 211 | throw new Error("Invalid ACP discriminator value"); |
| 212 | return { key, value: schema.const }; |
| 213 | }); |
| 214 | const discriminator = excluded[0]?.key; |
| 215 | const properties = normalizedObject.properties; |
| 216 | if ( |
| 217 | discriminator === undefined || |
| 218 | excluded.some((entry) => entry.key !== discriminator) || |
| 219 | properties === null || |
| 220 | typeof properties !== "object" || |
| 221 | isJsonArray(properties) |
| 222 | ) |
| 223 | throw new Error("Unsupported ACP discriminator exclusion"); |
| 224 | const property = properties[discriminator]; |
| 225 | if (property === null || typeof property !== "object" || isJsonArray(property)) |
| 226 | throw new Error("Missing ACP discriminator property"); |
| 227 | normalizedObject.properties = { |
| 228 | ...properties, |
| 229 | [discriminator]: { |
| 230 | ...property, |
| 231 | pattern: `^(?!(?:${excluded.map(({ value }) => RegExp.escape(value)).join("|")})$)[\\s\\S]*$`, |
| 232 | }, |
| 233 | }; |
| 234 | delete normalizedObject.not; |
| 235 | } |
| 236 | if (normalizedObject.unevaluatedProperties !== undefined) { |
| 237 | if (normalizedObject.unevaluatedProperties !== true) |
| 238 | throw new Error("Unsupported ACP unevaluated properties constraint"); |
| 239 | normalizedObject.additionalProperties = true; |
| 240 | delete normalizedObject.unevaluatedProperties; |
| 241 | } |
| 242 | if (isJsonArray(normalizedObject.oneOf)) { |
| 243 | const discriminator = normalizedObject.discriminator; |
| 244 | const propertyName = |
| 245 | discriminator !== null && typeof discriminator === "object" && !isJsonArray(discriminator) |
| 246 | ? discriminator.propertyName |
| 247 | : undefined; |
| 248 | const values = normalizedObject.oneOf.map((entry) => { |
| 249 | if (entry === null || typeof entry !== "object" || isJsonArray(entry)) return undefined; |
| 250 | if (typeof entry.const === "string") return entry.const; |
| 251 | const properties = entry.properties; |
| 252 | if ( |
| 253 | typeof propertyName !== "string" || |
| 254 | properties === null || |
| 255 | typeof properties !== "object" || |
| 256 | isJsonArray(properties) |
| 257 | ) |
| 258 | return undefined; |
| 259 | const property = properties[propertyName]; |
| 260 | return property !== null && typeof property === "object" && !isJsonArray(property) |
| 261 | ? property.const |
| 262 | : undefined; |
| 263 | }); |
| 264 | if ( |
| 265 | values.every((entry) => typeof entry === "string") && |
| 266 | new Set(values).size === values.length |
| 267 | ) { |
| 268 | normalizedObject.anyOf = normalizedObject.oneOf; |
| 269 | delete normalizedObject.oneOf; |
| 270 | } |
| 271 | } |
| 272 | const typeValue = normalizedObject.type; |
| 273 | |
| 274 | if (!isJsonArray(typeValue)) { |
| 275 | return normalizedObject; |
| 276 | } |
| 277 | |
| 278 | const normalizedTypes = typeValue.filter((entry): entry is string => typeof entry === "string"); |
| 279 | if (normalizedTypes.length !== typeValue.length || !normalizedTypes.includes("null")) { |
| 280 | return normalizedObject; |
| 281 | } |
| 282 | |
| 283 | const nonNullTypes = normalizedTypes.filter((entry) => entry !== "null"); |
| 284 | if (nonNullTypes.length !== 1) { |
| 285 | return normalizedObject; |
| 286 | } |
| 287 | const nonNullType = nonNullTypes[0]!; |
| 288 | |
| 289 | const nextObject: Record<string, Schema.Json> = {}; |
| 290 | for (const [key, child] of Object.entries(normalizedObject)) { |
| 291 | if (key !== "type") { |
| 292 | nextObject[key] = child; |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | return { |
| 297 | anyOf: [ |
| 298 | { |
| 299 | ...nextObject, |
| 300 | type: nonNullType, |
| 301 | }, |
| 302 | { type: "null" }, |
| 303 | ], |
| 304 | }; |
| 305 | } |
| 306 | |
| 307 | const isAnnotationKeyword = (key: string): boolean => |
| 308 | ["description", "title", "$comment", "default", "examples", "discriminator"].includes(key) || |
| 309 | key.startsWith("x-"); |
| 310 | |
| 311 | // Distribute object intersections over unions before handing schemas to Effect. |
| 312 | // ACP uses sibling `properties`/`anyOf` and `allOf` references for elicitation scopes. |
| 313 | function flattenCompositions( |
| 314 | value: Schema.Json, |
| 315 | definitions: Readonly<Record<string, Schema.Json>>, |
| 316 | ): Schema.Json { |
| 317 | if (isJsonArray(value)) return value.map((child) => flattenCompositions(child, definitions)); |
| 318 | if (value === null || typeof value !== "object") return value; |
| 319 | const normalized = Object.fromEntries( |
| 320 | Object.entries(value).map(([key, child]) => [key, flattenCompositions(child, definitions)]), |
| 321 | ); |
| 322 | const { allOf, anyOf, ...base } = normalized; |
| 323 | const merge = (left: Schema.Json, right: Schema.Json): Schema.Json => { |
| 324 | if ( |
| 325 | left === null || |
| 326 | typeof left !== "object" || |
| 327 | isJsonArray(left) || |
| 328 | right === null || |
| 329 | typeof right !== "object" || |
| 330 | isJsonArray(right) |
| 331 | ) |
| 332 | throw new Error("Unsupported ACP schema intersection"); |
| 333 | if (typeof right.$ref === "string") { |
| 334 | // Keep references when the sibling only describes the referenced schema. |
| 335 | // Inlining these repeats entire protocol unions in every request schema. |
| 336 | if (Object.keys(left).every(isAnnotationKeyword)) return { ...right, ...left }; |
| 337 | const target = definitions[right.$ref.replace("#/$defs/", "")]; |
| 338 | if (target === undefined) throw new Error(`Missing ACP definition ${right.$ref}`); |
| 339 | return merge(left, flattenCompositions(normalizeSchemaKeywords(target), definitions)); |
| 340 | } |
| 341 | if (isJsonArray(left.anyOf)) return { anyOf: left.anyOf.map((branch) => merge(branch, right)) }; |
| 342 | if (isJsonArray(right.anyOf)) |
| 343 | return { anyOf: right.anyOf.map((branch) => merge(left, branch)) }; |
| 344 | for (const key of Object.keys(left)) { |
| 345 | if (key === "properties" || key === "required" || isAnnotationKeyword(key)) continue; |
| 346 | if (right[key] !== undefined && JSON.stringify(left[key]) !== JSON.stringify(right[key])) { |
| 347 | throw new Error(`Unsupported conflicting ACP schema constraint: ${key}`); |
| 348 | } |
| 349 | } |
| 350 | const result = { ...left, ...right }; |
| 351 | if ( |
| 352 | left.properties && |
| 353 | right.properties && |
| 354 | typeof left.properties === "object" && |
| 355 | !isJsonArray(left.properties) && |
| 356 | typeof right.properties === "object" && |
| 357 | !isJsonArray(right.properties) |
| 358 | ) { |
| 359 | for (const key of Object.keys(left.properties)) { |
| 360 | if ( |
| 361 | right.properties[key] !== undefined && |
| 362 | JSON.stringify(left.properties[key]) !== JSON.stringify(right.properties[key]) |
| 363 | ) { |
| 364 | throw new Error(`Unsupported overlapping ACP schema property: ${key}`); |
| 365 | } |
| 366 | } |
| 367 | result.properties = { ...left.properties, ...right.properties }; |
| 368 | } |
| 369 | if (isJsonArray(left.required) || isJsonArray(right.required)) { |
| 370 | result.required = [ |
| 371 | ...new Set([ |
| 372 | ...(isJsonArray(left.required) ? left.required : []), |
| 373 | ...(isJsonArray(right.required) ? right.required : []), |
| 374 | ]), |
| 375 | ]; |
| 376 | } |
| 377 | return result; |
| 378 | }; |
| 379 | let combined: Schema.Json = base; |
| 380 | if (isJsonArray(allOf)) for (const part of allOf) combined = merge(combined, part); |
| 381 | if (isJsonArray(anyOf) && !isJsonArray(allOf) && Object.keys(base).every(isAnnotationKeyword)) { |
| 382 | return { ...base, anyOf }; |
| 383 | } |
| 384 | return isJsonArray(anyOf) ? merge(combined, { anyOf }) : combined; |
| 385 | } |
| 386 | |
| 387 | function generateSchemaOutput( |
| 388 | upstreamSchema: { readonly $defs: Readonly<Record<string, Schema.Json>> }, |
| 389 | release: string, |
| 390 | ): { readonly output: string; readonly count: number } { |
| 391 | const normalizedDefinitions = Object.fromEntries( |
| 392 | Object.entries(upstreamSchema.$defs).map(([name, schema]) => [ |
| 393 | name, |
| 394 | flattenCompositions(normalizeSchemaKeywords(schema), upstreamSchema.$defs), |
| 395 | ]), |
| 396 | ); |
| 397 | const sortedEntries = Object.entries(normalizedDefinitions).toSorted(([left], [right]) => |
| 398 | left.localeCompare(right), |
| 399 | ); |
| 400 | const generatedEntries = new Map<string, string>(); |
| 401 | const generator = makeJsonSchemaGenerator(); |
| 402 | |
| 403 | for (const [name, schema] of sortedEntries) { |
| 404 | generator.addSchema(name, schema as never); |
| 405 | } |
| 406 | |
| 407 | const generated = generator.generate("openapi-3.1", normalizedDefinitions as never, false).trim(); |
| 408 | if (generated.length > 0) { |
| 409 | for (const entry of collectSchemaEntries(generated)) { |
| 410 | if (!generatedEntries.has(entry.name)) { |
| 411 | generatedEntries.set(entry.name, entry.code); |
| 412 | } |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | return { |
| 417 | output: [ |
| 418 | "// This file is generated by the effect-acp package. Do not edit manually.", |
| 419 | `// Current ACP schema release: ${release}`, |
| 420 | "", |
| 421 | 'import * as Schema from "effect/Schema";', |
| 422 | "", |
| 423 | [...generatedEntries.values()].join("\n\n"), |
| 424 | "", |
| 425 | ].join("\n"), |
| 426 | count: generatedEntries.size, |
| 427 | }; |
| 428 | } |
| 429 | |
| 430 | const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: boolean) { |
| 431 | const { upstreamMetaPath, upstreamSchemaPath, upstreamV1MetaPath, upstreamV1SchemaPath } = |
| 432 | yield* getGeneratedPaths(); |
| 433 | |
| 434 | yield* ensureGeneratedDir(); |
| 435 | |
| 436 | if (!skipDownload) { |
| 437 | yield* Effect.log( |
| 438 | `Downloading ACP schema assets for ${V2_SCHEMA_RELEASE} and ${V1_SCHEMA_RELEASE}`, |
| 439 | ); |
| 440 | yield* Effect.all( |
| 441 | [ |
| 442 | downloadSchemas(V2_SCHEMA_RELEASE, { |
| 443 | schema: upstreamSchemaPath, |
| 444 | meta: upstreamMetaPath, |
| 445 | }), |
| 446 | downloadSchemas(V1_SCHEMA_RELEASE, { |
| 447 | schema: upstreamV1SchemaPath, |
| 448 | meta: upstreamV1MetaPath, |
| 449 | }), |
| 450 | ], |
| 451 | { concurrency: 2 }, |
| 452 | ); |
| 453 | } |
| 454 | |
| 455 | const upstreamSchema = yield* readFileString(upstreamSchemaPath).pipe( |
| 456 | Effect.flatMap(decodeUpstreamSchema), |
| 457 | ); |
| 458 | const upstreamV1Schema = yield* readFileString(upstreamV1SchemaPath).pipe( |
| 459 | Effect.flatMap(decodeUpstreamSchema), |
| 460 | ); |
| 461 | const upstreamMeta = yield* readFileString(upstreamMetaPath).pipe(Effect.flatMap(decodeMetaJson)); |
| 462 | const v2Schema = generateSchemaOutput(upstreamSchema, V2_SCHEMA_RELEASE); |
| 463 | const v1Schema = generateSchemaOutput(upstreamV1Schema, V1_SCHEMA_RELEASE); |
| 464 | |
| 465 | const prelude = [ |
| 466 | `// This file is generated by the effect-acp package. Do not edit manually.`, |
| 467 | `// Current ACP schema release: ${V2_SCHEMA_RELEASE}`, |
| 468 | "", |
| 469 | ]; |
| 470 | |
| 471 | const metaOutput = [ |
| 472 | ...prelude, |
| 473 | `export const AGENT_METHODS = ${yield* encodeAgentMethods(upstreamMeta.agentMethods)} as const;`, |
| 474 | "", |
| 475 | `export const CLIENT_METHODS = ${yield* encodeClientMethods(upstreamMeta.clientMethods)} as const;`, |
| 476 | "", |
| 477 | `export const PROTOCOL_VERSION = ${yield* encodeVersion(upstreamMeta.version)} as const;`, |
| 478 | "", |
| 479 | ].join("\n"); |
| 480 | |
| 481 | yield* writeGeneratedFiles(v2Schema.output, v1Schema.output, metaOutput); |
| 482 | yield* Effect.log( |
| 483 | `Generated ${v2Schema.count} ACP v2 schemas and ${v1Schema.count} ACP v1 compatibility schemas`, |
| 484 | ); |
| 485 | |
| 486 | const { generatedDir } = yield* getGeneratedPaths(); |
| 487 | yield* Effect.service(ChildProcessSpawner.ChildProcessSpawner).pipe( |
| 488 | Effect.flatMap((spawner) => |
| 489 | spawner.spawn( |
| 490 | ChildProcess.make("pnpm", ["exec", "vp", "fmt", generatedDir], { |
| 491 | stdin: "ignore", |
| 492 | stdout: "inherit", |
| 493 | stderr: "inherit", |
| 494 | }), |
| 495 | ), |
| 496 | ), |
| 497 | Effect.flatMap((child) => child.exitCode), |
| 498 | Effect.tap((code) => |
| 499 | code === 0 |
| 500 | ? Effect.void |
| 501 | : Effect.fail<GenerateCommandError>({ |
| 502 | _tag: "GenerateCommandError", |
| 503 | message: `oxfmt failed with exit code ${code}`, |
| 504 | }), |
| 505 | ), |
| 506 | ); |
| 507 | }); |
| 508 | |
| 509 | const generateCommand = Command.make( |
| 510 | "generate", |
| 511 | { |
| 512 | skipDownload: Flag.Boolean("skip-download").pipe(Flag.withDefault(false)), |
| 513 | }, |
| 514 | ({ skipDownload }) => generateSchemas(skipDownload), |
| 515 | ).pipe(Command.withDescription("Generate Effect ACP schemas from the pinned ACP release assets.")); |
| 516 | |
| 517 | const layerRuntime = Layer.mergeAll( |
| 518 | Logger.layer([Logger.consolePretty()]), |
| 519 | NodeServices.layer, |
| 520 | FetchHttpClient.layer, |
| 521 | ); |
| 522 | |
| 523 | Command.run(generateCommand, { version: "0.0.0" }).pipe( |
| 524 | Effect.scoped, |
| 525 | Effect.provide(layerRuntime), |
| 526 | NodeRuntime.runMain, |
| 527 | ); |